Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/wifi_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

379 lines
18 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.
/* wifi_page.c — MeowOS 系统设置「WiFi 独立子页面」(安卓风,M2.76;M6.0 迁移 netmgr IPC)。
*
* 页面(sub_page_t 脚手架 subpage.c 驱动):
* - 顶栏:返回 + 标题「WiFi」 + 右上开关(network.wifi_enabled)
* - 当前连接:SSID 行,点开 → 详情(SSID/本机IP/信号)+ [断开连接]
* - 已保存网络:每项点 → netmgr wifi connect(ssid,复用既有网络);当前连接的项标「已连接」
* - 附近网络:扫描结果(按 BSSID 去重;中文 SSID 经 wpa_decode_ssid 还原 \xNN → UTF-8)
* 点开 →【WiFi 新网络录入】加密网络弹 SDK IME 输密码(PW 模式)→ netmgr connect → 结果反馈;
* 开放网络直连。
*
* 【M6.0 迁移】全部查询/操作走 netmgr IPC(netmgr_ipc.c 的 nm_* 封装),netmgrd 是 WiFi 权威;
* netmgrd 不可达(NETMGR_UNREACH)→ 自动回退 net.c 直连 wpa_cli(双轨保功能),页面显示降级占位
* 「网络管理服务未运行(直连模式)」。
*
* 【M47 开关可信化】顶栏开关显示态只来自真实态三态查询 nm_wifi_power_state()(rfkill wlan
* soft/hard,回退 wlan0 IFF_UP;探测失败=未知),**读不到不再默认「已开」**;settings 的
* network.wifi_enabled 只承载用户意图。点击先出示「正在开启/关闭…(等待确认)」,下发 → 有界
* 轮询真实态 → 真实态定稿;失败/超时回滚并弹窗(不许乐观显示成功)。
*/
#include "meow_launcher_internal.h"
/* action code 判位(act 回调据 act 值定位网络) */
#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */
#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */
#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */
#define WF_ACT_MANUAL 2000 /* 「手动添加隐藏网络」动作行(SUB_ACT,connecth) */
typedef struct {
meow_net_t saved[S_NET_MAX];
meow_net_t scan[S_NET_MAX];
int saved_n, scan_n;
char ssid[64];
int connected; /* nm_wifi_connected()(IPC>直连回退 wlan_connected) */
int sw_state; /* 真实射频态三态:-1 未知 / 0 关 / 1 开(M47) */
unsigned long long sw_ts; /* 上面这次查询的时刻(缓存去重) */
long last_scan_ms; /* 扫描触发节流 */
} wifi_ctx_t;
/* M47:开关收敛参数(宿主测试可预先定义改小)。WiFi 走 wifi_up.sh(起 wpa_supplicant +
* 等关联/DHCP),比 BT 慢,所以上限 5s——且从**点击时刻**起算(含下发耗时,评审 r1 P2-3)。
* 快路径(rfkill 写入在 netmgrd 内是毫秒级)5s 内约 20 次探测;慢路径(netmgrd 卡住 →
* on/off 读超时 1.5s → 直连 wifi_up.sh 自身后台化)下探测只剩 1~2 次,此时只能确认
* 「探测超时」,不能宣称「射频无响应」——弹窗文案按此区分。
* 250ms 只是快路径的轮询间隔,不 20ms 级忙轮询。 */
#ifndef WF_TOGGLE_SETTLE_MS
#define WF_TOGGLE_SETTLE_MS 5000
#endif
#ifndef WF_TOGGLE_POLL_MS
#define WF_TOGGLE_POLL_MS 250
#endif
#define WF_STATE_TTL_MS 800
/* 真实态查询(带缓存)。定义见 netmgr_ipc.c:netmgrd 不报射频态,故取 rfkill/wlan0。 */
extern int nm_wifi_power_state(void);
/* 收敛判定纯函数,实现见 bt_page.c(两个页面共用一套;矩阵测试在 test_bt_page.c) */
extern int meow_toggle_display(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
extern int meow_toggle_pending(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
extern int meow_toggle_rollback(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
extern int meow_toggle_error(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
static int wifi_state_now(wifi_ctx_t *c, int force)
{
unsigned long long t = now_ms();
if (!force && c->sw_ts && t - c->sw_ts < WF_STATE_TTL_MS) return c->sw_state;
c->sw_state = nm_wifi_power_state();
c->sw_ts = t;
return c->sw_state;
}
/* 顶栏开关当前值:只报真实态(-1=未知 → subpage 画「?」)。绝不再「读不到就默认 1」。 */
static int wifi_top_sw(void *ctx, int *on)
{
wifi_ctx_t *c = ctx;
*on = wifi_state_now(c, 0);
return 1;
}
/* 顶栏开关切换:落盘意图 → 开/关网卡(netmgr on/off,回退直连)→ 有界等待真实态收敛 →
* 真实态定稿/回滚 + 可见错误提示。 */
static void wifi_top_sw_set(void *ctx, int on)
{
wifi_ctx_t *c = ctx;
unsigned long long t_click = now_ms(); /* 上限从点击起算(含下发耗时,见参数注释) */
int want = on ? 1 : 0;
int last_real = wifi_state_now(c, 0);
meow_settings_set_bool("network.wifi_enabled", want); /* 意图持久化 */
meow_settings_save();
int dispatch_ok = (nm_wifi_toggle(want) == MEOW_OK);
unsigned long long deadline = t_click + WF_TOGGLE_SETTLE_MS;
int probe = wifi_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(WF_TOGGLE_POLL_MS * 1000);
probe = wifi_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->sw_state = disp; c->sw_ts = now_ms();
/* 只在拿到真实态时改写 settings;disp == -1(探测未知)保留原意图不动——该键被
* notif_center/settings_page 当显示态读,写 0 会把「不知道」污染成「确定已关」(P2-1)。 */
if (disp >= 0) {
meow_settings_set_bool("network.wifi_enabled", disp == 1);
meow_settings_save();
}
if (disp >= 0) g_sys.wifi_on = disp;
paint_sb_right(); /* 状态栏 WiFi 图标随「已确认」状态刷新 */
if (disp == 1) { nm_wifi_scan_trigger(); c->last_scan_ms = now_ms(); }
logmsg("wifi_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;
if (disp < 0)
snprintf(line, sizeof line, "无法确认当前状态(上次已知:%s)",
last_real < 0 ? "未知" : (last_real ? "开启" : "关闭"));
else
snprintf(line, sizeof line, "切换未生效:当前仍为%s", disp ? "开启" : "关闭");
s_dd_modal("WiFi", txt, 1, -1);
}
}
/* 触发一次 WiFi 扫描(节流:1.5s 内不重复。IPC 路径 scan 子命令由 nm_wifi_scan 内含触发+读取) */
static void wifi_scan(void *ctx)
{
wifi_ctx_t *c = ctx;
if (!c->connected) return;
long t = (long)now_ms();
if (c->last_scan_ms && t - c->last_scan_ms < 1500) return;
c->last_scan_ms = t;
nm_wifi_scan_trigger();
}
/* 填行:刷新数据后构建分组列表(netmgr IPC 优先;不可达回退直连 + 降级占位) */
static int wifi_rows(void *ctx, sub_row_t *rbuf, int max)
{
wifi_ctx_t *c = ctx;
c->sw_state = wifi_state_now(c, 0); /* 真实射频态(三态);未知时下面显式说明 */
c->connected = nm_wifi_connected();
if (c->connected) {
nm_wifi_ssid(c->ssid, sizeof c->ssid);
wifi_scan(c);
} else {
c->ssid[0] = 0;
}
c->saved_n = nm_wifi_saved(c->saved, S_NET_MAX);
c->scan_n = nm_wifi_scan(c->scan, S_NET_MAX);
if (c->saved_n < 0) c->saved_n = 0;
if (c->scan_n < 0) c->scan_n = 0;
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++;
}
/* 第三态显式呈现(M47):射频态未知 ≠ 已关;顶栏开关同时画「?」,这里补文字说明。 */
if (c->sw_state < 0 && n < max) {
rbuf[n].kind = SUB_ROW; rbuf[n].label = "WiFi 开关状态未知";
rbuf[n].sub = "无法读取射频状态(rfkill/网卡无响应)";
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 = c->connected && c->ssid[0] ? "已连接" : "未连接";
rbuf[n].sub = c->connected && c->ssid[0] ? c->ssid : "";
rbuf[n].on = 0; rbuf[n].act = WF_ACT_CURR;
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->saved_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->saved_n && n < max; i++) {
rbuf[n].kind = SUB_ACT;
rbuf[n].label = c->saved[i].name[0] ? c->saved[i].name : "<未命名>";
rbuf[n].sub = (c->connected && c->ssid[0] && !strcmp(c->saved[i].name, c->ssid)) ? "已连接"
: (c->saved[i].mac[0] ? c->saved[i].mac : "");
rbuf[n].on = 0; rbuf[n].act = WF_ACT_SAVED + 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->scan_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->scan_n && n < max; i++) {
rbuf[n].kind = SUB_ROW;
rbuf[n].label = c->scan[i].name[0] ? c->scan[i].name : "(隐藏网络)";
rbuf[n].sub = c->scan[i].sec[0] && strcmp(c->scan[i].sec, "Open") ? c->scan[i].sec
: (c->scan[i].mac[0] ? c->scan[i].mac : "");
rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i;
n++;
}
/* bug2:附近网络末尾的「手动添加隐藏网络」入口(SUB_ACT → wifi_act)。 */
if (n < max) {
rbuf[n].kind = SUB_ACT;
rbuf[n].label = "手动添加隐藏网络";
rbuf[n].sub = "";
rbuf[n].on = 0;
rbuf[n].act = WF_ACT_MANUAL;
n++;
}
return n;
}
/* SDK IME 通用文本输入(MEOW_IME_ASCII:QWERTY,顶条「中」可切拼音输中文 SSID)。
* 返回 1=已输入;0=取消。 */
static int wifi_ask_text(char *out, int maxlen)
{
meow_ime_ctx ic;
memset(&ic, 0, sizeof ic);
ic.mode = MEOW_IME_ASCII;
ic.maxlen = maxlen;
ic.buf[0] = 0;
int res = meow_ui_text_input(&ic); /* 阻塞:提交后 ctx.buf 回填最终文本 */
if (res && ic.buf[0]) { snprintf(out, maxlen, "%s", ic.buf); return 1; }
return 0;
}
/* bug1:connect 阻塞前画非模态「正在连接…」横幅。子页单线程,阻塞期间画面停在此横幅。 */
static void wifi_connecting_banner(const char *ssid)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = meow_ui_width();
int y = 108, h = 44;
char line[160];
snprintf(line, sizeof line, "正在连接 %s ...", ssid && ssid[0] ? ssid : "网络");
meow_ui_fill_rect(0, y, W, h, th->surface_container);
meow_ui_text_center(W / 2, y + h / 2 - meow_ui_text_height(20) / 2, line, 20, th->on_surface);
meow_dirty_full();
meow_ui_flush();
}
/* bug2:手动添加隐藏网络(附近网络占位行点击 / 「手动添加隐藏网络」动作行共用)。 */
static void wifi_manual_add(wifi_ctx_t *c)
{
char ssid[96] = "";
if (!wifi_ask_text(ssid, sizeof ssid)) return;
const char *items[2];
items[0] = "加密网络(WPA/WPA2/WPA3)";
items[1] = "开放网络";
int pick = s_dd_modal("网络加密方式", items, 2, -1);
if (pick < 0) return;
int open = (pick == 1);
char psk[96] = "";
if (!open && !wifi_ask_text(psk, sizeof psk)) return;
char err[160]; err[0] = 0;
wifi_connecting_banner(ssid);
int rc = nm_wifi_connect_ex(ssid, open ? NULL : psk, 1, err, sizeof err);
char line[192]; const char *txt[1]; txt[0] = line;
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
logmsg("wifi_page: manual connect ssid=%s hidden=1 open=%d rc=%d err=%s", ssid, open, rc, err);
s_dd_modal(ssid, txt, 1, -1);
nm_wifi_scan_trigger();
c->last_scan_ms = now_ms();
}
/* 附近网络点选 →【WiFi 新网络录入】连接流程。返回 1=重绘。 */
static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx)
{
if (scan_idx < 0 || scan_idx >= c->scan_n) return 0;
meow_net_t *s = &c->scan[scan_idx];
if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) {
wifi_manual_add(c); /* bug2:占位隐藏网络 → 手动录入(SSID 待输) */
return 1;
}
int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open"));
char psk[96] = "";
if (!open) {
if (!wifi_ask_text(psk, sizeof psk)) return 1; /* 用户取消输密码 */
}
char err[160]; err[0] = 0;
wifi_connecting_banner(s->name); /* bug1:阻塞前横幅,避免「点完无反馈」 */
int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err);
char line[192];
const char *txt[1]; txt[0] = line;
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err);
/* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */
s_dd_modal(s->name, txt, 1, -1);
nm_wifi_scan_trigger(); /* 连上后重扫,已保存/当前连接尽量贴近最新 */
c->last_scan_ms = now_ms();
return 1;
}
/* 普通行(SUB_ROW)点击:当前连接详情(含断开)/ 附近网络连接 */
static int wifi_detail(void *ctx, int idx, int act)
{
(void)idx;
wifi_ctx_t *c = ctx;
if (act == WF_ACT_CURR) {
char info[S_NET_MAX][48];
const char *txt[S_NET_MAX];
int n = nm_wifi_info(info, S_NET_MAX);
if (n == 0) {
snprintf(info[0], sizeof info[0], "%s", c->connected ? "已连接(无详情)" : "未连接");
txt[0] = info[0]; n = 1;
} else {
for (int i = 0; i < n; i++) txt[i] = info[i];
}
if (c->connected && n < S_NET_MAX) { snprintf(info[n], sizeof info[n], "断开连接"); txt[n] = info[n]; n++; }
int pick = s_dd_modal(c->ssid[0] ? c->ssid : "当前连接", txt, n, -1);
if (pick >= 0 && c->connected && pick == n - 1) {
char err[96]; err[0] = 0;
int rc = nm_wifi_disconnect(err, sizeof err);
const char *rtxt[1]; char rline[128];
rtxt[0] = rline;
snprintf(rline, sizeof rline, "%s", rc == MEOW_OK ? "已断开" : (err[0] ? err : "断开失败"));
s_dd_modal("断开", rtxt, 1, -1);
c->last_scan_ms = now_ms();
}
return 1;
}
if (act >= WF_ACT_SCAN_BASE) return wifi_connect_scan(c, act - WF_ACT_SCAN_BASE);
return 0;
}
/* 动作行(SUB_ACT)点击:已保存网络 → netmgr connect(ssid;复用既有网络)。
* 另承接「手动添加隐藏网络」入口(bug2)。 */
static int wifi_act(void *ctx, int idx, int act)
{
(void)idx;
wifi_ctx_t *c = ctx;
if (act == WF_ACT_MANUAL) { wifi_manual_add(c); return 1; }
if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 SUB_ROW(detail),不走这里 */
int sidx = act - WF_ACT_SAVED;
if (sidx < 0 || sidx >= c->saved_n) return 0;
if (c->saved[sidx].id < 0) return 0;
char err[160]; err[0] = 0;
wifi_connecting_banner(c->saved[sidx].name); /* bug1:阻塞前横幅 */
int rc = nm_wifi_connect(c->saved[sidx].name, NULL, err, sizeof err);
char line[192]; const char *txt[1]; txt[0] = line;
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
logmsg("wifi_page: saved connect ssid=%s rc=%d err=%s", c->saved[sidx].name, rc, err);
s_dd_modal(c->saved[sidx].name, txt, 1, -1);
nm_wifi_scan_trigger();
c->last_scan_ms = now_ms();
return 1; /* 重绘 + 重填行 */
}
/* 运行 WiFi 独立子页面(阻塞到返回) */
int run_wifi_page(void)
{
static wifi_ctx_t c; /* static:行次指针稳定 */
memset(&c, 0, sizeof c);
sub_page_t p;
memset(&p, 0, sizeof p);
p.title = "WiFi";
p.top_sw = wifi_top_sw;
p.top_sw_set = wifi_top_sw_set;
p.rows = wifi_rows;
p.act = wifi_act;
p.detail = wifi_detail;
p.live = 0; /* WiFi 列表在动作/开关时重扫即可,避免驻留 popen 轮询 */
p.ctx = &c;
/* 打开页面立即触发一次扫描,附近列表更贴近最新 */
nm_wifi_scan_trigger();
c.last_scan_ms = now_ms();
bt_role_refresh(0); /* 顺带刷新网络缓存(不影响本页) */
return meow_subpage_run(&p);
}