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` 保持。真机项(耳机)未动:本轮不连蓝牙、不扫设备。
This commit is contained in:
1 parent
6e7c0d00c0
commit
6e67c74fe6
4 files changed
+222
-168
No files matched your search
@@ -82,7 +82,6 @@
|
||||
* @args = DEV PROFILE CODEC VOL SOFTVOL DELAY SRV)。DEV/CODEC 直接从 PCM 名内联传入,
|
||||
* 一次 snd_pcm_open 即完成「钉设备 + 钉 codec」,无需 DEV 自识别改写文件。
|
||||
* ⚠️ /usr/data/alsa.conf 是 HiBy 播放器的数据文件、语法非法、libasound 从不加载——不读也不写。 */
|
||||
#define BT_PCM_BARE "bluealsa" /* 兜底:仅当内联名失败时试一次配置缺省(DEV 通配) */
|
||||
#define BT_CODEC_AUTO "auto"
|
||||
#define BT_VOL_FULL_PCM "/usr/bin/bluealsa-cli" /* 一键设 AVRCP 100%(best-effort,不存在则跳过) */
|
||||
|
||||
@@ -976,39 +975,52 @@ static void bt_note_ok(void)
|
||||
g_bt_fail_n = 0;
|
||||
}
|
||||
|
||||
/* 找「现在要往哪台设备播」的 MAC。**主来源 = 内核 ACL(hcitool con)**:
|
||||
* 这是「耳机真的连着」的权威,且开销极小;bluealsa 的 list-pcms 只在 profile 配置后的
|
||||
* 秒级窗口里才列出端点(M50 真机:无人开 PCM 时 1~3s 就消失),**不能当唯一来源**
|
||||
* (否则探针一错过窗口就 bt=fail 回落)。list-pcms 仅作兜底,并顺手记下端点 D-Bus 路径
|
||||
* (volume/info 要用;PCM 被撑住后它才稳定,见 bt_refresh_endpoint)。
|
||||
* 返回 1=找到 MAC(写入 mac),0=当前没有可播的蓝牙设备。 */
|
||||
static int bt_find_mac(char *mac, size_t macsz)
|
||||
/* 收集「可以试着往它播」的设备 MAC 候选(按可信度排序,去重):
|
||||
* ① list-pcms 里**已经存在端点**的设备 —— 那就是真的 A2DP 连接(最可信,窗口内命中时用它);
|
||||
* ② `hcitool con` 的**全部** ACL 设备 —— 内核权威,覆盖「profile 还没配置好、端点还没出现」。
|
||||
* ⚠️ 多条 ACL 是常态(手机/手柄/第二副耳机);只取第一条会挑错设备 → 耳机连着却回落 DAC
|
||||
* (M50 r1 评审 P2-3)。候选交给 bt_try_open 逐个试开(上限 3 次尝试)。
|
||||
* 返回候选数(0=当前没有可播的蓝牙设备)。 */
|
||||
static int bt_collect_candidates(char (*cand)[18], int max)
|
||||
{
|
||||
if (mac && macsz) mac[0] = 0;
|
||||
char *o = popen_read_tmo(1200, "hcitool con 2>/dev/null");
|
||||
if (o && bt_dev_acl_mac(o, mac, macsz) == 0) {
|
||||
logmsg("bt: ACL device %s (source=hcitool con)", mac);
|
||||
return 1;
|
||||
if (!cand || max <= 0) return 0;
|
||||
int n = 0;
|
||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||
if (o && o[0]) {
|
||||
char m[18];
|
||||
if (bt_dev_detect_mac(o, m, sizeof m) == 0) {
|
||||
memcpy(cand[n++], m, 18);
|
||||
logmsg("bt: candidate#%d %s (source=list-pcms endpoint)", n, m);
|
||||
}
|
||||
}
|
||||
o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||
if (!o) { logmsg("bt: no ACL and no bluealsa-cli"); return 0; }
|
||||
char path[128];
|
||||
if (bt_dev_endpoint_path(o, path, sizeof path) == 0) snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path);
|
||||
if (bt_dev_detect_mac(o, mac, macsz) == 0) {
|
||||
logmsg("bt: a2dp dev %s (source=list-pcms fallback)", mac);
|
||||
return 1;
|
||||
o = popen_read_tmo(1200, "hcitool con 2>/dev/null");
|
||||
if (o) {
|
||||
char macs[4][18];
|
||||
int k = bt_dev_acl_macs(o, macs, 4);
|
||||
for (int i = 0; i < k && n < max; i++) {
|
||||
int dup = 0;
|
||||
for (int j = 0; j < n; j++) if (strcmp(cand[j], macs[i]) == 0) { dup = 1; break; }
|
||||
if (dup) continue;
|
||||
memcpy(cand[n++], macs[i], 18);
|
||||
logmsg("bt: candidate#%d %s (source=hcitool con ACL)", n, macs[i]);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* PCM 已打开后刷新端点路径(此时 bluealsa 的端点对象是稳定的),供 info/volume 用。 */
|
||||
/* 用当前 g_bt_mac 精确刷新端点路径(PCM 已打开后 bluealsa 端点对象才稳定)。
|
||||
* **必须按 MAC 匹配**:换耳机 A→B 后若沿用 A 的路径,volume/info 会打到旧设备上
|
||||
* (M50 r1 评审 P2-2)。找不到本设备的端点就留空(宁可跳过 volume/info 也不串台)。 */
|
||||
static void bt_refresh_endpoint(void)
|
||||
{
|
||||
if (g_bt_pcm[0]) return;
|
||||
g_bt_pcm[0] = 0;
|
||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||
if (!o) return;
|
||||
char path[128];
|
||||
if (bt_dev_endpoint_path(o, path, sizeof path) == 0) snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path);
|
||||
if (bt_dev_pick_endpoint(o, g_bt_mac, path, sizeof path) == 0)
|
||||
snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path);
|
||||
else
|
||||
logmsg("bt: endpoint for %s not listed yet (volume/info skipped)", g_bt_mac);
|
||||
}
|
||||
|
||||
/* 按 PCM 名打开(libasound → NONBLOCK open + set_params S16/RW/ch/rate/soft_resample/100ms)。
|
||||
@@ -1053,28 +1065,29 @@ static int bt_try_open(void)
|
||||
g_bt_fail_n, bt_dev_backoff_ms(g_bt_fail_n), since);
|
||||
return -1;
|
||||
}
|
||||
char mac[20] = "";
|
||||
if (!bt_find_mac(mac, sizeof mac)) { bt_note_fail("no connected BT device (hcitool con empty)"); return -1; }
|
||||
snprintf(g_bt_mac, sizeof g_bt_mac, "%s", mac);
|
||||
char cand[3][18];
|
||||
int ncand = bt_collect_candidates(cand, 3);
|
||||
if (ncand <= 0) { bt_note_fail("no BT device (no a2dp endpoint, no ACL)"); return -1; }
|
||||
bt_dev_codec_token(g_bt_codec, g_bt_tok, sizeof g_bt_tok);
|
||||
char name[96];
|
||||
if (bt_dev_pcm_name(g_bt_mac, g_bt_tok, name, sizeof name) != 0) { bt_note_fail("bad device MAC"); return -1; }
|
||||
logmsg("bt: try open %s (codec setting=%s -> token=%s)", name, g_bt_codec, g_bt_tok);
|
||||
/* 3 次 × 300ms 短重试:ACL 刚建立时 BlueZ 可能还没把 A2DP profile 配置完,
|
||||
* 这一下会返回 ENODEV/EBUSY;等 300ms 再来通常就通了。3 次仍失败才记账进退避
|
||||
* (短重试上限 600ms,不会在播放线程里形成长时间阻塞)。 */
|
||||
/* 3 次尝试、每次间隔 300ms(ACL 刚建立时 BlueZ 可能还没配置完 A2DP profile,等一拍就通)。
|
||||
* 多候选时轮流试:1 个候选→A,A,A(=r2 语义);2 个→A,B,A;3 个→A,B,C。
|
||||
* 候选都来自「真的连着」的集合,每次尝试都有意义;总耗时上限 ≈600ms + open 时延。
|
||||
* **不再用裸 "bluealsa" 兜底**:那会落到「最近连接」的设备上,多设备时等于挑错人。 */
|
||||
const char *opened = NULL;
|
||||
for (int i = 0; i < 3 && !opened; i++) {
|
||||
if (i) {
|
||||
struct timespec ts = { 0, 300 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
const char *mac = cand[i % ncand];
|
||||
if (i) { struct timespec ts = { 0, 300 * 1000000L }; nanosleep(&ts, NULL); }
|
||||
char name[96];
|
||||
if (bt_dev_pcm_name(mac, g_bt_tok, name, sizeof name) != 0) continue;
|
||||
logmsg("bt: try#%d open %s (codec setting=%s -> token=%s)", i + 1, name, g_bt_codec, g_bt_tok);
|
||||
if (bt_open_pcm(name) == 0) {
|
||||
opened = name;
|
||||
snprintf(g_bt_mac, sizeof g_bt_mac, "%s", mac);
|
||||
}
|
||||
if (bt_open_pcm(name) == 0) opened = name;
|
||||
else if (bt_open_pcm(BT_PCM_BARE) == 0) opened = BT_PCM_BARE;
|
||||
}
|
||||
if (!opened) { bt_note_fail("snd_pcm_open/set_params failed (3 tries)"); return -1; }
|
||||
bt_note_ok();
|
||||
bt_refresh_endpoint(); /* PCM 已撑住:端点对象稳定,取路径给 info/volume */
|
||||
bt_refresh_endpoint(); /* 端点对象此时稳定;按 MAC 精确取路径(换设备不串台) */
|
||||
bt_read_used_codec();
|
||||
g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 满量程 */
|
||||
bt_vol_full();
|
||||
@@ -1158,15 +1171,11 @@ static void bt_vol_full(void)
|
||||
{
|
||||
if (g_bt_vol_full_done) return;
|
||||
g_bt_vol_full_done = 1;
|
||||
/* 只用 bt_refresh_endpoint() 按 MAC 精确取到的路径;为空就**跳过**——
|
||||
* 宁可这一次不顶音量,也不拿别的设备的端点发 volume(换耳机 A→B 串台,评审 P2-2)。 */
|
||||
char pcm[128];
|
||||
snprintf(pcm, sizeof pcm, "%s", g_bt_pcm); /* bt_probe 已取到端点路径(省一次 popen) */
|
||||
if (!pcm[0]) {
|
||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||
if (!o) { logmsg("bt: avrcp set-max skipped (no bluealsa-cli)"); return; }
|
||||
if (bt_dev_endpoint_path(o, pcm, sizeof pcm) != 0) {
|
||||
logmsg("bt: avrcp set-max skipped (no a2dpsrc pcm found)"); return;
|
||||
}
|
||||
}
|
||||
snprintf(pcm, sizeof pcm, "%s", g_bt_pcm);
|
||||
if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no endpoint path for %s)", g_bt_mac); return; }
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof cmd, "%s volume %s 127 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm);
|
||||
char *r = popen_read_tmo(1500, cmd);
|
||||
@@ -1392,6 +1401,24 @@ static void dac_fallback_arm(void)
|
||||
apply_route(port);
|
||||
}
|
||||
|
||||
/* 把设备 fd 升到 ≥3:BT 打开成功时 dev_open() 返回 0,而 0/1/2 是 stdio——把设备 fd
|
||||
* 钉在 0..2 上等于给「谁误清了 g_bt_dev」埋地雷(dev_close 的 close_kind 守卫会跳过 0..2)。
|
||||
* open() 只在 stdio 被关掉时才可能返回 <3;这里 dup 一次让「设备 fd 恒 >2」成为不变式。
|
||||
* dup 也失败(极端 EMFILE)时把原 fd 关掉并报失败——绝不留一个「在 stdio 区间又没人认领」
|
||||
* 的设备 fd(那会变成打开的设备泄漏,比关一次 stdin 更糟)。 */
|
||||
static int fd_high(int fd)
|
||||
{
|
||||
if (fd < 0 || fd > 2) return fd;
|
||||
int nfd = fcntl(fd, F_DUPFD, 3);
|
||||
if (nfd < 0) {
|
||||
logmsg("fd %d in stdio range and dup failed: %s -> treat as open failure", fd, strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
return nfd;
|
||||
}
|
||||
|
||||
static int dev_open(void)
|
||||
{
|
||||
/* 输出目标=BT 且走真机 ALSA:打开 libasound 的 bluealsa PCM(内联 DEV/CODEC,不开 hw:0,0)。
|
||||
@@ -1411,7 +1438,7 @@ static int dev_open(void)
|
||||
#ifdef AUDIOD_HAVE_ALSA
|
||||
/* O_NONBLOCK:写路径走 poll(POLLOUT)+限时(dev_write),write 本身永不睡眠——
|
||||
* 驱动 wedge 时绝不能把播放线程永久压进内核(真机长流卡死根因防线)。 */
|
||||
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
|
||||
int fd = fd_high(open(DEV_PCM, O_RDWR | O_NONBLOCK));
|
||||
if (fd < 0) { logmsg("dev open %s: %s", DEV_PCM, strerror(errno)); return -1; }
|
||||
if (alsa_hw_params(fd, rate, ch, alsa_fmt_to_snd((int)fmt)) != 0) {
|
||||
logmsg("dev hw_params %u/%u/%s failed", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt));
|
||||
@@ -1425,7 +1452,7 @@ static int dev_open(void)
|
||||
#endif
|
||||
}
|
||||
if (g_sink == SINK_FILE) {
|
||||
int fd = open(g_out_wav, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
int fd = fd_high(open(g_out_wav, O_WRONLY | O_CREAT | O_TRUNC, 0644));
|
||||
if (fd < 0) { logmsg("dev open wav %s: %s", g_out_wav, strerror(errno)); return -1; }
|
||||
wav_header(fd, (int)rate, (int)ch, bits, 0);
|
||||
return fd;
|
||||
@@ -1519,9 +1546,15 @@ static void dev_write(uint8_t *buf, size_t n)
|
||||
|
||||
static void dev_close(void)
|
||||
{
|
||||
/* 分流用 g_bt_dev(而非 g_bt_open):写路径判死时 bt_write 已 bt_close()(g_bt_open=0),
|
||||
* 若此时按 g_bt_open 走 DAC 分支会把 g_dev_fd(=0) 当成真 fd 关掉(误关 stdin)。 */
|
||||
if (g_bt_dev) { bt_close(); g_bt_dev = 0; g_dev_fd = -1; g_dev_open = 0; return; }
|
||||
/* 关设备的决策交给 bt_dev_close_kind()(纯函数,宿主可单测):
|
||||
* - **BT 分支内自行清理**:认的是 g_bt_dev(不是 g_bt_open——写路径判死时 bt_write 已
|
||||
* bt_close(),g_bt_open 已是 0),进来后把 g_bt_dev/g_dev_fd 一起复位;
|
||||
* - ⚠️ **调用方不得提前清 g_bt_dev**:BT 打开成功时 dev_open() 返回 0,谁提前清 0,
|
||||
* 这里就会落到下面 close(g_dev_fd) 分支上误关 stdin(M50 r1 评审 P2-1 就是这么复现的);
|
||||
* - 非 BT 分支只关 fd>2 的真 fd(0/1/2 一律不动,第二道保险)。 */
|
||||
int kind = bt_dev_close_kind(g_bt_dev, g_dev_fd);
|
||||
if (kind == 1) { bt_close(); g_bt_dev = 0; g_dev_fd = -1; g_dev_open = 0; return; }
|
||||
if (kind == 0) { g_dev_fd = -1; g_dev_open = 0; return; }
|
||||
if (g_sink == SINK_FILE && g_dev_fd >= 0 && g_dev_fd != -2) {
|
||||
off_t end = lseek(g_dev_fd, 0, SEEK_END);
|
||||
if (end > 44) {
|
||||
@@ -1641,7 +1674,10 @@ static void *playback_thread(void *arg)
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_bt_fail_req) {
|
||||
g_bt_fail_req = 0;
|
||||
g_bt_dev = 0;
|
||||
/* **不要**在这里清 g_bt_dev:dev_close() 靠它认出「当前设备是 BT PCM」并走
|
||||
* BT 分支清理;提前清 0 会让它落到 close(g_dev_fd),而 BT 打开时 g_dev_fd==0
|
||||
* →误关 stdin(M50 r1 评审 P2-1)。g_dev_fd 同时置 -1 做第二道保险。 */
|
||||
g_dev_fd = -1;
|
||||
bt_note_fail("write path fatal (endpoint gone?)");
|
||||
g_reconfig_pending = 1;
|
||||
pthread_cond_signal(&g_cond);
|
||||
|
||||
Reference in new issue
Block a user