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

+117 -9
View File
@@ -10,14 +10,72 @@
#include <stdio.h>
#include <strings.h>
/* 是否 a2dpsrc / a2dp_source 端点行(bluealsa 4.1.1 list-pcms 的端点头) */
static int is_src_line(const char *p)
/* 是否 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 真机实测)。
* 行形如:`\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT`
* 返回 0=找到(17 字符大写冒号分隔),-1=无 ACL 连接/参数非法。 */
int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz)
{
if (!hcitool_con || !mac || macsz < 18) return -1;
mac[0] = 0;
const char *ln = hcitool_con;
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;
if (strstr(line, "ACL")) {
/* 行内找合法的 MAC token(6 组 2 位十六进制,冒号分隔) */
for (const char *p = line; *p; 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;
}
}
/* 必须是完整 token:前一个字符不是十六进制/冒号,后一个不是 */
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'))) {
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;
return 0;
}
}
}
if (!nl) break;
ln = nl + 1;
}
return -1;
}
/* 从 D-Bus 对象路径取 MAC。bluealsa 4.1.1 的 PCM 路径是
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source
* /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)
@@ -74,10 +132,10 @@ static int parse_tail_mac(const char *hit, char *mac, size_t macsz)
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
* 真实输出是 D-Bus 对象路径(每行一个):
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source
* /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/sink" 与行 3 "a2dpsrc/source" 的经典误判)。 */
* "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;
@@ -92,7 +150,7 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
memcpy(line, ln, linelen); line[linelen] = 0;
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (is_src_line(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");
@@ -112,15 +170,65 @@ int bt_dev_count_src(const char *listpcms)
const char *ln = listpcms;
while (ln && *ln) {
const char *nl = strchr(ln, '\n');
const char *p = ln;
while (*p == ' ' || *p == '\t') p++;
if (is_src_line(p)) n++;
if (line_is_src(ln)) n++;
if (!nl) break;
ln = nl + 1;
}
return n;
}
/* 取第一个 a2dpsrc/a2dp_source 端点的**完整 D-Bus 路径**(`bluealsa-cli volume|info` 要的就是它,
* 不是 MAC)。行内按空白截断(含 \r,防 CRLF 把换行带进命令);越界/无端点 → -1。 */
int bt_dev_endpoint_path(const char *listpcms, char *out, size_t outsz)
{
if (!listpcms || !out || outsz < 2) return -1;
out[0] = 0;
const char *ln = listpcms;
while (ln && *ln) {
const char *nl = strchr(ln, '\n');
const char *p = ln;
while (*p == ' ' || *p == '\t') p++;
if (line_is_src(ln)) {
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;
}
/* 从 `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 {
+16 -2
View File
@@ -20,9 +20,14 @@
#include <stddef.h>
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
/* 从 `hcitool con` 输出取第一条 ACL 连接的对端 BD_ADDR(**MAC 主来源**:内核 ACL 是连接权威,
* list-pcms 只在 profile 配置后的秒级窗口内存在)。返回 0=找到,-1=无 ACL/参数非法。 */
int bt_dev_acl_mac(const char *hcitool_con, char *mac, size_t macsz);
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC
* (**兜底来源**:list-pcms 可见时用)。
* 真实行为 D-Bus 对象路径(bluealsa 4.1.1):
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink
* 返回 0=找到并写入 mac(17 字符、大写冒号分隔,NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。
* macsz ≥ 18。 */
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz);
@@ -31,6 +36,15 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz);
* 用于「先探测端点、再决定是否打开」——无端点时省掉一次注定失败的 open。 */
int bt_dev_count_src(const char *listpcms);
/* 取 list-pcms 里第一个 a2dpsrc/a2dp_source 端点的完整 D-Bus 路径
* (/org/bluealsa/hci0/dev_XX_../a2dpsrc/sink)——`bluealsa-cli volume|info` 要的是它,不是 MAC。
* 返回 0=写好(out NUL 结尾),-1=无端点/参数非法。outsz 建议 ≥96。 */
int bt_dev_endpoint_path(const char *listpcms, char *out, size_t outsz);
/* 从 `bluealsa-cli info` 输出取实际协商 codec:行首 "Selected codec: AAC" → "AAC"。
* 返回 0=找到,-1=无该行/参数非法。 */
int bt_dev_info_codec(const char *info_out, char *out, size_t outsz);
/* settings.conf `bt.codec` → bluealsa CODEC 参数 token。
* bluealsa 4.1.1 的 a2dp_codecs_codec_id_from_string() 用 strcasecmp 匹配 canonical 名与别名,
* 故大小写不敏感;本函数把它归一成 canonical 名(日志/STATE 用),未知值/auto/unchanged →
+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;
}
+101 -3
View File
@@ -1,8 +1,9 @@
/* test_bt_dev.c — 蓝牙端点选择 / PCM 名构造 / 回落退避 宿主验证(纯逻辑,bt_dev.c)。
*
* 输入全部取自**真机实测格式**(bluealsa v4.1.1,设备 /usr/bin/bluealsa 的
* `dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X` + src/ba-transport.c 的 `<dev>/a2dpsrc/source`):
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source
* 输入全部取自**真机实测原文**(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;
@@ -12,6 +13,9 @@
* 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
@@ -99,6 +103,11 @@ int main(void)
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,
@@ -159,6 +168,95 @@ int main(void)
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");