Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/bt_page.c
T
AskaEth f49585dd62 fix(launcher): M47 r1 评审 P2 四项 — 未知态不写盘/不顶替 + deadline 从点击起算 + 开关弹窗后置清理
评审 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。
2026-11-11 17:59:13 +08:00

528 lines
27 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}