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(另立单)。
This commit is contained in:
AskaEth committed 2026-11-11 17:16:28 +08:00
1 parent ebd64d07e1
commit b20e3c43e9
10 files changed
+708 -69

No files matched your search

+65 -11
View File
@@ -541,7 +541,8 @@ static sub_page_t *g_sp; /* 当前运行子页(rows/act/detai
static sub_row_t g_rows[SUB_ROWS_MAX]; /* 行缓冲:rows() 一次性填满,listview bind 只读 */
static int g_nrows = 0; /* 当前行数(listview vsource count 实时读) */
static int g_top_sw_en = 0; /* 本页是否显示顶开关 */
static int g_top_on = 0; /* 顶开关当前值 */
static int g_top_on = 0; /* 顶开关当前值(0/1;未知时停 0 并置 g_top_sw_unknown) */
static int g_top_sw_unknown = 0; /* M47 第三态:真实态未知(页回调 top_sw 回 -1) */
static meow_widget *g_tb = NULL; /* topappbar */
static meow_widget *g_sw = NULL; /* 顶开关(仅在有顶开关的页创建/使用) */
@@ -668,22 +669,29 @@ static void sp_widgets_create(void)
g_sp_init = 1;
}
/* 顶开关控件置态(M47:未知 -1 时 SDK 开关只有两态,停在「关」+ 灰色「?」角标区分)。 */
static void sp_top_sw_apply(void)
{
if (g_sw) meow_switch_set(g_sw, g_top_on == 1 ? 1 : 0);
}
/* 逐页配置:topappbar 标题 + 顶开关(按需创建/置态)+ listview 行数。 */
static void sp_configure_page(const sub_page_t *p, int top_sw_en, int top_on)
{
if (p->title && g_tb) meow_topappbar_set_title(g_tb, p->title);
g_top_sw_en = top_sw_en;
g_top_on = top_on;
/* M47:top_sw 回调的 *on 是三态(-1 未知 / 0 / 1),见 bt_page/wifi_page。 */
g_top_sw_unknown = (top_on < 0) ? 1 : 0;
g_top_on = (top_on == 1) ? 1 : 0;
if (top_sw_en) {
if (!g_sw) {
meow_ui_activity_push(SUB_SENTINEL_ACT); /* owner 钉哨兵(命中闸门) */
int swx = fb_xres - SUB_PAD_X - 56; /* meow_switch 固定 56×32 */
int swy = SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2;
g_sw = meow_switch_create(swx, swy, top_on);
g_sw = meow_switch_create(swx, swy, g_top_on);
meow_ui_activity_pop();
} else if (g_sw) {
meow_switch_set(g_sw, top_on);
}
sp_top_sw_apply();
}
/* 批③:行完全自绘 on/off —— 页提供 row_draw 用自绘;否则清 NULL 回默认 renderer。 */
if (g_lv) meow_listview_set_item_draw(g_lv, p->row_draw ? sp_lv_draw : NULL, NULL);
@@ -697,7 +705,17 @@ static void sp_paint_page(void)
meow_ui_fill_rect(0, 0, W, H, mt->surface); /* 全屏底 surface */
draw_statusbar();
if (g_tb) meow_widget_draw(g_tb); /* topappbar(含底部分隔线) */
if (g_sw && g_top_sw_en) meow_widget_draw(g_sw); /* 顶开关(叠顶栏右缘) */
if (g_sw && g_top_sw_en) {
meow_widget_draw(g_sw); /* 顶开关(叠顶栏右缘) */
if (g_top_sw_unknown) {
/* M47 第三态:真实态未知。SDK 开关只有开/关两态,故开关停「关」并在左侧画灰色
* 「?」——明确表达「不是关着,是不知道」,而不是拿「关」冒充。 */
int qh = meow_ui_text_height(20);
int qw = ui_width("?", 20);
ui_draw(fb_xres - SUB_PAD_X - 56 - qw - 6,
SUB_TB_Y + (SUB_TB_H - qh) / 2, "?", mt->outline, 20);
}
}
sp_paint_top_action(); /* 批③:顶栏右侧动作按钮 */
int lvx, lvy, lvw, lvh;
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
@@ -710,13 +728,40 @@ static void sp_paint_page(void)
s_ui_theme()->outline, 18);
}
/* 顶部开关切换:写设置 + 生效 + 触发重扫 + 重填行。 */
/* 顶开关回读真实态(M47:UI 只认权威回调,不做「点了就翻面」的乐观更新)。 */
static void sp_top_sw_resync(void)
{
if (!g_sp || !g_sp->top_sw) return;
int on = 0;
if (!g_sp->top_sw(g_sp->ctx, &on)) { g_top_sw_en = 0; return; }
g_top_sw_en = 1;
g_top_sw_unknown = (on < 0) ? 1 : 0;
g_top_on = (on == 1) ? 1 : 0;
}
/* 「待确认」态:开关保持上一次已确认外观,内容区顶出示同步条——与稳定态肉眼可区分。
* 有界等待期间主循环阻塞在 top_sw_set 内,所以必须先画一帧再下发。 */
static void sp_paint_switching(int want)
{
const meow_theme *mt = meow_theme_get();
char line[64];
snprintf(line, sizeof line, "正在%s…(等待设备确认)", want ? "开启" : "关闭");
int h = 32, y = SUB_CONT_TOP;
meow_ui_fill_rect(0, y, fb_xres, h, mt->surfaceContainerHigh);
meow_ui_text_center(fb_xres / 2, y + (h - meow_ui_text_height(18)) / 2, line, 18, mt->primary);
meow_dirty_full();
meow_ui_flush();
}
/* 顶部开关切换:待确认态 → 页内下发+有界等待+失败弹窗 → 回读真实态 → 重填行。 */
static void sp_top_sw_toggle(void)
{
if (!g_sp || !g_sp->top_sw_set) return;
g_sp->top_sw_set(g_sp->ctx, !g_top_on);
g_top_on = !g_top_on;
if (g_sw) meow_switch_set(g_sw, g_top_on);
int want = (g_top_on == 1) ? 0 : 1; /* 未知(-1) 视同「关」:点一下表示「请打开」 */
sp_paint_switching(want);
g_sp->top_sw_set(g_sp->ctx, want); /* 页内:下发 + 有界轮询真实态 + 失败回滚/弹窗 */
sp_top_sw_resync(); /* 定稿只认真实态 */
sp_top_sw_apply();
if (g_sp->rows) {
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
@@ -977,7 +1022,8 @@ int meow_subpage_run(sub_page_t *p)
}
if (g_top_sw_en && g_sw && meow_widget_hit(g_sw, tx, ty)) {
sp_top_sw_toggle();
last_act = now;
last_act = now_ms(); /* 切换内可能阻塞数秒:取新鲜时刻,
* 否则空闲息屏会按陈旧时刻提前触发 */
need_paint = 1;
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
continue;
@@ -1035,6 +1081,14 @@ int meow_subpage_run(sub_page_t *p)
/* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */
if (g_sp && g_sp->live && now - last_live >= 2000) {
last_live = now;
/* M47:顶开关也按 live 周期回读真实态——外部(其它页面/守护进程)改了开关,
* 本页驻留期间也要跟着变,而不是停在进入页面时那一刻。 */
int sw_before = g_top_on, unk_before = g_top_sw_unknown;
if (g_top_sw_en) sp_top_sw_resync();
if (g_top_on != sw_before || g_top_sw_unknown != unk_before) {
sp_top_sw_apply();
need_paint = 1;
}
int new_n = g_sp->rows ? g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX) : 0;
if (new_n > SUB_ROWS_MAX) new_n = SUB_ROWS_MAX;
if (new_n != g_nrows) {