feat(bt): 蓝牙P1 — netmgrd BT_CTL权威收口(hcitool con判连接/paired-devices补名/a2dp详情/connect-disconnect-pair-unpair-scan)+launcher蓝牙子页sub_page_t重做(8分组,数据全走netmgrd)+设置键audio.output_bt/bt.codec;修act冲突+sub悬垂;宿主82PASS+bt_ctl/bt_page全过

This commit is contained in:
AskaEth committed 2026-09-03 20:01:13 +08:00
1 parent 8c677f127f
commit eb07415f7b
8 files changed
+1178 -90

No files matched your search

+235
View File
@@ -0,0 +1,235 @@
/* test_bt_page.c — MeowOS launcher 蓝牙子页(bt_page.c)sub_page_t 逻辑宿主自测(非生产代码)。
*
* 通过 #include bt_page.c 把其 FILE-STATIC 的 bt_top_sw/bt_rows/bt_act/bt_detail 平移进本 TU,
* 桩掉 netmgr IPC 客户端(nm_bt_*)+ 设置后端(meow_settings_*)+ UI(s_dd_modal/logmsg 等),
* 直接验证:
* P1 bt_rows 行骨架(plan §六 8 分组:②A2DP 摘要 ③输出目标 ④已连接 ⑤已配对 ⑥附近 ⑦音质 ⑧功耗)
* 与「已连接设备」数据(权威 netmgrd 状态 → mac/name/codec 渲染);
* P2 bt_act 命中 → 正确下发 netmgrd 命令(已连接→disconnect、已配对未连→connect、输出目标
* 切换、编解码器循环);
* P3 附近设备仅列未配对(已配对者归「已配对设备」)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined test_bt_page.c -o test_bt_page
* 运行:./test_bt_page
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <strings.h>
#include "meow_launcher_internal.h"
#include "bt_page.c" /* bt_rows/bt_act/bt_top_sw/bt_detail 平移到本 TU */
/* ---------- 测试态:netmgrd BT 状态桩(launcher 客户端视角) ---------- */
static meow_net_t g_conn[8], g_paired[8], g_near[8];
static int g_conn_n, g_paired_n, g_near_n;
static bt_ep_t g_eps[8]; static int g_eps_n;
static char g_last_cmd[128]; /* 记录 act 下发命令,如 "disconnect:<mac>" */
static int g_output_bt; /* audio.output_bt 桩值 */
static char g_codec[16] = "auto"; /* bt.codec 桩值 */
int nm_bt_powered(void) { return 1; }
int nm_bt_onoff(int on) { (void)on; return 0; }
int nm_bt_connected(meow_net_t *out, int max) {
if (max > g_conn_n) max = g_conn_n;
memcpy(out, g_conn, (size_t)max * sizeof *out); return max;
}
int nm_bt_paired(meow_net_t *out, int max) {
if (max > g_paired_n) max = g_paired_n;
memcpy(out, g_paired, (size_t)max * sizeof *out); return max;
}
int nm_bt_devices(meow_net_t *out, int max) {
if (max > g_near_n) max = g_near_n;
memcpy(out, g_near, (size_t)max * sizeof *out); return max;
}
int nm_bt_a2dp(bt_ep_t *eps, int max) {
if (max > g_eps_n) max = g_eps_n;
memcpy(eps, g_eps, (size_t)max * sizeof *eps); return max;
}
static void set_cmd(const char *cmd, const char *mac)
{
snprintf(g_last_cmd, sizeof g_last_cmd, "%s:%s", cmd, mac ? mac : "");
}
int nm_bt_connect(const char *mac) { set_cmd("connect", mac); return MEOW_OK; }
int nm_bt_disconnect(const char *mac) { set_cmd("disconnect", mac); return MEOW_OK; }
int nm_bt_pair(const char *mac) { set_cmd("pair", mac); return MEOW_OK; }
int nm_bt_unpair(const char *mac) { set_cmd("unpair", mac); return MEOW_OK; }
void nm_bt_scan_on(void) {}
void nm_bt_scan_off(void) {}
int netmgr_ok(void) { return 1; }
void net_bt_mode_str(char *buf, int len) { snprintf(buf, (size_t)len, "主模式·已连 Pudding"); }
/* ---------- 设置/UI 桩 ---------- */
int meow_settings_get_bool(const char *k, int *v)
{
if (v) *v = 0;
if (!strcmp(k, "network.bluetooth_enabled")) { if (v) *v = 1; return 0; }
if (!strcmp(k, "audio.output_bt")) { if (v) *v = g_output_bt; return 0; }
return -1;
}
int meow_settings_set_bool(const char *k, int v)
{
if (!strcmp(k, "audio.output_bt")) { g_output_bt = v; return 0; }
if (!strcmp(k, "network.bluetooth_enabled")) return 0;
return -1;
}
int meow_settings_get_str(const char *k, char *b, int n)
{
if (!strcmp(k, "bt.codec")) { snprintf(b, (size_t)n, "%s", g_codec); return 0; }
if (b && n > 0) b[0] = 0;
return -1;
}
int meow_settings_set_str(const char *k, const char *v)
{
if (!strcmp(k, "bt.codec")) { snprintf(g_codec, sizeof g_codec, "%s", v); return 0; }
return -1;
}
int meow_settings_save(void) { return 0; }
void paint_sb_right(void) {}
void logmsg(const char *fmt, ...) { (void)fmt; }
void bt_role_refresh(int force) { (void)force; }
meow_sys_state g_sys;
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur) { (void)title; (void)item; (void)n; (void)cur; return -1; }
int meow_subpage_run(sub_page_t *p) { (void)p; return 0; }
static int g_fail = 0;
#define CHECK(cond, ...) do { if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
/* 在 rows 里找下标:kind==HDR 且 label==lbl;返回该 HDR 的下一行下标起查找 */
static int find_hdr(const sub_row_t *r, int n, const char *lbl)
{
for (int i = 0; i < n; i++) if (r[i].kind == SUB_HDR && r[i].label && !strcmp(r[i].label, lbl)) return i;
return -1;
}
/* 在 [from,n) 找首个 SUB_ACT 且 label 前缀 == pref,返回下标 */
static int find_act(const sub_row_t *r, int n, int from, const char *pref)
{
for (int i = from; i < n; i++)
if (r[i].kind == SUB_ACT && r[i].label && !strncmp(r[i].label, pref, strlen(pref))) return i;
return -1;
}
static void fill_state(void)
{
/* 已连接:Pudding(a2dp 播放中)+ Speaker */
g_conn_n = 2;
snprintf(g_conn[0].mac, sizeof g_conn[0].mac, "33:DE:2E:21:AD:30");
snprintf(g_conn[0].name, sizeof g_conn[0].name, "Pudding");
snprintf(g_conn[1].mac, sizeof g_conn[1].mac, "11:22:33:44:55:66");
snprintf(g_conn[1].name, sizeof g_conn[1].name, "Speaker");
/* 已配对:+ 17OP15(未连) */
g_paired_n = 3;
memcpy(&g_paired[0], &g_conn[0], sizeof g_conn[0]);
memcpy(&g_paired[1], &g_conn[1], sizeof g_conn[1]);
snprintf(g_paired[2].mac, sizeof g_paired[2].mac, "E0:42:6D:79:C5:98");
snprintf(g_paired[2].name, sizeof g_paired[2].name, "17OP15");
/* 附近:Earbuds(未配对 → 附近列出) */
g_near_n = 2;
snprintf(g_near[0].mac, sizeof g_near[0].mac, "AA:BB:CC:DD:EE:FF");
snprintf(g_near[0].name, sizeof g_near[0].name, "Earbuds");
snprintf(g_near[1].mac, sizeof g_near[1].mac, "33:DE:2E:21:AD:30"); /* 已配对 → 附近应过滤 */
snprintf(g_near[1].name, sizeof g_near[1].name, "Pudding");
/* A2DP:Pudding SBC/44100 acquired */
g_eps_n = 1;
snprintf(g_eps[0].mac, sizeof g_eps[0].mac, "33:DE:2E:21:AD:30");
snprintf(g_eps[0].codec, sizeof g_eps[0].codec, "SBC");
snprintf(g_eps[0].rate, sizeof g_eps[0].rate, "44100");
snprintf(g_eps[0].chn, sizeof g_eps[0].chn, "2");
snprintf(g_eps[0].bits, sizeof g_eps[0].bits, "16");
g_eps[0].acquired = 1;
snprintf(g_eps[0].role, sizeof g_eps[0].role, "src");
g_output_bt = 0; /* DAC */
snprintf(g_codec, sizeof g_codec, "auto");
}
int main(void)
{
setbuf(stdout, NULL);
fill_state();
bt_ctx_t c;
memset(&c, 0, sizeof c);
c.full = 1;
c.output_bt = 0;
sub_row_t rows[SUB_ROWS_MAX];
int n = bt_rows(&c, rows, SUB_ROWS_MAX);
printf("== P1 bt_rows 分组骨架 ==\n");
CHECK(find_hdr(rows, n, "输出目标") >= 0, "分组③输出目标存在");
CHECK(find_hdr(rows, n, "已连接设备") > 0, "分组④已连接设备存在");
CHECK(find_hdr(rows, n, "已配对设备") > 0, "分组⑤已配对设备存在");
CHECK(find_hdr(rows, n, "附近设备") > 0, "分组⑥附近设备存在");
CHECK(find_hdr(rows, n, "音质 / 连接管理") > 0, "分组⑦音质/连接管理存在");
CHECK(find_hdr(rows, n, "功耗") > 0, "分组⑧功耗存在");
/* A2DP 摘要(首行 SUB_ROW,主=Pudding,副含 codec/采样率/输出=DAC) */
CHECK(rows[0].kind == SUB_ROW, "行0=A2DP摘要(SUB_ROW)");
CHECK(rows[0].label && !strcmp(rows[0].label, "Pudding"), "摘要主=Pudding (实际=%s)",
rows[0].label ? rows[0].label : "(null)");
CHECK(rows[0].sub && strstr(rows[0].sub, "SBC") && strstr(rows[0].sub, "44100") &&
strstr(rows[0].sub, "输出=DAC"), "摘要副含 codec/采样率/输出=DAC (%s)",
rows[0].sub ? rows[0].sub : "(null)");
printf("== P1 已连接设备(权威状态,含 codec join)==\n");
int hdr = find_hdr(rows, n, "已连接设备");
CHECK(hdr >= 0, "定位已连接 HDR");
int ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci >= 0, "已连接列出 Pudding");
CHECK(ci >= 0 && rows[ci].sub && strstr(rows[ci].sub, "SBC"), "已连接 Pudding sub 含 codec SBC");
CHECK(find_act(rows, n, hdr + 1, "Speaker") >= 0, "已连接列出 Speaker");
printf("== P1 已配对设备(已连者标「已连接」on=1)==\n");
hdr = find_hdr(rows, n, "已配对设备");
CHECK(hdr >= 0, "定位已配对 HDR");
ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci >= 0 && rows[ci].on == 1 && rows[ci].sub && strstr(rows[ci].sub, "已连接"),
"已配对 Pudding 标已连接(on=1)");
ci = find_act(rows, n, hdr + 1, "17OP15");
CHECK(ci >= 0 && rows[ci].on == 0, "已配对 17OP15 未连(on=0)");
printf("== P3 附近设备仅列未配对(已配对者过滤)==\n");
hdr = find_hdr(rows, n, "附近设备");
CHECK(hdr >= 0, "定位附近 HDR");
ci = find_act(rows, n, hdr + 1, "Earbuds");
CHECK(ci >= 0, "附近列出 Earbuds");
ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci < 0, "已配对 Pudding 不在附近列表");
printf("== P2 bt_act 命中 → netmgrd 命令 ==\n");
/* 已连接 Pudding tap → disconnect */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "已连接设备");
ci = find_act(rows, n, hdr + 1, "Pudding");
g_last_cmd[0] = 0;
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_last_cmd, "disconnect:33:DE:2E:21:AD:30") == 0, "已连接 tap → disconnect (实际=%s)", g_last_cmd);
/* 已配对 17OP15(未连)tap → connect */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "已配对设备");
ci = find_act(rows, n, hdr + 1, "17OP15");
g_last_cmd[0] = 0;
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_last_cmd, "connect:E0:42:6D:79:C5:98") == 0, "已配对未连 tap → connect (实际=%s)", g_last_cmd);
/* 输出目标 toggle → audio.output_bt 翻转为 1;bt_rows 摘要改输出=蓝牙 */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "输出目标");
ci = find_act(rows, n, hdr + 1, "输出目标");
bt_act(&c, ci, rows[ci].act);
CHECK(g_output_bt == 1, "输出目标 tap → output_bt=1 (蓝牙)");
n = bt_rows(&c, rows, SUB_ROWS_MAX);
CHECK(rows[0].sub && strstr(rows[0].sub, "输出=蓝牙"), "切蓝牙后 A2DP 摘要=输出=蓝牙");
/* 编解码器循环:auto → SBC */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "音质 / 连接管理");
ci = find_act(rows, n, hdr + 1, "编解码器");
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_codec, "SBC") == 0, "编解码器 auto → SBC (实际=%s)", g_codec);
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}