/* test_bt_dev.c — 蓝牙端点选择 / PCM 名构造 / 回落退避 宿主验证(纯逻辑,bt_dev.c)。 * * 输入全部取自**真机实测原文**(bluealsa v4.1.1;设备 /usr/bin/bluealsa 的 * `dev_%.2X_..` + src/ba-transport.c 的 `//`): * /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; * hfphf/a2dpsnk 不算;简化 `a2dpsrc/` 兼容;空/无源端点/NULL/坏 dev_ 段 → -1。 * D3 bt_dev_codec_token:settings bt.codec → bluealsa canonical(4.1.1 别名表 strcasecmp); * auto/空/unchanged/垃圾 → unchanged。 * D4 bt_dev_pcm_name:内联 DEV/PROFILE/CODEC;坏 MAC/小缓冲 → -1。 * D5 bt_dev_backoff_ms / should_try:2s→60s 封顶 + 门控(防重试风暴)。 * D7 bt_dev_info_codec:真机 `bluealsa-cli info` 原文 → Selected codec(不能误吃 Available 行)。 * D8 bt_dev_acl_macs:真机 `hcitool con` 原文 → **全部** ACL 对端 MAC(多设备一等公民: * 只取第一条会在手机/手柄同时连着时挑错设备,导致有耳机却回落 DAC)。 * D9 bt_dev_pick_endpoint:按 MAC 精确挑端点(换耳机 A→B 后不能拿 A 的路径发 volume/info)。 * D10 bt_dev_close_kind:关闭决策(BT 分支 / 真 fd / **0-2 一律不动**=护 stdin,P2-1 回归)。 * * 编译(在 patch_v2/meow_audiod 下): * cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c * 运行:./test_bt_dev */ #include "bt_dev.h" #include #include #include static int g_pass = 0, g_fail = 0; static void check(int cond, const char *name) { if (cond) { printf(" PASS %s\n", name); g_pass++; } else { printf(" FAIL %s\n", name); g_fail++; } } int main(void) { setbuf(stdout, NULL); /* ==== D1 设备列表 → 选中 a2dpsrc 连接设备(真实 D-Bus 路径格式) ==== */ { char mac[32] = ""; int rc = bt_dev_detect_mac( "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac); check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1a real D-Bus path -> MAC (dev_ underscores -> colons)"); } { char mac[32] = ""; int rc = bt_dev_detect_mac( "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n" "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsnk/sink\n" "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac); check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1b hfphf/a2dpsnk skipped -> first a2dpsrc"); } { char mac[32] = ""; int rc = bt_dev_detect_mac( "/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", mac, sizeof mac); check(rc == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, "D1c multi-device -> first a2dpsrc"); } { char mac[32] = ""; int rc = bt_dev_detect_mac("a2dpsrc/E0:42:6D:79:C5:98\n", mac, sizeof mac); check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1d legacy a2dpsrc/ still parsed"); } { char mac[32] = ""; int rc = bt_dev_detect_mac("", mac, sizeof mac); check(rc == -1, "D1e empty output -> -1"); } { char mac[32] = ""; int rc = bt_dev_detect_mac( "/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsnk/sink\n", mac, sizeof mac); check(rc == -1, "D1f only sink (no a2dpsrc) -> -1"); } { char mac[32] = ""; int rc = bt_dev_detect_mac(NULL, mac, sizeof mac); check(rc == -1, "D1g NULL input -> -1"); } { char mac[32] = ""; int rc = bt_dev_detect_mac( "/org/bluealsa/hci0/dev_ZZ_42_6D_79_C5_98/a2dpsrc/source\n", mac, sizeof mac); check(rc == -1 && mac[0] == 0, "D1h malformed dev_ seg -> -1 (no garbage MAC)"); } { char mac[32] = ""; int rc = bt_dev_detect_mac( "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source 1\n", mac, sizeof mac); check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1i trailing token tolerated"); } /* ==== D3 settings bt.codec → bluealsa canonical token ==== */ { struct { const char *in; const char *want; const char *name; } t[] = { { "LDAC", "LDAC", "D3a LDAC passthrough" }, { "ldac", "LDAC", "D3b lowercase ldac -> canonical" }, { "SBC", "SBC", "D3c SBC" }, { "AAC", "AAC", "D3d AAC" }, { "MPEG24", "AAC", "D3e MPEG24 alias -> AAC" }, { "aptX", "aptX", "D3f aptX" }, { "apt-X", "aptX", "D3g apt-X alias -> aptX" }, { "aptx-hd", "aptX-HD", "D3h aptx-hd -> aptX-HD" }, { "MPEG", "MP3", "D3i MPEG alias -> MP3" }, { "MP3", "MP3", "D3j MP3" }, { "FastStream", "FastStream", "D3k FastStream" }, { "FS", "FastStream", "D3l FS alias -> FastStream" }, { "auto", "unchanged", "D3m auto -> unchanged (do not touch negotiation)" }, { "", "unchanged", "D3n empty -> unchanged" }, { "unchanged", "unchanged", "D3o unchanged -> unchanged" }, { "BOGUS", "unchanged", "D3p unknown -> unchanged (never inject junk)" }, }; for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) { char tok[16] = ""; const char *r = bt_dev_codec_token(t[i].in, tok, sizeof tok); check(r == tok && strcmp(tok, t[i].want) == 0, t[i].name); } char tok[16] = ""; check(bt_dev_codec_token(NULL, tok, sizeof tok) == tok && !strcmp(tok, "unchanged"), "D3q NULL setting -> unchanged"); } /* ==== D4 PCM 名构造(20-bluealsa.conf @args 只认 DEV/PROFILE/CODEC 这套名字) ==== */ { char n[96] = ""; int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, sizeof n); check(rc == 0 && strcmp(n, "bluealsa:DEV=33:DE:2E:21:AD:30,PROFILE=a2dp,CODEC=LDAC") == 0, "D4a name = bluealsa:DEV=,PROFILE=a2dp,CODEC="); } { char n[96] = ""; int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "", n, sizeof n); check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4b empty token -> CODEC=unchanged"); } { char n[96] = ""; int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", NULL, n, sizeof n); check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4c NULL token -> CODEC=unchanged"); } { char n[96] = ""; check(bt_dev_pcm_name("33:DE:2E:21:AD", "LDAC", n, sizeof n) == -1, "D4d short MAC -> -1"); check(bt_dev_pcm_name("GG:DE:2E:21:AD:30", "LDAC", n, sizeof n) == -1, "D4e non-hex MAC -> -1"); check(bt_dev_pcm_name(NULL, "LDAC", n, sizeof n) == -1, "D4f NULL MAC -> -1"); check(bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, 16) == -1, "D4g tiny buffer -> -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 m[4][18]; check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0, "D8a real hcitool con (state 1 AUTH ENCRYPT) -> 1 ACL MAC"); } { const char *con = "Connections:\n" "\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n"; char m[4][18]; check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0, "D8b half-open ACL (state 5) still gives MAC (kernel is authority)"); } { /* 多设备:第二副耳机 + 手机同时连着 —— 两条都要拿到,且顺序=输出顺序 */ const char *con = "Connections:\n" "\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n" "\t< ACL 5C:FB:7C:11:22:33 handle 5 state 1 lm SLAVE \n"; char m[4][18]; int n = bt_dev_acl_macs(con, m, 4); check(n == 2 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0 && strcmp(m[1], "5C:FB:7C:11:22:33") == 0, "D8c two ACLs -> both MACs in order (not just the first one)"); } { /* 三设备 + 上限裁剪 + 去重(同一设备多 profile 行重复出现) */ const char *con = "Connections:\n" "\t< ACL AA:BB:CC:DD:EE:01 handle 1 state 1 lm MASTER \n" "\t< ACL AA:BB:CC:DD:EE:01 handle 2 state 1 lm SLAVE \n" "\t< ACL AA:BB:CC:DD:EE:02 handle 3 state 1 lm MASTER \n" "\t< ACL AA:BB:CC:DD:EE:03 handle 4 state 1 lm MASTER \n"; char m[3][18]; int n = bt_dev_acl_macs(con, m, 3); check(n == 3 && strcmp(m[0], "AA:BB:CC:DD:EE:01") == 0 && strcmp(m[1], "AA:BB:CC:DD:EE:02") == 0 && strcmp(m[2], "AA:BB:CC:DD:EE:03") == 0, "D8d dedup + cap at max (3 distinct)"); } { char m[4][18]; check(bt_dev_acl_macs("Connections:\n", m, 4) == 0, "D8e no ACL -> 0"); check(bt_dev_acl_macs(NULL, m, 4) == 0, "D8f NULL -> 0"); check(bt_dev_acl_macs("\t< ACL aa:bb:cc:dd:ee:ff handle 3 state 1 lm SLAVE \n", m, 4) == 1 && strcmp(m[0], "AA:BB:CC:DD:EE:FF") == 0, "D8g lowercase -> uppercase canonical"); } /* ==== D9 按 MAC 精确挑端点(换耳机 A→B 不串台) ==== */ { const char *pcms = "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink\n" "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink\n"; char path[128] = ""; check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:02", path, sizeof path) == 0 && strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink") == 0, "D9a picks B's path when MAC=B (not the first endpoint)"); check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:01", path, sizeof path) == 0 && strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink") == 0, "D9b picks A's path when MAC=A"); check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:03", path, sizeof path) == -1 && path[0] == 0, "D9c unknown MAC -> -1 (never borrow another device's path)"); check(bt_dev_pick_endpoint("/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/hfphf/source\n", "AA:BB:CC:DD:EE:01", path, sizeof path) == -1, "D9d non-a2dpsrc endpoint -> -1"); check(bt_dev_pick_endpoint(NULL, "AA:BB:CC:DD:EE:01", path, sizeof path) == -1, "D9e NULL -> -1"); } /* ==== D10 关闭决策(P2-1 回归:0/1/2 永不当设备 fd 关) ==== */ check(bt_dev_close_kind(1, 0) == 1, "D10a bt_dev=1 + fd=0 -> close BT only (never fd 0)"); check(bt_dev_close_kind(1, 5) == 1, "D10b bt_dev=1 + fd=5 -> close BT only"); check(bt_dev_close_kind(0, 0) == 0, "D10c bt_dev=0 + fd=0 -> NONE (stdin guard, P2-1)"); check(bt_dev_close_kind(0, 1) == 0, "D10d bt_dev=0 + fd=1 -> NONE (stdout guard)"); check(bt_dev_close_kind(0, 2) == 0, "D10e bt_dev=0 + fd=2 -> NONE (stderr guard)"); check(bt_dev_close_kind(0, 3) == 2, "D10f bt_dev=0 + fd=3 -> close FD"); check(bt_dev_close_kind(0, -1) == 0, "D10g bt_dev=0 + fd=-1 -> NONE"); check(bt_dev_close_kind(0, -2) == 0, "D10h bt_dev=0 + fd=-2 (NULL sink) -> NONE"); /* ==== 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"); check(bt_dev_backoff_ms(2) == 4000, "D5c fails=2 -> 4s"); check(bt_dev_backoff_ms(3) == 8000, "D5d fails=3 -> 8s"); check(bt_dev_backoff_ms(4) == 16000, "D5e fails=4 -> 16s"); check(bt_dev_backoff_ms(5) == 32000, "D5f fails=5 -> 32s"); check(bt_dev_backoff_ms(6) == 60000, "D5g fails=6 -> 60s (capped)"); check(bt_dev_backoff_ms(99) == 60000, "D5h fails=99 -> 60s (capped)"); check(bt_dev_should_try(0, 0) == 1, "D5i never failed -> try now"); check(bt_dev_should_try(1, 1999) == 0, "D5j fail#1 @1999ms -> gated"); check(bt_dev_should_try(1, 2000) == 1, "D5k fail#1 @2000ms -> allowed"); check(bt_dev_should_try(3, 7999) == 0, "D5l fail#3 @7999ms -> gated"); check(bt_dev_should_try(3, 8000) == 1, "D5m fail#3 @8000ms -> allowed"); check(bt_dev_should_try(2, -1) == 1, "D5n clock anomaly (negative) -> allowed (no deadlock)"); printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS (%d checks)\n", g_fail ? g_fail : g_pass); return g_fail ? 1 : 0; }