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;
|
||||
}
|
||||
Reference in new issue
Block a user