wip(mic): BT-side mic capture infrastructure + host-side diag

In-progress work on DualSense microphone capture over BT. Mic-add tap
itself is disabled (was decoding standard input bytes as Opus and
producing INT16_MIN garbage on the USB IN endpoint) but everything
around it is wired and ready to re-enable once we identify the actual
mic transport.

Firmware:
- src/audio.cpp: Opus decoder on core0, mic_fifo queue, audio_loop
  mic-in path with decode + mono->stereo + tud_audio_write. Decoder
  init in audio_init() (creates 48kHz mono OpusDecoder).
- src/audio.h: exports mic_add_queue() + per-frame diagnostic
  accessors (audio_mic_frames, last_decoded, last_want, last_wrote,
  last_toc).
- src/main.cpp on_bt_data(): BT-side instrumentation — counts every
  INTERRUPT input report, tracks min/max length, OR mask of byte[2],
  most recent non-0x31 report ID, hex prefix of last 0x31/other/any
  frame, full content of the longest 0x31 frame seen. Mic-tap call
  itself stubbed behind `if (false)` pending the real detector.
- src/state_mgr.cpp: state_init_data byte 6 (VolumeMic) 0xFF→0x40
  (was out of range), byte 9 (MuteControl) 0x0F→0x00 (clear all
  PowerSave bits — AudioPowerSave was muting DSP).
- src/cmd.cpp: two new vendor feature reports — 0xFD returns 32-byte
  diagnostic state (counters + prefixes), 0xFE returns the longest
  0x31 frame in full (up to 80 bytes). Both queryable via
  /dev/hidraw on Linux from the host script.
- src/oled.cpp: Diagnostics screen shows TOC + decode result + USB
  wrote/want bytes for live BT-side visibility.

Host-side:
- scripts/mic_diag.sh: subcommands `status`, `capture [secs]`,
  `watch`, `bt-trace`. The bt-trace subcommand reads the 0xFD
  feature report via hidraw ioctl, decodes counters + recent
  prefixes, computes per-second rates. Drastically cuts iteration
  time — no OLED relay or per-test flash cycle needed.

Findings to date:
- Upstream/mic's mic-flag bit ((data[2] >> 1) & 1) does NOT match
  this DS5 firmware; bit 1 of byte[2] is NEVER set. Bit 0 is the
  standard input report type indicator, not a mic tag — confirmed
  by stick-bytes appearing as our supposed "Opus prefix".
- DS5 sends both report ID 0x01 and 0x31 over BT; the longest frame
  is a standard 79-byte 0x31 input report with sticks/IMU/touchpad
  but no audio bytes appended.
- Conclusion in progress: the DS5 firmware on this controller is
  not currently streaming mic over BT at all, even with
  AllowAudioControl=1, VolumeMic=0x40, AudioPowerSave=0,
  MicMute=0. Next investigation step: compare against a USB-mode
  DS5 to see what a real mic stream looks like at the UAC1 layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MarcelineVPQ
2026-05-19 17:35:08 -06:00
co-authored by Claude Opus 4.7
parent 2209f9b8c7
commit 72f163ca50
7 changed files with 480 additions and 17 deletions
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env bash
# Mic-path host-side diagnostic for the DS5Dongle (OLED Edition).
#
# Subcommands:
# status — one-shot snapshot of dongle USB / ALSA / capture stream state.
# Prints whether the dongle enumerated, what ALSA card # it
# took, the capture stream's current alt setting + sync mode,
# and whether a paired DualSense is reachable.
# capture — runs a 3-second arecord on the mic IN endpoint, reports
# ALSA result code, captured byte count, and a non-silence
# indicator (peak abs sample value via Python's wave module).
# Tells us in one shot whether the firmware is producing
# actual isoc-IN data and whether anything audio-like is
# showing up.
# watch — loops `status` every 2 seconds, prints only on changes —
# useful for catching the moment pairing completes or the
# arecord stream opens/closes.
#
# Why a script: lets the assistant query mic-path state directly from
# the host rather than waiting for the user to relay OLED counters
# through chat, which dominated the early Phase-3 debugging time.
#
# Requirements (all already installed on the user's machine):
# - arecord (alsa-utils)
# - lsusb (usbutils)
# - python3 (for wave-file stats)
set -u
VID=054c
PID=0ce6
DEV_NAME_RE='DualSense Wireless Controller'
find_card() {
arecord -l 2>/dev/null | awk -v re="$DEV_NAME_RE" '
$0 ~ re {
for (i = 1; i <= NF; i++) {
if ($i == "card") { gsub(":", "", $(i+1)); print $(i+1); exit }
}
}'
}
show_status() {
local card
card="$(find_card)"
# USB layer — is the device visible?
if lsusb -d "${VID}:${PID}" >/dev/null 2>&1; then
printf 'usb: present (%s:%s)\n' "$VID" "$PID"
else
printf 'usb: NOT FOUND — is the dongle plugged in?\n'
return 1
fi
if [[ -z "$card" ]]; then
printf 'alsa: dongle is on USB but not exposed as an audio card\n'
return 1
fi
printf 'alsa: card %s\n' "$card"
# Capture stream details (interface 2 alt 1 mic-IN endpoint)
if [[ -r "/proc/asound/card${card}/stream0" ]]; then
# Grep just the Capture block so we see status + altset + endpoint
awk '/^Capture:/,0' "/proc/asound/card${card}/stream0" | head -10 | sed 's/^/ /'
else
printf ' (no /proc/asound/card%s/stream0 — older kernel?)\n' "$card"
fi
}
run_capture() {
local card secs="${1:-3}"
card="$(find_card)"
if [[ -z "$card" ]]; then
printf 'no dongle capture device found\n'
return 1
fi
local tmp
tmp="$(mktemp -t mic_diag.XXXXXX.wav)"
printf 'capturing %ss from card %s into %s ...\n' "$secs" "$card" "$tmp"
local err
err="$(arecord -q -D "plughw:${card},0" -f S16_LE -c 2 -r 48000 -d "$secs" "$tmp" 2>&1)"
local rc=$?
if (( rc != 0 )); then
printf 'arecord exit=%d: %s\n' "$rc" "$err"
rm -f "$tmp"
return "$rc"
fi
# Stats via Python — peak abs sample is enough to distinguish "stream
# produced silence" from "stream produced actual audio".
python3 - "$tmp" <<'PY'
import sys, wave, struct
path = sys.argv[1]
with wave.open(path, 'rb') as w:
nframes = w.getnframes()
sw = w.getsampwidth()
ch = w.getnchannels()
fr = w.getframerate()
raw = w.readframes(nframes)
nsamples = nframes * ch
fmt = '<' + ('h' * nsamples)
data = struct.unpack(fmt, raw)
peak = max(abs(s) for s in data) if data else 0
nonzero = sum(1 for s in data if s != 0)
rms = (sum(s*s for s in data) / max(len(data), 1)) ** 0.5
print(f'wav: {nframes} frames, {ch} ch, {sw*8}-bit, {fr} Hz')
print(f'samples: nonzero={nonzero}/{nsamples} peak={peak} rms={rms:.1f}')
if peak == 0:
print('verdict: STREAM IS SILENT — firmware not producing isoc-IN data')
elif peak < 100:
print('verdict: extremely quiet — possibly DC offset only')
else:
print('verdict: AUDIO PRESENT')
PY
rm -f "$tmp"
}
watch_status() {
local prev=""
while :; do
local now
now="$(show_status 2>&1)"
if [[ "$now" != "$prev" ]]; then
printf '\n=== %s ===\n%s\n' "$(date '+%H:%M:%S')" "$now"
prev="$now"
fi
sleep 2
done
}
bt_trace() {
# Query the firmware's 0xFD vendor feature report via /dev/hidraw —
# exposes BT-side packet counts, last seen non-0x31 report ID, byte
# prefixes. Lets us find where the mic stream actually lives without
# an OLED-relay flash cycle per change.
python3 - <<'PY'
import fcntl, glob, struct, sys, time
VID, PID = 0x054c, 0x0ce6
def find_dongle():
for path in sorted(glob.glob('/dev/hidraw*')):
try:
f = open(path, 'rb+')
info = bytearray(8)
HIDIOCGRAWINFO = 0x80084803
try:
fcntl.ioctl(f, HIDIOCGRAWINFO, info)
except OSError:
pass
# Try feature 0xFD; if it returns 64 bytes we know it's our dongle
buf = bytearray(32); buf[0] = 0xFD
ioctl_num = (3 << 30) | (32 << 16) | (ord('H') << 8) | 0x07
try:
fcntl.ioctl(f, ioctl_num, buf)
return f
except OSError:
f.close()
except (OSError, PermissionError):
pass
return None
f = find_dongle()
if f is None:
print('no dongle found (or no /dev/hidraw permission)')
sys.exit(1)
ioctl_num = (3 << 30) | (64 << 16) | (ord('H') << 8) | 0x07
def query():
buf = bytearray(32); buf[0] = 0xFD
ioctl_num_32 = (3 << 30) | (32 << 16) | (ord('H') << 8) | 0x07
fcntl.ioctl(f, ioctl_num_32, buf)
return bytes(buf)
def decode(b):
bt31 = struct.unpack('<I', b[0:4])[0]
btoth = struct.unpack('<I', b[4:8])[0]
other_id = b[8]
other_or = b[9]
b2_or = b[10]
b2_last = b[11]
lmin = struct.unpack('<H', b[12:14])[0]
lmax = struct.unpack('<H', b[14:16])[0]
othpfx = b[16:24].hex()
anypfx = b[24:32].hex()
return (bt31, btoth, other_id, other_or, b2_or, b2_last,
lmin, lmax, othpfx, anypfx)
s1 = query(); time.sleep(1.0); s2 = query()
d1 = decode(s1); d2 = decode(s2)
bt31_rate = d2[0] - d1[0]
btoth_rate = d2[1] - d1[1]
print(f'rates: 0x31={bt31_rate}/s, non-0x31={btoth_rate}/s')
print(f'len range: {d2[6]}-{d2[7]} bytes')
print(f'byte[2] OR mask across 0x31 frames: 0x{d2[4]:02X} last=0x{d2[5]:02X}')
print(f'non-0x31 report IDs: OR mask=0x{d2[3]:02X} most recent=0x{d2[2]:02X}')
print(f'last non-0x31 prefix (data[0..7]): {d2[8]}')
print(f'last ANY frame (data[0..7]): {d2[9]}')
PY
}
case "${1:-status}" in
status) show_status ;;
capture) shift; run_capture "${1:-3}" ;;
watch) watch_status ;;
bt-trace) bt_trace ;;
*)
printf 'usage: %s {status|capture [secs]|watch|bt-trace}\n' "$0" >&2
exit 2
;;
esac
+77
View File
@@ -25,6 +25,14 @@
// #define VOLUME_GAIN 2 // #define VOLUME_GAIN 2
// #define BUFFER_LENGTH 48 // #define BUFFER_LENGTH 48
// DualSense microphone, ported from awalol/DS5Dongle's `mic` branch.
// The DS5 sends mic audio as Opus packets embedded in BT input report
// 0x31 when bit 1 of byte 2 is set; payload is 71 bytes of Opus at
// offset 4, decoded to mono 48 kHz 10 ms frames (480 samples).
#define MIC_CHANNELS 1
#define MIC_FRAMES 480
#define MIC_OPUS_SIZE 71
using std::clamp; using std::clamp;
using std::max; using std::max;
@@ -37,6 +45,21 @@ queue_t audio_fifo;
static uint8_t opus_buf[200]; static uint8_t opus_buf[200];
critical_section_t opus_cs; critical_section_t opus_cs;
// Mic ingress queue — filled from on_bt_data() (BT poll, core0), drained
// at the top of audio_loop() on core0. The decoder is single-threaded
// (core0 only), so no critical section is needed around it.
queue_t mic_fifo;
struct mic_element { uint8_t data[MIC_OPUS_SIZE]; };
static OpusDecoder *mic_decoder = nullptr;
static volatile uint32_t g_mic_frames = 0;
static volatile int32_t g_mic_last_decoded = 0; // opus_decode return value
static volatile uint16_t g_mic_last_want = 0; // bytes we asked TinyUSB to send
static volatile uint16_t g_mic_last_wrote = 0; // bytes TinyUSB accepted
uint32_t audio_mic_frames() { return g_mic_frames; }
int32_t audio_mic_last_decoded() { return g_mic_last_decoded; }
uint16_t audio_mic_last_want() { return g_mic_last_want; }
uint16_t audio_mic_last_wrote() { return g_mic_last_wrote; }
struct audio_raw_element { struct audio_raw_element {
float data[512 * 2]; float data[512 * 2];
}; };
@@ -73,7 +96,51 @@ uint8_t audio_peak_haptic() {
return (uint8_t)(v >> 7); return (uint8_t)(v >> 7);
} }
// Most-recent Opus TOC byte (first byte of the packet). Used by the OLED
// Diagnostics screen to decode the frame's bandwidth + duration config
// without serial.
static volatile uint8_t g_mic_toc = 0;
uint8_t audio_mic_last_toc() { return g_mic_toc; }
// Push a 71-byte Opus mic packet from the BT handler into the mic_fifo.
// Called from src/main.cpp's on_bt_data() when the DS5 sends a mic-tagged
// 0x31 input report. Drops the oldest queued packet if the FIFO is full —
// preferring fresh audio over backlog on overload.
void mic_add_queue(const uint8_t *data) {
static mic_element packet{};
memcpy(packet.data, data, MIC_OPUS_SIZE);
g_mic_toc = data[0]; // first byte of the Opus packet
if (queue_is_full(&mic_fifo)) queue_try_remove(&mic_fifo, NULL);
queue_try_add(&mic_fifo, &packet);
}
void audio_loop() { 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),
// push to the host via tud_audio_write. Runs once per loop iteration so
// it keeps up with the ~100 Hz arrival rate of mic-tagged BT frames.
if (mic_decoder != nullptr) {
static mic_element packet{};
if (queue_try_remove(&mic_fifo, &packet)) {
static int16_t mono[MIC_FRAMES];
const int decoded = opus_decode(mic_decoder, packet.data,
MIC_OPUS_SIZE, mono, MIC_FRAMES, 0);
g_mic_last_decoded = decoded; // observed in OLED Diag
if (decoded > 0) {
static int16_t stereo[MIC_FRAMES * 2];
for (int i = 0; i < decoded; i++) {
stereo[i * 2] = mono[i];
stereo[i * 2 + 1] = mono[i];
}
const uint16_t want = (uint16_t)(decoded * 2 * sizeof(int16_t));
const uint16_t wrote = tud_audio_write(stereo, want);
g_mic_last_want = want;
g_mic_last_wrote = wrote;
g_mic_frames++;
}
}
}
// 1. 读取 USB 音频数据 // 1. 读取 USB 音频数据
if (!tud_audio_available()) return; if (!tud_audio_available()) return;
@@ -253,6 +320,16 @@ void audio_init() {
critical_section_init(&opus_cs); critical_section_init(&opus_cs);
multicore_launch_core1_with_stack(core1_entry, audio_core1_stack, sizeof(audio_core1_stack)); multicore_launch_core1_with_stack(core1_entry, audio_core1_stack, sizeof(audio_core1_stack));
#endif #endif
// Mic path: queue + decoder live on core0 (audio_loop), separate from
// the core1 speaker encoder. Mic Opus is mono / 48 kHz / 10 ms frames.
queue_init(&mic_fifo, sizeof(mic_element), 2);
int dec_error = 0;
mic_decoder = opus_decoder_create(48000, MIC_CHANNELS, &dec_error);
if (dec_error != 0 || mic_decoder == nullptr) {
printf("[Audio] OpusDecoder create failed (err=%d)\n", dec_error);
mic_decoder = nullptr; // ensure audio_loop's null-guard short-circuits
}
} }
static OpusEncoder *encoder; static OpusEncoder *encoder;
+10
View File
@@ -21,5 +21,15 @@ uint8_t audio_peak_haptic(); // 0..255, decays on read
// Byte-flow counters for the Diagnostics screen + web emulator. // Byte-flow counters for the Diagnostics screen + web emulator.
uint32_t audio_usb_frames(); uint32_t audio_usb_frames();
uint32_t audio_bt_packets(); uint32_t audio_bt_packets();
uint32_t audio_mic_frames(); // count of mic Opus frames decoded + written
int32_t audio_mic_last_decoded(); // last opus_decode return — neg = error, 480 = OK
uint16_t audio_mic_last_want(); // bytes asked of tud_audio_write
uint16_t audio_mic_last_wrote(); // bytes TinyUSB FIFO actually accepted
uint8_t audio_mic_last_toc(); // first byte of last Opus packet (frame config)
// Called from on_bt_data() in main.cpp when the DS5 sends a mic-tagged
// 0x31 input report. Buffer must point at MIC_OPUS_SIZE (71) bytes of
// Opus payload.
void mic_add_queue(const uint8_t *data);
#endif //DS5_BRIDGE_AUDIO_H #endif //DS5_BRIDGE_AUDIO_H
+71 -1
View File
@@ -48,6 +48,22 @@ uint16_t cpu_temp_raw_smoothed() {
return (uint16_t)(ema + 0.5f); return (uint16_t)(ema + 0.5f);
} }
// Mic-debug globals (defined in main.cpp). File-scope extern so the
// linker resolves them once and cmd.cpp's 0xFD handler reads the same
// memory main.cpp writes to.
extern volatile uint32_t g_bt_31_packets;
extern volatile uint32_t g_bt_other_packets;
extern volatile uint8_t g_last_other_id;
extern volatile uint8_t g_other_id_or;
extern volatile uint8_t g_31_b2_or;
extern volatile uint8_t g_last_31_b2;
extern volatile uint16_t g_31_len_min;
extern volatile uint16_t g_31_len_max;
extern volatile uint8_t g_last_other_prefix[8];
extern volatile uint8_t g_last_any_prefix[16];
extern volatile uint16_t g_longest_len;
extern volatile uint8_t g_longest_frame[80];
bool is_pico_cmd(uint8_t report_id) { bool is_pico_cmd(uint8_t report_id) {
if (report_id == 0xf6 || if (report_id == 0xf6 ||
report_id == 0xf7 || report_id == 0xf7 ||
@@ -55,7 +71,9 @@ bool is_pico_cmd(uint8_t report_id) {
report_id == 0xf9 || report_id == 0xf9 ||
report_id == 0xfa || report_id == 0xfa ||
report_id == 0xfb || report_id == 0xfb ||
report_id == 0xfc report_id == 0xfc ||
report_id == 0xfd || // mic-debug counters
report_id == 0xfe // mic-debug longest-frame dump
) { ) {
return true; return true;
} }
@@ -161,6 +179,58 @@ uint16_t pico_cmd_get(uint8_t report_id, uint8_t *buffer, uint16_t reqlen) {
memcpy(buffer + 9, &temp_raw, 2); memcpy(buffer + 9, &temp_raw, 2);
return want; return want;
} }
if (report_id == 0xfd) {
// Mic-debug feature report. 32-byte payload (under typical
// GET_REPORT control transfer cap; want=64 came back empty).
// [0..3] uint32 BT 0x31 input report count
// [4..7] uint32 BT non-0x31 input report count
// [8] uint8 last non-0x31 report ID seen
// [9] uint8 OR mask of all non-0x31 report IDs seen
// [10] uint8 OR mask of byte[2] across all 0x31 frames
// [11] uint8 last value of byte[2] in a 0x31 frame
// [12..13] uint16 min frame length seen
// [14..15] uint16 max frame length seen
// [16..23] uint8[8] first 8 bytes of last non-0x31 frame
// [24..31] uint8[8] first 8 bytes of most recent ANY frame
constexpr uint16_t want = 32;
// Diagnostic: do NOT bail if reqlen < want — write what we can
// and set sentinel. If we still see 0x00 at byte[31] the handler
// isn't reached at all.
for (uint16_t i = 0; i < want && i < reqlen; i++) buffer[i] = 0;
const uint32_t bt31 = g_bt_31_packets;
const uint32_t btother = g_bt_other_packets;
const uint16_t lmin = g_31_len_min == 0xFFFF ? 0 : g_31_len_min;
const uint16_t lmax = g_31_len_max;
memcpy(buffer + 0, &bt31, 4);
memcpy(buffer + 4, &btother, 4);
buffer[8] = g_last_other_id;
buffer[9] = g_other_id_or;
buffer[10] = g_31_b2_or;
buffer[11] = g_last_31_b2;
memcpy(buffer + 12, &lmin, 2);
memcpy(buffer + 14, &lmax, 2);
for (int i = 0; i < 8 && (16 + i) < reqlen; i++) buffer[16 + i] = g_last_other_prefix[i];
for (int i = 0; i < 8 && (24 + i) < reqlen; i++) buffer[24 + i] = g_last_any_prefix[i];
return (reqlen < want) ? reqlen : want;
}
if (report_id == 0xfe) {
// 0xFE: full content of the LONGEST 0x31 frame seen. Bytes 0-1
// = length (uint16 LE), bytes 2+ = the captured frame bytes.
constexpr uint16_t want = 82; // 2 length + 80 frame bytes
const uint16_t lim = (reqlen < want) ? reqlen : want;
for (uint16_t i = 0; i < lim; i++) buffer[i] = 0;
const uint16_t llen = g_longest_len;
if (lim >= 2) {
buffer[0] = (uint8_t)(llen & 0xFF);
buffer[1] = (uint8_t)((llen >> 8) & 0xFF);
}
for (uint16_t i = 0; i < 80 && (i + 2) < lim; i++) {
buffer[2 + i] = g_longest_frame[i];
}
return lim;
}
return 0; return 0;
} }
+68 -6
View File
@@ -30,6 +30,32 @@ int reportSeqCounter = 0;
uint8_t packetCounter = 0; uint8_t packetCounter = 0;
bool spk_active = false; bool spk_active = false;
// Mic-debug instrumentation: count every 0x31 BT input report regardless
// of mic-tag bit, accumulate OR-mask of every byte-2 value seen (tells us
// which bits ever fire) and remember the last byte-2 value. Also track
// observed frame-length range. Surfaced on the OLED Diagnostics screen.
volatile uint32_t g_bt_31_packets = 0;
volatile uint32_t g_bt_other_packets = 0;
volatile uint8_t g_last_other_id = 0;
volatile uint8_t g_other_id_or = 0;
volatile uint8_t g_last_31_b2 = 0;
volatile uint8_t g_31_b2_or = 0;
volatile uint16_t g_31_len_min = 0xFFFF;
volatile uint16_t g_31_len_max = 0;
volatile uint8_t g_mic_prefix[6] = {0};
volatile uint8_t g_last_other_prefix[8] = {0};
volatile uint8_t g_last_any_prefix[16] = {0};
volatile uint16_t g_longest_len = 0;
volatile uint8_t g_longest_frame[80] = {0};
uint32_t bt_31_packet_count() { return g_bt_31_packets; }
uint8_t bt_31_last_byte2() { return g_last_31_b2; }
uint8_t bt_31_b2_or_mask() { return g_31_b2_or; }
uint16_t bt_31_len_min() { return g_31_len_min == 0xFFFF ? 0 : g_31_len_min; }
uint16_t bt_31_len_max() { return g_31_len_max; }
void bt_31_mic_prefix(uint8_t out[6]) {
for (int i = 0; i < 6; i++) out[i] = g_mic_prefix[i];
}
uint8_t interrupt_in_data[63] = { uint8_t interrupt_in_data[63] = {
0x7f, 0x7d, 0x7f, 0x7e, 0x00, 0x00, 0xa7, 0x7f, 0x7d, 0x7f, 0x7e, 0x00, 0x00, 0xa7,
0x08, 0x00, 0x00, 0x00, 0x52, 0x43, 0x30, 0x41, 0x08, 0x00, 0x00, 0x00, 0x52, 0x43, 0x30, 0x41,
@@ -101,6 +127,48 @@ void interrupt_loop() {
void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) { void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
// printf("[Main] BT data callback: channel=%u len=%u\n", channel, len); // printf("[Main] BT data callback: channel=%u len=%u\n", channel, len);
// Track ALL INTERRUPT input reports, not just 0x31. The mic stream
// may live on a different report ID — confirmed 2026-05-19 that data[2]
// bit 0 (and bit 1) is NOT a mic flag, just the report-type indicator;
// every "mic-tagged" frame turned out to be standard input.
if (channel == INTERRUPT && len > 1) {
if (data[1] == 0x31) g_bt_31_packets++;
else {
g_bt_other_packets++;
g_last_other_id = data[1];
g_other_id_or = (uint8_t)(g_other_id_or | data[1]);
for (uint16_t i = 0; i < 8 && i < len; i++) {
g_last_other_prefix[i] = data[i];
}
}
if (len > 2) {
g_last_31_b2 = data[2];
g_31_b2_or = (uint8_t)(g_31_b2_or | data[2]);
}
if (len < g_31_len_min) g_31_len_min = len;
if (len > g_31_len_max) g_31_len_max = len;
for (uint16_t i = 0; i < 16 && i < len; i++) {
g_last_any_prefix[i] = data[i];
}
// Capture the entire content of the longest 0x31 frame we've
// seen. Long frames almost certainly carry the mic audio appended
// after the standard 63-byte input report — this lets us look
// at the trailing bytes directly via 0xFD diagnostic.
if (data[1] == 0x31 && len > g_longest_len) {
g_longest_len = len;
for (uint16_t i = 0; i < 80 && i < len; i++) {
g_longest_frame[i] = data[i];
}
}
}
// 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.)
if (channel == INTERRUPT && data[1] == 0x31) { if (channel == INTERRUPT && data[1] == 0x31) {
if ((data[56] & 1) != (interrupt_in_data[53] & 1)) { if ((data[56] & 1) != (interrupt_in_data[53] & 1)) {
set_headset(data[56] & 1); set_headset(data[56] & 1);
@@ -114,12 +182,6 @@ void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
return; return;
} }
// We add the critical section here to avoid any race conditions when writing to the interrupt_in_data buffer,
// which is shared between the main loop and this callback.
// The critical section ensures that only one thread can access the buffer at a time,
// preventing data corruption and ensuring thread safety.
// We also set the report_dirty flag to true to indicate that new data is available
// and needs to be sent in the next interrupt report.
critical_section_enter_blocking(&report_cs); critical_section_enter_blocking(&report_cs);
memcpy(interrupt_in_data, data + 3, 63); memcpy(interrupt_in_data, data + 3, 63);
report_dirty = true; report_dirty = true;
+39 -9
View File
@@ -17,6 +17,14 @@
extern uint8_t interrupt_in_data[63]; // defined in main.cpp extern uint8_t interrupt_in_data[63]; // defined in main.cpp
// Mic diagnostic counters (defined in main.cpp).
extern uint32_t bt_31_packet_count();
extern uint8_t bt_31_last_byte2();
extern uint8_t bt_31_b2_or_mask();
extern uint16_t bt_31_len_min();
extern uint16_t bt_31_len_max();
extern void bt_31_mic_prefix(uint8_t out[6]);
namespace { namespace {
constexpr uint kPinDC = 8; constexpr uint kPinDC = 8;
@@ -618,28 +626,38 @@ __attribute__((noinline)) void render_screen_diag() {
draw_text(kContentX, 9, buf); draw_text(kContentX, 9, buf);
// Per-second rates for the audio path counters — recompute every render. // Per-second rates for the audio path counters — recompute every render.
static uint32_t prev_us_frames = 0, prev_bt_packets = 0; static uint32_t prev_us_frames = 0, prev_bt_packets = 0, prev_mic_frames = 0, prev_bt31 = 0;
static uint32_t prev_sample_us = 0; static uint32_t prev_sample_us = 0;
const uint32_t now_us = time_us_32(); const uint32_t now_us = time_us_32();
const uint32_t cur_us_frames = audio_usb_frames(); const uint32_t cur_us_frames = audio_usb_frames();
const uint32_t cur_bt_packets = audio_bt_packets(); const uint32_t cur_bt_packets = audio_bt_packets();
uint32_t usb_rate = 0, bt_rate = 0; const uint32_t cur_mic_frames = audio_mic_frames();
const uint32_t cur_bt31 = bt_31_packet_count();
uint32_t usb_rate = 0, bt_rate = 0, mic_rate = 0, bt31_rate = 0;
if (prev_sample_us != 0 && now_us > prev_sample_us) { if (prev_sample_us != 0 && now_us > prev_sample_us) {
const uint32_t dt_us = now_us - prev_sample_us; const uint32_t dt_us = now_us - prev_sample_us;
if (dt_us > 0) { if (dt_us > 0) {
usb_rate = (uint32_t)(((uint64_t)(cur_us_frames - prev_us_frames) * 1000000u) / dt_us); usb_rate = (uint32_t)(((uint64_t)(cur_us_frames - prev_us_frames) * 1000000u) / dt_us);
bt_rate = (uint32_t)(((uint64_t)(cur_bt_packets - prev_bt_packets) * 1000000u) / dt_us); bt_rate = (uint32_t)(((uint64_t)(cur_bt_packets - prev_bt_packets) * 1000000u) / dt_us);
mic_rate = (uint32_t)(((uint64_t)(cur_mic_frames - prev_mic_frames) * 1000000u) / dt_us);
bt31_rate = (uint32_t)(((uint64_t)(cur_bt31 - prev_bt31) * 1000000u) / dt_us);
} }
} }
prev_us_frames = cur_us_frames; prev_us_frames = cur_us_frames;
prev_bt_packets = cur_bt_packets; prev_bt_packets = cur_bt_packets;
prev_sample_us = now_us; prev_mic_frames = cur_mic_frames;
prev_bt31 = cur_bt31;
prev_sample_us = now_us;
snprintf(buf, sizeof(buf), "USB aud %lu/s", (unsigned long)usb_rate); snprintf(buf, sizeof(buf), "BT31 %lu Mic %lu/s", (unsigned long)bt31_rate, (unsigned long)mic_rate);
draw_text(kContentX, 18, buf); draw_text(kContentX, 18, buf);
snprintf(buf, sizeof(buf), "BT 0x32 %lu/s", (unsigned long)bt_rate); uint8_t pfx[6]; bt_31_mic_prefix(pfx);
snprintf(buf, sizeof(buf), "%02X %02X %02X %02X %02X %02X",
pfx[0], pfx[1], pfx[2], pfx[3], pfx[4], pfx[5]);
draw_text(kContentX, 27, buf); draw_text(kContentX, 27, buf);
snprintf(buf, sizeof(buf), "HCI errs: %lu", (unsigned long)bt_hci_err_count()); snprintf(buf, sizeof(buf), "dec=%ld w=%u",
(long)audio_mic_last_decoded(),
(unsigned)audio_mic_last_wrote());
draw_text(kContentX, 36, buf); draw_text(kContentX, 36, buf);
snprintf(buf, sizeof(buf), "BT: %s", bt_is_connected() ? "connected" : "waiting"); snprintf(buf, sizeof(buf), "BT: %s", bt_is_connected() ? "connected" : "waiting");
@@ -1339,6 +1357,18 @@ void oled_loop() {
// caches its frequency-counter measurement here). // caches its frequency-counter measurement here).
static int last_rendered_screen = -1; static int last_rendered_screen = -1;
const bool screen_entered = (current_screen != last_rendered_screen); const bool screen_entered = (current_screen != last_rendered_screen);
// Leaving Trigger Test in either direction → reset the adaptive
// trigger preset to OFF and push it to the controller. Otherwise
// the last-cycled effect (Weapon snap, Galloping pulse, etc.)
// stays active on the DS5 indefinitely, which surprised users
// who'd just navigated away expecting a clean slate.
if (last_rendered_screen == kScreenTriggers
&& current_screen != kScreenTriggers) {
trigger_preset = 0;
send_trigger_effect(0);
}
last_rendered_screen = current_screen; last_rendered_screen = current_screen;
switch (current_screen) { switch (current_screen) {
+1 -1
View File
@@ -19,7 +19,7 @@ namespace {
static constexpr uint8_t state_init_data[63] = { static constexpr uint8_t state_init_data[63] = {
0xfd, 0xf7, 0x0, 0x0, 0xfd, 0xf7, 0x0, 0x0,
0x7f, 0x64, // Headphones, Speaker 0x7f, 0x64, // Headphones, Speaker
0xff, 0x9, 0x0, 0x0F, 0x0, 0x0, 0x0, 0x0, 0x40, 0x9, 0x0, 0x00, 0x0, 0x0, 0x0, 0x0, // VolumeMic=64, MuteControl all clear (no PowerSave)
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa,