/* test_bt_ctl.c — meow_netmgrd 蓝牙控制面 纯逻辑宿主自测(本程序非生产代码)。 * * 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问 * FILE-STATIC 函数/全局:三个纯解析器(bt_scan_line_parse / bt_pcm_paths_parse+bt_pcm_info_parse / * bt_acl_parse)、run_cmd_ex 四态、bt_cmd_is_write / bt_write_authorize、bt_ctl_execute * (status/paired/connected/a2dp/devices/on/off/connect/disconnect/pair/unpair/scan)。 * 后端工具经有状态桩脚本(g_bluetoothctl/g_hcitool/g_bluealsa_cli/g_hciconfig/g_bt_scan_cache) * 覆盖,不依赖真实蓝牙栈。 * * 字节样本来源:2026-11-11 真机 `od -c /tmp/meow_bt_scan.txt`(0.8.49,BluetoothCtl 5.54 交互输出)。 * * 用法:cc -Wall -Wextra -I../meow_launcherd test_bt_ctl.c -o test_bt_ctl && ./test_bt_ctl */ #define main meow_netmgrd_prod_main_unused #include "meow_netmgrd.c" #undef main #include #include #include #include #include static int g_fail = 0; #define CHECK(cond) do { if (!(cond)) { printf(" FAIL: %s (line %d)\n", #cond, __LINE__); g_fail++; } \ else { printf(" ok : %s\n", #cond); } } while (0) static void wf(const char *p, const char *s) { FILE *f = fopen(p, "w"); if (f) { fputs(s, f); fclose(f); } } /* ---- 真机原始字节样本(含 ANSI ESC / \001\002 标记 / \r\033[K / 提示符) ---- */ static const char *S_NEW_PUDDING = "\033[0;94m[bluetooth]\033[0m# \r\033[K" "[\001\033[0;92m\002NEW\001\033[0m\002] Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n"; static const char *S_CHG_POWERED = "\033[0;94m[bluetooth]\033[0m# \r\033[K" "[\001\033[0;93m\002CHG\001\033[0m\002] Controller 4C:BC:98:B0:98:DB Powered: no\n"; static const char *S_DEL_LINE = "[DEL] Device 00:11:22:33:44:55\n"; static const char *S_PROMPT_DEVICES = "\033[0;94m[bluetooth]\033[0m# devices\n"; static const char *S_PLAIN_BLOCK = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n"; static const char *S_TRUNC_MAC = "\033[0;94m[bluetooth]\033[0m# \r\033[K[NEW] Device 33:DE:2E:21:A"; int main(void) { /* 宿主临时目录:Termux/Android 上 /tmp 不可写,优先 $TMPDIR(mktemp 同规则) */ const char *base = getenv("TMPDIR"); if (!base || !base[0]) base = "/tmp"; static char Tbuf[256]; snprintf(Tbuf, sizeof Tbuf, "%s/nmg_bt", base); const char *T = Tbuf; char p[256]; char cmd[512]; mkdir(Tbuf, 0755); /* 假 /sys/class/bluetooth/hci0(available/powered/address) */ { snprintf(p, sizeof p, "%s/bt", T); mkdir(p, 0755); snprintf(p, sizeof p, "%s/bt/hci0", T); mkdir(p, 0755); snprintf(p, sizeof p, "%s/bt/hci0/address", T); wf(p, "4c:bc:98:b0:98:db\n"); snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "off\n"); } /* 假 rfkill(bt on/off 会写 soft) */ { snprintf(p, sizeof p, "%s/rfkill", T); mkdir(p, 0755); snprintf(p, sizeof p, "%s/rfkill/rfkill1", T); mkdir(p, 0755); snprintf(p, sizeof p, "%s/rfkill/rfkill1/type", T); wf(p, "bluetooth\n"); snprintf(p, sizeof p, "%s/rfkill/rfkill1/soft", T); wf(p, "1\n"); } /* 有状态桩的状态目录 */ snprintf(p, sizeof p, "%s/state", T); mkdir(p, 0755); /* 桩:bluetoothctl(paired-devices/devices/info/connect/disconnect/pair/remove,靠状态文件变迁) * 注意:生产代码给后端命令前置 PATH=/bin:/sbin:/usr/bin:/usr/sbin,Termux 上 cat/basename/grep * 都不在这几个目录里 —— 桩脚本只能用 shell 内建(read/echo/[ /参数展开),否则测试会假失败。 */ snprintf(p, sizeof p, "%s/bluetoothctl", T); { char sh[3200]; snprintf(sh, sizeof sh, "#!/bin/sh\n" "S='%s/state'\n" "catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\n" "case \"$1\" in\n" " paired-devices) catf \"$S/paired_list.txt\"; exit 0;;\n" " devices) catf \"$S/dev_list.txt\"; exit 0;;\n" " info)\n" " mac=\"$2\"\n" " if [ ! -f \"$S/paired_$mac\" ]; then echo \"Device $mac not available\"; exit 1; fi\n" " pv=no; IFS= read -r pv < \"$S/paired_$mac\"\n" " if [ \"$pv\" = gone ]; then echo \"Device $mac not available\"; exit 1; fi\n" " cv=no\n" " if [ -f \"$S/conn_$mac\" ]; then IFS= read -r cv < \"$S/conn_$mac\"; fi\n" " echo \"Device $mac\"\n" " echo \" Name: Pudding\"\n" " echo \" Paired: $pv\"\n" " echo \" Connected: $cv\"\n" " exit 0;;\n" " connect)\n" " case \"$2\" in AA:*) echo 'Failed to connect: org.bluez.Error.Failed br-connection-refused'; exit 1;; esac\n" " echo yes > \"$S/conn_$2\"; echo yes > \"$S/paired_$2\"\n" " echo 'Attempting to connect'; echo 'Connection successful'; exit 0;;\n" " disconnect)\n" " case \"$2\" in AA:*) echo 'Failed to disconnect: not connected'; exit 1;; esac\n" " case \"$2\" in BB:*) echo 'Successful disconnected'; exit 0;; esac\n" " echo no > \"$S/conn_$2\"; echo 'Successful disconnected'; exit 0;;\n" " pair)\n" " case \"$2\" in AA:*) echo 'Failed to pair: org.bluez.Error.AuthenticationFailed'; exit 1;; esac\n" " echo yes > \"$S/paired_$2\"; echo 'Pairing successful'; exit 0;;\n" " remove)\n" " echo gone > \"$S/paired_$2\"; echo no > \"$S/conn_$2\"; echo 'Device has been removed'; exit 0;;\n" "esac\n" "exit 0\n", T); wf(p, sh); chmod(p, 0755); } /* 桩:hcitool con —— 按 conn_=yes 标记生成 ACL 行 */ snprintf(p, sizeof p, "%s/hcitool", T); { char sh[900]; snprintf(sh, sizeof sh, "#!/bin/sh\n" "S='%s/state'\n" "echo 'Connections:'\n" "for f in \"$S\"/conn_*; do\n" " [ -f \"$f\" ] || continue\n" " v=no; IFS= read -r v < \"$f\"\n" " [ \"$v\" = yes ] || continue\n" " m=\"${f##*/}\"; m=\"${m#conn_}\"\n" " echo \"\t< ACL $m handle 12 state 1 lm MASTER\"\n" "done\n" "exit 0\n", T); wf(p, sh); chmod(p, 0755); } /* 桩:hciconfig —— `hci0 up/down` 改 sysfs power,`hci0` 打印状态 */ snprintf(p, sizeof p, "%s/hciconfig", T); { char sh[900]; snprintf(sh, sizeof sh, "#!/bin/sh\n" "S='%s'\n" "case \"$2\" in\n" " up) echo on > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' UP RUNNING';;\n" " down) echo off > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' DOWN';;\n" " *) echo 'hci0: Type: Primary Bus: UART'\n" " v=off; if [ -f \"$S/bt/hci0/power\" ]; then IFS= read -r v < \"$S/bt/hci0/power\"; fi\n" " if [ \"$v\" = on ]; then echo ' UP RUNNING'; else echo ' DOWN'; fi;;\n" "esac\n" "exit 0\n", T); wf(p, sh); chmod(p, 0755); } /* 桩:bluealsa-cli —— list-pcms 端点由 $S/pcms.txt 控制,info 按路径返回属性 */ snprintf(p, sizeof p, "%s/bluealsa-cli", T); { char sh[3200]; snprintf(sh, sizeof sh, "#!/bin/sh\n" "S='%s/state'\n" "catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\n" "case \"$1\" in\n" " list-pcms) catf \"$S/pcms.txt\"; exit 0;;\n" " info)\n" " path=\"$2\"; found=0\n" " if [ -f \"$S/pcms.txt\" ]; then\n" " while IFS= read -r l || [ -n \"$l\" ]; do\n" " if [ \"$l\" = \"$path\" ]; then found=1; fi\n" " done < \"$S/pcms.txt\"\n" " fi\n" " case \"$path\" in *33_DE_2E_21_AD_30*) ;; *) found=0;; esac\n" " if [ \"$found\" != 1 ]; then\n" " echo \"bluealsa-cli: E: CMD \\\"info\\\": Couldn't get BlueALSA PCM: Object path not found: '$path'\" >&2\n" " exit 1\n" " fi\n" " echo \"BlueALSA PCM: $path\"\n" " echo ' Transport: A2DP-source'\n" " echo ' Codec: SBC'\n" " echo ' CodecConfiguration: 0x3f'\n" " echo ' Sampling: 44100 Hz'\n" " echo ' Channels: 2'\n" " echo ' Bits per Sample: 16'\n" " echo ' Running: true'\n" " echo ' State: RUNNING'\n" " exit 0;;\n" "esac\n" "exit 0\n", T); wf(p, sh); chmod(p, 0755); } /* 初始状态:两台已配对(真机 MOONDROP / 17OP15),无 ACL */ snprintf(p, sizeof p, "%s/state/paired_list.txt", T); wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n"); snprintf(p, sizeof p, "%s/state/dev_list.txt", T); wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n" "Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n"); snprintf(p, sizeof p, "%s/state/paired_33:DE:2E:21:AD:30", T); wf(p, "yes\n"); snprintf(p, sizeof p, "%s/state/paired_E0:42:6D:79:C5:98", T); wf(p, "yes\n"); /* 指向桩(g_* 是 const char*,用静态缓冲承载) */ static char sb_bt[256], sb_hci[256], sb_ba[256], sb_hc[256], sb_cache[256]; snprintf(sb_bt, sizeof sb_bt, "%s/bluetoothctl", T); snprintf(sb_hci, sizeof sb_hci, "%s/hcitool", T); snprintf(sb_ba, sizeof sb_ba, "%s/bluealsa-cli", T); snprintf(sb_hc, sizeof sb_hc, "%s/hciconfig", T); snprintf(sb_cache, sizeof sb_cache, "%s/bt_scan.txt", T); g_bluetoothctl = sb_bt; g_hcitool = sb_hci; g_bluealsa_cli = sb_ba; g_hciconfig = sb_hc; g_bt_scan_cache = sb_cache; g_bt_dir[0] = 0; snprintf(g_bt_dir, sizeof g_bt_dir, "%s/bt", T); g_rfkill_dir[0] = 0; snprintf(g_rfkill_dir, sizeof g_rfkill_dir, "%s/rfkill", T); ctl_result_t r; /* ================= 1. run_cmd_ex 四态(M49 新增) ================= */ printf("== run_cmd_ex 四态:成功有输出/成功无输出/非 0 退出/超时/命令不存在/stderr 抓取 ==\n"); { runcmd_res_t rr; char b[128]; int n = run_cmd_ex(b, sizeof b, 1500, "echo hi", &rr); CHECK(n == 3); CHECK(rr.status == RUNCMD_OK); CHECK(rr.exit_code == 0); n = run_cmd_ex(b, sizeof b, 1500, "true", &rr); CHECK(n == 0); CHECK(rr.status == RUNCMD_EMPTY); /* 成功但无输出 ≠ 失败 */ n = run_cmd_ex(b, sizeof b, 1500, "exit 3", &rr); CHECK(n == 0); CHECK(rr.status == RUNCMD_EXIT); CHECK(rr.exit_code == 3); n = run_cmd_ex(b, sizeof b, 1500, "nosuchcmd_m49_xyz", &rr); CHECK(rr.status == RUNCMD_EXIT); CHECK(rr.exit_code == 127); /* 命令不存在(sh 报 127) */ n = run_cmd_ex(b, sizeof b, 150, "sleep 5", &rr); CHECK(rr.status == RUNCMD_TIMEOUT); /* 限时内不退出 */ CHECK(n >= 0); n = run_cmd_ex(b, sizeof b, 1500, "echo boom >&2; exit 4", &rr); CHECK(rr.status == RUNCMD_EXIT); CHECK(rr.exit_code == 4); CHECK(strstr(rr.err, "boom") != NULL); /* stderr 不再丢 /dev/null */ int isfail = 0; const char *why = run_cmd_why(&rr, &isfail); CHECK(isfail == 1); CHECK(strstr(why, "exit 4") != NULL); CHECK(strstr(why, "boom") != NULL); CHECK(run_cmd(b, sizeof b, 1500, "echo ok") == 3); /* 旧外壳契约不变 */ } /* ================= 2. 扫描缓存解析(真机字节) ================= */ printf("== bt_clean_ansi / bt_scan_line_parse(真机 od 字节样本) ==\n"); { char clean[512], mac[24], name[64]; bt_clean_ansi(S_NEW_PUDDING, clean, sizeof clean); CHECK(strstr(clean, "\033") == NULL); CHECK(strstr(clean, "\001") == NULL && strstr(clean, "\002") == NULL); CHECK(strstr(clean, "\r") == NULL); CHECK(strcmp(clean, "[bluetooth]# [NEW] Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63") == 0); CHECK(bt_scan_line_parse(S_NEW_PUDDING, mac, sizeof mac, name, sizeof name) == 1); CHECK(strcmp(mac, "79:85:D6:7B:73:63") == 0); CHECK(strcmp(name, "79-85-D6-7B-73-63") == 0); CHECK(bt_scan_line_parse(S_PLAIN_BLOCK, mac, sizeof mac, name, sizeof name) == 1); CHECK(strcmp(mac, "33:DE:2E:21:AD:30") == 0); CHECK(strcmp(name, "MOONDROP Pudding") == 0); /* [CHG]/[DEL] 不是「附近有这台」 */ CHECK(bt_scan_line_parse(S_CHG_POWERED, mac, sizeof mac, name, sizeof name) == 0); CHECK(bt_scan_line_parse(S_DEL_LINE, mac, sizeof mac, name, sizeof name) == 0); /* 提示符回显行 / 空行 / 截断到半截 MAC / 无关行 */ CHECK(bt_scan_line_parse(S_PROMPT_DEVICES, mac, sizeof mac, name, sizeof name) == 0); CHECK(bt_scan_line_parse("", mac, sizeof mac, name, sizeof name) == 0); CHECK(bt_scan_line_parse(S_TRUNC_MAC, mac, sizeof mac, name, sizeof name) == 0); CHECK(bt_scan_line_parse("Waiting to connect to bluetoothd...\r\n", mac, sizeof mac, name, sizeof name) == 0); /* MAC 后无名字(截断输出)也要给出设备 */ CHECK(bt_scan_line_parse("[NEW] Device AA:BB:CC:DD:EE:FF\n", mac, sizeof mac, name, sizeof name) == 1); CHECK(strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0); CHECK(name[0] == 0); } /* ================= 3. hcitool con 解析 ================= */ printf("== bt_acl_parse(hcitool con 原始输出/空/截断) ==\n"); { char macs[8][24]; CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0); CHECK(bt_acl_parse("", macs, 8) == 0); int n = bt_acl_parse("Connections:\n" "\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER\n" "\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE\n", macs, 8); CHECK(n == 2); CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0); CHECK(strcmp(macs[1], "11:22:33:44:55:66") == 0); /* SCO 行不是 ACL;截断 MAC 丢掉 */ n = bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 12 state 1\n" "\t< ACL 33:DE:2E:21:A\n", macs, 8); CHECK(n == 0); } /* ================= 4. list-pcms / info 解析 ================= */ printf("== bt_pcm_paths_parse / bt_path_mac / bt_pcm_info_parse ==\n"); { char paths[4][80]; int n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n", paths, 4); CHECK(n == 1); CHECK(strcmp(paths[0], "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc") == 0); n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n" "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk\r\n" "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/sco\n", paths, 4); CHECK(n == 2); /* SCO/HFP 端点不算 A2DP */ CHECK(strcmp(paths[1], "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk") == 0); CHECK(bt_pcm_paths_parse("", paths, 4) == 0); /* 真机 bluealsa v4.x 端点路径带末端段,且**结尾字面量不可依赖**: * 塔 2026-11-11 首连实测是 `/a2dpsrc/sink`(不是 /source),M50 的旧 bt_dev_detect_mac() * 按「最后一个 / 之后」取 token 就会拿到 "sink" 而不是 MAC。此处两种结尾都验。 */ n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n" "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk/source\n", paths, 4); CHECK(n == 2); CHECK(strcmp(paths[0], "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0); char mac2[24]; CHECK(bt_path_mac(paths[0], mac2, sizeof mac2) == 1); CHECK(strcmp(mac2, "33:DE:2E:21:AD:30") == 0); /* 不是 "sink" */ CHECK(bt_path_mac(paths[1], mac2, sizeof mac2) == 1); CHECK(strcmp(mac2, "E0:42:6D:79:C5:98") == 0); char mac3[24]; /* 无末端段的老形态也兼容 */ CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc", mac3, sizeof mac3) == 1); CHECK(strcmp(mac3, "33:DE:2E:21:AD:30") == 0); /* 截断输出(半截路径)也不许崩、不许当成端点 */ bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_D", paths, 4); char mac[24]; CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc", mac, sizeof mac) == 1); CHECK(strcmp(mac, "33:DE:2E:21:AD:30") == 0); CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_3", mac, sizeof mac) == 0); CHECK(bt_path_mac("/org/bluealsa/hci0/nothing", mac, sizeof mac) == 0); bt_pcm_info_t info; bt_pcm_info_parse("BlueALSA PCM: /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n" " Transport: A2DP-source\n" " Codec: SBC\n" " CodecConfiguration: 0x3f\n" " Sampling: 44100 Hz\n" " Channels: 2\n" " Bits per Sample: 16\n" " Running: true\n" " State: RUNNING\n", &info); CHECK(info.has == 1); CHECK(strcmp(info.codec, "SBC") == 0); /* 不被 CodecConfiguration 抢走 */ CHECK(strcmp(info.rate, "44100 Hz") == 0); CHECK(strcmp(info.channels, "2") == 0); CHECK(strcmp(info.bits, "16") == 0); CHECK(info.running == 1); /* 只有 Format 一行(属性打包)时的兜底 */ bt_pcm_info_parse(" Format: 48000 Hz, 2 channels, 24 bits\n", &info); CHECK(info.has == 1); CHECK(strcmp(info.rate, "48000") == 0); CHECK(strcmp(info.channels, "2") == 0); CHECK(strcmp(info.bits, "24") == 0); CHECK(info.running == -1); /* 空输出 / 错误输出(endpoint 不存在)→ has=0,不许编造 */ bt_pcm_info_parse("", &info); CHECK(info.has == 0); bt_pcm_info_parse("bluealsa-cli: E: CMD \"info\": Couldn't get BlueALSA PCM: " "Object path not found: '/org/bluealsa/hci0/dev_00/a2dpsrc'\n", &info); CHECK(info.has == 0); } /* ================= 5. 读写分类与授权 ================= */ printf("== bt_cmd_is_write 分类(读/写级) ==\n"); CHECK(bt_cmd_is_write("status") == 0); CHECK(bt_cmd_is_write("paired") == 0); CHECK(bt_cmd_is_write("connected") == 0); CHECK(bt_cmd_is_write("a2dp") == 0); CHECK(bt_cmd_is_write("devices") == 0); CHECK(bt_cmd_is_write("on") == 1); CHECK(bt_cmd_is_write("off") == 1); CHECK(bt_cmd_is_write("connect") == 1); CHECK(bt_cmd_is_write("disconnect") == 1); CHECK(bt_cmd_is_write("pair") == 1); CHECK(bt_cmd_is_write("unpair") == 1); CHECK(bt_cmd_is_write("scan") == 1); /* ================= 6. 读级(status/paired/connected/a2dp/devices) ================= */ printf("== 读级:status/paired/connected/a2dp/devices ==\n"); bt_ctl_execute("status", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "paired=2") != NULL); /* paired-devices 计数 */ CHECK(strstr(r.text, "connected=0") != NULL); /* 内核 ACL 权威:当前无连接 */ bt_ctl_execute("paired", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL); CHECK(strstr(r.text, "Device E0:42:6D:79:C5:98 17OP15") != NULL); /* 没有已配对设备 = 空列表(不是错误) */ snprintf(p, sizeof p, "%s/state/paired_list.txt", T); wf(p, ""); bt_ctl_execute("paired", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(r.text[0] == 0); snprintf(p, sizeof p, "%s/state/paired_list.txt", T); wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n"); /* a2dp:list-pcms → info,无端点时 "A2DP none" */ snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n"); bt_ctl_execute("a2dp", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "A2DP 33:DE:2E:21:AD:30") != NULL); CHECK(strstr(r.text, "codec=SBC") != NULL); CHECK(strstr(r.text, "rate=44100 Hz") != NULL); CHECK(strstr(r.text, "channels=2") != NULL); CHECK(strstr(r.text, "bits=16") != NULL); CHECK(strstr(r.text, "acquired=1") != NULL); CHECK(strstr(r.text, "role=src") != NULL); /* 列表里有端点但 info 查不到(刚断开)→ 跳过,不编造 */ wf(p, "/org/bluealsa/hci0/dev_00_00_00_00_00_00/a2dpsrc\n"); bt_ctl_execute("a2dp", NULL, &r); CHECK(strcmp(r.text, "A2DP none\n") == 0); wf(p, ""); bt_ctl_execute("a2dp", NULL, &r); CHECK(strcmp(r.text, "A2DP none\n") == 0); printf("== 扫描缓存 → bt devices(真机 ANSI 字节 + devices 尾块 + 去重) ==\n"); { char cache[4096]; /* 结构与真机 /tmp/meow_bt_scan.txt 一致:开头等待行 + ESC[P + 提示符,实时 NEW 行, * 末尾 devices 块,最后一行被截断(没有换行)。 */ snprintf(cache, sizeof cache, "Waiting to connect to bluetoothd...\r\033[P\033[P\033[P" "\033[0;94m[bluetooth]\033[0m# \033[K\r\033[KAgent registered\n" "\033[0;94m[bluetooth]\033[0m# scan on\n" "%s" "%s" "\033[0;94m[bluetooth]\033[0m# scan off\n" "\033[0;94m[bluetooth]\033[0m# devices\n" "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n" "Device E0:42:6D:79:C5:98 17OP15\n" "Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n" "\033[0;94m[bluetooth]\033[0m# quit\n" "\033[0;94m[bluetooth]\033[0m# \r\033[K" "[\001\033[0;92m\002NEW\001\033[0m\002] Device AA:BB:CC:DD:EE:FF Earbuds", S_NEW_PUDDING, S_CHG_POWERED); snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p, cache); bt_ctl_execute("devices", NULL, &r); CHECK(r.code == MEOW_OK); /* 实时 NEW 行(ANSI 开头)现在能解析出来 */ CHECK(strstr(r.text, "Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63") != NULL); CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL); CHECK(strstr(r.text, "Device E0:42:6D:79:C5:98 17OP15") != NULL); /* 截断的最后一行(无换行)也要收;[CHG] 行不许混进来 */ CHECK(strstr(r.text, "Device AA:BB:CC:DD:EE:FF Earbuds") != NULL); CHECK(strstr(r.text, "Controller 4C:BC:98") == NULL); CHECK(strstr(r.text, "Powered") == NULL); /* 去重:79:.. 实时行与 devices 块各一次,输出只一行 */ { int cnt = 0; const char *q = r.text; while ((q = strstr(q, "79:85:D6:7B:73:63")) != NULL) { cnt++; q += 17; } CHECK(cnt == 1); } /* 缓存不存在 → 空文本 */ unlink(p); bt_ctl_execute("devices", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(r.text[0] == 0); } /* ================= 7. 写级:on/off + connect/disconnect/pair/unpair ================= */ printf("== bt on/off:按 power 真态定稿 ==\n"); bt_ctl_execute("on", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "已复查") != NULL); CHECK(bt_powered_now() == 1); bt_ctl_execute("off", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(bt_powered_now() == 0); printf("== bt connect:成功路径以 hcitool con 复查 ==\n"); bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "已连接") != NULL); CHECK(strstr(r.text, "hcitool con") != NULL); CHECK(bt_acl_has("33:DE:2E:21:AD:30") == 1); printf("== bt disconnect:成功路径以 hcitool con 复查 ==\n"); bt_ctl_execute("disconnect", "33:DE:2E:21:AD:30", &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "已断开") != NULL); CHECK(bt_acl_has("33:DE:2E:21:AD:30") == 0); printf("== bt connect 失败:bluetoothctl 报错 + 无 ACL → ERR_NODEV(旧实现恒报成功) ==\n"); bt_ctl_execute("connect", "AA:AA:AA:AA:AA:AA", &r); CHECK(r.code == MEOW_ERR_NODEV); CHECK(strstr(r.text, "未生效") != NULL); CHECK(strstr(r.text, "br-connection-refused") != NULL); /* 失败原因来自 stderr/stdout 片段 */ printf("== bt disconnect 假成功:命令 exit 0 但 ACL 仍在 → 仍报 ERR_NODEV ==\n"); /* BB:* 的 disconnect 桩打印成功但不删 ACL(模拟「命令说成功、链路其实还在」)。 */ snprintf(p, sizeof p, "%s/state/conn_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n"); snprintf(p, sizeof p, "%s/state/paired_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n"); bt_ctl_execute("disconnect", "BB:BB:BB:BB:BB:BB", &r); CHECK(r.code == MEOW_ERR_NODEV); CHECK(strstr(r.text, "仍有该 ACL") != NULL); bt_ctl_execute("disconnect", "AA:AA:AA:AA:AA:AA", &r); /* 本来就没 ACL:真态=已断开 */ CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "已断开") != NULL); bt_ctl_execute("connect", "11:22:33:44:55:66", &r); CHECK(r.code == MEOW_OK); printf("== bt pair/unpair:以 bluetoothctl info Paired: 复查 ==\n"); bt_ctl_execute("pair", "11:22:33:44:55:66", &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "已配对") != NULL); CHECK(bt_info_flag("11:22:33:44:55:66", "Paired") == 1); bt_ctl_execute("unpair", "11:22:33:44:55:66", &r); /* remove → info 查不到也算解除 */ CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "已解除配对") != NULL); CHECK(bt_info_flag("11:22:33:44:55:66", "Paired") == -1); bt_ctl_execute("pair", "AA:AA:AA:AA:AA:AA", &r); CHECK(r.code == MEOW_ERR_NODEV); CHECK(strstr(r.text, "未生效") != NULL); bt_ctl_execute("connect", "", &r); CHECK(r.code == MEOW_ERR_INVAL); /* 缺 mac → INVAL */ printf("== bt scan on/off(写级;桩 bluetoothctl 现在被 scan 真正调用) ==\n"); bt_ctl_execute("scan", NULL, &r); CHECK(r.code == MEOW_OK); CHECK(strstr(r.text, "scan on") != NULL); bt_ctl_execute("scan", "off", &r); CHECK(r.code == MEOW_OK); printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n"); caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER; caller_t sys; memset(&sys, 0, sizeof sys); sys.kind = CALL_SYS; snprintf(sys.pkg, sizeof sys.pkg, "meow_net"); CHECK(bt_write_authorize(&other, 1, "connect") == MEOW_ERR_PERM); /* 非系统 → 拒 */ CHECK(bt_write_authorize(&sys, 1, "connect") == MEOW_OK); /* 系统+meow → 放 */ CHECK(bt_write_authorize(&sys, 0, "connect") == MEOW_ERR_PERM); /* 系统+hiby → 拒 */ CHECK(bt_write_authorize(&other, 1, "status") == MEOW_OK); /* 读级任意 → 放 */ (void)cmd; if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; } printf("\nBT_CTL TEST: ALL PASS\n"); return 0; }