Files
MeowOS-For-R3P2/patch_v2/meow_netmgrd/test_bt_ctl.c
T
worker-m49 370c290160 fix(bt): M49 蓝牙控制面重建 — paired-devices/a2dp list-pcms+info/run_cmd 四态+写后真态复查+扫描 ANSI 解析
真机 0.8.49 现场 /usr/bin/meow_netmgrd 是 P1(eb07415) 之前的旧件:`bt status` 无
connected= 字段、`bt connected|devices|a2dp` 连子命令都不存在(被当成写级拒绝)、
`bt paired` 落到 `bluetoothctl paired` → "Invalid command in menu main"。上游只有
NETMGR_UNREACH 才回退直连,INVAL/PERM 直接返回 → 三个列表恒空。

本单修掉「部署新件后依然存在」的三个真 bug + 一个解析缺陷:

1. `bluetoothctl paired` → `paired-devices`(bt_status 计数、bt paired 子命令两处);
   `devices` 只用于补名,不再当配对权威。
2. A2DP:v4.1.1 没有 `a2dp list`。改为 `list-pcms` 取端点 → 每端点 `info <path>`;
   MAC 只从 D-Bus 路径的 `dev_XX_..` 段解(真机路径带 `/a2dpsrc/source` 段,
   按「最后一段取 token」会拿到 "source")。info 查不到就跳过,不编造。
3. `run_cmd` 四态(成功有输出/成功无输出/非 0 退出/超时/起不来)+ stderr 有界抓取
   (≤256B):旧实现只回字节数、失败也返回 0 → on/off/connect/disconnect/pair 恒报成功。
   写操作改为下发后复查真态(hcitool con / bluetoothctl info Paired: / hciconfig
   power),超时或未收敛如实报 MEOW_ERR_NODEV。兼容外壳 run_cmd() 契约不变。
4. 扫描缓存:实时 `[NEW] Device` 行以 ANSI ESC 开头(真机 od -c 证据),旧解析器只认
   行首 → 实时行全丢。新增 bt_clean_ansi()/bt_scan_line_parse():剥 ESC/CSI/控制符、
   去 `[bluetooth]# ` 提示符、只认 [NEW] 与裸 Device、排除 [CHG]/[DEL];bt_scan_on 改用
   g_bluetoothctl(原来硬编码,桩对 scan 无效)。

解析器全部抽成纯函数并用真机原始字节做单测(test_bt_ctl.c 扩充:四态、ANSI 样本、
截断/空输出、list-pcms/info、hcitool con)。test_netmgrd.sh 桩改 list-pcms/info +
真机 ANSI 缓存样本;顺带修掉两处宿主可移植性问题(/tmp 不可写→$TMPDIR、桩脚本在
PATH=/bin:/usr/bin 下用了 cat/rm→改 shell 内建,test_wifi_connect 的 g_wpa_cli 悬垂
栈指针)。

按塔要求:本 commit 不含二进制、不改 fw_content.conf(netmgrd 产物由 M51 统一重编)。
build_netmgrd.sh 只做 ROOT/ZIGCC/OUT 环境变量化(旧硬编码 $HOME/Project/R3ProII),
M51 复用;time32 接入仍归 M43。
2026-11-11 18:56:06 +08:00

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