/* 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 #include #include #include /* 是否 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 ` 输出取实际协商到的 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; }