Files
MeowOS-For-R3P2/patch_v2/meow_audiod/test_bt_dev.c
T
AskaEth 6e67c74fe6 fix(audiod): M50 r3 评审 P2 三条 — fd 卫生 / 端点路径按 MAC 精确匹配 / 多 ACL 逐个试开
评审 r1(p2-3items/merge,reviewed 26c6077)三条:

① P2-1 dev_close() 在「BT 写路径判死 → 回落」这条路上会 close(0) 误关 stdin:
   播放线程处理 g_bt_fail_req 时**提前把 g_bt_dev 清 0**,下拍 dev_close() 就落到
   close(g_dev_fd) 分支,而 BT 打开成功时 dev_open() 返回 0(g_dev_fd==0)。
   - 去掉那行提前清零(让 dev_close 走 BT 分支自己清),并同时置 g_dev_fd = -1 做第二道保险;
   - 把关闭决策抽成纯函数 bt_dev_close_kind(bt_dev, fd)(0=NONE/1=BT/2=FD):**fd 0/1/2
     一律不当设备 fd 关**,宿主单测 D10 覆盖(含 bt_dev=0+fd=0 这条回归用例);
   - 补不变式「设备 fd 恒 >2」:dev_open 的 DAC/WAV open 过 fd_high()(0..2 时 F_DUPFD 升位,
     dup 失败则关掉并报打开失败——绝不留 stdio 区间里没人认领的设备 fd);
   - dev_close 注释改成诚实版:不再声称「已消除」,写明「BT 分支内自行清理;调用方不得
     提前清 g_bt_dev」。

② P2-2 g_bt_pcm 只写一次 → 换耳机 A→B 后 volume/info 打在 A 的端点上:
   - bt_refresh_endpoint() 每次先清空再探测,并用新 bt_dev_pick_endpoint() **按 g_bt_mac
     精确挑端点**(找不到就留空,宁可跳过 volume/info 也不串台);
   - bt_vol_full() 删除「拿 list-pcms 第一条端点凑数」的兜底(同样会串台);
   - 单测 D9:两条端点里按 MAC 分别取 A/B、未知 MAC 返回 -1。

③ P2-3 bt_dev_acl_mac() 只取第一条 ACL,多设备(手机/手柄并存,M49 正在测 BT 控制面)
   会挑错设备 → 耳机连着却回落 DAC:
   - 改名/重写为 bt_dev_acl_macs():收集**全部** ACL MAC(去重、上限由调用方给);
   - bt_find_mac() → bt_collect_candidates():①list-pcms 里已有端点的设备优先(真 A2DP)
     ②其后全部 ACL 设备;候选按可信度排序、去重;
   - bt_try_open() 改为对候选**轮流试开**,3 次尝试、300ms 间隔(1 候选=A,A,A 与 r2 语义
     相同;2 候选=A,B,A;3 候选=A,B,C),仍受退避门控;**删除裸 "bluealsa" 兜底**
     (那会落到「最近连接」的错设备,review §P2-3 明确指出);
   - 单测 D8c/D8d:两条 ACL 全取到且保序、三条含重复时去重并受上限裁剪。

顺带清死代码(评审「其余观察 2」):删除 bt_dev_count_src()、bt_dev_endpoint_path()
(生产路径已无调用者,前者早已只剩单测)。

宿主:test_bt_dev 71/71(新增 D8c/D8d/D9/D10 共 10 条),test_audiod_host 109/0,
交叉编译 0 warning(68604B,md5 9f58b2197ed91d24951918d41a78cf29,NEEDED 三条、
interpreter /lib/ld.so.1、GLIBC≤2.17),test_time32_abi.sh --builds 里
`build meow_audio src=yes macro=yes` 保持。真机项(耳机)未动:本轮不连蓝牙、不扫设备。
2026-11-11 19:37:03 +08:00

289 lines
14 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。
* 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 封顶 + 门控(防重试风暴)。
* D7 bt_dev_info_codec:真机 `bluealsa-cli info` 原文 → Selected codec(不能误吃 Available 行)。
* D8 bt_dev_acl_macs:真机 `hcitool con` 原文 → **全部** ACL 对端 MAC(多设备一等公民:
* 只取第一条会在手机/手柄同时连着时挑错设备,导致有耳机却回落 DAC)。
* D9 bt_dev_pick_endpoint:按 MAC 精确挑端点(换耳机 A→B 后不能拿 A 的路径发 volume/info)。
* D10 bt_dev_close_kind:关闭决策(BT 分支 / 真 fd / **0-2 一律不动**=护 stdin,P2-1 回归)。
*
* 编译(在 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");
}
/* ==== 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");
}
/* ==== 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 m[4][18];
check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0,
"D8a real hcitool con (state 1 AUTH ENCRYPT) -> 1 ACL MAC");
}
{
const char *con =
"Connections:\n"
"\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n";
char m[4][18];
check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0,
"D8b half-open ACL (state 5) still gives MAC (kernel is authority)");
}
{
/* 多设备:第二副耳机 + 手机同时连着 —— 两条都要拿到,且顺序=输出顺序 */
const char *con =
"Connections:\n"
"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n"
"\t< ACL 5C:FB:7C:11:22:33 handle 5 state 1 lm SLAVE \n";
char m[4][18];
int n = bt_dev_acl_macs(con, m, 4);
check(n == 2 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0 && strcmp(m[1], "5C:FB:7C:11:22:33") == 0,
"D8c two ACLs -> both MACs in order (not just the first one)");
}
{
/* 三设备 + 上限裁剪 + 去重(同一设备多 profile 行重复出现) */
const char *con =
"Connections:\n"
"\t< ACL AA:BB:CC:DD:EE:01 handle 1 state 1 lm MASTER \n"
"\t< ACL AA:BB:CC:DD:EE:01 handle 2 state 1 lm SLAVE \n"
"\t< ACL AA:BB:CC:DD:EE:02 handle 3 state 1 lm MASTER \n"
"\t< ACL AA:BB:CC:DD:EE:03 handle 4 state 1 lm MASTER \n";
char m[3][18];
int n = bt_dev_acl_macs(con, m, 3);
check(n == 3 && strcmp(m[0], "AA:BB:CC:DD:EE:01") == 0 &&
strcmp(m[1], "AA:BB:CC:DD:EE:02") == 0 && strcmp(m[2], "AA:BB:CC:DD:EE:03") == 0,
"D8d dedup + cap at max (3 distinct)");
}
{
char m[4][18];
check(bt_dev_acl_macs("Connections:\n", m, 4) == 0, "D8e no ACL -> 0");
check(bt_dev_acl_macs(NULL, m, 4) == 0, "D8f NULL -> 0");
check(bt_dev_acl_macs("\t< ACL aa:bb:cc:dd:ee:ff handle 3 state 1 lm SLAVE \n", m, 4) == 1 &&
strcmp(m[0], "AA:BB:CC:DD:EE:FF") == 0, "D8g lowercase -> uppercase canonical");
}
/* ==== D9 按 MAC 精确挑端点(换耳机 A→B 不串台) ==== */
{
const char *pcms =
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink\n";
char path[128] = "";
check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:02", path, sizeof path) == 0 &&
strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink") == 0,
"D9a picks B's path when MAC=B (not the first endpoint)");
check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:01", path, sizeof path) == 0 &&
strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink") == 0,
"D9b picks A's path when MAC=A");
check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:03", path, sizeof path) == -1 &&
path[0] == 0, "D9c unknown MAC -> -1 (never borrow another device's path)");
check(bt_dev_pick_endpoint("/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/hfphf/source\n",
"AA:BB:CC:DD:EE:01", path, sizeof path) == -1,
"D9d non-a2dpsrc endpoint -> -1");
check(bt_dev_pick_endpoint(NULL, "AA:BB:CC:DD:EE:01", path, sizeof path) == -1, "D9e NULL -> -1");
}
/* ==== D10 关闭决策(P2-1 回归:0/1/2 永不当设备 fd 关) ==== */
check(bt_dev_close_kind(1, 0) == 1, "D10a bt_dev=1 + fd=0 -> close BT only (never fd 0)");
check(bt_dev_close_kind(1, 5) == 1, "D10b bt_dev=1 + fd=5 -> close BT only");
check(bt_dev_close_kind(0, 0) == 0, "D10c bt_dev=0 + fd=0 -> NONE (stdin guard, P2-1)");
check(bt_dev_close_kind(0, 1) == 0, "D10d bt_dev=0 + fd=1 -> NONE (stdout guard)");
check(bt_dev_close_kind(0, 2) == 0, "D10e bt_dev=0 + fd=2 -> NONE (stderr guard)");
check(bt_dev_close_kind(0, 3) == 2, "D10f bt_dev=0 + fd=3 -> close FD");
check(bt_dev_close_kind(0, -1) == 0, "D10g bt_dev=0 + fd=-1 -> NONE");
check(bt_dev_close_kind(0, -2) == 0, "D10h bt_dev=0 + fd=-2 (NULL sink) -> NONE");
/* ==== 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;
}