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:
1 parent
8c677f127f
commit
eb07415f7b
8 files changed
+1178
-90
No files matched your search
@@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 <MAC> <Name>` 行解析(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 <mac> 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 <sub> <mac>(带超时)。返回 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 <MAC> <Name>"。 */
|
||||
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 <mac> codec=<..> rate=<..> channels=<n> bits=<n> acquired=<0|1> role=<src|snk|->"
|
||||
* 解析宽松:字段缺失留空/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 <MAC> <Name>"(去重)。 */
|
||||
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: 需 <mac>", 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 <ssid>[,psk]|disconnect|on|off"
|
||||
" | bt status|on|off|paired | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
|
||||
" | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair <mac>"
|
||||
" | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
|
||||
" | apps | conns [entity] | top | dns status|stats|flush"
|
||||
" | policy show [entity] | policy set <entity> allow|block | policy domain <entity> +host|-host | policy del <entity>"
|
||||
" | 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 需 <mac>\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;
|
||||
|
||||
@@ -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 <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); }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 # 等守护下一拍热切换
|
||||
|
||||
Reference in new issue
Block a user