Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/bt_page.c
T
AskaEth b20e3c43e9 fix(launcher): M47 设置页蓝牙/WiFi 开关只认真实态 — 三态查询 + 待确认收敛 + 未知态呈现
用户原话:「开关都存在显示问题 我都不能判断它是不是真的开启还是关闭(包括 WiFi 页也是)」。
根因不是「没读到硬件态」,而是「读到/没读到都被拿去当用户意图的装饰」:
- bt_page.c:103 开关初值来自 settings 布尔;:112-114 下发后立刻 g_sys.bt_on = on(显示意图);
  :129 c->bt_on = nm_bt_powered() 赋值后全文无人引用(M45 也独立发现)。
- wifi_page.c:40 读不到 settings 时默认 1(显示已开)。
- netmgrd 的 bt/wifi 查询本身把「问不到」塌缩成 0(nm_bt_powered/nm_wifi_connected)。

本单只解「状态可信」,不碰扫描/配对/音频(另有 M45/M46 单)。

1) 真实态三态(-1 未知 / 0 关 / 1 开)
- net.c 新增 net_bt_power_probe()(hciconfig hci0 UP/DOWN;无输出=未知)与
  net_wifi_power_probe()(rfkill wlan soft/hard 汇总,回退 wlan0 IFF_UP;读不到=未知)。
- netmgr_ipc.c 新增 nm_bt_power_state()(daemon bt status 的 available/powered 权威,不可达
  回退直连探测)与 nm_wifi_power_state()(netmgrd 的 wifi status 只有 wpa_state、不报射频态,
  故取本地 rfkill——daemon 的 wifi on/off 改的就是同一份内核状态)。
- 两个子页的 top_sw/rows 只读这两个函数;settings 布尔降级为「意图持久化」,不再决定外观。
- 「读不到默认 1」删除:未知就是未知。

2) 点击 → 待确认 → 真实态定稿
- 纯函数 meow_toggle_{display,pending,rollback,error}(bt_page.c,无 I/O/UI):
  只有 probe 确认才算收敛;probe 未知继续等,绝不因 last_real 恰好等于意图就宣布成功。
- 页面:落盘意图 → 下发 → 250ms 轮询(BT 上限 4s、WiFi 上限 5s;hciconfig 0.3~2s、
  wifi_up.sh 起 supplicant/DHCP 更慢,故不用 20ms 级忙轮询)→ 真实态定稿;失败/超时把
  settings 回滚到已确认真实态 + s_dd_modal 可见报错,不静默、不乐观。
- subpage.c:切换前先画「正在开启/关闭…(等待设备确认)」条(待确认态与稳定态可区分);
  切换后回读 top_sw 定稿(取消原「点了就翻面」),live tick 也回读,外部改动驻留期间可见。

3) 第三态呈现(未知 ≠ 已关)
- SDK meow_switch 只有两态:未知时开关停「关」并在左侧画灰色「?」;行内另有
  「蓝牙状态未知 / WiFi 开关状态未知」文字说明。选「?」是因为它不占用开关控件本身、
  且「关+?」不会被读成「已关」。

4) 顺带修 net.c 三条开关失真(塔追加的 M45 盘点,同一病根)
- s_net_toggle_bt() 补 rfkill soft(与 netmgrd 的 bt on/off 同一语义;开:先 unblock 再 up;
  关:先 down 再 block)。此前只发 hciconfig → 「BT 页关过再从头开」会被 rfkill 拦下。
- s_net_sync_from_sys() WiFi 判据从 wlan_connected()(已关联)改为射频态;BT 用三态,
  读不到不写(旧写法把「网卡 up 未关联」写成 0,把未知写成已关)。
- rfkill 同类多节点(本机 2 wlan + 2 bluetooth)按「任一 block 即 block」读、全部写入。

测试(`bash patch_v2/meow_launcherd/build_launcher.sh test`,新增 test 入口)
- test_bt_page.c:P4 三态与未知占位、P5 收敛四例(达成/下发失败/超时/未知)、P6 收敛矩阵
  12 组(真实开/关 × 下发成功/失败/超时 × probe 未知),全 PASS(64 条断言)。
- test_netmgr_ipc.c:新增 nm_bt_power_state 解析(powered=1/0、available=0、缺字段→-1)、
  nm_wifi_power_state 透传、不可达回退=未知、wifi_rows 未知态占位;并修掉两处既有测试缺陷
  (行内 UI 桩缺失导致本文件此前根本链不过;直连连接桩统一返回 "0\n" 导致等满 10s 必失败)。
- 交叉编译 0 warning(-Wall -Wextra,逐文件单独编 + 整链);launcherd 899388 B
  md5 1c88661a9410ac1a4139304362281cdd,同 commit 同步 scripts/build/fw_content.conf 钉值。

真机(10.58.215.50,仅只读自检,未替换 /usr/bin/meow_launcherd、未刷机)
- 二进制放 /usr/data/mnt/sd_0/MeowOS/tmp/ 跑 --sysdump:exit 0,输出 wifi_on=1 / bt_seen=1
  bt_on=0,与设备现状一致(rfkill 全 0 + wlan0 up + hci0 DOWN + netmgrd powered=0)。
- 卡上 du -s /usr/data/mnt/sd_0/Music 前后恒 199164544;临时文件已删;settings 未改。
- launcherd 在 squashfs 里 → 本改动需刷机生效。

已知不在本单:输出目标只写 settings(audiod 仅启动读一次)→ M45 finding / M46;
netmgrd 侧四处失真(未知当关、up 与 unblock 顺序、rfkill 只写首节点、wifi status 无射频字段)
已报 finding;设置主页/通知中心的开关显示仍是 settings(另立单)。
2026-11-11 17:16:28 +08:00

509 lines
26 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)。
* 依据:本机慢——hciconfig 一次 0.3~2s,netmgrd 的 on/off 自身还带 ~1.5s 等待;
* 250ms 间隔在 4s 内最多探 16 次,既不 20ms 级忙轮询,又能在 1s 内看到首次收敛。 */
#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;probe 未知才回退
* last_real(上一次「已知」值),两者都未知 → -1。
* meow_toggle_pending() 1=还要继续等:下发成功、未超时、且 probe 未等于意图。
* 注意「probe 未知」也算没收敛——绝不因为 last_real 恰好等于意图就收工。
* meow_toggle_rollback() 1=意图未达成(显示态 != 意图)→ UI/settings 必须回滚到真实态。
* meow_toggle_error() 1=收尾后仍未确认 → 必须给用户可见的错误提示(不许静默失败)。
* 判定矩阵见 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)
: ((last_real >= 0) ? (last_real ? 1 : 0) : -1);
int confirmed = (probe >= 0) && ((probe ? 1 : 0) == want);
*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;
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 = now_ms() + 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);
c->bt_on = disp; c->bt_on_ts = now_ms();
/* 回滚语义:settings 只留「已确认的真实态」——失败/超时不留一个骗人的意图值 */
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",
want, dispatch_ok, probe, timed_out, disp, rb, err);
if (err) {
char line[96];
const char *txt[1]; txt[0] = line;
if (disp < 0) snprintf(line, sizeof line, "无法确认蓝牙状态(服务/控制器无响应)");
else snprintf(line, sizeof line, "切换未生效:当前仍为%s(%dms 内未确认)",
disp ? "开启" : "关闭", BT_TOGGLE_SETTLE_MS);
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;
}