评审 r1 status=p2-4items / merge=fix-then-merge(reviewed e4eda3a)。逐条: P2-1 未知态被写成 settings=0,把失真推到别的入口 - bt_page.c / wifi_page.c:`meow_settings_set_bool(..., disp == 1)` 加 `disp >= 0` 门。 disp == -1(daemon 与直连都读不到)时**保留用户原意图不动**。该键被 notif_center / settings_page 当显示态读、被 app_launcher 用来决定自动拉起 WiFi,写 0 会把「不知道」 污染成「确定已关」。 - test_bt_page.c 5d 期望同步改:未知 → settings 保持原意图 1(不再期望「回滚为 0」)。 P2-2 收尾后「探测未知」仍显示成确定态,第三态丢失 - mtog_calc:显示态**只认 probe**,probe < 0 直接返回 -1;last_real 不再顶替确定态 (`(void)last_real`,仅由调用方用于弹窗文案)。超时/下发失败且探测未知 → 顶栏保留 「关+?」、行内保留「状态未知」,与文件头契约一致。 - 弹窗文案按结论区分:「无法确认当前状态(上次已知:开启/关闭/未知)」vs 「切换未生效:当前仍为开启/关闭」——不再出现「当前仍为开启」自相矛盾(该文案只在 probe 已知且 != 意图时出现)。日志加 elapsed。 P2-3 deadline 起点错,注释上限是假的 - bt_page/wifi_page:`deadline` 从**点击时刻**起算(含下发耗时)。 - 注释改成如实上限:快路径 1~2s / ≈16 次探测;慢路径(on-off 读超时 1.5s + hciconfig 0.3~2s,之后一次探测 status 1.5s + 直连 1.8s ≈3.3s)总阻塞最坏 ≈7s、预算内只够 1 次 探测,此时只能确认「探测超时」而非「设备无响应」——文案已按此区分。异步化留给后续单。 P2-4 开关路径弹窗后缺 after_modal / 按键基线复位 - subpage.c:顶开关命中分支补上与行命中分支相同的后置清理(排空触摸 + 40ms 二次排空、 触摸基线/cooldown、key_router_rebase、kd_*/kp_poll 基线、m_prev_*、after_modal=1)。 s_dd_modal 是全屏压暗层,否则关闭后状态栏(0..27)残留压暗到下一次时钟重绘,电源/播放键 也可能带进弹窗里的陈旧边沿。 顺带(评审观察,未改全局超时策略) - netmgr_ipc.c:拆出 `nm_cmd_t(..., int *read_timeout)`;`nm_bt_power_state()` 走它, **读超时 = 状态未知且不把可达性缓存打成 down**(netmgrd 单线程忙时 `bt status` 自己 跑 hciconfig 可能超 1.5s;旧行为会标 down 1s,令紧随的 bt_rows 闪出「服务未运行(直连 模式)」并把后续探测切到 1.8s 直连)。WiFi/DNS 等其它调用保持旧语义(传 NULL)。 验证 - `bash patch_v2/meow_launcherd/build_launcher.sh test` → exit 0,**65 PASS / 0 FAIL / 0 warning** (矩阵 13 组,新增「下发失败·探测未知 → 未知不顶替」)。 - 交叉编译 0 warning;产物(临时副本,不入库)901020 B / md5 e21450ff6621a67fd854165413c58be1。 - `test_time32_abi.sh` → 4263=3 / 4403=0 ALL PASS;`pkg_install` 与 7 条切换中/未知态文案 均在 strings 中(旧的两条「无法确认蓝牙/WiFi 状态」已合并为「无法确认当前状态(上次已知:%s)」)。 - 真机 --sysdump(只读,临时文件已删):exit 0,bt_on=1/wifi_on=1 与设备现状一致; Music du 前后 199164544;未替换 /usr/bin/meow_launcherd、未重启 launcherd。 - 交付形态不变:本 commit 只动 5 个源码文件;二进制与 scripts/build/fw_content.conf 对 main 零 diff。
528 lines
27 KiB
C
528 lines
27 KiB
C
/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76;P1 权威收口重做)。
|
||
*
|
||
* 【P1 背景】BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八:连接/断开/配对/取消配对/扫描/A2DP 状态
|
||
* 查询全部收口进 netmgrd(BT_CTL 消息扩展),本子页**只做客户端**——经 netmgrd socket/IPC 查询
|
||
* 与下发,不再 bluetoothctl popen 直连(直连仅作 netmgrd 不可达时的回退保留)。
|
||
*
|
||
* 【已连接设备显示不全根因修复】旧实现用 `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 统一控。
|
||
*
|
||
* 【M47 开关可信化】顶栏开关的显示态只来自真实态查询(daemon `bt status` 的 powered,
|
||
* 不可达时直连 hciconfig 探测),三态:-1 未知 / 0 关 / 1 开;探测失败=未知(画「?」,不当已关)。
|
||
* settings 的 network.bluetooth_enabled 降级为「意图持久化」,不再决定开关外观。点击后先出示
|
||
* 「正在开启/关闭…(等待设备确认)」,下发命令 → 有界轮询真实态收敛 → 用真实态定稿;失败/超时
|
||
* 回滚并弹窗告知(不许乐观显示成功)。
|
||
*
|
||
* 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。连接/断开/配对经 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→断开) */
|
||
#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];
|
||
bt_ep_t a2dp[S_NET_MAX];
|
||
int conn_n, paired_n, near_n, a2dp_n;
|
||
int bt_on; /* 真实态三态(-1 未知 / 0 关 / 1 开)——只由 bt_state_now() 写 */
|
||
unsigned long long bt_on_ts; /* 上面这次查询的时刻(rows/top_sw 同一 tick 只查一次) */
|
||
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[768];
|
||
int pool_off;
|
||
} bt_ctx_t;
|
||
|
||
/* M47:settings 里的网络开关布尔值降级为「用户意图的持久化」,不再参与显示判定;
|
||
* 显示态一律来自 nm_bt_power_state()。 */
|
||
extern int nm_bt_power_state(void); /* netmgr_ipc.c:daemon 权威 → 直连探测,三态 */
|
||
|
||
/* 开关收敛参数(宿主测试可在 include 前改小,见 test_bt_page.c)。
|
||
* 上限从**点击时刻**起算(评审 r1 P2-3),故它约束的是「从点击到出结论」的总时长:
|
||
* - 快路径(daemon 正常,`bt status` 本地 IPC 几 ms):4s 内约 16 次探测,通常 1~2s 收敛;
|
||
* - 慢路径(netmgrd 卡住:on/off 读超时 1.5s → 直连 s_net_toggle_bt 的 hciconfig 0.3~2s;
|
||
* 之后一次探测 = status 1.5s 超时 + hciconfig 兜底 1.8s ≈ 3.3s):总阻塞最坏
|
||
* ≈3.5s(下发) + 3.3s(最后一次探测) ≈ **7s**,预算内只够 1 次探测。此时能确认的只是
|
||
* 「探测超时」,**不能**宣称「设备无响应」——弹窗文案按此区分(「无法确认当前状态」)。
|
||
* 250ms 只是快路径的轮询间隔(不 20ms 级忙轮询);慢路径由探测自身耗时决定。 */
|
||
#ifndef BT_TOGGLE_SETTLE_MS
|
||
#define BT_TOGGLE_SETTLE_MS 4000
|
||
#endif
|
||
#ifndef BT_TOGGLE_POLL_MS
|
||
#define BT_TOGGLE_POLL_MS 250
|
||
#endif
|
||
#define BT_STATE_TTL_MS 800 /* 真实态缓存:rows/top_sw 同一帧共用一次查询 */
|
||
|
||
|
||
|
||
/* 格式化并拷入 ctx 行副文本池(返回稳定指针;溢出则绕回——768B 远超单页行数所需)。 */
|
||
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;
|
||
}
|
||
|
||
|
||
/* ===== M47:开关「意图 → 真实态」收敛判定(纯函数;无 I/O、无 UI、无全局态)=====
|
||
* 输入:intent(1=要开/0=要关)、last_real(下发前最后一次可信真实态,-1 未知)、
|
||
* dispatch_ok(下发命令是否成功)、probe(有界等待后的探测结果,-1 未知)、
|
||
* timed_out(是否已到等待上限)。
|
||
* 输出(四个访问器共用同一套判定,拆开只为调用处可读):
|
||
* meow_toggle_display() 应显示的态(-1 未知 / 0 / 1):**只认 probe**。probe 未知 → -1,
|
||
* 不许回退 last_real 冒充确定态(评审 r1 P2-2:从没被确认过的旧值
|
||
* 一旦显示成确定态,就与本单「探测失败=未知」的契约自相矛盾)。
|
||
* meow_toggle_pending() 1=还要继续等:下发成功、未超时、且 probe 未等于意图。
|
||
* 注意「probe 未知」也算没收敛——绝不因为 last_real 恰好等于意图就收工。
|
||
* meow_toggle_rollback() 1=意图未达成(显示态 != 意图)→ UI 必须回滚到真实态。
|
||
* meow_toggle_error() 1=收尾后仍未确认 → 必须给用户可见的错误提示(不许静默失败)。
|
||
* last_real 不参与状态判定:它只由调用方用于弹窗文案(「上次已知为开启,当前无法确认」),
|
||
* 以及后续异步化时判断「回滚到哪」;保留在接口里是因为它是本单约定的输入维度。
|
||
* 判定矩阵见 test_bt_page.c「收敛矩阵」;wifi_page.c 复用同一实现。 */
|
||
static void mtog_calc(int intent, int last_real, int dispatch_ok, int probe, int timed_out,
|
||
int *disp, int *pending, int *rollback, int *error)
|
||
{
|
||
int want = intent ? 1 : 0;
|
||
int d = (probe >= 0) ? (probe ? 1 : 0) : -1; /* 未确认 = 未知,绝不拿旧值顶替 */
|
||
int confirmed = (probe >= 0) && ((probe ? 1 : 0) == want);
|
||
(void)last_real; /* 仅弹窗文案用,见函数头注释 */
|
||
*disp = d;
|
||
*pending = (dispatch_ok && !timed_out && !confirmed) ? 1 : 0;
|
||
*rollback = (d == want) ? 0 : 1;
|
||
*error = (!confirmed && !*pending) ? 1 : 0;
|
||
}
|
||
|
||
int meow_toggle_display(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||
{
|
||
int d, p, r, e;
|
||
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||
return d;
|
||
}
|
||
int meow_toggle_pending(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||
{
|
||
int d, p, r, e;
|
||
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||
return p;
|
||
}
|
||
int meow_toggle_rollback(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||
{
|
||
int d, p, r, e;
|
||
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||
return r;
|
||
}
|
||
int meow_toggle_error(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||
{
|
||
int d, p, r, e;
|
||
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||
return e;
|
||
}
|
||
|
||
|
||
/* 真实态(带 800ms 缓存:top_sw 与 rows 在同一帧各查一次,别为同一问题问 daemon 两遍)。 */
|
||
static int bt_state_now(bt_ctx_t *c, int force)
|
||
{
|
||
unsigned long long t = now_ms();
|
||
if (!force && c->bt_on_ts && t - c->bt_on_ts < BT_STATE_TTL_MS) return c->bt_on;
|
||
c->bt_on = nm_bt_power_state();
|
||
c->bt_on_ts = t;
|
||
return c->bt_on;
|
||
}
|
||
|
||
/* 顶栏开关当前值:只报真实态(-1=未知,subpage 画「?」第三态) */
|
||
static int bt_top_sw(void *ctx, int *on)
|
||
{
|
||
bt_ctx_t *c = ctx;
|
||
*on = bt_state_now(c, 0);
|
||
return 1;
|
||
}
|
||
|
||
/* 顶栏开关切换:落盘意图 → 下发 → 有界等待真实态收敛 → 用真实态定稿/回滚 + 可见错误提示。
|
||
* 绝不在下发后就直接把 UI 设成用户想要的样子(那是「显示意图」而不是「显示状态」)。 */
|
||
static void bt_top_sw_set(void *ctx, int on)
|
||
{
|
||
bt_ctx_t *c = ctx;
|
||
unsigned long long t_click = now_ms(); /* 上限从点击起算(含下发耗时,见参数注释) */
|
||
int want = on ? 1 : 0;
|
||
int last_real = bt_state_now(c, 0);
|
||
meow_settings_set_bool("network.bluetooth_enabled", want); /* 意图持久化 */
|
||
meow_settings_save();
|
||
int dispatch_ok = (nm_bt_onoff(want) == MEOW_OK);
|
||
unsigned long long deadline = t_click + BT_TOGGLE_SETTLE_MS;
|
||
int probe = bt_state_now(c, 1), timed_out = 0;
|
||
while (meow_toggle_pending(want, last_real, dispatch_ok, probe, timed_out)) {
|
||
if (now_ms() >= deadline) { timed_out = 1; break; }
|
||
usleep(BT_TOGGLE_POLL_MS * 1000);
|
||
probe = bt_state_now(c, 1);
|
||
}
|
||
int disp = meow_toggle_display(want, last_real, dispatch_ok, probe, timed_out);
|
||
int rb = meow_toggle_rollback(want, last_real, dispatch_ok, probe, timed_out);
|
||
int err = meow_toggle_error(want, last_real, dispatch_ok, probe, timed_out);
|
||
unsigned long long elapsed = now_ms() - t_click;
|
||
c->bt_on = disp; c->bt_on_ts = now_ms();
|
||
/* 回滚语义:只在拿到**真实态**时才改写 settings(确认态或确认的相反态)。
|
||
* disp == -1(探测未知)时保留用户原意图不动——本键被 notif_center / settings_page 当
|
||
* 显示态读、被 app_launcher 用来决定自动拉起 WiFi,写 0 会把「不知道」污染成「确定已关」
|
||
* (评审 r1 P2-1)。 */
|
||
if (disp >= 0) {
|
||
meow_settings_set_bool("network.bluetooth_enabled", disp == 1);
|
||
meow_settings_save();
|
||
}
|
||
bt_role_refresh(1); /* 先刷新角色(它会写 g_sys.bt_on),再用真实态覆盖 */
|
||
if (disp >= 0) g_sys.bt_on = disp;
|
||
paint_sb_right();
|
||
if (disp == 1) nm_bt_scan_on();
|
||
logmsg("bt_page: toggle want=%d ok=%d probe=%d to=%d disp=%d rollback=%d err=%d elapsed=%llums",
|
||
want, dispatch_ok, probe, timed_out, disp, rb, err, elapsed);
|
||
if (err) {
|
||
char line[128];
|
||
const char *txt[1]; txt[0] = line;
|
||
/* 文案必须与结论同义:probe 未知 = 「无法确认」(不冒充未生效/未响应);
|
||
* probe 已知且 != 意图 = 确认未生效。last_real 只在这里出现。 */
|
||
if (disp < 0)
|
||
snprintf(line, sizeof line, "无法确认当前状态(上次已知:%s)",
|
||
last_real < 0 ? "未知" : (last_real ? "开启" : "关闭"));
|
||
else
|
||
snprintf(line, sizeof line, "切换未生效:当前仍为%s", disp ? "开启" : "关闭");
|
||
s_dd_modal("蓝牙", txt, 1, -1);
|
||
}
|
||
}
|
||
|
||
|
||
/* 填行:刷新数据后构建分组列表(权威=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 = bt_state_now(c, 0); /* 真实态三态(未知名 -1);见文件头 M47 说明 */
|
||
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 = 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 不可达 → 提示(直连回退仍在跑,保功能) */
|
||
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++;
|
||
}
|
||
/* 第三态显式呈现:真实态未知 ≠ 已关。顶栏开关此时也画「?」(subpage.c),这里再给一行
|
||
* 文字说明,避免用户把「不知道」读成「关着」。 */
|
||
if (c->bt_on < 0 && n < max) {
|
||
rbuf[n].kind = SUB_ROW; rbuf[n].label = "蓝牙状态未知";
|
||
rbuf[n].sub = "无法读取开关状态(服务/控制器无响应)";
|
||
rbuf[n].on = 0; rbuf[n].act = -2; 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 = 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++; }
|
||
} else for (int i = 0; i < c->paired_n && n < max; i++) {
|
||
rbuf[n].kind = SUB_ACT;
|
||
rbuf[n].label = c->paired[i].name[0] ? c->paired[i].name : c->paired[i].mac;
|
||
rbuf[n].sub = bt_is_conn(c, c->paired[i].mac) ? "已连接"
|
||
: (c->paired[i].mac[0] ? c->paired[i].mac : "");
|
||
rbuf[n].on = bt_is_conn(c, c->paired[i].mac);
|
||
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++; }
|
||
} else for (int i = 0; i < c->near_n && n < max; i++) {
|
||
int is_paired = bt_is_paired(c, c->near[i].mac);
|
||
if (is_paired) continue; /* 已配对者归于「已配对设备」,附近只列未配对 */
|
||
rbuf[n].kind = SUB_ACT;
|
||
rbuf[n].label = c->near[i].name[0] ? c->near[i].name : c->near[i].mac;
|
||
rbuf[n].sub = c->near[i].mac;
|
||
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;
|
||
}
|
||
|
||
|
||
/* 动作反馈(netmgrd 结果 / 直连回退结果)弹窗反馈 */
|
||
static void bt_feedback(const char *title, int rc)
|
||
{
|
||
char line[48];
|
||
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)点击:连接/断开/配对/取消配对/输出目标/编解码器 */
|
||
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) {
|
||
/* 注意返回值语义:return 1 = 「请重绘/重填行」,**不是**「切换已生效」。本分支只写
|
||
* settings(audiod 仅启动时读 audio.output_bt),在线切需要 MSG_AUDIO_OUTPUT,
|
||
* 属 M46 输出目标单的范围(M45 finding: 20261111-survey-m45-btctl-bug-bt-settings-audiod)。
|
||
* 这里不假装成功:不发 IPC、不写 g_sys,只落偏好。 */
|
||
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 (preference only; audiod reads at start)", 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;
|
||
if (i < 0 || i >= c->near_n) return 0;
|
||
snprintf(mac, sizeof mac, "%s", c->near[i].mac);
|
||
} else if (act >= BT_ACT_PAIRED_BASE) {
|
||
int i = act - BT_ACT_PAIRED_BASE;
|
||
if (i < 0 || i >= c->paired_n) return 0;
|
||
snprintf(mac, sizeof mac, "%s", c->paired[i].mac);
|
||
} else if (act >= BT_ACT_CONN_BASE) {
|
||
int i = act - BT_ACT_CONN_BASE;
|
||
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 */
|
||
bt_feedback("配对", nm_bt_pair(mac));
|
||
} else if (act >= BT_ACT_PAIRED_BASE) {
|
||
/* 已配对:未连 → connect;已连 → disconnect */
|
||
if (bt_is_conn(c, mac)) bt_feedback("断开", nm_bt_disconnect(mac));
|
||
else bt_feedback("连接", nm_bt_connect(mac));
|
||
} else {
|
||
/* 已连接 → disconnect */
|
||
bt_feedback("断开", nm_bt_disconnect(mac));
|
||
}
|
||
bt_role_refresh(1);
|
||
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 = "蓝牙";
|
||
p.top_sw = bt_top_sw;
|
||
p.top_sw_set = bt_top_sw_set;
|
||
p.rows = bt_rows;
|
||
p.act = bt_act;
|
||
p.detail = bt_detail; /* SUB_ROW(A2DP 摘要等)→ A2DP 详情弹窗 */
|
||
p.live = 1; /* 附近扫描驻留更新(约 2s 重扫行;仅附近/角色,不反复 spawn) */
|
||
p.ctx = &c;
|
||
c.full = 1; /* 首行全量重扫 */
|
||
nm_bt_scan_on(); /* 进页:触发一次附近扫描(节流;不进蓝牙不空转 §七) */
|
||
bt_role_refresh(1);
|
||
int rc = meow_subpage_run(&p);
|
||
nm_bt_scan_off(); /* 退页:扫描收尾(§七 省电) */
|
||
return rc;
|
||
}
|