Files
MeowOS-For-R3P2/patch_v2/meow_audiod/bt_dev.c
T
AskaEth 6e7c0d00c0 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 主路径未能在真机复验
(耳机不可达),详见报告。
2026-11-11 19:37:03 +08:00

312 lines
12 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.
/* 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 真机实测)。
* 行形如:`\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/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;
}
int bt_dev_count_src(const char *listpcms)
{
if (!listpcms) return 0;
int n = 0;
const char *ln = listpcms;
while (ln && *ln) {
const char *nl = strchr(ln, '\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 {
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;
}