feat(diag): trigger-flow counters + scroll-only Diagnostics screen

Issue #3 (Few things — Triggers and Screen) reports that adaptive
trigger tension is missing through the dongle. The send_trigger_effect
preset cycle on the OLED Trigger Test screen works fine, so the BT
forwarding path is sound — meaning the loss is either at the host
(driver / Steam Input / game not emitting trigger output) or in our
forwarding of the host's 0x02 output report. Adds counters so we can
tell which.

Counters in main.cpp's tud_hid_set_report_cb 0x02 handler:
  - host_out02_total       — every 0x02 OUT report seen
  - host_out02_trig_allow  — those where the host set
                             AllowRight/LeftTriggerFFB
                             (valid_flag0 bits 2 & 3)
  - host_out02_to_bt       — 0x02 reports forwarded as BT 0x31

Diagnosis matrix:
  total grows, trig_allow flat → host driver never sets the trigger
                                 Allow bits = host-side issue
  trig_allow grows, to_bt lags → we're dropping (audio path stealing
                                 the BT pipe while speaker is active)
  all three grow proportionally → forwarding works; controller is
                                  receiving but not actuating
                                  = Sony / DS5 firmware limit

Same flash gets a scroll-only Diagnostics screen — the old layout
was already at the 6-row ceiling and three new counters push it over.
Rows are now a single switch-formatted list (10 rows), 5 visible at
a time, D-pad up/down scrolls. Caret glyphs at the right edge mark
"more above/below". Read-only screen so no cursor (unlike Settings).

Order is by triage relevance: Uptime, BT state, host02, trig/tx,
BT31 in/s fit on the default view; audio + parked-mic counters are
below the fold.

flush_fb split earlier (flush_fb_raw + flush_fb) is unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MarcelineVPQ
2026-05-19 19:08:49 -06:00
co-authored by Claude Opus 4.7
parent 0a2841bbfe
commit 5da1be498e
2 changed files with 133 additions and 31 deletions
+25
View File
@@ -56,6 +56,24 @@ void bt_31_mic_prefix(uint8_t out[6]) {
for (int i = 0; i < 6; i++) out[i] = g_mic_prefix[i];
}
// Trigger-flow diagnostics. Counts host → dongle → BT path for adaptive
// trigger effects. Lets us tell which link in the chain breaks when games
// like Death Stranding 2 don't produce trigger tension via the dongle:
// out02_total - every 0x02 HID OUT report received from host
// out02_trig_allow - of those, how many set AllowRight/LeftTriggerFFB
// (valid_flag0 bits 2 & 3) — i.e. the host actually
// told us "apply trigger FFB"
// out02_to_bt - 0x02 reports that we forwarded to the controller as
// a BT 0x31 sub-0x10 packet (gated off when speaker is
// active; audio.cpp's 0x36 path carries state then)
// Surfaced on the OLED Diagnostics screen.
volatile uint32_t g_host_out02_total = 0;
volatile uint32_t g_host_out02_trig_allow = 0;
volatile uint32_t g_host_out02_to_bt = 0;
uint32_t host_out02_total() { return g_host_out02_total; }
uint32_t host_out02_trig_allow() { return g_host_out02_trig_allow; }
uint32_t host_out02_to_bt() { return g_host_out02_to_bt; }
uint8_t interrupt_in_data[63] = {
0x7f, 0x7d, 0x7f, 0x7e, 0x00, 0x00, 0xa7,
0x08, 0x00, 0x00, 0x00, 0x52, 0x43, 0x30, 0x41,
@@ -248,6 +266,12 @@ void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t rep
if (report_id == 0) {
switch (buffer[0]) {
case 0x02: {
g_host_out02_total++;
// valid_flag0 lives at buffer[1] (right after the 0x02 report id).
// Bits 2 & 3 are AllowRight/LeftTriggerFFB.
if (bufsize > 1 && (buffer[1] & 0x0C)) {
g_host_out02_trig_allow++;
}
state_update(buffer + 1, bufsize - 1);
if (spk_active) {
break;
@@ -262,6 +286,7 @@ void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t rep
// memcpy(outputData + 3, buffer + 1, bufsize - 1);
state_set(outputData + 3,sizeof(SetStateData));
bt_write(outputData, sizeof(outputData));
g_host_out02_to_bt++;
break;
}
}
+108 -31
View File
@@ -19,6 +19,9 @@ 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 uint32_t host_out02_total();
extern uint32_t host_out02_trig_allow();
extern uint32_t host_out02_to_bt();
extern uint8_t bt_31_last_byte2();
extern uint8_t bt_31_b2_or_mask();
extern uint16_t bt_31_len_min();
@@ -627,35 +630,36 @@ __attribute__((noinline)) void render_screen_rssi() {
flush_fb();
}
__attribute__((noinline)) void render_screen_diag() {
fb_clear();
// Diagnostics screen state. Read-only viewport that scrolls with controller
// D-pad up/down. No cursor — there's nothing to select.
int diag_scroll = 0;
uint8_t diag_last_dpad = 8; // edge-trigger N/E/S/W like settings_handle_input
draw_text(kContentX, 0, "Diagnostics");
// Per-second rates sampled once per render, shared across format_diag_row's
// rate-based rows so they stay in sync.
struct DiagRates {
uint32_t usb_rate;
uint32_t bt_rate;
uint32_t mic_rate;
uint32_t bt31_rate;
};
DiagRates g_diag_rates{};
const uint32_t uptime_s = time_us_32() / 1000000u;
const uint32_t h = uptime_s / 3600u;
const uint32_t m = (uptime_s / 60u) % 60u;
const uint32_t s = uptime_s % 60u;
char buf[24];
snprintf(buf, sizeof(buf), "Up:%luh %02lum %02lus", (unsigned long)h, (unsigned long)m, (unsigned long)s);
draw_text(kContentX, 9, buf);
// Per-second rates for the audio path counters — recompute every render.
void sample_diag_rates() {
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_us_frames = audio_usb_frames();
const uint32_t cur_bt_packets = audio_bt_packets();
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;
const uint32_t cur_bt31 = bt_31_packet_count();
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);
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);
g_diag_rates.usb_rate = (uint32_t)(((uint64_t)(cur_us_frames - prev_us_frames) * 1000000u) / dt_us);
g_diag_rates.bt_rate = (uint32_t)(((uint64_t)(cur_bt_packets - prev_bt_packets) * 1000000u) / dt_us);
g_diag_rates.mic_rate = (uint32_t)(((uint64_t)(cur_mic_frames - prev_mic_frames) * 1000000u) / dt_us);
g_diag_rates.bt31_rate = (uint32_t)(((uint64_t)(cur_bt31 - prev_bt31) * 1000000u) / dt_us);
}
}
prev_us_frames = cur_us_frames;
@@ -663,20 +667,93 @@ __attribute__((noinline)) void render_screen_diag() {
prev_mic_frames = cur_mic_frames;
prev_bt31 = cur_bt31;
prev_sample_us = now_us;
}
snprintf(buf, sizeof(buf), "BT31 %lu Mic %lu/s", (unsigned long)bt31_rate, (unsigned long)mic_rate);
draw_text(kContentX, 18, buf);
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), "dec=%ld w=%u",
(long)audio_mic_last_decoded(),
(unsigned)audio_mic_last_wrote());
draw_text(kContentX, 36, buf);
// Row list ordered by relevance: always-useful at top, parked-mic-investigation
// data at bottom. To add a row, bump kNumDiagRows and add a case.
constexpr int kNumDiagRows = 10;
__attribute__((noinline))
void format_diag_row(int idx, char* line, size_t n) {
switch (idx) {
case 0: {
const uint32_t s = time_us_32() / 1000000u;
snprintf(line, n, "Up:%luh %02lum %02lus",
(unsigned long)(s / 3600u),
(unsigned long)((s / 60u) % 60u),
(unsigned long)(s % 60u));
break;
}
case 1:
snprintf(line, n, "BT: %s", bt_is_connected() ? "connected" : "waiting");
break;
case 2:
snprintf(line, n, "host02: %lu", (unsigned long)host_out02_total());
break;
case 3:
snprintf(line, n, "trig %lu / tx %lu",
(unsigned long)host_out02_trig_allow(),
(unsigned long)host_out02_to_bt());
break;
case 4:
snprintf(line, n, "BT31 in: %lu/s", (unsigned long)g_diag_rates.bt31_rate);
break;
case 5:
snprintf(line, n, "USB aud: %lu/s", (unsigned long)g_diag_rates.usb_rate);
break;
case 6:
snprintf(line, n, "BT32 out: %lu/s", (unsigned long)g_diag_rates.bt_rate);
break;
case 7:
snprintf(line, n, "Mic in: %lu/s", (unsigned long)g_diag_rates.mic_rate);
break;
case 8:
snprintf(line, n, "Mic dec=%ld w=%u",
(long)audio_mic_last_decoded(),
(unsigned)audio_mic_last_wrote());
break;
case 9: {
uint8_t pfx[6]; bt_31_mic_prefix(pfx);
snprintf(line, n, "%02X %02X %02X %02X %02X %02X",
pfx[0], pfx[1], pfx[2], pfx[3], pfx[4], pfx[5]);
break;
}
default:
line[0] = '\0';
break;
}
}
snprintf(buf, sizeof(buf), "BT: %s", bt_is_connected() ? "connected" : "waiting");
draw_text(kContentX, 45, buf);
void diag_handle_input(int visible) {
if (!bt_is_connected()) return;
const uint8_t dpad = (uint8_t)(interrupt_in_data[7] & 0x0F);
if (dpad != diag_last_dpad && dpad != 8) {
if (dpad == 0) diag_scroll--; // up
else if (dpad == 4) diag_scroll++; // down
}
diag_last_dpad = dpad;
const int max_top = (kNumDiagRows > visible) ? (kNumDiagRows - visible) : 0;
if (diag_scroll < 0) diag_scroll = 0;
if (diag_scroll > max_top) diag_scroll = max_top;
}
__attribute__((noinline)) void render_screen_diag() {
fb_clear();
draw_text(kContentX, 0, "Diagnostics");
sample_diag_rates();
constexpr int kVisible = 5;
diag_handle_input(kVisible);
char line[28];
for (int i = 0; i < kVisible && diag_scroll + i < kNumDiagRows; i++) {
format_diag_row(diag_scroll + i, line, sizeof(line));
draw_text(kContentX, 9 + i * 9, line);
}
// Scroll indicators along the right edge, adjacent to the visible content
// rather than in a footer — keeps the bottom row available for content.
if (diag_scroll > 0) draw_text(120, 9, "^");
if (diag_scroll + kVisible < kNumDiagRows) draw_text(120, 45, "v");
flush_fb();
}