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

+144 -8
View File
@@ -128,6 +128,129 @@ char *popen_read_tmo(int tmo_ms, const char *cmd)
}
/* ===== M47:WiFi/BT「真实开/关」三态探测 =========================================
* 背景:设置页两个开关此前把 settings 布尔(用户意图)当显示态,读不到时 WiFi 还默认 1,
* 于是「开关长什么样」与硬件真实态无关(用户无法判断到底开没开)。M47 起 UI 只认真实态,
* 且真实态一律三态:-1=未知 / 0=关 / 1=开。探测失败/解析不出 = 未知,绝不用 0 冒充「已关」,
* 也不许用 1 冒充「已开」。netmgrd 可达时 BT 以 daemon 的 bt status 为准(见 netmgr_ipc.c);
* 下列函数是「daemon 不可达」时的直连回退,以及 WiFi 的权威探测(netmgrd 的 wifi on/off
* 就是 rfkill wlan block/unblock,而 wifi status 只报 wpa_state、不报射频态)。 */
/* 读一份 sysfs 字符串(去尾部换行);失败返回 -1,buf 置空。 */
static int net_read_sys(const char *path, char *buf, int n)
{
buf[0] = 0;
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
ssize_t r = read(fd, buf, (size_t)n - 1);
close(fd);
if (r <= 0) { buf[0] = 0; return -1; }
buf[r] = 0;
while (r > 0 && (buf[r - 1] == '\n' || buf[r - 1] == '\r')) buf[--r] = 0;
return 0;
}
/* 按 rfkill type 读 soft/hard。返回 1=找到 / 0=无该类型 / -1=不可读。
* 本机实测有 2 个 wlan + 2 个 bluetooth 节点(rfkill0..3):同一 type 只要有一个被 block,
* 射频就不可用,故按「任一 block 即 block」汇总(保守方向:宁可报关,不假装开着)。 */
static int rfkill_state(const char *type, int *soft, int *hard)
{
DIR *d = opendir("/sys/class/rfkill");
if (!d) return -1;
int found = 0, s_any = 0, h_any = 0;
struct dirent *de;
while ((de = readdir(d))) {
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
char path[128], ty[32];
snprintf(path, sizeof path, "/sys/class/rfkill/%s/type", de->d_name);
if (net_read_sys(path, ty, sizeof ty) != 0) continue;
if (strcmp(ty, type) != 0) continue;
snprintf(path, sizeof path, "/sys/class/rfkill/%s/soft", de->d_name);
int s = read_file_int(path);
snprintf(path, sizeof path, "/sys/class/rfkill/%s/hard", de->d_name);
int h = read_file_int(path);
if (s < 0 || h < 0) continue; /* 读出失败:当作没找到这一类 */
found = 1;
if (s) s_any = 1;
if (h) h_any = 1;
}
closedir(d);
if (!found) return 0;
*soft = s_any; *hard = h_any;
return 1;
}
/* 写 rfkill soft(block=1 关 / 0 开)。返回 0=至少写成功一个;-1=无该类型设备或全写失败。
* 同类多个节点全写(只写第一个会漏掉真正绑定驱动的那个)。netmgrd 的 rfkill_set_block
* 只写首个匹配节点(见 meow_netmgrd.c:992-1016)——已作为 finding 报给塔,不在本单改。
* M47:netmgrd 的 bt on/off 就是「hciconfig + rfkill soft」,直连路径必须写同一份状态,
* 否则「从本页关过蓝牙」之后从别的入口 `hciconfig up` 会被内核 rfkill 拦下(开关失真)。 */
static int rfkill_set_soft(const char *type, int block)
{
DIR *d = opendir("/sys/class/rfkill");
if (!d) return -1;
int rc = -1;
struct dirent *de;
while ((de = readdir(d))) {
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
char path[128], ty[32];
snprintf(path, sizeof path, "/sys/class/rfkill/%s/type", de->d_name);
if (net_read_sys(path, ty, sizeof ty) != 0) continue;
if (strcmp(ty, type) != 0) continue;
snprintf(path, sizeof path, "/sys/class/rfkill/%s/soft", de->d_name);
int fd = open(path, O_WRONLY);
if (fd < 0) continue;
char v[2] = { block ? '1' : '0', 0 };
if (write(fd, v, 1) == 1) rc = 0; /* 任一成功即视为已下发(不因后一个失败翻回 -1) */
close(fd);
}
closedir(d);
return rc;
}
/* WiFi 射频真实态:-1 未知 / 0 关 / 1 开。
* 1) 有 wlan rfkill 设备(netmgrd 的 wifi on/off 就是写它):hard/soft 任一 block = 关;
* 未 block 时接口存在才算开(无 wlan0 = 驱动没载,无法判定 → 未知)。
* 2) 无 rfkill 设备:退 wlan0 管理态 IFF_UP(与状态栏 wlan_radio_up 同源)。
* 3) 都读不到 = 未知(不当「已关」)。 */
int net_wifi_power_probe(void)
{
int soft = 0, hard = 0;
int rk = rfkill_state("wlan", &soft, &hard);
if (rk == 1) {
if (hard || soft) return 0;
return access("/sys/class/net/wlan0", F_OK) == 0 ? 1 : -1;
}
if (rk < 0) return -1;
if (access("/sys/class/net/wlan0", F_OK) != 0) return -1;
char b[16];
if (net_read_sys("/sys/class/net/wlan0/flags", b, sizeof b) != 0) return -1;
return (int)(strtoul(b, NULL, 16) & 0x1UL) ? 1 : 0;
}
/* BT 控制器真实态:-1 未知 / 0 关 / 1 开。
* 权威 = hciconfig hci0 的 UP/DOWN 状态行(与 netmgrd bt_status 的回退解析同源)。
* 命令超时/无输出/既无 UP 也无 DOWN(如 hci0 不存在)= 未知。 */
int net_bt_power_probe(void)
{
char *o = popen_read_tmo(1800,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin hciconfig -a hci0 2>/dev/null");
if (!o) return -1;
int r = -1;
char *line = o;
while (line && *line) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (!strncmp(p, "DOWN", 4)) { r = 0; break; }
if (!strncmp(p, "UP", 2)) { r = 1; break; }
line = eol ? eol + 1 : NULL;
}
free(o);
return r;
}
/* 十六进制字符值('0'-'9'/'a'-'f'/'A'-'F');非法返回 -1。避免依赖 ctype.h(musl/内存受限)。
* M2.40:wpa_cli 对非 ASCII SSID 输出 "\xNN" 转义文字(`\`+x+两位hex 的纯文字,非真字节),
* 需先还原成真实 UTF-8 字节才能进界面文本。 */
@@ -733,7 +856,11 @@ int s_net_get(const srow_t *r, char *buf, int len)
/* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。
* 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。 */
* 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。
* M47:蓝牙补上 rfkill soft(与 netmgrd 的 bt on/off 同一语义)——此前本函数只发 hciconfig,
* 于是「在蓝牙页关过(rfkill soft=1)→ 再从通知中心/设置主页打开」时 `hciconfig up` 被内核
* rfkill 拦下、settings 却已写 1,开关显示 ON 实际 OFF(M45 盘点的开关失真)。开:先解 rfkill
* 再 up(顺序颠倒会先失败一次);关:先 down 再 block(与 netmgrd 一致)。 */
void s_net_toggle_wifi(int on)
{
if (on) system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
@@ -742,24 +869,33 @@ void s_net_toggle_wifi(int on)
void s_net_toggle_bt(int on)
{
if (on) system("hciconfig hci0 up 2>/dev/null");
else system("hciconfig hci0 down 2>/dev/null");
if (on) {
rfkill_set_soft("bluetooth", 0);
system("hciconfig hci0 up 2>/dev/null");
} else {
system("hciconfig hci0 down 2>/dev/null");
rfkill_set_soft("bluetooth", 1);
}
}
/* 进入设置页时把 network.wifi_enabled/bluetooth_enabled 同步为真实硬件态,
* 让开关行反映 reality(仅同步内存 + 刷新缓存;不落盘 —— s_commit 的 save 才真正写)。
* settings 未 init/取值失败则不动(保默认)。 */
* settings 未 init/取值失败则不动(保默认)。
* M47(M45 盘点):WiFi 判据不再是 wlan_connected()(=operstate up,即「已关联」)——网卡 up
* 但未关联时 operstate=dormant,旧写法会把 key 改成 0、WiFi 页开关显示 OFF 而射频明明开着;
* 改用 net_wifi_power_probe()(rfkill/网卡 up)。BT 改用三态探测:读不到(-1)就不写,
* 不拿 0 猜「已关」。 */
void s_net_sync_from_sys(void)
{
int cur = 0;
if (meow_settings_get_bool("network.wifi_enabled", &cur) == 0) {
int real = wlan_connected() ? 1 : 0;
if (real != cur) meow_settings_set_bool("network.wifi_enabled", real);
int real = net_wifi_power_probe();
if (real >= 0 && real != cur) meow_settings_set_bool("network.wifi_enabled", real);
}
if (meow_settings_get_bool("network.bluetooth_enabled", &cur) == 0) {
int real = hci_powered() ? 1 : 0;
if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
int real = net_bt_power_probe();
if (real >= 0 && real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
}
/* 只同步开关(快)+ 异步 kick;状态/列表由 run_settings_page 主循环 pump,绝不阻塞入口。 */
s_net_refresh_kick();