评审 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` 保持。真机项(耳机)未动:本轮不连蓝牙、不扫设备。
319 lines
13 KiB
C
319 lines
13 KiB
C
/* bt_dev.c — 蓝牙 A2DP 端点选择 + PCM 名构造 + 回落退避(纯逻辑实现,见 bt_dev.h 契约)。
|
||
*
|
||
* 纯 libc(string / stdlib),无任何 meow_* 调用、无 popen、无文件 I/O —— meow_audiod 负责
|
||
* 执行 `bluealsa-cli list-pcms`、调用 libasound 及维护运行时状态。
|
||
*/
|
||
#include "bt_dev.h"
|
||
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
#include <strings.h>
|
||
|
||
/* 是否 a2dpsrc / a2dp_source 端点行(bluealsa 4.1.1 list-pcms 的端点头)。
|
||
* ⚠️ 必须**行内**判定:直接对整串 strstr 会让「本行」借到「下一行」里的 a2dpsrc
|
||
* (真机 hfphf/source 行会被误判成 A2DP 源端点——M50 实测踩到)。 */
|
||
static int line_is_src(const char *ln)
|
||
{
|
||
const char *nl = strchr(ln, '\n');
|
||
size_t len = nl ? (size_t)(nl - ln) : strlen(ln);
|
||
char line[256];
|
||
if (len >= sizeof line) len = sizeof line - 1;
|
||
memcpy(line, ln, len);
|
||
line[len] = 0;
|
||
const char *p = line;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
return strstr(p, "a2dpsrc") != NULL || strstr(p, "a2dp_source") != NULL;
|
||
}
|
||
|
||
/* 从 `hcitool con` 输出收集**所有** ACL 连接的对端 BD_ADDR(按出现顺序去重)。
|
||
* 内核 ACL 是「设备是否真的连着」的权威(M45/M49 结论);bluealsa 的 list-pcms 只在
|
||
* profile 配置好后的**秒级窗口**里存在,不能当 MAC 的唯一来源(M50 真机实测)。
|
||
* ⚠️ 多条 ACL 是常态(手机/手柄/第二个耳机同时连着)——**不能只取第一条**:
|
||
* 取错设备会 snd_pcm_open 失败 → 有耳机却回落 DAC(M50 r1 评审 P2-3)。
|
||
* 行形如:`\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT`
|
||
* 返回找到的条数(0=无 ACL 连接)。macs 是 [max][18] 的二维数组,元素写成 NUL 结尾
|
||
* 17 字符大写冒号 MAC。 */
|
||
int bt_dev_acl_macs(const char *hcitool_con, char (*macs)[18], int max)
|
||
{
|
||
if (!hcitool_con || !macs || max <= 0) return 0;
|
||
for (int i = 0; i < max; i++) macs[i][0] = 0;
|
||
int n = 0;
|
||
const char *ln = hcitool_con;
|
||
while (ln && *ln && n < max) {
|
||
const char *nl = strchr(ln, '\n');
|
||
const char *le = nl ? nl : (ln + strlen(ln));
|
||
size_t linelen = (size_t)(le - ln);
|
||
char line[256];
|
||
if (linelen >= sizeof line) linelen = sizeof line - 1;
|
||
memcpy(line, ln, linelen); line[linelen] = 0;
|
||
if (strstr(line, "ACL")) {
|
||
for (const char *p = line; *p && n < max; p++) {
|
||
int ok = 1;
|
||
for (int i = 0; i < 17 && ok; i++) {
|
||
if ((i % 3) == 2) { if (p[i] != ':') ok = 0; }
|
||
else {
|
||
char c = p[i];
|
||
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) ok = 0;
|
||
}
|
||
}
|
||
if (!ok ||
|
||
!(p == line || !(p[-1] == ':' || (p[-1] >= '0' && p[-1] <= '9') ||
|
||
(p[-1] >= 'A' && p[-1] <= 'F') || (p[-1] >= 'a' && p[-1] <= 'f'))) ||
|
||
(p[17] == ':' || (p[17] >= '0' && p[17] <= '9') ||
|
||
(p[17] >= 'A' && p[17] <= 'F') || (p[17] >= 'a' && p[17] <= 'f')))
|
||
continue;
|
||
char mac[18];
|
||
for (int i = 0; i < 17; i++) {
|
||
char c = p[i];
|
||
mac[i] = (c >= 'a' && c <= 'f') ? (char)(c - 'a' + 'A') : c;
|
||
}
|
||
mac[17] = 0;
|
||
int dup = 0; /* 同一设备可能在多行/多 profile 出现 */
|
||
for (int i = 0; i < n; i++) if (strcmp(macs[i], mac) == 0) { dup = 1; break; }
|
||
if (!dup) memcpy(macs[n++], mac, 18);
|
||
}
|
||
}
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* 从 D-Bus 对象路径取 MAC。bluealsa 4.1.1 的 PCM 路径是
|
||
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink
|
||
* (dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X,见 src/ba-transport.c / src/shared/dbus-client.c)。
|
||
* 返回 0=解析出 17 字符 MAC(大写、冒号分隔)。 */
|
||
static int parse_dev_seg(const char *line, char *mac, size_t macsz)
|
||
{
|
||
if (!line || !mac || macsz < 18) return -1;
|
||
const char *d = strstr(line, "dev_");
|
||
if (!d) return -1;
|
||
const char *p = d + 4;
|
||
char out[18];
|
||
size_t o = 0;
|
||
for (int i = 0; i < 6; i++) {
|
||
unsigned v = 0;
|
||
int digits = 0;
|
||
for (; digits < 2; digits++) {
|
||
char c = *p;
|
||
int h = -1;
|
||
if (c >= '0' && c <= '9') h = c - '0';
|
||
else if (c >= 'A' && c <= 'F') h = c - 'A' + 10;
|
||
else if (c >= 'a' && c <= 'f') h = c - 'a' + 10;
|
||
if (h < 0) break;
|
||
v = v * 16u + (unsigned)h;
|
||
p++;
|
||
}
|
||
if (digits == 0) return -1;
|
||
o += (size_t)snprintf(out + o, sizeof out - o, (i == 0) ? "%02X" : ":%02X", v);
|
||
if (i < 5) {
|
||
if (*p != '_') return -1;
|
||
p++;
|
||
}
|
||
}
|
||
if (o != 17) return -1;
|
||
memcpy(mac, out, 18);
|
||
return 0;
|
||
}
|
||
|
||
/* 兜底:端点 token 的最后一段直接是 MAC(老式/简化输出 `a2dpsrc/AA:BB:...`)。 */
|
||
static int parse_tail_mac(const char *hit, char *mac, size_t macsz)
|
||
{
|
||
const char *te = hit;
|
||
while (*te && *te != ' ' && *te != '\t' && *te != '\n') te++;
|
||
const char *slash = NULL;
|
||
for (const char *q = hit; q < te; q++) if (*q == '/') slash = q;
|
||
if (!slash || (slash + 1) >= te) return -1;
|
||
const char *start = slash + 1;
|
||
size_t len = (size_t)(te - start);
|
||
if (len != 17 || len >= macsz) return -1;
|
||
int colons = 0;
|
||
for (size_t i = 0; i < len; i++) if (start[i] == ':') colons++;
|
||
if (colons != 5) return -1;
|
||
memcpy(mac, start, len);
|
||
mac[len] = 0;
|
||
return 0;
|
||
}
|
||
|
||
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
|
||
* 真实输出是 D-Bus 对象路径(每行一个):
|
||
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink
|
||
* MAC 取自 `dev_XX_XX_XX_XX_XX_XX` 段(下划线 → 冒号),另兼容简化形式 `a2dpsrc/AA:BB:...`。
|
||
* 逐个**有界行**匹配(strstr 必须在行内,避免跨换行误把后一行当本行端点——如行 1
|
||
* "hfphf/source" 与行 2 "a2dpsrc/sink" 的经典误判)。 */
|
||
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
||
{
|
||
if (!listpcms || !mac || macsz < 2) return -1;
|
||
mac[0] = 0;
|
||
const char *ln = listpcms;
|
||
while (ln && *ln) {
|
||
const char *nl = strchr(ln, '\n');
|
||
const char *le = nl ? nl : (ln + strlen(ln));
|
||
size_t linelen = (size_t)(le - ln);
|
||
char line[256];
|
||
if (linelen >= sizeof line) linelen = sizeof line - 1;
|
||
memcpy(line, ln, linelen); line[linelen] = 0;
|
||
const char *p = line;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (line_is_src(line)) {
|
||
if (parse_dev_seg(p, mac, macsz) == 0) return 0;
|
||
const char *hit = strstr(p, "a2dpsrc");
|
||
if (!hit) hit = strstr(p, "a2dp_source");
|
||
if (hit && parse_tail_mac(hit, mac, macsz) == 0) return 0;
|
||
}
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
mac[0] = 0;
|
||
return -1;
|
||
}
|
||
|
||
/* 在 list-pcms 里按「设备 MAC」精确挑端点路径(A→B 换耳机后不能拿 A 的路径去发 volume/info)。
|
||
* 返回 0=找到(写到 out),-1=列表里没有该 MAC 的端点。 */
|
||
int bt_dev_pick_endpoint(const char *listpcms, const char *mac, char *out, size_t outsz)
|
||
{
|
||
if (!listpcms || !mac || !mac[0] || !out || outsz < 2) return -1;
|
||
out[0] = 0;
|
||
const char *ln = listpcms;
|
||
while (ln && *ln) {
|
||
const char *nl = strchr(ln, '\n');
|
||
const char *le = nl ? nl : (ln + strlen(ln));
|
||
size_t linelen = (size_t)(le - ln);
|
||
char line[256], lmac[18];
|
||
if (linelen >= sizeof line) linelen = sizeof line - 1;
|
||
memcpy(line, ln, linelen); line[linelen] = 0;
|
||
if (line_is_src(line) && parse_dev_seg(line, lmac, sizeof lmac) == 0 &&
|
||
strcasecmp(lmac, mac) == 0) {
|
||
const char *p = line;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
size_t n = 0;
|
||
while (p[n] && p[n] != ' ' && p[n] != '\t' && p[n] != '\n' && p[n] != '\r' &&
|
||
n + 1 < outsz) { out[n] = p[n]; n++; }
|
||
out[n] = 0;
|
||
return n ? 0 : -1;
|
||
}
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* 关闭决策(纯函数,宿主可单测):当前「设备」该关谁。
|
||
* bt_dev=1 → 关 BT PCM(句柄在 audiod 的 g_bt_hdl),**不碰 fd**;
|
||
* 否则 fd>2 才当设备 fd 关;**0/1/2 一律不动**——BT 打开成功时 dev_open() 返回 0,
|
||
* 任何「提前清 bt_dev」的路径都会落到 close(g_dev_fd) 上误关 stdin(M50 r1 评审 P2-1)。
|
||
* 返回 0=NONE / 1=BT / 2=FD。 */
|
||
int bt_dev_close_kind(int bt_dev, int fd)
|
||
{
|
||
if (bt_dev) return 1;
|
||
return fd > 2 ? 2 : 0;
|
||
}
|
||
|
||
/* 从 `bluealsa-cli info <path>` 输出取实际协商到的 codec("Selected codec: AAC" → "AAC")。
|
||
* 只认行首 "Selected codec:"("Available codecs: SBC AAC" 那行不能误匹配)。 */
|
||
int bt_dev_info_codec(const char *info_out, char *out, size_t outsz)
|
||
{
|
||
if (!info_out || !out || outsz < 2) return -1;
|
||
out[0] = 0;
|
||
const char *ln = info_out;
|
||
while (ln && *ln) {
|
||
const char *nl = strchr(ln, '\n');
|
||
const char *p = ln;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (strncmp(p, "Selected codec:", 15) == 0) {
|
||
p += 15;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
size_t n = 0;
|
||
while (p[n] && p[n] != '\n' && p[n] != '\r' && n + 1 < outsz) { out[n] = p[n]; n++; }
|
||
while (n > 0 && (out[n-1] == ' ' || out[n-1] == '\t')) n--;
|
||
out[n] = 0;
|
||
return n ? 0 : -1;
|
||
}
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* bluealsa 4.1.1 src/shared/a2dp-codecs.c 的 canonical 名与别名(strcasecmp 匹配)。
|
||
* 只列本机可达的音频 codec;auto/unchanged/未知 → unchanged(不干预协商)。 */
|
||
static const struct {
|
||
const char *canonical;
|
||
const char *aliases[4];
|
||
} codecs[] = {
|
||
{ "SBC", { "SBC", NULL } },
|
||
{ "MP3", { "MP3", "MPEG12", "MPEG", NULL } },
|
||
{ "AAC", { "AAC", "MPEG24", NULL } },
|
||
{ "ATRAC", { "ATRAC", NULL } },
|
||
{ "aptX", { "aptX", "apt-X", NULL } },
|
||
{ "aptX-AD", { "aptX-AD", "apt-X-AD", NULL } },
|
||
{ "aptX-HD", { "aptX-HD", "apt-X-HD", NULL } },
|
||
{ "aptX-LL", { "aptX-LL", "apt-X-LL", NULL } },
|
||
{ "aptX-TWS", { "aptX-TWS", "apt-X-TWS", NULL } },
|
||
{ "FastStream", { "FastStream", "FS", NULL } },
|
||
{ "LC3plus", { "LC3plus", NULL } },
|
||
{ "LDAC", { "LDAC", NULL } },
|
||
{ "LHDC", { "LHDC", NULL } },
|
||
};
|
||
|
||
const char *bt_dev_codec_token(const char *setting, char *out, size_t outsz)
|
||
{
|
||
const char *auto_tok = "unchanged";
|
||
if (!out || outsz < 2) return auto_tok;
|
||
if (!setting || !setting[0]) { snprintf(out, outsz, "%s", auto_tok); return out; }
|
||
if (!strcasecmp(setting, "auto") || !strcasecmp(setting, "unchanged")) {
|
||
snprintf(out, outsz, "%s", auto_tok);
|
||
return out;
|
||
}
|
||
for (size_t i = 0; i < sizeof codecs / sizeof codecs[0]; i++)
|
||
for (size_t n = 0; n < 4 && codecs[i].aliases[n]; n++)
|
||
if (!strcasecmp(codecs[i].aliases[n], setting)) {
|
||
snprintf(out, outsz, "%s", codecs[i].canonical);
|
||
return out;
|
||
}
|
||
snprintf(out, outsz, "%s", auto_tok); /* 未知值:不干预协商(不把垃圾塞进 PCM 名) */
|
||
return out;
|
||
}
|
||
|
||
/* MAC 合法性:6 组两位十六进制,'/' 或 ':' 分隔都不接受(只认冒号,bluealsa 约定)。 */
|
||
static int mac_valid(const char *mac)
|
||
{
|
||
if (!mac || strlen(mac) != 17) return 0;
|
||
for (int i = 0; i < 17; i++) {
|
||
if ((i % 3) == 2) { if (mac[i] != ':') return 0; }
|
||
else {
|
||
char c = mac[i];
|
||
int hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||
if (!hex) return 0;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
int bt_dev_pcm_name(const char *mac, const char *codec_token, char *out, size_t outsz)
|
||
{
|
||
if (!out || outsz < 32 || !mac_valid(mac)) return -1;
|
||
const char *tok = (codec_token && codec_token[0]) ? codec_token : "unchanged";
|
||
int n = snprintf(out, outsz, "bluealsa:DEV=%s,PROFILE=a2dp,CODEC=%s", mac, tok);
|
||
if (n < 0 || (size_t)n >= outsz) { out[0] = 0; return -1; }
|
||
return 0;
|
||
}
|
||
|
||
unsigned bt_dev_backoff_ms(unsigned fails)
|
||
{
|
||
if (fails == 0) return 0;
|
||
unsigned shift = fails - 1;
|
||
if (shift > 5) shift = 5;
|
||
unsigned long d = 2000UL << shift; /* 2s,4s,8s,16s,32s,64s */
|
||
if (d > 60000UL) d = 60000UL;
|
||
return (unsigned)d;
|
||
}
|
||
|
||
int bt_dev_should_try(unsigned fails, long long since_ms)
|
||
{
|
||
if (fails == 0) return 1;
|
||
if (since_ms < 0) return 1;
|
||
return since_ms >= (long long)bt_dev_backoff_ms(fails) ? 1 : 0;
|
||
}
|