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 主路径未能在真机复验
(耳机不可达),详见报告。
This commit is contained in:
AskaEth committed 2026-11-11 19:37:03 +08:00
1 parent 33bef36d16
commit 6e7c0d00c0
4 files changed
+321 -57

No files matched your search

+87 -43
View File
@@ -223,7 +223,9 @@ static int g_bt_state = BT_ST_IDLE; /* BT_ST_*(见上) */
static unsigned g_bt_fail_n = 0; /* 连续失败次数(成功后归零) */
static long long g_bt_last_fail_ms = 0; /* 上次失败时刻(CLOCK_MONOTONIC ms) */
static char g_bt_mac[20] = ""; /* 已打开/最近探测到的 A2DP 端点 MAC */
static char g_bt_tok[16] = "unchanged"; /* 实际下发的 CODEC token(canonical) */
static char g_bt_tok[16] = "unchanged"; /* 下发的 CODEC token(canonical) */
static char g_bt_used[16] = ""; /* bluealsa info 回报的实际协商 codec(AAC/SBC…) */
static char g_bt_pcm[128] = ""; /* 端点 D-Bus 路径(volume/info 用) */
static int g_bt_fail_req = 0; /* 写路径判死:请求播放线程下一拍重配(回落 DAC) */
static int g_bt_dev = 0; /* 当前「设备」是 BT PCM(dev_close 分流;g_mu 保护) */
@@ -944,8 +946,9 @@ int snd_pcm_drain(void *pcm);
int snd_pcm_close(void *pcm);
const char *snd_strerror(int errnum);
static void bt_close(void); /* 前向:bt_write 判死路径要用 */
static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */
static void bt_close(void); /* 前向:bt_write 判死路径要用 */
static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */
static void bt_read_used_codec(void); /* 前向:bt_open 成功时读回实际协商 codec */
/* 单调时钟 ms(退避计时;CLOCK_MONOTONIC 不受系统时间跳变影响) */
static long long now_ms(void)
@@ -973,17 +976,39 @@ static void bt_note_ok(void)
g_bt_fail_n = 0;
}
/* 探测当前可用的 A2DP source 端点(bluealsa-cli list-pcms,1.5s 有界 popen)。
* 返回端点数量;mac 非空时写入第一个 a2dpsrc 端点的设备 MAC。
* 注意:bluealsa-cli v4.1.1 **没有** `a2dp list` 子命令,只有 list-pcms/list-services。 */
static int bt_probe(char *mac, size_t macsz)
/* 找「现在要往哪台设备播」的 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)
{
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;
}
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;
}
return 0;
}
/* PCM 已打开后刷新端点路径(此时 bluealsa 的端点对象是稳定的),供 info/volume 用。 */
static void bt_refresh_endpoint(void)
{
if (g_bt_pcm[0]) return;
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
if (!o) { logmsg("bt: probe skipped (no bluealsa-cli)"); return 0; }
int n = bt_dev_count_src(o);
if (n > 0 && mac && macsz && bt_dev_detect_mac(o, mac, macsz) != 0) mac[0] = 0;
return n;
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);
}
/* 按 PCM 名打开(libasound → NONBLOCK open + set_params S16/RW/ch/rate/soft_resample/100ms)。
@@ -1029,24 +1054,31 @@ static int bt_try_open(void)
return -1;
}
char mac[20] = "";
int eps = bt_probe(mac, sizeof mac);
if (eps <= 0 || !mac[0]) { bt_note_fail("no a2dp endpoint (device not connected?)"); return -1; }
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);
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 endpoint MAC"); return -1; }
logmsg("bt: endpoint %s (x%d) codec setting=%s -> token=%s", g_bt_mac, eps, g_bt_codec, g_bt_tok);
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,不会在播放线程里形成长时间阻塞)。 */
const char *opened = NULL;
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_ok();
g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 100% */
bt_vol_full();
return 0;
for (int i = 0; i < 3 && !opened; i++) {
if (i) {
struct timespec ts = { 0, 300 * 1000000L };
nanosleep(&ts, NULL);
}
if (bt_open_pcm(name) == 0) opened = name;
else if (bt_open_pcm(BT_PCM_BARE) == 0) opened = BT_PCM_BARE;
}
bt_note_fail("snd_pcm_open/set_params failed");
return -1;
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_read_used_codec();
g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 满量程 */
bt_vol_full();
return 0;
}
/* BT 写:非阻塞 writei + wait 超时(镜像 DAC O_NONBLOCK+poll 护栏),xrun recover。
@@ -1126,25 +1158,15 @@ static void bt_vol_full(void)
{
if (g_bt_vol_full_done) return;
g_bt_vol_full_done = 1;
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; }
char pcm[128] = "";
const char *ln = o;
while (ln && *ln) {
const char *nl = strchr(ln, '\n');
const char *p = ln;
while (*p == ' ' || *p == '\t') p++;
if (strstr(p, "a2dpsrc") || strstr(p, "a2dp_source")) {
const char *tok = p;
while (*tok && *tok != ' ' && *tok != '\t') tok++;
size_t tlen = (size_t)(tok - p);
if (tlen && tlen < sizeof pcm) { memcpy(pcm, p, tlen); pcm[tlen] = 0; }
break;
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;
}
if (!nl) break;
ln = nl + 1;
}
if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no a2dpsrc pcm found)"); 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);
@@ -1173,6 +1195,28 @@ static const char *bt_state_name(void)
}
}
/* 读回 bluealsa 实际协商到的 codec(`bluealsa-cli info <端点路径>` → "Selected codec:")。
* 为什么需要:耳机不一定支持 settings 里点名的 codec——真机 MOONDROP Pudding 只有 SBC/AAC,
* 我们请求 LDAC 时 bluealsa 接受参数但实际选中 AAC。STATE 报实际值(btacodec=)才不撒谎。
* 失败仅日志(best-effort,不影响播放)。 */
static void bt_read_used_codec(void)
{
g_bt_used[0] = 0;
if (!g_bt_pcm[0]) return;
char cmd[256];
snprintf(cmd, sizeof cmd, "%s info %s 2>/dev/null", BT_VOL_FULL_PCM, g_bt_pcm);
char *o = popen_read_tmo(1500, cmd);
if (!o) return;
char codec[16];
if (bt_dev_info_codec(o, codec, sizeof codec) == 0) {
snprintf(g_bt_used, sizeof g_bt_used, "%s", codec);
if (strcasecmp(codec, g_bt_tok) != 0)
logmsg("bt: codec requested=%s but bluealsa selected=%s (device codec set)", g_bt_tok, codec);
else
logmsg("bt: codec %s confirmed by bluealsa", codec);
}
}
/* 音量应用(含 BT 隔离):BT(真实走 A2DP)→ 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、
* 不持久化 audio.volume(那是 DAC 音量键);DAC 及「BT 回落 DAC」→ 原感知锥度反转 + 持久化
* (回落时声音实际从 DAC 出,音量必须由 DAC 控,否则面板拖动无感)。
@@ -1824,7 +1868,7 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
"active=%d app=%s sid=%u rate=%u ch=%u fmt=%s dsd=%d "
"written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d "
"route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u "
"target=%s bt=%s btdev=%s btcodec=%s codec=%s\n",
"target=%s bt=%s btdev=%s btcodec=%s btacodec=%s codec=%s\n",
g_st.active ? 1 : 0,
g_st.active ? g_st.app_id : "-",
(unsigned)g_st.sid, (unsigned)g_st.rate, (unsigned)g_st.ch,
@@ -1840,7 +1884,7 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
vol, volctl_name(),
g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq,
target_name(), bt_state_name(), g_bt_mac[0] ? g_bt_mac : "-",
bttok, g_bt_codec);
bttok, g_bt_used[0] ? g_bt_used : "-", g_bt_codec);
send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1);
break;
}