评审指出 S_CON_INBOUND 注释声称「od -c 逐字节校对」,但仓库与设备上都找不到 那份捕获件,traceability 不可审计。把 2026-11-11 20:53(0.8.50 / 10.58.215.50 / 耳机自主回连现场)`hcitool con 2>&1 | od -c` 的原样输出内联进注释块,并写清 逐字节读法、80 字节/末偏移 0000120 的自校验、以及复核命令(printf | od -c)。 同时如实注明该次采集未单独落盘(devdocs/evidence/ 只有 m50 那一份),出站样本 仍指 devdocs/evidence/m50_bt_result_20261111.log:7-8 的 cat -A 证据。 仅注释变化:meow_netmgrd.c 一字未动,测试断言未动(宿主单测 206 ok / 0 FAIL, 连跑 3 次 ALL PASS)。
820 lines
44 KiB
C
820 lines
44 KiB
C
/* 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` 原始字节样本(两条都可从仓库内自证)----
|
||
*
|
||
* S_CON_INBOUND —— 0.8.50 / 设备 10.58.215.50 / 2026-11-11 20:53 / **耳机自主回连**现场,
|
||
* `hcitool con 2>&1 | od -c` 的原样输出(每行 16 字节;偏移是八进制,末行 0000120 = 80 字节,
|
||
* 与本字面量长度一致;每字节右侧的空格是 od 的字段填充,不是数据):
|
||
* 0000000 C o n n e c t i o n s : \n \t >
|
||
* 0000020 A C L 3 3 : D E : 2 E : 2 1 :
|
||
* 0000040 A D : 3 0 h a n d l e 1 1
|
||
* 0000060 s t a t e 1 l m M A S T E
|
||
* 0000100 R A U T H E N C R Y P T \n
|
||
* 0000120
|
||
* 逐字节读法:`Connections:` + LF + TAB + `>` + SPC + `ACL` + SPC + 17 字符 MAC +
|
||
* ` handle 11 ` + `state 1 lm MASTER AUTH ENCRYPT` + SPC + LF ⇒ 字面量见下方 S_CON_INBOUND。
|
||
* 复核方法:`printf '<上面的字面量>' | od -c`,与本块除「行尾字段填充空格」外逐行相同
|
||
* (本块为免源码尾随空格删掉了那点填充;数据以 80 字节 / 末偏移 0000120 为准)。
|
||
* 这次采集没有单独落盘成证据文件(`devdocs/evidence/` 只有 m50 那一份),上面这块 od 输出
|
||
* 就是它的原样记录;同一条命令的「`Connections:` 表头 + 行首 TAB + 行尾 SPC 后 LF」框架字节
|
||
* 可与 S_CON_OUTBOUND 的来源(下面那份 `cat -A` 证据)互相印证。
|
||
*
|
||
* S_CON_OUTBOUND —— devdocs/evidence/m50_bt_result_20261111.log:7-8(同机同耳机,本端发起)。
|
||
* `cat -A` 核为 `Connections:$` + `^I< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER $`,
|
||
* 与下面的字面量逐字节一致(行首 TAB、行尾一个空格再 LF)。 */
|
||
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;
|
||
}
|