Files
MeowOS-For-R3P2/patch_v2/meow_audiod/test_bt_dev.c
T
AskaEth 6e7c0d00c0 fix(audiod): M50 r2 蓝牙打开时序 — MAC 改从 hcitool con 取(list-pcms 易失)+ 短重试 + 行内匹配修正
真机(2026-11-11 18:2x/18:3x,MOONDROP Pudding 33:DE:2E:21:AD:30)暴露两个时序/解析问题:

① **list-pcms 易失,不能当 MAC 主来源**:bluealsa 的 a2dpsrc PCM 对象只在 A2DP profile
   配置好后的秒级窗口内存在(无人开 PCM 时 1~3s 就消失)。r1 用它取 MAC → 探针一错过窗口
   就 btdev=- / bt=fail 回落。改为 **主来源 `hcitool con` 的 ACL 行**(内核 ACL 是连接权威,
   开销一个 popen),拿到 MAC 直接 snd_pcm_open("bluealsa:DEV=<mac>,...") 让 bluealsa
   在 open 时自己创建 PCM;list-pcms 降级为兜底来源。
② **ACL 刚建立时 profile 可能还没配置好** → open 会瞬时 ENODEV。改 3 次 × 300ms 短重试
   (上限 600ms,不占播放线程太久);失败后才记为 fail#n 进退避(仍只在开流/重配边界)。
③ **端点路径要 D-Bus 全串**(`bluealsa-cli volume|info` 要它,不是 MAC):新增
   bt_dev_endpoint_path();PCM 撑住后再 list-pcms 取路径(此时稳定)→ bt_refresh_endpoint()。
④ **命令换行 bug**:旧 vol_full 的 token 扫描不认 '\n',真机把端点路径连换行一起塞进
   `bluealsa-cli volume <path>\n 127`,第二行成了独立命令 → 音量设置实际没下发。改为
   bt_dev_endpoint_path() 严格按空白/\r/\n 截断。
⑤ **行内匹配 bug**:is_src_line() 对整串 strstr 会借到下一行的 a2dpsrc —— hfphf/source
   行被误判成 A2DP 源端点(count_src 少算/endpoint_path 取错行)。改 line_is_src() 只在
   本行内判定。
⑥ STATE 增 `btacodec=`:读回 `bluealsa-cli info` 的 Selected codec。真机 MOONDROP Pudding
   只有 SBC/AAC,settings 请求 LDAC 时 bluealsa 接受参数但实际选 AAC —— 面板必须看到
   实际值(btcodec= 仍是请求值,btacodec= 是实际值)。
⑦ AVRCP 满量程 127(bluealsa-cli volume 取值范围 [0,127],旧值 100 只到 79%)。

宿主:test_bt_dev 66/66(新增 hcitool con 真机原文 / 端点路径 / info 原文解析用例),
test_audiod_host 109/0,交叉编译 0 warning(67944B,md5 0ec3b9a6…)。
真机:r1 二进制 18:26 那次已实测 BT 出声(bt=ok / played=55296 / bluealsa Running:true /
Delay 17.2ms / Volume 127),18:33 起耳机自动关机,r2 的 hcitool-con 主路径未能在真机复验
(耳机不可达),详见报告。
2026-11-11 19:37:03 +08:00

280 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_bt_dev.c — 蓝牙端点选择 / PCM 名构造 / 回落退避 宿主验证(纯逻辑,bt_dev.c)。
*
* 输入全部取自**真机实测原文**(bluealsa v4.1.1;设备 /usr/bin/bluealsa 的
* `dev_%.2X_..` + src/ba-transport.c 的 `<dev>/<profile>/<source|sink>`):
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink ← 实机(本机 a2dp-source)
* (同一格式下 /a2dpsrc/source 只是 PCM 模式不同,解析只认 dev_XX_.. 段)
*
* 覆盖矩阵:
* D1 bt_dev_detect_mac:真实 D-Bus 路径 → MAC;多端点/多设备取第一个 a2dpsrc;
* hfphf/a2dpsnk 不算;简化 `a2dpsrc/<MAC>` 兼容;空/无源端点/NULL/坏 dev_ 段 → -1。
* D2 bt_dev_count_src:0/1/2 个源端点。
* D3 bt_dev_codec_token:settings bt.codec → bluealsa canonical(4.1.1 别名表 strcasecmp);
* auto/空/unchanged/垃圾 → unchanged。
* D4 bt_dev_pcm_name:内联 DEV/PROFILE/CODEC;坏 MAC/小缓冲 → -1。
* D5 bt_dev_backoff_ms / should_try:2s→60s 封顶 + 门控(防重试风暴)。
* D6 bt_dev_endpoint_path:取整条 D-Bus 路径给 bluealsa-cli volume|info(真机行含 /sink)。
* D7 bt_dev_info_codec:真机 `bluealsa-cli info` 原文 → Selected codec(不能误吃 Available 行)。
* D8 bt_dev_acl_mac:真机 `hcitool con` 原文 → ACL 对端 MAC(MAC 主来源;list-pcms 只在秒级窗口存在)。
*
* 编译(在 patch_v2/meow_audiod 下):
* cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c
* 运行:./test_bt_dev
*/
#include "bt_dev.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_pass = 0, g_fail = 0;
static void check(int cond, const char *name)
{
if (cond) { printf(" PASS %s\n", name); g_pass++; }
else { printf(" FAIL %s\n", name); g_fail++; }
}
int main(void)
{
setbuf(stdout, NULL);
/* ==== D1 设备列表 → 选中 a2dpsrc 连接设备(真实 D-Bus 路径格式) ==== */
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0,
"D1a real D-Bus path -> MAC (dev_ underscores -> colons)");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n"
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsnk/sink\n"
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0,
"D1b hfphf/a2dpsnk skipped -> first a2dpsrc");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/source\n"
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/source\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, "D1c multi-device -> first a2dpsrc");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac("a2dpsrc/E0:42:6D:79:C5:98\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1d legacy a2dpsrc/<MAC> still parsed");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac("", mac, sizeof mac);
check(rc == -1, "D1e empty output -> -1");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsnk/sink\n", mac, sizeof mac);
check(rc == -1, "D1f only sink (no a2dpsrc) -> -1");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(NULL, mac, sizeof mac);
check(rc == -1, "D1g NULL input -> -1");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"/org/bluealsa/hci0/dev_ZZ_42_6D_79_C5_98/a2dpsrc/source\n", mac, sizeof mac);
check(rc == -1 && mac[0] == 0, "D1h malformed dev_ seg -> -1 (no garbage MAC)");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source 1\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1i trailing token tolerated");
}
/* ==== D2 源端点计数(有无 A2DP 端点的快速判定) ==== */
check(bt_dev_count_src("") == 0, "D2a empty -> 0");
check(bt_dev_count_src(
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsnk/sink\n") == 0,
"D2b only sink -> 0");
check(bt_dev_count_src(
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n") == 1,
"D2c one a2dpsrc -> 1");
/* 行内判定回归:hfphf 行在 a2dpsrc 行之前,不能被算成源端点(整串 strstr 会误判) */
check(bt_dev_count_src(
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n"
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n") == 1,
"D2e hfphf line before a2dpsrc -> count 1 (per-line match, not whole-buffer)");
check(bt_dev_count_src(
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/source\n"
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/source\n") == 2,
"D2d two a2dpsrc -> 2");
/* ==== D3 settings bt.codec → bluealsa canonical token ==== */
{
struct { const char *in; const char *want; const char *name; } t[] = {
{ "LDAC", "LDAC", "D3a LDAC passthrough" },
{ "ldac", "LDAC", "D3b lowercase ldac -> canonical" },
{ "SBC", "SBC", "D3c SBC" },
{ "AAC", "AAC", "D3d AAC" },
{ "MPEG24", "AAC", "D3e MPEG24 alias -> AAC" },
{ "aptX", "aptX", "D3f aptX" },
{ "apt-X", "aptX", "D3g apt-X alias -> aptX" },
{ "aptx-hd", "aptX-HD", "D3h aptx-hd -> aptX-HD" },
{ "MPEG", "MP3", "D3i MPEG alias -> MP3" },
{ "MP3", "MP3", "D3j MP3" },
{ "FastStream", "FastStream", "D3k FastStream" },
{ "FS", "FastStream", "D3l FS alias -> FastStream" },
{ "auto", "unchanged", "D3m auto -> unchanged (do not touch negotiation)" },
{ "", "unchanged", "D3n empty -> unchanged" },
{ "unchanged", "unchanged", "D3o unchanged -> unchanged" },
{ "BOGUS", "unchanged", "D3p unknown -> unchanged (never inject junk)" },
};
for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) {
char tok[16] = "";
const char *r = bt_dev_codec_token(t[i].in, tok, sizeof tok);
check(r == tok && strcmp(tok, t[i].want) == 0, t[i].name);
}
char tok[16] = "";
check(bt_dev_codec_token(NULL, tok, sizeof tok) == tok && !strcmp(tok, "unchanged"),
"D3q NULL setting -> unchanged");
}
/* ==== D4 PCM 名构造(20-bluealsa.conf @args 只认 DEV/PROFILE/CODEC 这套名字) ==== */
{
char n[96] = "";
int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, sizeof n);
check(rc == 0 && strcmp(n, "bluealsa:DEV=33:DE:2E:21:AD:30,PROFILE=a2dp,CODEC=LDAC") == 0,
"D4a name = bluealsa:DEV=<mac>,PROFILE=a2dp,CODEC=<token>");
}
{
char n[96] = "";
int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "", n, sizeof n);
check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4b empty token -> CODEC=unchanged");
}
{
char n[96] = "";
int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", NULL, n, sizeof n);
check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4c NULL token -> CODEC=unchanged");
}
{
char n[96] = "";
check(bt_dev_pcm_name("33:DE:2E:21:AD", "LDAC", n, sizeof n) == -1, "D4d short MAC -> -1");
check(bt_dev_pcm_name("GG:DE:2E:21:AD:30", "LDAC", n, sizeof n) == -1, "D4e non-hex MAC -> -1");
check(bt_dev_pcm_name(NULL, "LDAC", n, sizeof n) == -1, "D4f NULL MAC -> -1");
check(bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, 16) == -1, "D4g tiny buffer -> -1");
}
/* ==== D6 端点完整路径(volume/info 要 D-Bus 路径,不是 MAC) ==== */
{
char path[128] = "";
int rc = bt_dev_endpoint_path(
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n", path, sizeof path);
check(rc == 0 && strcmp(path, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0,
"D6a real /a2dpsrc/sink line -> full path (trailing newline trimmed)");
}
{
char path[128] = "";
int rc = bt_dev_endpoint_path(
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n"
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\r\n", path, sizeof path);
check(rc == 0 && strcmp(path, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0,
"D6b skips hfphf, strips CRLF (no newline leaks into popen cmd)");
}
{
char path[128] = "";
check(bt_dev_endpoint_path("/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsnk/sink\n",
path, sizeof path) == -1, "D6c no a2dpsrc -> -1");
check(bt_dev_endpoint_path("", path, sizeof path) == -1, "D6d empty -> -1");
check(bt_dev_endpoint_path(NULL, path, sizeof path) == -1, "D6e NULL -> -1");
}
/* ==== D7 实际协商 codec(真机 info 原文:MOONDROP Pudding 只有 SBC/AAC,请求 LDAC 实际选 AAC) ==== */
{
const char *info =
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
"Sequence: 0\n"
"Transport: A2DP-source\n"
"Mode: sink\n"
"Running: true\n"
"Format: S16_LE\n"
"Channels: 2\n"
"Sampling: 48000 Hz\n"
"Available codecs: SBC AAC\n"
"Selected codec: AAC\n"
"Delay: 17.2 ms\n"
"SoftVolume: false\n"
"Volume: L: 127 R: 127\n"
"Muted: L: false R: false\n";
char codec[16] = "";
check(bt_dev_info_codec(info, codec, sizeof codec) == 0 && strcmp(codec, "AAC") == 0,
"D7a real info -> Selected codec=AAC (not the Available line)");
}
{
const char *info = "Available codecs: SBC AAC\nSelected codec: SBC\r\n";
char codec[16] = "";
check(bt_dev_info_codec(info, codec, sizeof codec) == 0 && strcmp(codec, "SBC") == 0,
"D7b CRLF trimmed");
}
{
char codec[16] = "";
check(bt_dev_info_codec("Running: false\n", codec, sizeof codec) == -1,
"D7c no Selected line -> -1");
check(bt_dev_info_codec(NULL, codec, sizeof codec) == -1, "D7d NULL -> -1");
}
/* ==== D8 hcitool con → ACL MAC(真机原文;MAC 主来源) ==== */
{
const char *con =
"Connections:\n"
"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n";
char mac[32] = "";
check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0,
"D8a real hcitool con (state 1 AUTH ENCRYPT) -> ACL MAC");
}
{
const char *con =
"Connections:\n"
"\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n";
char mac[32] = "";
check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0,
"D8b half-open ACL (state 5) still gives MAC (kernel is authority)");
}
{
const char *con = "Connections:\n";
char mac[32] = "";
check(bt_dev_acl_mac(con, mac, sizeof mac) == -1 && mac[0] == 0, "D8c no ACL -> -1");
check(bt_dev_acl_mac(NULL, mac, sizeof mac) == -1, "D8d NULL -> -1");
}
{
/* 小写输入归一为大写(bluealsa DEV 参数按大写比较) */
const char *con = "\t< ACL aa:bb:cc:dd:ee:ff handle 3 state 1 lm SLAVE \n";
char mac[32] = "";
check(bt_dev_acl_mac(con, mac, sizeof mac) == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0,
"D8e lowercase -> uppercase canonical");
}
/* ==== D5 回落退避(防重试风暴:M10 教训) ==== */
check(bt_dev_backoff_ms(0) == 0, "D5a fails=0 -> 0ms (immediate)");
check(bt_dev_backoff_ms(1) == 2000, "D5b fails=1 -> 2s");
check(bt_dev_backoff_ms(2) == 4000, "D5c fails=2 -> 4s");
check(bt_dev_backoff_ms(3) == 8000, "D5d fails=3 -> 8s");
check(bt_dev_backoff_ms(4) == 16000, "D5e fails=4 -> 16s");
check(bt_dev_backoff_ms(5) == 32000, "D5f fails=5 -> 32s");
check(bt_dev_backoff_ms(6) == 60000, "D5g fails=6 -> 60s (capped)");
check(bt_dev_backoff_ms(99) == 60000, "D5h fails=99 -> 60s (capped)");
check(bt_dev_should_try(0, 0) == 1, "D5i never failed -> try now");
check(bt_dev_should_try(1, 1999) == 0, "D5j fail#1 @1999ms -> gated");
check(bt_dev_should_try(1, 2000) == 1, "D5k fail#1 @2000ms -> allowed");
check(bt_dev_should_try(3, 7999) == 0, "D5l fail#3 @7999ms -> gated");
check(bt_dev_should_try(3, 8000) == 1, "D5m fail#3 @8000ms -> allowed");
check(bt_dev_should_try(2, -1) == 1, "D5n clock anomaly (negative) -> allowed (no deadlock)");
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS (%d checks)\n",
g_fail ? g_fail : g_pass);
return g_fail ? 1 : 0;
}