Files
MeowOS-For-R3P2/patch_v2/meow_netmgrd/test_bt_ctl.c
T
worker-m49 239cebae01 fix(bt): M49 r1 评审 — 写操作预算压回 1500ms 窗口内 + A2DP 总预算/TTL 缓存 + 测试可重复 + rfkill 顺序;rebase 到 main 保住 M43 time32
rebase 到 main(00e7271,M43 time32 已合入) 时解开 build_netmgrd.sh 冲突:**两者都保留**——
M43 的 `../sdk/meow_time32.c` + `-DMEOW_TIME32_LIBC_OVERRIDES`,以及我的 ROOT/ZIGCC/OUT
环境变量化。编译改在 mktemp 目录里做(产物名仍是字面量 `-o meow_netmgrd`,供
sdk/test_time32_abi.sh 的按名定位),编完再落到 OUT —— 设了 OUT 也不会覆盖/删除仓库里
那份被跟踪的二进制(r1 期间 mv 曾把它删掉过)。门禁自证:`--builds` → build meow_netmgrd
src=yes macro=yes;`--no-builds` → 4263=3 4403=0 16B-layout=OK ALL PASS。

P1-1(写操作越过客户端 1500ms 窗口):预算模型改成**每一层都受限**——
`BT_ACT_TMO_MS 800 / BT_ACT_SHORT_MS 600 / BT_VERIFY_BUDGET_MS 220 / gap 90`,
`bt_poll_probe` 把**剩余预算**传给探测函数(`bt_acl_has_tri`/`bt_info_flag` 新增 tmo 形参),
探测自身那次 run_cmd 也受同一个 deadline 约束;`bt_onoff` 同理(up 失败只在剩余预算内重试
一次);成功文案不再补设备名(省掉 1-2 次 fork,UI 本来就按 MAC join)。
探测改为三态:命令失败/超时 = -1(**未知**),不再被当成「没有 ACL」——否则 disconnect 会在
查不到的情况下报「已断开」,又是一次假成功。
实测(宿主桩驱动 bt_ctl_execute,命令卡死 5s 的形态):connect 1042-1047ms、pair 1042-1045ms、
bt on 831ms,全部 < 1450 且有 400ms+ 余量(r1 实测 1528/2424ms)。设备侧实测 fork 成本
≈25-30ms/次(bt a2dp 50ms、bt status 100ms),余量足够。

P2-1(A2DP 1+N 子进程塞进 status JSON):`bt_a2dp_query(buf,cap,budget)` 加**总预算**,
list-pcms 单命令 ≤400ms、每个 info ≤300ms、剩下端点超预算就不再查;`bt a2dp` 命令用 900ms。
status JSON 不在无连接时查(0 次 fork),有连接时走 10s TTL 缓存 + 500ms 预算;
`a2dp_codec`/`a2dp_rate` 字段保留(MeowPanel btCard 依赖,见 docs/plan/FUTURE_MEOWPANEL.md:48)。
实测 4 端点全卡死:911ms(r1 6053ms);status JSON 首次 350-384ms、缓存命中 31-49ms。

P2-2(test_bt_ctl 不可重复):开跑先 `rm -rf` 状态目录、退出再清;连跑 3 次 ALL PASS(贴了输出)。

P2-3(bt_onoff 顺序 + 多节点):开=先 rfkill unblock(**全部**同类型节点)再 hciconfig up,
up 未生效时剩余预算内重试一次;关=先 down 再 block。新增 `rfkill_set_block_all()`(旧
`rfkill_set_block` 仍只写首个节点,WiFi 路径行为不变)。桩改成「blocked 时 up 必败」,并加了
反向用例证明桩真的会失败(否则断言空转)。

P3:①info 的 RUNCMD_EMPTY(exit 0 无输出)不再当失败跳过;②`bt_pcm_paths_parse` 的
`/a2dpsrc` 搜索限制在行内 [p,e)(原来会跨 \n 命中下一行);③`bt_devices_text` 注释写明
「本轮扫描见到过的设备」而非「此刻在场」([DEL] 不删除、devices 块含 BlueZ 历史缓存)。

附带(塔 11:06 建议,BT 作用域内先落地):连续 3 次 RUNCMD_TIMEOUT 后熔断 30s,冷却期内
直接回「熔断中」不再 fork(真机 55 个挂死 bluetoothctl 吃光进程槽的事故);任何非超时返回清零。
单测覆盖:第 4 次调用 0ms 返回。范围只限 BT 写操作,WiFi/DNS 调用点未动。

真机(本轮只读,设备已恢复在线):新件 md5 e62e7512 上卡只读验收 status/paired/connected/
devices/a2dp 全通;`ps w | grep -c bluetoothctl` 前后均为 2(≤3);Music du 199164544;
临时目录/独立实例清理干净,/usr/bin 未动。
2026-11-11 19:15:00 +08:00

725 lines
37 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";
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"
" 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 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>=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"
"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 原始输出/空/截断) ==\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);
/* 真机原文(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("== 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, "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", 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;
}