Files
MeowOS-For-R3P2/patch_v2/meow_netmgrd/test_bt_ctl.c
T
AskaEth 6e2774ff5c fix(bt): M54 入站 ACL(> ACL)解析 — 耳机自主回连后 connected=0 而声音正常
bt_acl_parse 原先只认行内 "< ACL ",而 hcitool con 对**远端发起**的连接打的是
"> ACL "。耳机自动回连(最常见的连接方式)时于是:bt connected 空、bt status
connected=0、bt connect 报「未生效(hcitool con 无该 ACL)」,而 audiod 用
strstr("ACL") 认得出、声音正常从耳机出 —— 用户看到的就是「看不出到底连没连」。

改法:白名单取代裸子串匹配 —— 行首(可带空白)→ '<' 或 '>' → ' ' → "ACL" → ' '
→ 17 字符 BD_ADDR(且必须在此断词:再跟 hex/':' 不算)。因此 SCO 行、表头、
截断行都不收。MAC 统一大写(与 audiod bt_dev_acl_macs / bluetoothctl 一致)。
单一解析器:bt_acl_macs / bt_acl_has_tri(写后真态复查)/ bt_connected_text /
bt_status.connected_n 全部经 bt_acl_parse,无第二套。

单测用真机原始字节(od -c 校对):
  入站 0.8.50 设备 10.58.215.50 2026-11-11 20:53 "\t> ACL 33:DE:2E:21:AD:30
  handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
  出站 devdocs/evidence/m50_bt_result_20261111.log:8 "\t< ACL ... handle 0
  state 5 lm MASTER \n"
覆盖 >/</>SCO/<SCO/表头/空/NULL/无方向符/截断(行中与行尾)/伪 MAC/大小写/CRLF/
max 裁剪,另加端到端:入站时 connected、status connected=1、connect 不报未生效、
SCO-only 不计连接、disconnect 假成功对入站 ACL 也报「仍有该 ACL」。
用旧解析器跑同一套测试 18 条 FAIL(证明用例不是自证)。

真机验证:同一时刻 0.8.50 旧二进制 bt status connected=0,而按新源码交叉编译
(mipsel 静态)的探针在设备上对**实时 > ACL** 链路报 connected=1 且
bt_connected_text 列出 "Device 33:DE:2E:21:AD:30 MOONDROP Pudding"。
2026-11-11 21:50:28 +08:00

802 lines
42 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.
/* 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
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";
/* ---- 真机 `hcitool con` 原始字节(od -c 逐字节校对,行首 TAB、行尾一个空格 + \n)----
* S_CON_INBOUND:0.8.50 设备 10.58.215.50,2026-11-11 20:53,**耳机自主回连**(M54 复现现场):
* od -c → "Connections:\n\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
* S_CON_OUTBOUND:devdocs/evidence/m50_bt_result_20261111.log:8(同机同耳机,本端发起) */
static const char *S_CON_INBOUND =
"Connections:\n"
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n";
static const char *S_CON_OUTBOUND =
"Connections:\n"
"\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n";
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];
/* 可重复运行(r1 评审 P2):上一轮遗留的 conn_ / paired_ 状态文件会把「无连接」断言推翻。
* 每次开跑先整棵删掉重来,退出时也清(见文件末尾)。 */
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", Tbuf); (void)!system(rm); }
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:真机有两个 bluetooth 节点(rfkill0/rfkill3),任一仍 block 就等于没开 */
{ snprintf(p, sizeof p, "%s/rfkill", T); mkdir(p, 0755);
for (int i = 0; i < 2; i++) {
char d[300];
snprintf(d, sizeof d, "%s/rfkill/rfkill%d", T, i ? 3 : 0); mkdir(d, 0755);
snprintf(p, sizeof p, "%s/type", d); wf(p, "bluetooth\n");
snprintf(p, sizeof p, "%s/soft", d); 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 CC:*) sleep 5;; esac\n"
" case \"$2\" in AA:*) echo 'Failed to connect: org.bluez.Error.Failed br-connection-refused'; exit 1;; esac\n"
/* 已入站连接时 connect 是幂等 no-op:真机上内核 ACL 仍是 `>` 行,别把方向改成 `<` */
" prev=; [ -f \"$S/conn_$2\" ] && IFS= read -r prev < \"$S/conn_$2\"\n"
" [ \"$prev\" = in ] || echo yes > \"$S/conn_$2\"\n"
" 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 CC:*) sleep 5;; esac\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_<mac> 文件内容选方向/链路类型(真机字节形态):
* in → "\t> ACL … "(远端发起,耳机自主回连,M54 真机 od -c 原文)
* sco → "\t< SCO … "(SCO 链路,不是 ACL)
* 其它非空值(yes)→ "\t< ACL … "(本端发起) */
snprintf(p, sizeof p, "%s/hcitool", T);
{
char sh[1200];
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"
" case \"$v\" in yes|in|sco) ;; *) continue;; esac\n"
" m=\"${f##*/}\"; m=\"${m#conn_}\"\n"
" case \"$v\" in\n"
" in) echo \"\t> ACL $m handle 11 state 1 lm MASTER AUTH ENCRYPT \";;\n"
" sco) echo \"\t< SCO $m handle 13 state 1 lm MASTER \";;\n"
" *) echo \"\t< ACL $m handle 12 state 1 lm MASTER \";;\n"
" esac\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"
"if [ -f \"$S/state/hang_hciconfig\" ]; then sleep 5; fi\n"
"blocked() { for t in \"$S\"/rfkill/*; do\n"
" [ -f \"$t/type\" ] || continue\n"
" v=\"\"; IFS= read -r v < \"$t/type\"; [ \"$v\" = bluetooth ] || continue\n"
" b=0; IFS= read -r b < \"$t/soft\"; [ \"$b\" = 1 ] && return 0\n"
" done; return 1; }\n"
"case \"$2\" in\n"
" up) if blocked; then echo 'hci0: Operation not possible due to RF-kill' >&2; exit 1; fi\n"
" 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"
" if [ -f \"$S/hang_info\" ]; then sleep 5; fi\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 *dev_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"
" case \"$path\" in *dev_33_DE_2E_21_AD_30*) ;; *) exit 1;; esac\n"
" if [ -f \"$S/running\" ]; then run=true; else run=false; fi\n"
" echo \"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\"\n" /* ← 真机原文(M50 2026-11-11 18:26) */
" echo 'Sequence: 0'\n"
" echo 'Transport: A2DP-source'\n"
" echo 'Mode: sink'\n"
" echo \"Running: $run\"\n"
" echo 'Format: S16_LE'\n"
" echo 'Channels: 2'\n"
" echo 'Sampling: 48000 Hz'\n"
" echo 'Available codecs: SBC AAC'\n"
" echo 'Selected codec: AAC'\n"
" echo 'Delay: 15.0 ms'\n"
" echo 'SoftVolume: false'\n"
" echo 'Volume: L: 127 R: 127'\n"
" echo 'Muted: L: false R: false'\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 真机字节:> / < / SCO / 表头 / 空 / 截断 / 大小写) ==\n");
{
char macs[8][24];
/* 入站(远端发起):M54 的 bug 现场 —— 只认 "< ACL" 时这里返回 0 */
CHECK(bt_acl_parse(S_CON_INBOUND, macs, 8) == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* 出站(本端发起):M49 起就该过,修入站时不许回归 */
CHECK(bt_acl_parse(S_CON_OUTBOUND, macs, 8) == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* 两副耳机混排(一副自主回连、一副本端发起)→ 按输出顺序全收 */
int n = bt_acl_parse("Connections:\n"
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
"\t< ACL 11:22:33:44:55:66 handle 12 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:两个方向都不收 */
CHECK(bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
CHECK(bt_acl_parse("\t> SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
/* 表头 / 空 / NULL / 无方向符(裸 "ACL" 不能被当连接) */
CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0);
CHECK(bt_acl_parse("", macs, 8) == 0);
CHECK(bt_acl_parse(NULL, macs, 8) == 0);
CHECK(bt_acl_parse("\tACL 33:DE:2E:21:AD:30 handle 1 state 1 lm MASTER \n", macs, 8) == 0);
CHECK(bt_acl_parse("no ACL connection\n", macs, 8) == 0);
/* 截断 MAC(行中 / 行尾都算)与「MAC 后面还跟 hex/':'」的伪 MAC */
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:A\n", macs, 8) == 0);
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:3", macs, 8) == 0);
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30:99 handle 1 state 1\n", macs, 8) == 0);
/* 大小写归一(hcitool 理论上恒大写,别让下游 join 因大小写错过) */
n = bt_acl_parse("\t> ACL 33:de:2e:21:ad:30 handle 11 state 1 lm MASTER \n", macs, 8);
CHECK(n == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* CRLF 行尾(终端/串口转发形态) */
n = bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \r\n", macs, 8);
CHECK(n == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* max 上限裁剪 */
n = bt_acl_parse("Connections:\n"
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \n"
"\t< ACL 11:22:33:44:55:66 handle 12 state 1 lm SLAVE \n"
"\t> ACL 22:33:44:55:66:77 handle 13 state 1 lm SLAVE \n", macs, 2);
CHECK(n == 2);
}
/* ================= 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);
/* 真机原文(M50 2026-11-11 18:26 实测,idle 与 running 各一份),逐字照抄 */
static const char *REAL_IDLE =
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
"Sequence: 0\n"
"Transport: A2DP-source\n"
"Mode: sink\n"
"Running: false\n"
"Format: S16_LE\n"
"Channels: 2\n"
"Sampling: 48000 Hz\n"
"Available codecs: SBC AAC\n"
"Selected codec: AAC\n"
"Delay: 15.0 ms\n"
"SoftVolume: false\n"
"Volume: L: 127 R: 127\n"
"Muted: L: false R: false\n";
static const char *REAL_RUNNING =
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
"Sequence: 3\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";
bt_pcm_info_t info;
bt_pcm_info_parse(REAL_IDLE, &info);
CHECK(info.has == 1);
CHECK(strcmp(info.codec, "AAC") == 0); /* Selected codec,不是 Available 列表 */
CHECK(strcmp(info.rate, "48000 Hz") == 0);
CHECK(strcmp(info.channels, "2") == 0);
CHECK(strcmp(info.bits, "16") == 0); /* 从 Format: S16_LE 推出来 */
CHECK(info.running == 0); /* Running: false */
bt_pcm_info_parse(REAL_RUNNING, &info);
CHECK(info.running == 1); /* Running: true 才代表真在传 */
CHECK(strcmp(info.codec, "AAC") == 0);
CHECK(strcmp(info.bits, "16") == 0);
/* 兼容旧/其它形态(有人给的是 Codec:/Bits per Sample:/State: RUNNING) */
bt_pcm_info_parse(" Transport: A2DP-source\n"
" Codec: SBC\n"
" CodecConfiguration: 0x3f\n"
" Sampling: 44100 Hz\n"
" Channels: 2\n"
" Bits per Sample: 16\n"
" State: RUNNING\n", &info);
CHECK(strcmp(info.codec, "SBC") == 0); /* 不被 CodecConfiguration 抢走 */
CHECK(strcmp(info.rate, "44100 Hz") == 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=AAC") != NULL); /* 真机 Selected codec */
CHECK(strstr(r.text, "rate=48000 Hz") != NULL);
CHECK(strstr(r.text, "channels=2") != NULL);
CHECK(strstr(r.text, "bits=16") != NULL);
CHECK(strstr(r.text, "acquired=0") != NULL); /* Running: false(空闲,端点还在) */
CHECK(strstr(r.text, "role=src") != NULL);
{ /* Running: true → acquired=1(权威布尔) */
snprintf(p, sizeof p, "%s/state/running", T); wf(p, "1\n");
bt_ctl_execute("a2dp", NULL, &r);
CHECK(strstr(r.text, "acquired=1") != NULL);
unlink(p);
}
/* 列表里有端点但 info 查不到(刚断开)→ 跳过,不编造 */
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
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("== A2DP 预算:端点全卡死时不许把守护拖住(r1 评审 P2-1) ==\n");
{
char pcms[512];
snprintf(pcms, sizeof pcms,
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_12_34_56_78_9A_BC/a2dpsnk/source\n");
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, pcms);
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
long t0 = mono_ms();
bt_ctl_execute("a2dp", NULL, &r);
long dt = mono_ms() - t0;
printf(" budget-a2dp(4 端点全卡死) = %ld ms\n", dt);
CHECK(dt < 1200); /* 旧实现实测 6053ms */
CHECK(r.code == MEOW_OK);
unlink(p);
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
/* status JSON 的 a2dp 摘要:无连接→0 次 fork;有连接→TTL 缓存(第二次几乎免费) */
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "yes\n");
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
wf(p, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n");
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
static char js[8192];
g_bt_a2dp_cache_ms = 0;
long t1 = mono_ms();
build_status_json(js, sizeof js);
long d1 = mono_ms() - t1;
long t2 = mono_ms();
build_status_json(js, sizeof js);
long d2 = mono_ms() - t2;
printf(" budget-status-json 1st = %ld ms, 2nd(cache) = %ld ms\n", d1, d2);
CHECK(d1 < 700); /* 单次刷新受 BT_A2DP_STATUS_BUDGET_MS 约束 */
CHECK(d2 < 60); /* 缓存命中:不再 fork */
CHECK(strstr(js, "\"a2dp_codec\"") != NULL); /* 字段保留(MeowPanel btCard 依赖) */
unlink(p);
unlink(snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T) ? p : p);
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
}
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 真态定稿 + rfkill 顺序(先 unblock 再 up / 先 down 再 block) ==\n");
/* 初始两个 bluetooth rfkill 节点 soft=1(真机形态)。旧实现在这里必败:
* 先 hciconfig up 被 rfkill 拦下,再 unblock 也不会重试 → 报「未生效」(假失败)。 */
bt_ctl_execute("on", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已复查") != NULL);
CHECK(bt_powered_now(1500) == 1);
{ /* 两个节点都必须被解 block(只写首个会让另一个仍 block) */
int all_unblocked = 1;
for (int i = 0; i < 2; i++) {
char f[300], v[8] = "1";
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
FILE *fp = fopen(f, "r");
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
if (strcmp(v, "0") != 0) all_unblocked = 0;
}
CHECK(all_unblocked == 1);
}
bt_ctl_execute("off", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(bt_powered_now(1500) == 0);
{ /* 关也要写全部节点 */
int all_blocked = 1;
for (int i = 0; i < 2; i++) {
char f[300], v[8] = "";
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
FILE *fp = fopen(f, "r");
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
if (strcmp(v, "1") != 0) all_blocked = 0;
}
CHECK(all_blocked == 1);
}
/* 反向验证:桩确实会在 blocked 时拒绝 up(否则上面的断言是空转) */
{ char f[300]; snprintf(f, sizeof f, "%s/rfkill/rfkill0/soft", T); wf(f, "1\n");
snprintf(f, sizeof f, "%s/rfkill/rfkill3/soft", T); wf(f, "1\n");
char out[128]; snprintf(out, sizeof out, "%s/hciconfig hci0 up 2>&1", sb_hc);
runcmd_res_t rr; run_cmd_ex(out, sizeof out, 800, out, &rr);
CHECK(rr.status != RUNCMD_OK); }
{ char f[300]; snprintf(f, sizeof f, "%s/bt/hci0/power", T); wf(f, "off\n");
for (int i = 0; i < 2; i++) { snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0); wf(f, "0\n"); } }
printf("== 预算上界:命令卡死时写操作必须落在客户端 1500ms IPC 窗口内(r1 评审 P1) ==\n");
{ /* connect:bluetoothctl 卡住 5s(耳机没上电的真实形态)→ 总耗时必须 < 1450ms */
long t0 = mono_ms();
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
long dt = mono_ms() - t0;
printf(" budget-connect = %ld ms (code=%d)\n", dt, r.code);
CHECK(dt < 1450);
CHECK(r.code == MEOW_ERR_NODEV); /* 未连上就必须报未生效 */
CHECK(strstr(r.text, "未生效") != NULL);
}
{ long t0 = mono_ms();
bt_ctl_execute("pair", "CC:CC:CC:CC:CC:CC", &r);
long dt = mono_ms() - t0;
printf(" budget-pair = %ld ms (code=%d)\n", dt, r.code);
CHECK(dt < 1450);
CHECK(r.code == MEOW_ERR_NODEV);
}
{ /* bt on:hciconfig 卡住 5s → 同样必须 < 1450ms(旧实现实测 2424ms) */
char f[300]; snprintf(f, sizeof f, "%s/state/hang_hciconfig", T); wf(f, "1\n");
long t0 = mono_ms();
bt_ctl_execute("on", NULL, &r);
long dt = mono_ms() - t0;
printf(" budget-bt-on = %ld ms (code=%d)\n", dt, r.code);
CHECK(dt < 1450);
CHECK(r.code == MEOW_ERR_NODEV); /* 卡住未生效:如实报 */
unlink(f);
}
{ /* 熔断:连续 3 次超时后,第 4 次必须**立刻**拒绝、不再 fork(防 55 个挂死 bluetoothctl
* 吃光进程槽的事故)。冷却 30s,测试里手工复位继续后续用例。 */
CHECK(g_bt_tmo_streak >= BT_TMO_STREAK_MAX);
long t0 = mono_ms();
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
long dt = mono_ms() - t0;
printf(" circuit-breaker 4th = %ld ms, code=%d\n", dt, r.code);
CHECK(dt < 100); /* 没再起子进程 */
CHECK(r.code == MEOW_ERR_NODEV);
CHECK(strstr(r.text, "熔断中") != NULL);
g_bt_tmo_streak = 0; /* 测试专用复位(生产靠 30s 冷却+成功清零) */
g_bt_tmo_until = 0;
}
{ /* 恢复:正常 on,供后面的用例使用;同时验证「非超时返回会清零熔断计数」 */
bt_ctl_execute("on", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(g_bt_tmo_streak == 0);
}
printf("== 入站回连(hcitool con 的 `> ACL`):connected / status / connect 复查都必须认 ==\n");
{ /* 真机复现场景(M54):耳机自己回连,行首是 '>'。修前:connected 空、connected=0、
* connect 报「未生效(hcitool con 无该 ACL)」。桩按 conn 文件内容生成方向。 */
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "in\n");
bt_ctl_execute("connected", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
bt_ctl_execute("status", NULL, &r);
CHECK(strstr(r.text, "connected=1") != NULL);
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 1); /* 写后复查走的也是这个解析器 */
/* 已连(入站)时再点连接:ACL 仍是 '>' 行,必须报已连接,不许报「未生效」 */
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, "未生效") == NULL);
/* 只改了链路类型(SCO 行)→ 不算连接:防止「把 SCO 当 ACL」的宽匹配回归 */
wf(p, "sco\n");
bt_ctl_execute("status", NULL, &r);
CHECK(strstr(r.text, "connected=0") != NULL);
bt_ctl_execute("connected", NULL, &r);
CHECK(r.text[0] == 0);
unlink(p);
}
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_tri("33:DE:2E:21:AD:30", 1500) == 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_tri("33:DE:2E:21:AD:30", 1500) == 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, "in\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); /* 入站 ACL 残留也必须被看见 */
CHECK(r.code == MEOW_ERR_NODEV);
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
snprintf(p, sizeof p, "%s/state/conn_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", 1500) == 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", 1500) == -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;
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", T); (void)!system(rm); } /* 退出即清,保证可重复 */
if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; }
printf("\nBT_CTL TEST: ALL PASS\n");
return 0;
}