From 72f163ca5059369026fb6b4e7e93ccb47af11a76 Mon Sep 17 00:00:00 2001 From: MarcelineVPQ Date: Tue, 19 May 2026 17:35:08 -0600 Subject: [PATCH] wip(mic): BT-side mic capture infrastructure + host-side diag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scripts/mic_diag.sh | 214 ++++++++++++++++++++++++++++++++++++++++++++ src/audio.cpp | 77 ++++++++++++++++ src/audio.h | 10 +++ src/cmd.cpp | 72 ++++++++++++++- src/main.cpp | 74 +++++++++++++-- src/oled.cpp | 48 ++++++++-- src/state_mgr.cpp | 2 +- 7 files changed, 480 insertions(+), 17 deletions(-) create mode 100755 scripts/mic_diag.sh diff --git a/scripts/mic_diag.sh b/scripts/mic_diag.sh new file mode 100755 index 0000000..a7fbd5f --- /dev/null +++ b/scripts/mic_diag.sh @@ -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('&2 + exit 2 + ;; +esac diff --git a/src/audio.cpp b/src/audio.cpp index 1c9bf2e..d16444b 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -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; diff --git a/src/audio.h b/src/audio.h index b42411d..bd00c5b 100644 --- a/src/audio.h +++ b/src/audio.h @@ -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 \ No newline at end of file diff --git a/src/cmd.cpp b/src/cmd.cpp index fd0af35..238bb2d 100644 --- a/src/cmd.cpp +++ b/src/cmd.cpp @@ -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; } diff --git a/src/main.cpp b/src/main.cpp index f83e52e..3c704de 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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; diff --git a/src/oled.cpp b/src/oled.cpp index d00286b..6eac997 100644 --- a/src/oled.cpp +++ b/src/oled.cpp @@ -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) { diff --git a/src/state_mgr.cpp b/src/state_mgr.cpp index 49f288e..28a2dc9 100644 --- a/src/state_mgr.cpp +++ b/src/state_mgr.cpp @@ -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,