From eb07415f7b4b3447e00c3a8fcc517d9fd4df0d51 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 3 Sep 2026 20:01:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(bt):=20=E8=93=9D=E7=89=99P1=20=E2=80=94=20?= =?UTF-8?q?netmgrd=20BT=5FCTL=E6=9D=83=E5=A8=81=E6=94=B6=E5=8F=A3(hcitool?= =?UTF-8?q?=20con=E5=88=A4=E8=BF=9E=E6=8E=A5/paired-devices=E8=A1=A5?= =?UTF-8?q?=E5=90=8D/a2dp=E8=AF=A6=E6=83=85/connect-disconnect-pair-unpair?= =?UTF-8?q?-scan)+launcher=E8=93=9D=E7=89=99=E5=AD=90=E9=A1=B5sub=5Fpage?= =?UTF-8?q?=5Ft=E9=87=8D=E5=81=9A(8=E5=88=86=E7=BB=84,=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=85=A8=E8=B5=B0netmgrd)+=E8=AE=BE=E7=BD=AE=E9=94=AEaudio.out?= =?UTF-8?q?put=5Fbt/bt.codec;=E4=BF=AEact=E5=86=B2=E7=AA=81+sub=E6=82=AC?= =?UTF-8?q?=E5=9E=82;=E5=AE=BF=E4=B8=BB82PASS+bt=5Fctl/bt=5Fpage=E5=85=A8?= =?UTF-8?q?=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patch_v2/meow_launcherd/bt_page.c | 284 +++++++++++--- .../meow_launcherd/meow_launcher_internal.h | 23 +- patch_v2/meow_launcherd/meow_settings.c | 6 + patch_v2/meow_launcherd/netmgr_ipc.c | 163 +++++++- patch_v2/meow_launcherd/test_bt_page.c | 235 ++++++++++++ patch_v2/meow_netmgrd/meow_netmgrd.c | 353 +++++++++++++++++- patch_v2/meow_netmgrd/test_bt_ctl.c | 136 +++++++ patch_v2/meow_netmgrd/test_netmgrd.sh | 68 +++- 8 files changed, 1178 insertions(+), 90 deletions(-) create mode 100644 patch_v2/meow_launcherd/test_bt_page.c create mode 100644 patch_v2/meow_netmgrd/test_bt_ctl.c diff --git a/patch_v2/meow_launcherd/bt_page.c b/patch_v2/meow_launcherd/bt_page.c index 84a67c6..35b8d77 100644 --- a/patch_v2/meow_launcherd/bt_page.c +++ b/patch_v2/meow_launcherd/bt_page.c @@ -1,46 +1,98 @@ -/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76)。 +/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76;P1 权威收口重做)。 * - * 页面(sub_page_t 脚手架 subpage.c 驱动): - * - 顶栏:返回 + 标题「蓝牙」 + 右上开关(network.bluetooth_enabled) - * - 已连接设备:tap → bluetoothctl disconnect(角色经 bt_role_refresh 判定) - * - 已配对设备:tap → bluetoothctl connect(已连者标「已连接」) - * - 附近设备:扫描发现(net_bt_scan,后台有界 bluetoothctl 写 /tmp 缓存,异步不阻塞)—— - * 未配对者 tap → pair;已配对者 tap → connect。实机配对成功率/超时需真机验证。 + * 【P1 背景】BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八:连接/断开/配对/取消配对/扫描/A2DP 状态 + * 查询全部收口进 netmgrd(BT_CTL 消息扩展),本子页**只做客户端**——经 netmgrd socket/IPC 查询 + * 与下发,不再 bluetoothctl popen 直连(直连仅作 netmgrd 不可达时的回退保留)。 * - * 与网络线共用 net.c(net_bt_connected/paired/scan/scan_trigger/hci_powered/bt_role)。 - * 开关写 network.bluetooth_enabled + hciconfig;连接/断开/配对走 bluetoothctl(带超时)。 + * 【已连接设备显示不全根因修复】旧实现用 `bluetoothctl devices Connected`(慢 + 只反映 agent + * 缓存视图,popen 1500ms 易截断)→ 已连接列表不全。P1 改权威 = netmgrd 读**内核 ACL** + * (hcitool con,快速/权威/不受 bluetoothctl 过滤影响),名经 paired/devices 缓存兜底,A2DP 详情 + * 按 MAC join(codec/采样率/acquired/role)。见 netmgrd `bt connected`/`bt a2dp`。 + * + * 【页面结构(sub_page_t,plan §六)】顶栏「蓝牙」+ 右上开关,分组列表: + * ① 蓝牙开关(顶栏)② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC)③ 输出目标 + * (蓝牙/本地,切下发 audio.output_bt,P2 接 audiod)④ 已连接设备(tap 断开/详情)⑤ 已配对 + * 设备(tap 连接/断开)⑥ 附近设备(扫描节流触发)⑦ 音质/连接管理(codec 选择、P2 参数说明) + * ⑧ 功耗档(扫描节流/发现超时/后台保持说明)。 + * + * 【扫描节流(§七)】进页 nm_bt_scan_on、退页/超时 nm_bt_scan_off;7s 节流由 netmgrd 统一控。 + * + * 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。开关写 network.bluetooth_enabled + + * netmgrd on/off;连接/断开/配对经 netmgrd(netmgr_ipc.c nm_bt_* 封装)。 */ #include "meow_launcher_internal.h" +#include +#include +#include +#include -#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→disconnect) */ -#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→connect) */ -#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→pair/connect) */ +#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→断开) */ +#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→连接/断开) */ +#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→配对/连接) */ +#define BT_ACT_OUTPUT 5000 /* 输出目标(tap→蓝牙/本地切换) */ +#define BT_ACT_CODEC 5001 /* 编解码器(tap→循环选择) */ + +#define BT_CODEC_AUTO 0 +#define BT_CODEC_N 5 /* auto/SBC/AAC/aptX/LDAC */ +static const char *bt_codec_names[BT_CODEC_N] = { "auto", "SBC", "AAC", "aptX", "LDAC" }; typedef struct { meow_net_t conn[S_NET_MAX]; meow_net_t paired[S_NET_MAX]; meow_net_t near[S_NET_MAX]; - int conn_n, paired_n, near_n; + bt_ep_t a2dp[S_NET_MAX]; + int conn_n, paired_n, near_n, a2dp_n; int bt_on; - int full; /* 1=本次 rows() 全量重扫(conn/paired popen);0=仅附近(file)+角色(节流) */ + int output_bt; /* 0=DAC/本地,1=蓝牙 */ + int full; /* 1=本次 rows() 全量重扫(conn/paired/a2dp);0=仅附近(缓存)+角色(节流) */ char mode[48]; /* bt_role 中文描述(次行摘要) */ + /* 行副文本池:sub_page_t 行 label/sub 为 const char*,跨 rows() 返回后仍须有效(直到下次 + * rows() 重填)。本地栈缓冲会悬垂(agent-396 根因同类),故格式化 sub 一律写进本池。 */ + char pool[512]; + int pool_off; } bt_ctx_t; -/* 设备 MAC(net_bt_* 已填 mac) */ +/* 格式化并拷入 ctx 行副文本池(返回稳定指针;溢出则绕回——512B 远超单页行数所需)。 */ +static char *bt_pool(bt_ctx_t *c, const char *fmt, ...) +{ + va_list ap; char tmp[128]; + va_start(ap, fmt); vsnprintf(tmp, sizeof tmp, fmt, ap); va_end(ap); + int l = (int)strlen(tmp); + if (c->pool_off + l + 1 > (int)sizeof c->pool) c->pool_off = 0; + char *p = &c->pool[c->pool_off]; + memcpy(p, tmp, (size_t)l + 1); + c->pool_off += l + 1; + return p; +} + + +/* ---- 设备 MAC 相关 ---- */ static int bt_is_conn(const bt_ctx_t *c, const char *mac) { if (!mac[0]) return 0; for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return 1; return 0; } - static int bt_is_paired(const bt_ctx_t *c, const char *mac) { if (!mac[0]) return 0; for (int i = 0; i < c->paired_n; i++) if (!strcmp(c->paired[i].mac, mac)) return 1; return 0; } +/* 在 a2dp 端点表找 MAC(返回 idx;-1=无) */ +static int bt_ep_idx(const bt_ctx_t *c, const char *mac) +{ + if (!mac[0]) return -1; + for (int i = 0; i < c->a2dp_n; i++) if (!strcmp(c->a2dp[i].mac, mac)) return i; + return -1; +} +/* 从 conn 表找设备名(返回指针到 c->conn[i].name,未命中返回 mac) */ +static const char *bt_conn_name(const bt_ctx_t *c, const char *mac) +{ + if (mac[0]) for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return c->conn[i].name; + return mac; +} /* 顶栏开关当前值 */ @@ -53,7 +105,7 @@ static int bt_top_sw(void *ctx, int *on) return 1; } -/* 顶栏开关切换:写设置 + on/off(netmgr IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */ +/* 顶栏开关切换:写设置 + on/off(netmgrd IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */ static void bt_top_sw_set(void *ctx, int on) { (void)ctx; @@ -63,48 +115,91 @@ static void bt_top_sw_set(void *ctx, int on) bt_role_refresh(1); paint_sb_right(); meow_settings_save(); - if (on) net_bt_scan_trigger(); + if (on) nm_bt_scan_on(); } -/* 填行:刷新数据后构建分组列表。 - * conn/paired 走 bluetoothctl(慢);仅全量(full)时重扫,避开子页 live tick 反复 spawn。 */ +/* 填行:刷新数据后构建分组列表(权威=netmgrd;仅全量(full)时重扫 conn/paired/a2dp, + * 避开子页 live tick 反复 IPC)。 */ static int bt_rows(void *ctx, sub_row_t *rbuf, int max) { bt_ctx_t *c = ctx; int full = c->full; c->full = 0; /* 消费一次全量标记 */ + c->pool_off = 0; /* 行副文本池绕回(上轮行字符串已被重填替换) */ c->bt_on = nm_bt_powered(); /* IPC status;回退直连 hci_powered() */ - if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired */ - /* conn 列表 netmgrd 无子命令(BT_CTL=status/on/off/paired),保持直连 bluetoothctl; - * paired 走 IPC(权威),回退直连。 */ - c->conn_n = net_bt_connected(c->conn, S_NET_MAX); + c->output_bt = 0; + if (meow_settings_get_bool("audio.output_bt", &c->output_bt) != 0) c->output_bt = 0; + if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired/a2dp(全权威) */ + c->conn_n = nm_bt_connected(c->conn, S_NET_MAX); c->paired_n = nm_bt_paired(c->paired, S_NET_MAX); + c->a2dp_n = nm_bt_a2dp(c->a2dp, S_NET_MAX); if (c->conn_n < 0) c->conn_n = 0; if (c->paired_n < 0) c->paired_n = 0; + if (c->a2dp_n < 0) c->a2dp_n = 0; } - c->near_n = net_bt_scan(c->near, S_NET_MAX); /* 附近:读 /tmp 缓存(快,实测无 popen) */ + c->near_n = nm_bt_devices(c->near, S_NET_MAX); /* 附近:扫描缓存(netmgrd 读 /tmp,快) */ if (c->near_n < 0) c->near_n = 0; /* 角色描述:节流(≤5s)重探,读缓存,避免子页驻留期间反复 popen */ net_bt_mode_str(c->mode, sizeof c->mode); int n = 0; - /* 降级占位:netmgrd 不可达 → 提示(直连仍在跑,保功能) */ + /* 降级占位:netmgrd 不可达 → 提示(直连回退仍在跑,保功能) */ if (!netmgr_ok() && n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } - /* 已连接设备 */ + + /* ② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC) */ + { + const bt_ep_t *ep = NULL; + for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; } + if (!ep && c->a2dp_n > 0) ep = &c->a2dp[0]; + const char *sub; + if (c->conn_n > 0 && n < max) { + const char *nm = bt_conn_name(c, ep ? ep->mac : c->conn[0].mac); + if (ep && (ep->codec[0] || ep->rate[0])) + sub = bt_pool(c, "%s%s%s · 输出=%s", + ep->codec[0] ? ep->codec : "?", ep->codec[0] && ep->rate[0] ? " " : "", + ep->rate[0] ? ep->rate : "", c->output_bt ? "蓝牙" : "DAC"); + else + sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC"); + rbuf[n].kind = SUB_ROW; rbuf[n].label = nm ? nm : c->conn[0].mac; + rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++; + } else if (n < max) { + sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC"); + rbuf[n].kind = SUB_ROW; rbuf[n].label = "未连接蓝牙设备"; + rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++; + } + } + + /* ③ 输出目标(蓝牙/本地) */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "输出目标"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (n < max) { + rbuf[n].kind = SUB_ACT; rbuf[n].label = "输出目标"; + rbuf[n].sub = c->output_bt ? "蓝牙(耳机/远端)" : "本地(DAC)"; + rbuf[n].on = c->output_bt; rbuf[n].act = BT_ACT_OUTPUT; n++; + } + + /* ④ 已连接设备 */ if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已连接设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } if (c->conn_n == 0) { if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已连接设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } } else for (int i = 0; i < c->conn_n && n < max; i++) { + const bt_ep_t *ep = bt_ep_idx(c, c->conn[i].mac) >= 0 ? &c->a2dp[bt_ep_idx(c, c->conn[i].mac)] : NULL; + const char *sub; + if (ep && ep->codec[0]) + sub = bt_pool(c, "%s%s%s%s", + ep->codec[0] ? ep->codec : "", ep->rate[0] ? " " : "", ep->rate[0] ? "· " : "", + ep->acquired ? "播放中" : ""); + else sub = c->conn[i].mac; /* mac 拷在 ctx(稳定) */ rbuf[n].kind = SUB_ACT; rbuf[n].label = c->conn[i].name[0] ? c->conn[i].name : c->conn[i].mac; - rbuf[n].sub = c->conn[i].mac; + rbuf[n].sub = sub[0] ? sub : c->conn[i].mac; rbuf[n].on = 1; rbuf[n].act = BT_ACT_CONN_BASE + i; n++; } - /* 已配对设备 */ + + /* ⑤ 已配对设备 */ if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已配对设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } if (c->paired_n == 0) { if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已配对设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } @@ -117,7 +212,8 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max) rbuf[n].act = BT_ACT_PAIRED_BASE + i; n++; } - /* 附近设备(扫描发现,未配对) */ + + /* ⑥ 附近设备(扫描发现,未配对;netmgrd 权威扫描缓存) */ if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "附近设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } if (c->near_n == 0) { if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "附近无设备(点顶部开关后自动扫描)"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; } @@ -130,30 +226,66 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max) rbuf[n].on = 0; rbuf[n].act = BT_ACT_NEAR_BASE + i; n++; } + + /* ⑦ 音质/连接管理 */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "音质 / 连接管理"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + { + int idx = BT_CODEC_AUTO; + char codec[16] = "auto"; + if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0) + for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx = k; break; } + rbuf[n].kind = SUB_ACT; rbuf[n].label = "编解码器"; + rbuf[n].sub = bt_pool(c, "%s%s", bt_codec_names[idx], idx ? "(偏好,P2 生效)" : ""); + rbuf[n].on = 0; + rbuf[n].act = BT_ACT_CODEC; n++; + } + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "SBC 质量"; rbuf[n].sub = "P2:bluealsa --sbc-quality 启动参数"; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "强制 44.1k"; rbuf[n].sub = "P2:--a2dp-force-audio-cd"; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "音量控制"; rbuf[n].sub = "蓝牙音量由耳机/远端(AVRCP)控制"; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + + /* ⑧ 功耗档 */ + if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "功耗"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; } + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "扫描节流"; rbuf[n].sub = "进页自动扫描,退页/7s 停止"; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "发现超时"; rbuf[n].sub = "source 无需可发现(有限超时)"; rbuf[n].on = 0; rbuf[n].act = -2; n++; } + if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "后台保持"; rbuf[n].sub = "未播放时无流零开销"; rbuf[n].on = 0; rbuf[n].act = -2; n++; } return n; } -/* 执行 bluetoothctl 命令(带超时)+ 结果弹窗反馈 */ -static void bt_run(const char *title, const char *cmd) +/* 动作反馈(netmgrd 结果 / 直连回退结果)弹窗反馈 */ +static void bt_feedback(const char *title, int rc) { - char *o = popen_read_tmo(8000, cmd); char line[48]; - const char *txt[4]; txt[0] = line; - int ok = o && (strstr(o, "successful") || strstr(o, "Connected") || - strstr(o, "already") || strstr(o, "Paired")); - snprintf(line, sizeof line, "%s", ok ? "操作成功" : "操作失败/超时"); - if (o) free(o); + const char *txt[S_NET_MAX]; txt[0] = line; + snprintf(line, sizeof line, "%s", rc == MEOW_OK ? "操作成功" : "操作失败/超时"); s_dd_modal(title, txt, 1, -1); } -/* 动作行(SUB_ACT)点击:连接/断开/配对 */ +/* 动作行(SUB_ACT)点击:连接/断开/配对/取消配对/输出目标/编解码器 */ static int bt_act(void *ctx, int idx, int act) { (void)idx; bt_ctx_t *c = ctx; char mac[24] = ""; + /* 输出目标/编解码器:与本页设备行 act 段(CONN/PAIRED/NEAR base)不冲突,先处理 */ + if (act == BT_ACT_OUTPUT) { + c->output_bt = !c->output_bt; + meow_settings_set_bool("audio.output_bt", c->output_bt); + meow_settings_save(); + logmsg("bt_page: output_target=%s", c->output_bt ? "bt" : "dac"); + return 1; /* 重绘 + 重填行 */ + } + if (act == BT_ACT_CODEC) { + char codec[16] = "auto"; int idx0 = BT_CODEC_AUTO; + if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0) + for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx0 = k; break; } + int ni = (idx0 + 1) % BT_CODEC_N; + meow_settings_set_str("bt.codec", bt_codec_names[ni]); + meow_settings_save(); + logmsg("bt_page: codec=%s", bt_codec_names[ni]); + return 1; + } /* 定位 MAC(按 act 段) */ if (act >= BT_ACT_NEAR_BASE) { int i = act - BT_ACT_NEAR_BASE; @@ -168,43 +300,75 @@ static int bt_act(void *ctx, int idx, int act) if (i < 0 || i >= c->conn_n) return 0; snprintf(mac, sizeof mac, "%s", c->conn[i].mac); } + if (!mac[0]) return 0; if (act >= BT_ACT_NEAR_BASE) { - /* 附近未配对 → pair;已配对(理论上过滤了)→ connect */ - char cmd[192]; - snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl pair %s 2>/dev/null", mac); - bt_run("配对", cmd); + /* 附近:未配对 → pair;已配对(理论上过滤了)→ connect */ + bt_feedback("配对", nm_bt_pair(mac)); } else if (act >= BT_ACT_PAIRED_BASE) { /* 已配对:未连 → connect;已连 → disconnect */ - if (bt_is_conn(c, mac)) { - char cmd[192]; - snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac); - bt_run("断开", cmd); - } else { - char cmd[192]; - snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl connect %s 2>/dev/null", mac); - bt_run("连接", cmd); - } + if (bt_is_conn(c, mac)) bt_feedback("断开", nm_bt_disconnect(mac)); + else bt_feedback("连接", nm_bt_connect(mac)); } else { /* 已连接 → disconnect */ - char cmd[192]; - snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac); - bt_run("断开", cmd); + bt_feedback("断开", nm_bt_disconnect(mac)); } bt_role_refresh(1); - net_bt_scan_trigger(); + nm_bt_scan_on(); c->full = 1; /* 动作后:下一次 rows() 全量重扫 */ logmsg("bt_page: act act=%d mac=%s", act, mac); return 1; /* 重绘 + 重填行 */ } +/* 点普通行(A2DP 摘要等):弹 A2DP/设备详情。仅 A2DP 摘要(act=-1)响应;其余 info 行(act=-2)无操作。 */ +static int bt_detail(void *ctx, int idx, int act) +{ + (void)idx; + if (act != -1) return 0; + bt_ctx_t *c = ctx; + if (c->a2dp_n > 0) { + const bt_ep_t *ep = &c->a2dp[0]; + char buf[S_NET_MAX][48]; + const char *item[S_NET_MAX]; + int n = 0; + for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; } + snprintf(buf[n], 48, "设备名: %s", bt_conn_name(c, ep->mac)); item[n] = buf[n]; n++; + snprintf(buf[n], 48, "MAC: %s", ep->mac[0] ? ep->mac : "-"); item[n] = buf[n]; n++; + snprintf(buf[n], 48, "codec: %s", ep->codec[0] ? ep->codec : "-"); item[n] = buf[n]; n++; + snprintf(buf[n], 48, "采样率: %s", ep->rate[0] ? ep->rate : "-"); item[n] = buf[n]; n++; + snprintf(buf[n], 48, "声道/位深: %s/%s", ep->chn[0] ? ep->chn : "-", ep->bits[0] ? ep->bits : "-"); + item[n] = buf[n]; n++; + snprintf(buf[n], 48, "传输: %s", ep->acquired ? "播放中" : "空闲"); item[n] = buf[n]; n++; + snprintf(buf[n], 48, "角色: %s", !strcmp(ep->role, "src") ? "设备=source(送流)" : + !strcmp(ep->role, "snk") ? "设备=sink(被连)" : "未知"); + item[n] = buf[n]; n++; + s_dd_modal("A2DP 详情", item, n, -1); + return 1; + } + if (c->conn_n > 0) { + char buf[S_NET_MAX][48]; + const char *item[S_NET_MAX]; + int n = 0; + snprintf(buf[n], 48, "设备名: %s", c->conn[0].name[0] ? c->conn[0].name : c->conn[0].mac); + item[n] = buf[n]; n++; + snprintf(buf[n], 48, "MAC: %s", c->conn[0].mac); item[n] = buf[n]; n++; + snprintf(buf[n], 48, "状态: 已连接"); item[n] = buf[n]; n++; + snprintf(buf[n], 48, "输出: %s", c->output_bt ? "蓝牙" : "DAC"); item[n] = buf[n]; n++; + s_dd_modal("蓝牙详情", item, n, -1); + return 1; + } + return 0; +} + + /* 运行蓝牙独立子页面(阻塞到返回) */ int run_bt_page(void) { static bt_ctx_t c; memset(&c, 0, sizeof c); + c.conn_n = -1; sub_page_t p; memset(&p, 0, sizeof p); p.title = "蓝牙"; @@ -212,11 +376,13 @@ int run_bt_page(void) p.top_sw_set = bt_top_sw_set; p.rows = bt_rows; p.act = bt_act; - p.detail = NULL; /* 蓝牙动作都走 SUB_ACT(act),无 SUB_ROW 详情 */ + p.detail = bt_detail; /* SUB_ROW(A2DP 摘要等)→ A2DP 详情弹窗 */ p.live = 1; /* 附近扫描驻留更新(约 2s 重扫行;仅附近/角色,不反复 spawn) */ p.ctx = &c; c.full = 1; /* 首行全量重扫 */ - net_bt_scan_trigger(); /* 进页立即触发一次附近扫描(异步,不阻塞) */ + nm_bt_scan_on(); /* 进页:触发一次附近扫描(节流;不进蓝牙不空转 §七) */ bt_role_refresh(1); - return meow_subpage_run(&p); + int rc = meow_subpage_run(&p); + nm_bt_scan_off(); /* 退页:扫描收尾(§七 省电) */ + return rc; } diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index 656332f..072c4cc 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -821,11 +821,32 @@ int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* int nm_wifi_disconnect(char *err, int elen); int nm_wifi_toggle(int on); -/* BT(IPC 优先:on/off/status/paired;连接/断开/配对/附近扫描 netmgrd 无子命令,仍直连) */ +/* BT(IPC 优先:全量子命令权威收口 P1;netmgrd 不可达回退 net.c 直连)。 + * P1 收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八):connect/disconnect/pair/unpair/scan 写级 + * 迁 netmgrd;connected/devices/a2dp 读级也迁 netmgrd(launcher 只做客户端)。 */ int nm_bt_powered(void); int nm_bt_onoff(int on); int nm_bt_status(int *powered, int *paired_n, char *addr, int alen); int nm_bt_paired(meow_net_t *out, int max); +int nm_bt_connected(meow_net_t *out, int max); +int nm_bt_devices(meow_net_t *out, int max); +/* A2DP 单端点详情(codec/采样率/声道/位深/acquired/role;netmgrd bt a2dp 解析) */ +typedef struct { + char mac[24]; /* 端点设备 MAC(与 connected/paired 列表可 join) */ + char codec[16]; /* SBC/AAC/aptX/LDAC...;空=未知 */ + char rate[16]; /* 采样率(如 "44100");空=未知 */ + char chn[8]; /* 声道数 */ + char bits[8]; /* 位深 */ + int acquired; /* 1=传输已获取(streaming 中) */ + char role[8]; /* "src"/"snk"/"-"(设备侧 A2DP 角色) */ +} bt_ep_t; +int nm_bt_a2dp(bt_ep_t *eps, int max); /* 返回端点数 */ +int nm_bt_connect(const char *mac); +int nm_bt_disconnect(const char *mac); +int nm_bt_pair(const char *mac); +int nm_bt_unpair(const char *mac); +void nm_bt_scan_on(void); +void nm_bt_scan_off(void); const meow_ui_theme_t *s_ui_theme(void); void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h); int scan_apps(struct app apps[MAX_APPS]); diff --git a/patch_v2/meow_launcherd/meow_settings.c b/patch_v2/meow_launcherd/meow_settings.c index 1046305..92ea084 100644 --- a/patch_v2/meow_launcherd/meow_settings.c +++ b/patch_v2/meow_launcherd/meow_settings.c @@ -140,6 +140,12 @@ static const meow_setting_def g_defs[] = { { "audio.volume_max", SET_INT, "100", 0, 100, NULL, NULL }, { "audio.volume_step", SET_INT, "1", 1, 5, NULL, NULL }, { "audio.output", SET_STR, "hw:0,0", 0,0, NULL, NULL }, + /* P1 蓝牙 P1:输出目标偏好(0=DAC/本地,1=蓝牙 OUT_BT)。audiod 侧 target 切换在 P2 + * 实装;P1 仅作为「输出目标」行/摘要的持久化键(蓝牙子页写,DAC 侧不受影响)。 */ + { "audio.output_bt", SET_BOOL, "0", 0, 0, NULL, NULL }, + /* P1 蓝牙:codec 偏好(auto/SBC/AAC/aptX/LDAC;P2 接 alsa.conf pcm.bt/bluealsa 生效)。 + * 当前实际协商 codec 由 a2dp 状态实时读,本键只是页面「编解码器」选项的持久化。 */ + { "bt.codec", SET_STR, "auto", 0, 0, NULL, NULL }, /* 网络(M2.39 设置页 WiFi/蓝牙开关;wifi 默认开=允许 ensure_wifi 自动连网, * 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */ { "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL }, diff --git a/patch_v2/meow_launcherd/netmgr_ipc.c b/patch_v2/meow_launcherd/netmgr_ipc.c index 5184abf..5c36f7b 100644 --- a/patch_v2/meow_launcherd/netmgr_ipc.c +++ b/patch_v2/meow_launcherd/netmgr_ipc.c @@ -151,6 +151,15 @@ static void nm_decode_ssid(const char *src, char *dst, int dlen) dst[o] = 0; } +/* P1:拷一个「空格分隔的值 token」到 dst(a2dp 字段 extraction 用)。 */ +static void nm_val_field(char **p, char *dst, int dlen) +{ + while (**p == ' ' || **p == '\t') (*p)++; + int o = 0; + while (**p && **p != ' ' && **p != '\t' && **p != '\n' && o < dlen - 1) dst[o++] = *(*p)++; + dst[o] = 0; +} + /* —— netmgrd `wifi status` 单行摘要解析:`wifi state=X connected=N ssid=S ip=I rssi=R`。 * 已知字段以「空格+key=」分隔;ssid 可能含空格,需读到下一个已知字段为止。 —— */ static int nm_is_status_key(const char *p) @@ -432,7 +441,9 @@ int nm_wifi_toggle(int on) } -/* ================= BT(IPC 优先:on/off/status/paired;其余子命令 netmgrd 无,直连)================= */ +/* ================= BT(IPC 优先:全量子命令权威收口 P1;不可达回退直连)================= */ + +static int nm_parse_devices(const char *txt, meow_net_t *out, int max); /* 前向声明 */ int nm_bt_powered(void) { @@ -478,28 +489,142 @@ int nm_bt_paired(meow_net_t *out, int max) int rc = netmgr_bt("paired", NULL, text, sizeof text); if (rc == NETMGR_UNREACH) return net_bt_paired(out, max); if (rc != MEOW_OK) return 0; + return nm_parse_devices(text, out, max); +} + +/* ---- P1 收口:连接/断开/配对/扫描/A2DP 全部迁 netmgrd(权威),不可达回退直连 ---- */ + +/* 通用 `Device ` 行解析(netmgrd bt {connected,paired,devices} 文本;launcher 只做客户端)。 */ +static int nm_parse_devices(const char *txt, meow_net_t *out, int max) +{ int n = 0; + if (!txt || max <= 0) return 0; if (max > S_NET_MAX) max = S_NET_MAX; - if (text[0]) { - char *line = text; - while (line && *line && n < max) { - char *eol = strchr(line, '\n'); - if (eol) *eol = 0; - char *p = strstr(line, "Device "); - if (p) { - p += 7; - char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++; - char *nm2 = mac; while (*nm2 == ' ' || *nm2 == '\t') nm2++; - int ml = (int)(mac - p); if (ml > 47) ml = 47; - if (ml > 0 && ml < (int)sizeof out[n].mac) { - memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0; - } - snprintf(out[n].name, sizeof out[n].name, "%s", *nm2 ? nm2 : p); - out[n].id = -1; out[n].sec[0] = 0; - n++; + char *line = (char *)txt; + while (line && *line && n < max) { + char *eol = strchr(line, '\n'); if (eol) *eol = 0; + char *p = strstr(line, "Device "); + if (p) { + p += 7; + char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++; + char *nm2 = mac; while (*nm2 == ' ' || *nm2 == '\t') nm2++; + int ml = (int)(mac - p); if (ml > 47) ml = 47; + if (ml > 0 && ml < (int)sizeof out[n].mac) { + memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0; } - line = eol ? eol + 1 : NULL; + snprintf(out[n].name, sizeof out[n].name, "%s", *nm2 ? nm2 : p); + out[n].id = -1; out[n].sec[0] = 0; + n++; } + line = eol ? eol + 1 : (char *)0; } return n; } + +int nm_bt_connected(meow_net_t *out, int max) +{ + char text[MEOW_IPC_MAX - 32]; + int rc = netmgr_bt("connected", NULL, text, sizeof text); + if (rc == NETMGR_UNREACH) return net_bt_connected(out, max); /* 直连回退 */ + if (rc != MEOW_OK) return 0; + return nm_parse_devices(text, out, max); +} + +int nm_bt_devices(meow_net_t *out, int max) +{ + char text[MEOW_IPC_MAX - 32]; + int rc = netmgr_bt("devices", NULL, text, sizeof text); + if (rc == NETMGR_UNREACH) return net_bt_scan(out, max); /* 直连回退(读扫描缓存) */ + if (rc != MEOW_OK) return 0; + return nm_parse_devices(text, out, max); +} + +/* A2DP 端点解析:"A2DP codec=.. rate=.. channels=.. bits=.. acquired=.. role=.." */ +int nm_bt_a2dp(bt_ep_t *eps, int max) +{ + if (max <= 0) return 0; + char text[MEOW_IPC_MAX - 32]; + int rc = netmgr_bt("a2dp", NULL, text, sizeof text); + if (rc == NETMGR_UNREACH || rc != MEOW_OK) return 0; /* A2DP 详情无直连回退(P2 bluealsa 才接) */ + int n = 0; + char *line = text; + while (line && *line && n < max) { + char *eol = strchr(line, '\n'); if (eol) *eol = 0; + if (!strncmp(line, "A2DP ", 5)) { + char *p = line + 5; + bt_ep_t *e = &eps[n]; + memset(e, 0, sizeof *e); + snprintf(e->role, sizeof e->role, "%s", "-"); + e->acquired = 0; + char *mac = p; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n') mac++; + int ml = (int)(mac - p); if (ml > 23) ml = 23; + if (ml > 0) { memcpy(e->mac, p, (size_t)ml); e->mac[ml] = 0; } + char *t = mac; + while (*t) { + while (*t == ' ' || *t == '\t') t++; + if (!strncmp(t, "codec=", 6)) { t += 6; nm_val_field(&t, e->codec, sizeof e->codec); } + else if (!strncmp(t, "rate=", 5)) { t += 5; nm_val_field(&t, e->rate, sizeof e->rate); } + else if (!strncmp(t, "channels=", 9)) { t += 9; nm_val_field(&t, e->chn, sizeof e->chn); } + else if (!strncmp(t, "bits=", 5)) { t += 5; nm_val_field(&t, e->bits, sizeof e->bits); } + else if (!strncmp(t, "acquired=", 9)) { t += 9; e->acquired = atoi(t); } + else if (!strncmp(t, "role=", 5)) { t += 5; nm_val_field(&t, e->role, sizeof e->role); } + while (*t && *t != ' ' && *t != '\t' && *t != '\n') t++; + } + n++; + } + line = eol ? eol + 1 : NULL; + } + return n; +} + +/* 直连回退:bluetoothctl (带超时)。返回 MEOW_OK/MEOW_ERR_*。 */ +static int bt_direct_cmd(const char *sub, const char *mac) +{ + if (!mac || !mac[0]) return MEOW_ERR_INVAL; + char cmd[192]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl %s %s 2>/dev/null", sub, mac); + char *o = popen_read_tmo(8000, cmd); + int ok = o && (strstr(o, "successful") || strstr(o, "Connected") || + strstr(o, "already") || strstr(o, "Paired")); + if (o) free(o); + return ok ? MEOW_OK : MEOW_ERR_NODEV; +} + +int nm_bt_connect(const char *mac) +{ + char text[128]; + int rc = netmgr_bt("connect", mac, text, sizeof text); + if (rc == NETMGR_UNREACH) return bt_direct_cmd("connect", mac); + return rc; +} +int nm_bt_disconnect(const char *mac) +{ + char text[128]; + int rc = netmgr_bt("disconnect", mac, text, sizeof text); + if (rc == NETMGR_UNREACH) return bt_direct_cmd("disconnect", mac); + return rc; +} +int nm_bt_pair(const char *mac) +{ + char text[128]; + int rc = netmgr_bt("pair", mac, text, sizeof text); + if (rc == NETMGR_UNREACH) return bt_direct_cmd("pair", mac); + return rc; +} +int nm_bt_unpair(const char *mac) +{ + char text[128]; + int rc = netmgr_bt("unpair", mac, text, sizeof text); + if (rc == NETMGR_UNREACH) return bt_direct_cmd("remove", mac); + return rc; +} +void nm_bt_scan_on(void) +{ + if (!netmgr_ok()) { net_bt_scan_trigger(); return; } /* 直连回退 */ + netmgr_bt("scan", "on", NULL, 0); +} +void nm_bt_scan_off(void) +{ + if (!netmgr_ok()) return; + netmgr_bt("scan", "off", NULL, 0); +} diff --git a/patch_v2/meow_launcherd/test_bt_page.c b/patch_v2/meow_launcherd/test_bt_page.c new file mode 100644 index 0000000..818247a --- /dev/null +++ b/patch_v2/meow_launcherd/test_bt_page.c @@ -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 +#include +#include +#include +#include + +#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:" */ +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; +} diff --git a/patch_v2/meow_netmgrd/meow_netmgrd.c b/patch_v2/meow_netmgrd/meow_netmgrd.c index c89cd24..bc20185 100644 --- a/patch_v2/meow_netmgrd/meow_netmgrd.c +++ b/patch_v2/meow_netmgrd/meow_netmgrd.c @@ -69,6 +69,9 @@ static const char *g_sock = "/usr/data/meow_net.sock"; /* MEOW_NET_S static const char *g_wpa_cli = "wpa_cli"; /* MEOW_WPA_CLI */ static const char *g_hciconfig = "hciconfig"; /* MEOW_HCICONFIG */ static const char *g_bluetoothctl= "bluetoothctl"; /* MEOW_BLUETOOTHCTL */ +static const char *g_hcitool = "hcitool"; /* MEOW_HCITOOL(内核 ACL 连接权威) */ +static const char *g_bluealsa_cli= "bluealsa-cli"; /* MEOW_BLUEALSA_CLI(A2DP 传输详情) */ +static const char *g_bt_scan_cache = "/tmp/meow_bt_scan.txt"; /* MEOW_BT_SCAN_CACHE(扫描缓存;launcher net.c 同路径) */ static const char *g_ip = "ip"; /* MEOW_IP */ static const char *g_wifi_if = "wlan0"; /* MEOW_WIFI_IF */ @@ -353,6 +356,7 @@ typedef struct { int powered; char address[32]; int paired_n; /* -1=null */ + int connected_n; /* 已连接(内核 ACL 权威)计数;-1=null */ } bt_t; typedef struct { @@ -603,10 +607,12 @@ static void wifi_status(wifi_t *w) } /* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */ +static int bt_acl_macs(char macs[][24], int max); /* 前向声明(bt_status 早于其定义) */ static void bt_status(bt_t *b) { memset(b, 0, sizeof *b); b->paired_n = -1; + b->connected_n = -1; char p[192]; snprintf(p, sizeof p, "%s/hci0", g_bt_dir); if (access(p, F_OK) == 0) b->available = 1; @@ -670,6 +676,286 @@ static void bt_status(bt_t *b) } b->paired_n = n; } + /* 已连接数:内核 ACL(hcitool con)权威;快速,不受 bluetoothctl 过滤/慢启动影响。 */ + { + char macs[8][24]; + b->connected_n = bt_acl_macs(macs, 8); + } +} + +/* ================================================================ + * P1:蓝牙权威收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八 P1)。 + * 权威路径设计(修复「已连接设备显示不全」根因): + * - 已连接 = 内核 ACL(hcitool con,快速/权威/不受 bluetoothctl 过滤影响), + * 名/配对来自 bluetoothctl `paired-devices`/`devices`(蓝虫子缓存)——不再依赖 + * bluetoothctl `devices Connected`(慢 + 只反映 agent 缓存视图,易截断)。 + * - A2DP 详情 = bluealsa-cli a2dp list(codec/采样率/声道/位深/传输状态/角色)。 + * - 扫描 = netmgrd 统一生命周期(scan on 才扫、off/超时收尾;缓存 /tmp/meow_bt_scan.txt)。 + * 输出均为文本(m_net_cmd_result_t.text),launcher 只做客户端解析,不再 bluetoothctl popen。 + * ================================================================ */ +static int bt_hex(int c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} +static int bt_mac_eq(const char *a, const char *b) +{ + for (int i = 0; i < 17; i++) { + if (bt_hex((unsigned char)a[i]) != bt_hex((unsigned char)b[i])) return 0; + if (i % 3 == 2 && a[i] != b[i]) return 0; + } + return 1; +} +/* 在行内找第一个形如 XX:XX:XX:XX:XX:XX 的 MAC,返其指针(否则 NULL)。 */ +static const char *bt_find_mac(const char *s) +{ + while (*s) { + int ok = 1; + for (int i = 0; i < 17; i++) { + int exp_colon = (i % 3 == 2); + if (exp_colon) { if (s[i] != ':') { ok = 0; break; } } + else if (bt_hex((unsigned char)s[i]) < 0) { ok = 0; break; } + } + if (ok) return s; + s++; + } + return NULL; +} + +/* 内核 ACL 连接 MAC(hcitool con)。返回数(≤max)。 */ +static int bt_acl_macs(char macs[][24], int max) +{ + char cmd[160]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s con 2>/dev/null", g_hcitool); + char buf[1024]; + if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) return 0; + int n = 0; + char *line = buf; + while (line && *line && n < max) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + const char *m = bt_find_mac(line); + if (m) { memcpy(macs[n], m, 17); macs[n][17] = 0; n++; } + line = nl ? nl + 1 : NULL; + } + return n; +} + +/* 设备名解析(bluetoothctl `paired-devices`)+`devices` 两次兜底;命中返 1。 */ +static int bt_name_for_mac(const char *mac, char *name, int nlen) +{ + if (name && nlen > 0) name[0] = 0; + static const char *subs[] = { "paired-devices", "devices" }; + for (int s = 0; s < 2; s++) { + char cmd[176]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null", + g_bluetoothctl, subs[s]); + char buf[1024]; + if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) continue; + char *line = buf; + while (line && *line) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + char *q = line; while (*q == ' ' || *q == '\t') q++; + if (!strncmp(q, "Device ", 7)) { + q += 7; + char *m = q; while (*m && *m != ' ' && *m != '\t' && *m != '\n' && *m != '\r') m++; + int ml = (int)(m - q); + if (ml == 17 && bt_mac_eq(q, mac)) { + char *nm = m; while (*nm == ' ' || *nm == '\t') nm++; + char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; + *e = 0; + int l = (int)(e - nm); if (l > nlen - 1) l = nlen - 1; + if (l > 0) memcpy(name, nm, (size_t)l), name[l] = 0; + else { memcpy(name, mac, 17); name[17] = 0; } + return 1; + } + } + line = nl ? nl + 1 : NULL; + } + } + return 0; +} + +/* 已连接设备文本(权威=内核 ACL;名经 paired/devices 兜底)。行格式 "Device "。 */ +static void bt_connected_text(char *buf, int cap) +{ + int off = 0; + char macs[8][24]; + int n = bt_acl_macs(macs, 8); + for (int i = 0; i < n; i++) { + char name[64] = ""; + bt_name_for_mac(macs[i], name, sizeof name); + int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", macs[i], name); + if (l <= 0 || off + l >= cap) break; + off += l; + } + buf[off] = 0; +} + +/* A2DP 传输详情:bluealsa-cli a2dp list → 每端点一行 + * "A2DP codec=<..> rate=<..> channels= bits= acquired=<0|1> role=" + * 解析宽松:字段缺失留空/0;整串不可辨认时退回原文。 */ +static void bt_a2dp_text(char *buf, int cap) +{ + char cmd[176]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s a2dp list 2>/dev/null", g_bluealsa_cli); + char raw[MEOW_IPC_MAX - 16]; + if (run_cmd(raw, sizeof raw, RUNCMD_TMO_MS, cmd) <= 0) { + snprintf(buf, (size_t)cap, "A2DP none\n"); + return; + } + /* 端点累积器 */ + char emac[24] = "", role[4] = "-"; + char codec[16] = "", rate[16] = "", chn[8] = "", bits[8] = ""; + int acquired = -1, has_mac = 0, wrote = 0, off = 0; + char *line = raw; + while (line && *line) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + char *p = line; while (*p == ' ' || *p == '\t') p++; + if (*p == '/') { /* 新端点:/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc[:] */ + if (has_mac && wrote) { + int l = snprintf(buf + off, (size_t)(cap - off), + "A2DP %s codec=%s rate=%s channels=%s bits=%s acquired=%d role=%s\n", + emac, codec[0] ? codec : "-", rate[0] ? rate : "-", + chn[0] ? chn : "-", bits[0] ? bits : "-", + acquired >= 0 ? acquired : 0, role); + if (l > 0 && off + l < cap) off += l; + } + /* 重置并为新端点解析 MAC/role */ + emac[0] = role[0] = codec[0] = rate[0] = chn[0] = bits[0] = 0; + snprintf(role, sizeof role, "%s", "-"); + acquired = -1; has_mac = 0; wrote = 0; + const char *dv = strstr(p, "dev_"); + if (dv) { + dv += 4; + int j = 0; + const char *q = dv; + while (*q && j < 17) { + if (*q == '_') { if (j == 2 || j == 5 || j == 8 || j == 11 || j == 14) emac[j] = ':'; else break; } + else if (bt_hex((unsigned char)*q) >= 0) emac[j] = *q; + else break; + j++; q++; + } + emac[17] = 0; + has_mac = (j == 17); + if (strstr(p, "a2dpsrc")) snprintf(role, sizeof role, "%s", "src"); + else if (strstr(p, "a2dpsnk")) snprintf(role, sizeof role, "%s", "snk"); + } else has_mac = 0; + wrote = 1; + line = nl ? nl + 1 : NULL; + continue; + } + /* 字段行:Codec:/Sampling:/Channels:/Bits:/State:/Acquired: */ + { const char *v = strstr(p, "Codec:"); if (v && codec[0] == 0) { + v += 6; while (*v == ' ' || *v == '\t') v++; + { const char *e = v; while (*e && *e != '\n' && *e != '\r') e++; + while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--; + int l = (int)(e - v); if (l > 15) l = 15; memcpy(codec, v, (size_t)l); codec[l] = 0; } } } + { const char *v = strstr(p, "Sampling"); if (v && rate[0] == 0) { + v = strchr(v, ':'); if (v) { v++; while (*v == ' ' || *v == '\t') v++; + { const char *e = v; while (*e && *e != '\n' && *e != '\r') e++; + while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--; + int l = (int)(e - v); if (l > 15) l = 15; memcpy(rate, v, (size_t)l); rate[l] = 0; } } } } + { const char *v = strstr(p, "Channels:"); if (v && chn[0] == 0) { + v += 9; while (*v == ' ' || *v == '\t') v++; + { const char *e = v; while (*e && *e != '\n' && *e != '\r') e++; + while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--; + int l = (int)(e - v); if (l > 7) l = 7; memcpy(chn, v, (size_t)l); chn[l] = 0; } } } + { const char *v = strstr(p, "Bits"); if (v && bits[0] == 0) { + v = strchr(v, ':'); if (v) { v++; while (*v == ' ' || *v == '\t') v++; + { const char *e = v; while (*e && *e != '\n' && *e != '\r') e++; + while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--; + int l = (int)(e - v); if (l > 7) l = 7; memcpy(bits, v, (size_t)l); bits[l] = 0; } } } } + if (strstr(p, "State:") || strstr(p, "State :") || strstr(p, "Acquired")) { + if (strstr(p, "RUNNING") || strstr(p, "ACTIVE") || strstr(p, "acquired") || strstr(p, "yes")) + acquired = 1; + } + line = nl ? nl + 1 : NULL; + } + /* 尾端点收尾 */ + if (has_mac && wrote) { + int l = snprintf(buf + off, (size_t)(cap - off), + "A2DP %s codec=%s rate=%s channels=%s bits=%s acquired=%d role=%s\n", + emac, codec[0] ? codec : "-", rate[0] ? rate : "-", + chn[0] ? chn : "-", bits[0] ? bits : "-", + acquired >= 0 ? acquired : 0, role); + if (l > 0 && off + l < cap) off += l; + } + if (off == 0) snprintf(buf, (size_t)cap, "A2DP none\n"); + else buf[off] = 0; +} + +/* A2DP 摘要(首个带 codec/rate 的端点)给 overview JSON;失败置空。 */ +static void bt_a2dp_summary(char *codec, int clen, char *rate, int rlen) +{ + if (codec && clen > 0) codec[0] = 0; + if (rate && rlen > 0) rate[0] = 0; + char buf[MEOW_IPC_MAX - 16]; + bt_a2dp_text(buf, sizeof buf); + char *line = buf; + while (line && *line) { + char *nl = strchr(line, '\n'); if (nl) *nl = 0; + if (!strncmp(line, "A2DP ", 5)) { + const char *c = strstr(line, " codec="); + const char *r = strstr(line, " rate="); + if (c && codec) { c += 7; const char *e = c; while (*e && *e != ' ' && *e != '\n') e++; + int l = (int)(e - c); if (l > clen - 1) l = clen - 1; memcpy(codec, c, (size_t)l); codec[l] = 0; } + if (r && rate) { r += 6; const char *e = r; while (*e && *e != ' ' && *e != '\n') e++; + int l = (int)(e - r); if (l > rlen - 1) l = rlen - 1; memcpy(rate, r, (size_t)l); rate[l] = 0; } + if ((codec && codec[0]) || (rate && rate[0])) return; + } + line = nl ? nl + 1 : NULL; + } +} + +/* 附近设备文本:netmgrd 权威扫描缓存(/tmp/meow_bt_scan.txt),行 "Device "(去重)。 */ +static void bt_devices_text(char *buf, int cap) +{ + int off = 0; + FILE *f = fopen(g_bt_scan_cache, "r"); + if (!f) { buf[0] = 0; return; } + char seen[16][24]; int n_seen = 0; + char line[256]; + while (fgets(line, sizeof line, f) && n_seen < 16) { + char *p = line; while (*p == ' ' || *p == '\t') p++; + const char *dv = NULL; + if (!strncmp(p, "Device ", 7)) dv = p + 7; + else if (!strncmp(p, "[NEW] Device ", 13)) dv = p + 13; + if (!dv) continue; + const char *mac = bt_find_mac(dv); + if (!mac) continue; + int dup = 0; + for (int i = 0; i < n_seen; i++) if (bt_mac_eq(seen[i], mac)) { dup = 1; break; } + if (dup) continue; + memcpy(seen[n_seen], mac, 17); seen[n_seen][17] = 0; n_seen++; + char *nm = (char *)(mac + 17); while (*nm == ' ' || *nm == '\t') nm++; + char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; + *e = 0; + int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", mac, nm); + if (l <= 0 || off + l >= cap) break; + off += l; + } + fclose(f); + buf[off] = 0; +} + +/* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。 */ +static long g_bt_scan_ms = 0; +static void bt_scan_on(void) +{ + long t = mono_now(); + if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; + g_bt_scan_ms = t; + char cmd[256]; + snprintf(cmd, sizeof cmd, + "(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | " + "bluetoothctl > '%s' 2>&1 &", g_bt_scan_cache); + system(cmd); +} +static void bt_scan_off(void) +{ + system("PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl scan off 2>/dev/null"); } /* ---------------- rfkill:统一软开关 ---------------- */ @@ -974,6 +1260,11 @@ static void build_status_json(char *out, size_t cap) bt.powered ? "true" : "false"); jadd_str_or_null(&w, "address", bt.address); jadd_int_or_null(&w, "paired", bt.paired_n); + jadd_int_or_null(&w, "connected", bt.connected_n); + { char ac[16] = "", ar[16] = ""; + bt_a2dp_summary(ac, sizeof ac, ar, sizeof ar); + jadd_str_or_null(&w, "a2dp_codec", ac); + jadd_str_or_null(&w, "a2dp_rate", ar); } jadd(&w, "\"dns\":["); { char ns[DNS_MAX][40]; @@ -2326,8 +2617,9 @@ static int wifi_cmd_is_write(const char *cmd, const char *arg) } static int bt_cmd_is_write(const char *cmd) { - if (!strcmp(cmd, "status") || !strcmp(cmd, "paired")) return 0; - return 1; /* on/off */ + if (!strcmp(cmd, "status") || !strcmp(cmd, "paired") || + !strcmp(cmd, "connected") || !strcmp(cmd, "a2dp") || !strcmp(cmd, "devices")) return 0; + return 1; /* on/off/connect/disconnect/pair/unpair/scan */ } /* 写级授权决策(纯函数,宿主自测用;handle_conn 复用):读级任意;写级仅系统身份 + MeowOS 模式。 @@ -2597,14 +2889,13 @@ static void bt_onoff(const char *cmd, ctl_result_t *r) static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r) { - (void)arg; ctl_reset(r, MEOW_OK); if (!strcmp(cmd, "status")) { bt_t b; bt_status(&b); char msg[192]; - snprintf(msg, sizeof msg, "bt available=%d powered=%d address=%s paired=%d", - b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n); + snprintf(msg, sizeof msg, "bt available=%d powered=%d address=%s paired=%d connected=%d", + b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n, b.connected_n); ctl_sets(r, MEOW_OK, msg); } else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) { bt_onoff(cmd, r); @@ -2615,6 +2906,33 @@ static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r) int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); if (n <= 0) ctl_sets(r, MEOW_ERR_NODEV, "bt paired: 无输出 (bluetoothctl 不可用/未开)"); else ctl_sets(r, MEOW_OK, buf); + } else if (!strcmp(cmd, "connected")) { + char buf[MEOW_IPC_MAX - 16]; + bt_connected_text(buf, sizeof buf); + ctl_sets(r, MEOW_OK, buf); + } else if (!strcmp(cmd, "a2dp")) { + char buf[MEOW_IPC_MAX - 16]; + bt_a2dp_text(buf, sizeof buf); + ctl_sets(r, MEOW_OK, buf); + } else if (!strcmp(cmd, "devices")) { + char buf[MEOW_IPC_MAX - 16]; + bt_devices_text(buf, sizeof buf); + ctl_sets(r, MEOW_OK, buf); + } else if (!strcmp(cmd, "connect") || !strcmp(cmd, "disconnect") || + !strcmp(cmd, "pair") || !strcmp(cmd, "unpair")) { + if (!arg || !arg[0]) { char m[48]; snprintf(m, sizeof m, "bt %s: 需 ", cmd); ctl_sets(r, MEOW_ERR_INVAL, m); return; } + const char *sub = (!strcmp(cmd, "unpair")) ? "remove" : cmd; /* bluetoothctl 取消配对=remove */ + char cmd2[192]; + snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s %s 2>/dev/null", + g_bluetoothctl, sub, arg); + char buf[256]; + int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); + if (n >= 0) ctl_sets(r, MEOW_OK, buf[0] ? buf : "bt cmd: 已下发"); + else ctl_sets(r, MEOW_ERR_NODEV, "bt cmd: 后端执行失败"); + } else if (!strcmp(cmd, "scan")) { + if (!arg || !strcmp(arg, "on")) { bt_scan_on(); ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); } + else if (!strcmp(arg, "off")) { bt_scan_off(); ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); } + else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off"); } else { ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令"); } @@ -3204,9 +3522,15 @@ static int cli_status(void) int bpow = strstr(obj, "\"powered\":true") != NULL; jstr(obj, "address", baddr, sizeof baddr); int bpaired = -1; + int bconn = -1; jint(obj, "paired", &bpaired); - printf("bt available=%d powered=%d address=%s paired=%d\n", - bavail, bpow, baddr[0] ? baddr : "-", bpaired); + jint(obj, "connected", &bconn); + char bcodec[24] = "", brate[24] = ""; + jstr(obj, "a2dp_codec", bcodec, sizeof bcodec); + jstr(obj, "a2dp_rate", brate, sizeof brate); + printf("bt available=%d powered=%d address=%s paired=%d connected=%d a2dp=%s@%s\n", + bavail, bpow, baddr[0] ? baddr : "-", bpaired, bconn, + bcodec[0] ? bcodec : "-", brate[0] ? brate : "-"); } /* rfkill 数组 */ { @@ -3381,7 +3705,8 @@ static int cli_policy(int op, const char *entity, const char *arg) static const char *usage(void) { return "status | iface [name] | wifi scan|networks|connect [,psk]|disconnect|on|off" - " | bt status|on|off|paired | rfkill [ ] | udhcpc renew|release [if]" + " | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair " + " | rfkill [ ] | udhcpc renew|release [if]" " | apps | conns [entity] | top | dns status|stats|flush" " | policy show [entity] | policy set allow|block | policy domain +host|-host | policy del " " | help"; @@ -3444,6 +3769,15 @@ static int cli_run(int n, char **v) } if (!strcmp(cmd, "bt")) { if (!arg) { fprintf(stderr, "bt 子命令缺失(%s)\n", usage()); return 1; } + if (!strcmp(arg, "connect") || !strcmp(arg, "disconnect") || + !strcmp(arg, "pair") || !strcmp(arg, "unpair")) { + if (!arg2) { fprintf(stderr, "bt %s 需 \n", arg); return 1; } + return cli_cmd_result(MSG_NET_BT_CTL, arg, arg2); + } + if (!strcmp(arg, "scan")) { + if (arg2 && !strcmp(arg2, "off")) return cli_cmd_result(MSG_NET_BT_CTL, "scan", "off"); + return cli_cmd_result(MSG_NET_BT_CTL, "scan", "on"); + } return cli_cmd_result(MSG_NET_BT_CTL, arg, NULL); } if (!strcmp(cmd, "rfkill")) { @@ -3480,6 +3814,9 @@ int main(int argc, char **argv) if ((e = getenv("MEOW_WPA_CLI")) && e[0]) g_wpa_cli = e; if ((e = getenv("MEOW_HCICONFIG")) && e[0]) g_hciconfig = e; if ((e = getenv("MEOW_BLUETOOTHCTL")) && e[0]) g_bluetoothctl = e; + if ((e = getenv("MEOW_HCITOOL")) && e[0]) g_hcitool = e; + if ((e = getenv("MEOW_BLUEALSA_CLI")) && e[0]) g_bluealsa_cli = e; + if ((e = getenv("MEOW_BT_SCAN_CACHE")) && e[0]) g_bt_scan_cache = e; if ((e = getenv("MEOW_IP")) && e[0]) g_ip = e; if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e; if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e; diff --git a/patch_v2/meow_netmgrd/test_bt_ctl.c b/patch_v2/meow_netmgrd/test_bt_ctl.c new file mode 100644 index 0000000..32a0983 --- /dev/null +++ b/patch_v2/meow_netmgrd/test_bt_ctl.c @@ -0,0 +1,136 @@ +/* test_bt_ctl.c — meow_netmgrd 蓝牙 P1 权威收口 纯逻辑宿主自测(本程序非生产代码)。 + * + * 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问 + * FILE-STATIC 函数/全局:bt_cmd_is_write / bt_write_authorize / bt_ctl_execute(connect/disconnect/ + * pair/unpair/connected/a2dp/devices/status)。工具经桩脚本(g_bluetoothctl/g_hcitool/g_bluealsa_cli/ + * g_bt_scan_cache 指向 $T/桩)覆盖,不依赖真实蓝牙栈。 + * + * 用法: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 +#include +#include +#include +#include + +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); } +} + +int main(void) +{ + static const char *T = "/tmp/nmg_bt"; + char p[192]; + mkdir(T, 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, "00:1A:7D:DA:71:13\n"); + snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "on\n"); } + + /* 桩:bluetoothctl(paired/devices/connect/remove)、hcitool con、bluealsa-cli a2dp、扫描缓存 */ + snprintf(p, sizeof p, "%s/bluetoothctl", T); wf(p, + "#!/bin/sh\ncase \"$1\" in\n" + " paired|paired-devices|devices) echo 'Device 33:DE:2E:21:AD:30 Pudding'; echo 'Device 11:22:33:44:55:66 Speaker';;\n" + " connect|disconnect|pair|remove) echo 'Controller 00:1A:7D:DA:71:13'; echo 'Device 33:DE:2E:21:AD:30 Connected: yes';;\n" + "esac\nexit 0\n"); chmod(p, 0755); + snprintf(p, sizeof p, "%s/hcitool", T); wf(p, + "#!/bin/sh\necho '\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER'\n" + "echo '\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE'\nexit 0\n"); chmod(p, 0755); + snprintf(p, sizeof p, "%s/bluealsa-cli", T); wf(p, + "#!/bin/sh\nif [ \"${1:-}\" = \"a2dp\" ]; then\n" + "echo '/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:'\n" + "echo '\tCodec: SBC'\necho '\tSampling: 44100 Hz'\necho '\tChannels: 2'\n" + "echo '\tBits per Sample: 16'\necho '\tState: RUNNING'\necho '\tAcquired: yes'\nfi\nexit 0\n"); chmod(p, 0755); + snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p, + "[NEW] Device 33:DE:2E:21:AD:30 Pudding\n[NEW] Device AA:BB:CC:DD:EE:FF Earbuds\n"); + + g_bluetoothctl = ""; { char x[160]; snprintf(x, sizeof x, "%s/bluetoothctl", T); static char s[160]; + snprintf(s, sizeof s, "%s", x); g_bluetoothctl = s; } + g_hcitool = ""; { char x[160]; snprintf(x, sizeof x, "%s/hcitool", T); static char s[160]; + snprintf(s, sizeof s, "%s", x); g_hcitool = s; } + g_bluealsa_cli = ""; { char x[160]; snprintf(x, sizeof x, "%s/bluealsa-cli", T); static char s[160]; + snprintf(s, sizeof s, "%s", x); g_bluealsa_cli = s; } + g_bt_scan_cache = ""; { char x[160]; snprintf(x, sizeof x, "%s/bt_scan.txt", T); static char s[160]; + snprintf(s, sizeof s, "%s", x); g_bt_scan_cache = s; } + g_bt_dir[0] = 0; snprintf(g_bt_dir, sizeof g_bt_dir, "%s/bt", T); + + ctl_result_t r; + + 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); + + printf("== bt_ctl_execute 写级(connect/disconnect/pair/unpair,经桩 bluetoothctl) ==\n"); + bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r); + CHECK(r.code == MEOW_OK); + CHECK(strstr(r.text, "Connected") != NULL); + bt_ctl_execute("disconnect", "33:DE:2E:21:AD:30", &r); + CHECK(r.code == MEOW_OK); + bt_ctl_execute("pair", "33:DE:2E:21:AD:30", &r); + CHECK(r.code == MEOW_OK); + bt_ctl_execute("unpair", "33:DE:2E:21:AD:30", &r); + CHECK(r.code == MEOW_OK); + CHECK(strstr(r.text, "Connected") != NULL); /* remove 桩回 Connected: yes */ + bt_ctl_execute("connect", "", &r); + CHECK(r.code == MEOW_ERR_INVAL); /* 缺 mac → INVAL */ + bt_ctl_execute("scan", NULL, &r); + CHECK(r.code == MEOW_OK); /* scan on(默认) */ + CHECK(strstr(r.text, "scan on") != NULL); + bt_ctl_execute("scan", "off", &r); + CHECK(r.code == MEOW_OK); + + printf("== bt_ctl_execute 读级(connected/a2dp/devices/status,权威) ==\n"); + bt_ctl_execute("connected", NULL, &r); + CHECK(r.code == MEOW_OK); + CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30") != NULL); + CHECK(strstr(r.text, "Pudding") != NULL); + bt_ctl_execute("a2dp", NULL, &r); + CHECK(r.code == MEOW_OK); + CHECK(strstr(r.text, "codec=SBC") != NULL); + CHECK(strstr(r.text, "rate=44100") != NULL); + CHECK(strstr(r.text, "acquired=1") != NULL); + CHECK(strstr(r.text, "role=src") != NULL); + snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p, /* 重写(bt scan on 曾覆盖) */ + "[NEW] Device 33:DE:2E:21:AD:30 Pudding\n[NEW] Device AA:BB:CC:DD:EE:FF Earbuds\n"); + bt_ctl_execute("devices", NULL, &r); + CHECK(r.code == MEOW_OK); + CHECK(strstr(r.text, "Earbuds") != NULL); + CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30") != NULL); + bt_ctl_execute("status", NULL, &r); + CHECK(r.code == MEOW_OK); + CHECK(strstr(r.text, "connected=2") != NULL); + + 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); /* 读级任意 → 放 */ + + if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; } + printf("\nBT_CTL TEST: ALL PASS\n"); + return 0; +} diff --git a/patch_v2/meow_netmgrd/test_netmgrd.sh b/patch_v2/meow_netmgrd/test_netmgrd.sh index 96c32be..f1bc6ea 100755 --- a/patch_v2/meow_netmgrd/test_netmgrd.sh +++ b/patch_v2/meow_netmgrd/test_netmgrd.sh @@ -75,8 +75,39 @@ echo " UP RUNNING PSCAN" EOF cat > "$T/bluetoothctl" <<'EOF' #!/bin/sh -echo "Device 00:1A:7D:DA:71:13 My Headset" -echo "Device 11:22:33:44:55:66 Speaker" +case "${1:-}" in + paired|paired-devices) + echo "Device 00:1A:7D:DA:71:13 My Headset" + echo "Device 11:22:33:44:55:66 Speaker";; + devices) + echo "Device 00:1A:7D:DA:71:13 My Headset" + echo "Device 11:22:33:44:55:66 Speaker" + echo "Device 33:DE:2E:21:AD:30 Pudding";; + connect|disconnect|pair|remove) + echo "Controller 00:1A:7D:DA:71:13 Connected: yes";; + *) echo "Device 00:1A:7D:DA:71:13 My Headset"; echo "Device 11:22:33:44:55:66 Speaker";; +esac +exit 0 +EOF +cat > "$T/hcitool" <<'EOF' +#!/bin/sh +echo " < ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER" +echo " < ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE" +exit 0 +EOF +cat > "$T/bluealsa-cli" <<'EOF' +#!/bin/sh +if [ "${1:-}" = "a2dp" ]; then +echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:" +echo " Type: A2DP Source" +echo " Codec: SBC" +echo " Sampling: 44100 Hz" +echo " Channels: 2" +echo " Bits per Sample: 16" +echo " State: RUNNING" +echo " Acquired: yes" +fi +exit 0 EOF cat > "$T/ip" <<'EOF' #!/bin/sh @@ -86,13 +117,14 @@ for a in "$@"; do done exit 0 EOF -for f in wpa_cli hciconfig bluetoothctl ip; do chmod +x "$T/$f"; done +for f in wpa_cli hciconfig bluetoothctl hcitool bluealsa-cli ip; do chmod +x "$T/$f"; done mkdir -p "$T/logs" "$T/pid" # 期2:DNS 禁用(宿主无 root 不绑 53),观测层经 MEOW_TCP/UDP 打桩;amsd/svcd 缺省(实体归因走 proc:comm→unknown)。 ENVS="MEOW_NET_DIR=$NET MEOW_NETDEV=$T/netdev MEOW_NET_ROUTE=$T/route MEOW_RESOLV=$T/resolv MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT MEOW_BOOT_MODE=$T/boot_mode MEOW_BOOTTS_CACHE=$T/bootts MEOW_IP=$T/ip MEOW_WPA_CLI=$T/wpa_cli MEOW_HCICONFIG=$T/hciconfig MEOW_BLUETOOTHCTL=$T/bluetoothctl + MEOW_HCITOOL=$T/hcitool MEOW_BLUEALSA_CLI=$T/bluealsa-cli MEOW_BT_SCAN_CACHE=$T/bt_scan.txt MEOW_NET_SOCK=$T/net.sock MEOW_NET_POLL_SEC=1 MEOW_LOG_DIR=$T/logs MEOW_TCP=$T/tcp MEOW_UDP=$T/udp MEOW_NET_POLICY=$T/policy MEOW_AMS_SOCK=$T/no_ams MEOW_SVCD_SOCK=$T/no_svcd MEOW_DNS_DISABLE=1" @@ -137,10 +169,40 @@ ck T4a "permission denied" "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" bt status 2>&1) ck T5a "available=1 powered=1" "$OUT" ck T5b "paired=2" "$OUT" +ck T5b2 "connected=2" "$OUT" # P1:内核 ACL 权威已连接数 OUT=$(env $ENVS $Q "$T/meow_net" bt paired 2>&1) ck T5c "My Headset" "$OUT" ck T5d "Speaker" "$OUT" +# ---- T5e: bt connected(P1:内核 ACL 权威,名经 devices 缓存兜底)---- +OUT=$(env $ENVS $Q "$T/meow_net" bt connected 2>&1) +ck T5e "Device 33:DE:2E:21:AD:30 Pudding" "$OUT" +ck T5e2 "Device 11:22:33:44:55:66 Speaker" "$OUT" + +# ---- T5f: bt a2dp(P1:bluealsa-cli a2dp list → codec/rate/acquired/role)---- +OUT=$(env $ENVS $Q "$T/meow_net" bt a2dp 2>&1) +ck T5f "codec=SBC" "$OUT" +ck T5f2 "rate=44100" "$OUT" +ck T5f3 "acquired=1" "$OUT" +ck T5f4 "role=src" "$OUT" + +# ---- T5g: bt devices(P1:netmgrd 权威扫描缓存)---- +cat > "$T/bt_scan.txt" <<'EOF' +[NEW] Device 33:DE:2E:21:AD:30 Pudding +[NEW] Device AA:BB:CC:DD:EE:FF Earbuds +[DEL] Device 00:11:22:33:44:55 +EOF +OUT=$(env $ENVS $Q "$T/meow_net" bt devices 2>&1) +ck T5g "Device 33:DE:2E:21:AD:30 Pudding" "$OUT" +ck T5g2 "Device AA:BB:CC:DD:EE:FF Earbuds" "$OUT" + +# ---- T5h: bt 写级(connect/pair 等)在宿主 CLI=CALL_OTHER → 拒(仅系统身份+MeowOS 放行,见 T6)---- +OUT=$(env $ENVS $Q "$T/meow_net" bt connect 33:DE:2E:21:AD:30 2>&1); RC=$? +ck T5h "permission denied" "$OUT" +ck T5h2 "rc=1" "rc=$RC" +OUT=$(env $ENVS $Q "$T/meow_net" bt scan on 2>&1) +ck T5i "permission denied" "$OUT" + # ---- T6: HibyOS 只读降级(读可查,写拒)---- echo hiby > "$T/boot_mode" sleep 2 # 等守护下一拍热切换