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:
co-authored by
Claude Opus 4.7
parent
2209f9b8c7
commit
72f163ca50
Executable
+214
@@ -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
|
||||
@@ -25,6 +25,14 @@
|
||||
// #define VOLUME_GAIN 2
|
||||
// #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::max;
|
||||
|
||||
@@ -37,6 +45,21 @@ queue_t audio_fifo;
|
||||
static uint8_t opus_buf[200];
|
||||
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 {
|
||||
float data[512 * 2];
|
||||
};
|
||||
@@ -73,7 +96,51 @@ uint8_t audio_peak_haptic() {
|
||||
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() {
|
||||
// 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 音频数据
|
||||
if (!tud_audio_available()) return;
|
||||
|
||||
@@ -253,6 +320,16 @@ void audio_init() {
|
||||
critical_section_init(&opus_cs);
|
||||
multicore_launch_core1_with_stack(core1_entry, audio_core1_stack, sizeof(audio_core1_stack));
|
||||
#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;
|
||||
|
||||
+10
@@ -21,5 +21,15 @@ uint8_t audio_peak_haptic(); // 0..255, decays on read
|
||||
// Byte-flow counters for the Diagnostics screen + web emulator.
|
||||
uint32_t audio_usb_frames();
|
||||
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
|
||||
+71
-1
@@ -48,6 +48,22 @@ uint16_t cpu_temp_raw_smoothed() {
|
||||
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) {
|
||||
if (report_id == 0xf6 ||
|
||||
report_id == 0xf7 ||
|
||||
@@ -55,7 +71,9 @@ bool is_pico_cmd(uint8_t report_id) {
|
||||
report_id == 0xf9 ||
|
||||
report_id == 0xfa ||
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+68
-6
@@ -30,6 +30,32 @@ int reportSeqCounter = 0;
|
||||
uint8_t packetCounter = 0;
|
||||
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] = {
|
||||
0x7f, 0x7d, 0x7f, 0x7e, 0x00, 0x00, 0xa7,
|
||||
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) {
|
||||
// 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 ((data[56] & 1) != (interrupt_in_data[53] & 1)) {
|
||||
set_headset(data[56] & 1);
|
||||
@@ -114,12 +182,6 @@ void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
|
||||
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);
|
||||
memcpy(interrupt_in_data, data + 3, 63);
|
||||
report_dirty = true;
|
||||
|
||||
+39
-9
@@ -17,6 +17,14 @@
|
||||
|
||||
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 {
|
||||
|
||||
constexpr uint kPinDC = 8;
|
||||
@@ -618,28 +626,38 @@ __attribute__((noinline)) void render_screen_diag() {
|
||||
draw_text(kContentX, 9, buf);
|
||||
|
||||
// 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;
|
||||
const uint32_t now_us = time_us_32();
|
||||
const uint32_t cur_us_frames = audio_usb_frames();
|
||||
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) {
|
||||
const uint32_t dt_us = now_us - prev_sample_us;
|
||||
if (dt_us > 0) {
|
||||
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);
|
||||
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);
|
||||
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_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);
|
||||
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);
|
||||
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);
|
||||
|
||||
snprintf(buf, sizeof(buf), "BT: %s", bt_is_connected() ? "connected" : "waiting");
|
||||
@@ -1339,6 +1357,18 @@ void oled_loop() {
|
||||
// caches its frequency-counter measurement here).
|
||||
static int last_rendered_screen = -1;
|
||||
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;
|
||||
|
||||
switch (current_screen) {
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ namespace {
|
||||
static constexpr uint8_t state_init_data[63] = {
|
||||
0xfd, 0xf7, 0x0, 0x0,
|
||||
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, 0xa,
|
||||
|
||||
Reference in New Issue
Block a user