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

968 lines
44 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.
/* net.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
/* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */
int wlan_connected(void)
{
int fd = open("/sys/class/net/wlan0/operstate", O_RDONLY);
if (fd < 0) return 0;
char x[8];
int n = read(fd, x, sizeof x - 1);
close(fd);
if (n <= 0) return 0;
x[n] = 0;
return !strncmp(x, "up", 2);
}
/* wlan0 网卡是否 up(管理态 IFF_UP,非载波):读 /sys/class/net/wlan0/flags(十六进制如 "0x1003")。
* M2.48:用于区分「网卡 up 但未关联 AP」(→显示 WiFi 开未连接)与「已关联」(wlan_connected 才是关联态)。
* 接口不存在返回 0。IFF_UP=0x1。不依赖 read_sys_str(其定义在本函数之后)。 */
int wlan_radio_up(void)
{
if (access("/sys/class/net/wlan0", F_OK)) return 0;
int fd = open("/sys/class/net/wlan0/flags", O_RDONLY);
if (fd < 0) return 0;
char b[16];
ssize_t r = read(fd, b, sizeof b - 1);
close(fd);
if (r <= 0) return 0;
b[r] = 0;
return (int)(strtoul(b, NULL, 16) & 0x1UL);
}
/* 是否有 comm==name 的进程在运行(避免重复拉起 wifi 进程) */
int proc_by_comm(const char *target)
{
DIR *d = opendir("/proc");
if (!d) return 0;
struct dirent *de;
int found = 0;
while ((de = readdir(d))) {
if (de->d_name[0] < '0' || de->d_name[0] > '9') continue;
char path[64], buf[256];
snprintf(path, sizeof path, "/proc/%s/comm", de->d_name);
int fd = open(path, O_RDONLY);
if (fd < 0) continue;
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) continue;
buf[n] = 0;
if (!strncmp(buf, target, strlen(target))) { found = 1; break; }
}
closedir(d);
return found;
}
static char g_bt_mode[48] = "开启未连接"; /* 设置页"蓝牙模式"行展示(bt_role_refresh 刷新) */
int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 popen 带超时),跨模块给 bt_page 用 */
/* ===== M2.39/M2.40 网络/WiFi/蓝牙(设置页「网络」分组)=====
* 背景:launcher 已读 wlan0 operstate(wlan_connected)/ hci_powered(hciconfig UP)。
* 本组在设置页内做 WiFi/蓝牙开关 + 当前连接 SSID + 已保存网络 + 附近WiFi(WiFi 扫描,
* 按 BSSID 去重)+ 蓝牙设备(已配对 + 附近扫描发现)。读取全部经带超时 popen(wpa_cli/
* bluetoothctl 在 supplicant/BlueZ 未跑时可能挂起),绝不阻塞主循环;WiFi/蓝牙扫描均异步
* (先触发、后读缓存/结果),超时余量。开关/计数/列表只读展示走 meow_settings 新键
* network.wifi_enabled/network.bluetooth_enabled。
* M2.40:wpa_cli 对非 ASCII(中文)SSID 输出转义文字 \xNN,新增 wpa_decode_ssid() 还原成
* 真实 UTF-8 字节;net_wifi_scan 按 BSSID 去重(同 BSSID 只留一条,优先具名);蓝牙附近
* 扫描经后台有界 bluetoothctl 写 /tmp 缓存(system() 尾部 & 立即返回,不阻塞主循环)。
* 设计:绘制路径只读缓存(g_net_*,s_net_refresh 刷新一次),下拉打开时才重新 popen 解析,
* 避免绘制热点反复 spawn。 */
/* 带超时跑一条命令并读 stdout(防 wpa_supplicant/BlueZ 未跑时命令挂起)。
* 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时被 SIGKILL。 */
char *popen_read_tmo(int tmo_ms, const char *cmd)
{
int fds[2];
if (pipe(fds) != 0) return NULL;
pid_t pid = fork();
if (pid < 0) { close(fds[0]); close(fds[1]); return NULL; }
if (pid == 0) {
close(fds[0]);
dup2(fds[1], 1);
dup2(fds[1], 2);
close(fds[1]);
execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
_exit(127);
}
close(fds[1]);
int fd = fds[0];
int fl = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
char *out = NULL; size_t cap = 0, len = 0;
char tmp[256];
int done = 0;
unsigned long long deadline = now_ms() + tmo_ms;
while (!done) {
struct pollfd pfd = { fd, POLLIN, 0 };
int pr = poll(&pfd, 1, 25);
if (pr < 0) break;
if (pr == 0) { if (now_ms() > deadline) break; continue; }
ssize_t r = read(fd, tmp, sizeof tmp);
if (r > 0) {
if (len + (size_t)r + 1 > cap) {
size_t nc = cap ? cap * 2 : 512;
char *nn = realloc(out, nc);
if (!nn) { free(out); out = NULL; break; }
out = nn; cap = nc;
}
memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0;
} else if (r == 0) {
done = 1;
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
done = 1;
}
}
/* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时)则 SIGKILL,防僵尸。 */
int status = 0;
pid_t rp = waitpid(pid, &status, WNOHANG);
if (rp == 0) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
close(fd);
return out;
}
/* ===== 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 字节才能进界面文本。 */
static int hex_val(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
/* wpa_decode_ssid:把 wpa_cli 的 "\xNN" 转义文字还原成真实字节(\xe5\xa4\xa9… = "天…")。
* 遇 `\x`+两位合法 hex → 拼一字节;普通字符原样拷贝;dlen 为 dst 容量(含 NUL)。
* 无 \x 的纯 ASCII 名(如 "<ssid>")原样通过。 */
static void wpa_decode_ssid(const char *src, char *dst, int dlen)
{
int o = 0;
while (*src && o < dlen - 1) {
if (src[0] == '\\' && src[1] == 'x' &&
hex_val(src[2]) >= 0 && hex_val(src[3]) >= 0) {
dst[o++] = (char)((hex_val(src[2]) << 4) | hex_val(src[3]));
src += 4;
} else {
dst[o++] = *src++;
}
}
dst[o] = 0;
}
/* 取 WiFi 当前连接 SSID(wpa_cli -i wlan0 status 的 ssid= 行);未连接返回空串 */
void net_wifi_ssid(char *buf, int len)
{
buf[0] = 0;
if (!wlan_connected()) return;
char *o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null");
if (!o) return;
char *s = strstr(o, "ssid=");
if (s) {
s += 5;
char *e = s; while (*e && *e != '\n' && *e != '\r') e++;
int l = (int)(e - s);
/* M2.40:status 的 ssid= 同样可能是中文 \xNN 转义文字,还原成真实 UTF-8 字节 */
char raw[128];
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, s, (size_t)l); raw[l] = 0;
wpa_decode_ssid(raw, buf, len);
}
free(o);
}
/* 当前 WiFi 连接详情(点开「WiFi连接」行展示):解析 wpa_cli status + 路由网关。
* 填充 info[][48](调用方提供的二维缓冲),每行 "标签: 值",返回行数(≤max)。
* 当前SSID / AP MAC(bssid) / 本机MAC(address) / AP IP(默认网关) / 本机IP(ip_address) / 频道freq。
* 未连接返回 0(调用方显示"未连接")。绝不阻塞:popen 带超时。 */
int net_wifi_info(char (*info)[48], int max)
{
if (!wlan_connected()) return 0;
char *o = popen_read_tmo(1500,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null; ip route 2>/dev/null | grep default");
if (!o) return 0;
char ssid[64] = "", bssid[24] = "", addr[24] = "", ip[32] = "", freq[16] = "", gw[32] = "";
char *p = o;
while (p && *p) {
char *e = strchr(p, '\n'); if (e) *e = 0;
if (!strncmp(p, "ssid=", 5)) { char raw[128]; snprintf(raw, sizeof raw, "%s", p + 5); wpa_decode_ssid(raw, ssid, sizeof ssid); }
else if (!strncmp(p, "bssid=", 6)) snprintf(bssid, sizeof bssid, "%s", p + 6);
else if (!strncmp(p, "address=", 8)) snprintf(addr, sizeof addr, "%s", p + 8);
else if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11);
else if (!strncmp(p, "freq=", 5)) snprintf(freq, sizeof freq, "%s", p + 5);
else if (!strncmp(p, "default via ", 11)) { char *s = p + 11; char *g = s; while (*g && *g != ' ') g++; snprintf(gw, sizeof gw, "%s", s); }
p = e ? e + 1 : NULL;
}
free(o);
int n = 0;
if (n < max && ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid);
if (n < max && bssid[0]) snprintf(info[n++], 48, "AP MAC: %s", bssid);
if (n < max && addr[0]) snprintf(info[n++], 48, "本机MAC: %s", addr);
if (n < max && gw[0]) snprintf(info[n++], 48, "AP IP: %s", gw);
if (n < max && ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip);
if (n < max && freq[0]) snprintf(info[n++], 48, "频道: %s MHz", freq);
return n;
}
/* 已保存网络(wpa_cli -i wlan0 list_networks):首行为表头,其余每行 "<id>\t<ssid>\t<bssid>\t<flag>"。
* 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN)/.mac(BSSID),返回项数(≤max)。 */
int net_wifi_saved(meow_net_t *out, int max)
{
int n = 0;
char *o = popen_read_tmo(1500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 list_networks 2>/dev/null");
if (!o) return 0;
char *line = strchr(o, '\n');
if (line) line++; /* 跳表头 */
while (line && *line && n < max) {
char *eol = strchr(line, '\n');
if (eol) *eol = 0;
char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p && *p != '#') {
int id = atoi(p);
char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++;
while (*sp == ' ' || *sp == '\t') sp++;
char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++;
/* BSSID 列(SSID 后):<id>\t<ssid>\t<bssid>\t<flag>,拷进 mac 供详情/连接展示 */
char *bp = se; while (*bp == ' ' || *bp == '\t') bp++;
char *be = bp; while (*be && *be != ' ' && *be != '\t' && *be != '\n' && *be != '\r') be++;
if (sp != se) {
/* M2.40:list_networks 对中文 SSID 输出 \xNN 转义文字(如 \xe5\xa4\xa9… = "天…"),
* 先拷进临时缓冲再还原成真实 UTF-8 字节存入 name,否则界面显示超长乱码。 */
char raw[160];
int l = (int)(se - sp);
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, sp, (size_t)l); raw[l] = 0;
out[n].id = id;
wpa_decode_ssid(raw, out[n].name, sizeof out[n].name);
if (be > bp && (be - bp) < (int)sizeof out[n].mac) {
memcpy(out[n].mac, bp, (size_t)(be - bp));
out[n].mac[be - bp] = 0;
}
n++;
}
}
line = eol ? eol + 1 : NULL;
}
free(o);
return n;
}
/* WiFi 扫描触发(异步:wpa_cli scan 只是通知 supplicant 发起扫描,立即返回)。
* 结果稍后经 net_wifi_scan() 读。 */
void net_wifi_scan_trigger(void)
{
if (!wlan_connected()) return;
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan 2>/dev/null");
}
/* 读 WiFi 扫描结果(wpa_cli -i wlan0 scan_results):首行列头,数据行形如
* <bssid> <freq> <signal> [flags..] [ssid...]
* flags 恒以 '[' 起;SSID 为 flags 后的整段余量(可含空格;为空=隐藏网络)。
* M2.40:同一 AP 常出现多条(不同频段/空 SSID/重名),按 BSSID 去重——每个 BSSID 只留一条,
* 优先采用含完整 SSID 的行(后到且有 SSID 则覆盖先到的空 SSID 行),隐藏网络显示 "(隐藏网络)"。
* 填 out[i].id=-1;name 存 SSID(或隐藏标记)/mac 存 BSSID。返回项数(≤max,且 ≤S_NET_MAX)。 */
int net_wifi_scan(meow_net_t *out, int max)
{
int n = 0;
if (max > S_NET_MAX) max = S_NET_MAX;
char *o = popen_read_tmo(2500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan_results 2>/dev/null");
if (!o) return 0;
char seen_bssid[S_NET_MAX][24];
int named[S_NET_MAX] = {0};
char *line = strchr(o, '\n');
if (line) line++;
while (line && *line && n < max) {
char *eol = strchr(line, '\n');
if (eol) *eol = 0;
char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p) {
/* 首 token = bssid */
char bssid[24] = "";
{ char *b = p; while (*p && *p != ' ' && *p != '\t') p++;
int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; }
/* 找 flags(首个以 '[' 起始的 token),之后(trim 掉前导空白)才是 SSID */
const char *ssid = "";
char *br = strchr(p, '[');
if (br) {
char *q = br;
while (*q) {
while (*q == ' ' || *q == '\t') q++;
if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; }
else break;
}
ssid = q;
while (*ssid == ' ' || *ssid == '\t') ssid++;
}
/* SSID 去尾部空白/换行 */
char *se = (char *)ssid;
while (*se && *se != '\n' && *se != '\r') se++;
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
int has = (se > ssid && *ssid);
/* M6.0:flags 首个方括号内容 = 安全类型([WPA-PSK-CCMP] 等);纯 [ESS]/[IBSS] = 开放。
* 供「WiFi 新网络录入」判断是否需要弹 IME 输密码。 */
char sec[16] = "Open";
if (br) {
char *be = strchr(br + 1, ']');
if (be) {
int sl = (int)(be - (br + 1));
if (sl > 15) sl = 15;
if (sl > 0) {
memcpy(sec, br + 1, (size_t)sl); sec[sl] = 0;
if (!strstr(sec, "WPA") && !strstr(sec, "RSN") && !strstr(sec, "WEP") &&
!strstr(sec, "SAE") && !strstr(sec, "EAP"))
snprintf(sec, sizeof sec, "Open");
}
}
}
/* 去重:同一 BSSID 只留一条,优先具名 SSID */
int idx = -1;
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
if (idx < 0) {
if (n >= max) { line = eol ? eol + 1 : NULL; continue; }
idx = n++;
strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1);
seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0;
named[idx] = has;
out[idx].id = -1;
snprintf(out[idx].mac, sizeof out[idx].mac, "%s", bssid); /* 保留 BSSID 供详情展示 */
snprintf(out[idx].sec, sizeof out[idx].sec, "%s", sec);
if (has) {
/* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
} else {
snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)");
}
} else if (has && !named[idx]) {
/* 同 BSSID 先出现无 SSID 行、后有具名行 → 用后者 SSID 覆盖(解码 \xNN) */
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
named[idx] = 1;
}
}
line = eol ? eol + 1 : NULL;
}
free(o);
return n;
}
/* 蓝牙已配对设备(bluetoothctl paired-devices):每行 "Device <MAC> <Name>"。
* 填 out[i].mac=MAC / .name=名称;hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */
int net_bt_paired(meow_net_t *out, int max)
{
if (!hci_powered()) return 0;
int n = 0;
char *o = popen_read_tmo(1800, "bluetoothctl paired-devices 2>/dev/null");
if (!o) return 0;
char *line = o;
while (line && *line && n < max) {
char *eol = strchr(line, '\n');
if (eol) *eol = 0;
char *p = strstr(line, "Device ");
if (p) {
p += 7;
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
/* 设备名每行即名称 token;名称可含空格,这里取整行剩余部分(含 MAC 后的名字) */
int ml = (int)(mac - p); if (ml > 47) ml = 47;
if (ml > 0 && ml < (int)sizeof out[n].mac) {
memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0;
}
if (*nm)
snprintf(out[n].name, sizeof out[n].name, "%s", nm);
else {
int l = ml; if (l > 47) l = 47;
memcpy(out[n].name, p, (size_t)l); out[n].name[l] = 0;
}
out[n].id = -1;
n++;
}
line = eol ? eol + 1 : NULL;
}
free(o);
return n;
}
/* 蓝牙已连接设备(bluetoothctl `devices Connected`):每行 "Device <MAC> <Name>"。
* M2.76:供蓝牙独立子页面「已连接设备」分组展示。hci 未上电返回 0。 */
int net_bt_connected(meow_net_t *out, int max)
{
if (!hci_powered()) return 0;
char *o = popen_read_tmo(1500,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin printf 'devices Connected\\nquit\\n' | bluetoothctl 2>/dev/null");
if (!o) return 0;
int n = 0;
char *line = o;
while (line && *line && n < max) {
char *eol = strchr(line, '\n');
if (eol) *eol = 0;
char *q = line; while (*q == ' ' || *q == '\t') q++;
if (!strncmp(q, "Device ", 7)) {
q += 7;
char *mac = q; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n' && *mac != '\r') mac++;
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
int ml = (int)(mac - q); if (ml > 47) ml = 47;
if (ml > 0 && ml < (int)sizeof out[n].mac) {
memcpy(out[n].mac, q, (size_t)ml); out[n].mac[ml] = 0;
}
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
*e = 0;
if (*nm) snprintf(out[n].name, sizeof out[n].name, "%s", nm);
else { int l = ml; if (l > 47) l = 47; memcpy(out[n].name, q, (size_t)l); out[n].name[l] = 0; }
out[n].id = -1;
n++;
}
line = eol ? eol + 1 : NULL;
}
free(o);
return n;
}
/* ---- 蓝牙附近设备(未配对扫描发现;异步,不阻塞主循环)----
* M2.40:用户要的「附近的设备」= 蓝牙扫描发现(含未配对),而不只是已配对。扫描较慢(2-6s),
* 故后台有界驱动 bluetoothctl:scan on → sleep 4 → scan off → devices → quit,输出写 /tmp 缓存;
* system() 因尾部 & 立即返回(约 5s 后自灭),绝不卡死主循环。读取端 net_bt_scan 只解析缓存
* (bluetoothctl `devices` 行 "Device <MAC> <Name>",含已配对+未配对发现,去重)。 */
static long g_bt_scan_ms = 0; /* 最近一次发起扫描的 now_ms();0=从未 */
void net_bt_scan_trigger(void)
{
if (!hci_powered()) return;
long t = (long)now_ms();
if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; /* 7s 内不重复触发,防堆积 */
g_bt_scan_ms = t;
system("(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | bluetoothctl > /tmp/meow_bt_scan.txt 2>&1 &");
}
/* 读蓝牙附近设备缓存:解析 bluetoothctl `devices` 输出("Device <MAC> <Name>",去重;含已配对
* 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。
* 返回项数(≤max,且 ≤S_NET_MAX)。 */
int net_bt_scan(meow_net_t *out, int max)
{
if (!hci_powered()) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
int n = 0;
FILE *f = fopen("/tmp/meow_bt_scan.txt", "r");
if (f) {
char line[256];
char seen[S_NET_MAX][24];
while (fgets(line, sizeof line, f)) {
char *p = line; while (*p == ' ' || *p == '\t') p++;
/* 认两种行:`devices` 输出的 "Device <MAC> <Name>";发现会话里的 "[NEW] Device <MAC> <Name>"。
* 跳过 [CHG]/[DEL] 等属性/移除行(无稳定名称)。 */
char *dv = NULL;
if (!strncmp(p, "Device ", 7)) dv = p + 7;
else if (!strncmp(p, "[NEW] Device ", 13)) dv = p + 13;
if (!dv) continue;
char *mac = dv; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n' && *mac != '\r') mac++;
int ml = (int)(mac - dv);
if (ml < 6 || !memchr(dv, ':', (size_t)ml)) continue; /* 需像 MAC(含冒号);过滤属性行 */
/* 去重(同 MAC 只留一条,取首个出现) */
int dup = 0;
for (int j = 0; j < n; j++) if (!strncmp(seen[j], dv, (size_t)ml)) { dup = 1; break; }
if (dup) continue;
if (n >= max) break;
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
*e = 0; /* 去行尾换行 */
strncpy(seen[n], dv, (size_t)ml); seen[n][ml] = 0;
if (ml < (int)sizeof out[n].mac) {
memcpy(out[n].mac, dv, (size_t)ml); out[n].mac[ml] = 0; /* 保留 MAC 供配对/连接 */
}
if (*nm)
snprintf(out[n].name, sizeof out[n].name, "%s", nm);
else {
int l = ml; if (l > 47) l = 47;
memcpy(out[n].name, dv, (size_t)l); out[n].name[l] = 0;
}
out[n].id = -1;
n++;
}
fclose(f);
if (n > 0) return n;
}
return net_bt_paired(out, max); /* 无扫描缓存 → 退回已配对列表 */
}
/* ---- 蓝牙角色判定(M2.48:状态栏多态 + 设置页"蓝牙模式"行)----
* 返回 BT_ROLE_*;desc 非空则写中文描述(供设置页只读行展示)。全部 popen 带超时,绝不阻塞。
* 判定(best-effort,蓝牙栈=BlueZ 5.54):
* 关 :hci0 不存在或 DOWN(hci_powered)。
* 开未连接:hciconfig UP 但 hcitool con 无 ACL 连接。
* 主模式 :有 ACL 连接(连了外围设备如耳机);设备侧 A2DP sink 命中 → 主。
* 从模式 :有 ACL 连接且设备侧 A2DP source 命中(远端接入,设备被当耳机用)。
* 注:master/slave 依赖 bluealsa-cli a2dp list 端点角色,其输出随固件格式不同;解析不到时,
* 有 ACL 一律按「主模式」兜底(仍可靠区分 关/开未连接/已连接三态)。若真机 a2dp 角色
* 与注释相反,只需交换 a2dp_side() 的 1/2 返回值即可。 */
static void copy_utf8_trunc(char *dst, int dlen, const char *src)
{
int o = 0;
while (*src && o < dlen - 1) {
unsigned char c = (unsigned char)*src;
int nb = 1;
if ((c & 0xE0) == 0xC0) nb = 2;
else if ((c & 0xF0) == 0xE0) nb = 3;
else if ((c & 0xF8) == 0xF0) nb = 4;
if (o + nb > dlen - 1) break; /* 放不下整个字符就停(不截半字符) */
for (int k = 0; k < nb; k++) { if (!src[k]) break; dst[o++] = src[k]; }
src += nb;
}
dst[o] = 0;
}
/* A2DP 段端点角色:1=设备侧 sink(主模式),2=设备侧 source(从模式),-1=无法判定。
* 宽松匹配:同段内出现 connected/running 且出现对应角色词。优先认 BlueALSA 对象路径
* a2dpsnk/a2dpsrc(跨版本稳定),再退到 source/sink/source 单词。 */
static int a2dp_side(const char *sec)
{
if (!sec) return -1;
if (!strstr(sec, "connected") && !strstr(sec, "running")) return -1;
int src = (strstr(sec, "a2dpsrc") || strstr(sec, "source") || strstr(sec, "Source")) ? 1 : 0;
int snk = (strstr(sec, "a2dpsnk") || strstr(sec, "sink") ||
strstr(sec, "Sink") || strstr(sec, "target")) ? 1 : 0;
if (src && !snk) return 2; /* 设备侧 source → 被当耳机(从) */
if (snk && !src) return 1; /* 设备侧 sink → 送流给外围(主) */
return -1;
}
/* 蓝牙角色:见上方注释。一次 popen 拿 ACL + A2DP + 已连设备名三类信息,避免反复 spawn。 */
int bt_role(char *desc, int len)
{
if (desc && len > 0) desc[0] = 0;
if (!g_sys.bt_seen || !hci_powered()) {
if (desc && len > 0) snprintf(desc, len, "关闭");
return BT_ROLE_OFF;
}
/* 快的放前(HCI 连接、A2DP 端点),bluetoothctl 最慢放最后;整体超时防挂起(超时 SIGKILL)。 */
char *o = popen_read_tmo(1800,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin hcitool con 2>/dev/null; "
"echo '===A2DP==='; bluealsa-cli a2dp list 2>/dev/null; "
"echo '===NAMES==='; printf 'devices Connected\\nquit\\n' | bluetoothctl 2>/dev/null");
if (!o) { /* 探测失败:保守按「开未连接」 */
if (desc && len > 0) snprintf(desc, len, "开启未连接");
return BT_ROLE_ON_NO_CONN;
}
/* 按标记分节(把标记首字符置 0 切断) */
char *s_acl = o;
char *s_a2dp = NULL, *s_names = NULL;
char *p = strstr(o, "===A2DP===");
if (p) { *p = 0; s_a2dp = p + 10; } /* "===A2DP===" 长 10 */
if (s_a2dp) { char *q = strstr(s_a2dp, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } }
else { char *q = strstr(o, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } }
/* ACL:hcitool con 的连接行含 "ACL <MAC>"(如 " < ACL 00:1A:.. handle.. state.. lm MASTER") */
int has_acl = 0;
{
char *line = s_acl;
while (line && *line) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
if (strstr(line, "ACL") && strchr(line, ':')) { has_acl = 1; break; }
line = eol ? eol + 1 : NULL;
}
}
/* 已连设备名(bluetoothctl `devices Connected`:"Device <MAC> <Name>",取首个)。
* 定长 22:配合「从模式·被连(耳机…」前缀(UTF-8 约 25B)仍落在展示缓冲 48B 内。
* 同时也作「已连接」的第二证据(hcitool 缺失时 bluetoothctl 兜底)。 */
char dev[22] = "";
int has_conn = has_acl;
if (s_names) {
char *line = s_names;
while (line && *line) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
char *q = line; while (*q == ' ' || *q == '\t') q++;
if (!strncmp(q, "Device ", 7)) {
has_conn = 1; /* bluetoothctl 也确认有设备已连 */
if (!dev[0]) {
q += 7;
char *mac = q; while (*mac && *mac != ' ' && *mac != '\t' &&
*mac != '\n' && *mac != '\r') mac++;
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
*e = 0;
if (*nm) copy_utf8_trunc(dev, sizeof dev, nm);
else copy_utf8_trunc(dev, sizeof dev, q);
}
}
line = eol ? eol + 1 : NULL;
}
}
int side = a2dp_side(s_a2dp);
int role;
if (!has_conn) role = BT_ROLE_ON_NO_CONN;
else if (side == 2) role = BT_ROLE_SLAVE; /* 设备侧 source → 被当耳机 */
else role = BT_ROLE_MASTER; /* sink 或 a2dp 不明但有连接 → 主 */
if (desc && len > 0) {
if (role == BT_ROLE_ON_NO_CONN) snprintf(desc, len, "开启未连接");
else if (role == BT_ROLE_SLAVE) snprintf(desc, len, "从模式·被连(耳机%s%s)",
dev[0] ? " " : "", dev);
else snprintf(desc, len, "主模式·已连%s%s",
dev[0] ? " " : "", dev[0] ? dev : "设备");
}
free(o);
return role;
}
/* 节流重探蓝牙角色(约 5s 一次;force=1 立即)。结果写 g_sys.bt_role + g_bt_mode,
* 并同步 g_sys.bt_on/bt_seen,保证状态栏与本行一致。蓝牙关时免费(只 spawn hciconfig)。 */
static long g_bt_role_ms = 0; /* 最近一次 bt_role 探测的 now_ms();0=从未 */
void bt_role_refresh(int force)
{
if (!force && g_bt_role_ms && (now_ms() - g_bt_role_ms) < 5000) return;
g_bt_role_ms = now_ms();
char d[48];
g_sys.bt_role = bt_role(d, sizeof d);
snprintf(g_bt_mode, sizeof g_bt_mode, "%s", d);
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
g_sys.bt_on = (g_sys.bt_role != BT_ROLE_OFF) ? 1 : 0;
}
/* M2.76:蓝牙角色中文描述(节流刷新,读缓存 g_bt_mode;供蓝牙子页摘要行,
* 避免子页驻留期间每 tick 反复 popen bt_role)。 */
void net_bt_mode_str(char *buf, int len)
{
bt_role_refresh(0);
snprintf(buf, len, "%s", g_bt_mode);
}
/* 网络信息缓存(绘制路径只读,s_net_refresh 刷新;下拉打开时才重新解析列表) */
static char g_net_ssid[64] = "未连接";
static int g_net_saved_n = 0;
static int g_net_scan_n = 0;
static int g_net_bt_n = 0;
/* ---- 设置页异步网络刷新(进设置不再在入口同步跑子进程查询)----
* 背景:进设置页此前在入口同步跑 s_net_refresh()——wpa_cli/bluetoothctl/hcitool 逐条
* popen(各自带 1.2~2.5s 有界超时),5 条串行叠加达数秒,真机表现为「点设置卡 ≥5s」。
* 修法:入口只 draw + 触发扫描(快),wifi/bt 状态与列表改由 run_settings_page 主循环
* 逐步骤 pump(每步一个带超时 popen,数据变化即 need_paint)。s_net_refresh() 保留同步
* 语义(kick + drain)供需要立即结果的调用方(通知中心等)使用。
* 步骤:1=SSID 2=已保存 3=附近WiFi 4=蓝牙角色 5=附近蓝牙(各写 g_net_* 与 g_bt_mode)。 */
static int g_net_rstep = 0; /* 0=idle;1..N=步骤机位置 */
void s_net_refresh_kick(void)
{
net_wifi_scan_trigger(); /* 异步发起扫描(wpa_cli scan 快返回) */
net_bt_scan_trigger(); /* 异步发起 BT 附近扫描(& 立即返回) */
g_net_rstep = 1;
}
int s_net_refresh_pump(void)
{
meow_net_t tmp[S_NET_MAX];
switch (g_net_rstep) {
case 1:
g_net_rstep = 2;
if (wlan_connected()) {
char ss[64]; net_wifi_ssid(ss, sizeof ss);
if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss);
else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接");
} else {
snprintf(g_net_ssid, sizeof g_net_ssid, "未连接");
}
return 1;
case 2: g_net_rstep = 3; g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX); return 1;
case 3: g_net_rstep = 4; g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX); return 1;
case 4: g_net_rstep = 5; bt_role_refresh(1); return 1; /* 蓝牙角色 → g_bt_mode */
case 5: g_net_rstep = 0; g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); return 1;
default: return 0;
}
}
/* 刷新网络信息缓存(同步:kick + 排空全部异步步骤;需要立即结果的调用方用)。
* 进设置页走 s_net_sync_from_sys()(非阻塞:toggle 同步 + kick,由主循环 pump)。 */
void s_net_refresh(void)
{
s_net_refresh_kick();
while (s_net_refresh_pump()) { /* drain: 同步排空所有异步步骤 */ }
}
/* SC_INFO/SC_LIST 行的即时展示文本(读缓存;draw 路径不 popen) */
int s_net_get(const srow_t *r, char *buf, int len)
{
switch (r->net_list) {
case NET_LIST_WIFI_SSID: snprintf(buf, len, "%s", g_net_ssid); return 0;
case NET_LIST_WIFI_SAVED: snprintf(buf, len, "%d 个网络", g_net_saved_n); return 0;
case NET_LIST_WIFI_SCAN: snprintf(buf, len, "%d 个附近WiFi", g_net_scan_n); return 0;
case NET_LIST_BT_NEARBY: snprintf(buf, len, "%d 台设备", g_net_bt_n); return 0;
case NET_LIST_BT_MODE: snprintf(buf, len, "%s", g_bt_mode); return 0;
case NET_LIST_ABOUT: snprintf(buf, len, "MeowOS v%s", g_meowos_version); return 0;
case NET_LIST_WIFI_PAGE: snprintf(buf, len, "%s", g_net_ssid); return 0; /* M2.76:子页入口摘要 */
case NET_LIST_BT_PAGE: snprintf(buf, len, "%s", g_bt_mode); return 0; /* M2.76 */
case NET_LIST_PIN_PAGE: { /* M6.x:锁屏 PIN 入口摘要 */
if (!lck_pin_configured()) { snprintf(buf, len, "未设置"); }
else if (lck_lock_remaining() > 0) { snprintf(buf, len, "锁定中"); }
else { snprintf(buf, len, "已设置"); }
return 0; }
}
snprintf(buf, len, "-");
return 0;
}
/* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。
* 走 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");
else system("PATH=/bin:/sbin:/usr/bin:/usr/sbin ip link set wlan0 down 2>/dev/null");
}
void s_net_toggle_bt(int on)
{
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/取值失败则不动(保默认)。
* 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 = 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 = 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();
}
/* 打开 SC_LIST 网络下拉(重新解析最新列表):已保存=可 select_network 连接;
* 附近WiFi(扫描)/蓝牙设备(附近扫描+已配对)=只读展示(选择即关闭;带密码/配对仍留后期)。
* 返回后刷新缓存。 */
void s_net_open(const srow_t *r)
{
meow_net_t items[S_NET_MAX];
const char *txt[S_NET_MAX];
char back[S_NET_MAX][48];
int n = 0;
switch (r->net_list) {
case NET_LIST_WIFI_SSID: /* 点开「WiFi连接」→ 显示当前连接详情(只读) */
n = net_wifi_info(back, S_NET_MAX);
if (n == 0) { snprintf(back[0], sizeof back[0], "%s", wlan_connected() ? "已连接(无详情)" : "未连接"); n = 1; }
for (int i = 0; i < n; i++) txt[i] = back[i];
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
break;
case NET_LIST_WIFI_SAVED:
n = net_wifi_saved(items, S_NET_MAX);
if (n == 0) { snprintf(back[0], sizeof back[0], "无已保存网络"); txt[0] = back[0]; n = 1; items[0].id = -1; }
else for (int i = 0; i < n; i++) {
snprintf(back[i], sizeof back[i], "%s", items[i].name[0] ? items[i].name : "<未命名>");
txt[i] = back[i];
}
{
int pick = s_dd_modal(r->label, txt, n, -1); /* cur=-1:不预勾选 */
if (pick >= 0 && pick < n && items[pick].id >= 0) {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 select_network %d 2>/dev/null",
items[pick].id);
system(cmd);
}
}
break;
case NET_LIST_WIFI_SCAN:
net_wifi_scan_trigger();
n = net_wifi_scan(items, S_NET_MAX);
if (n == 0) { snprintf(back[0], sizeof back[0], "未扫描到附近网络"); txt[0] = back[0]; n = 1; }
else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; }
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
break;
case NET_LIST_BT_NEARBY:
net_bt_scan_trigger(); /* M2.40:异步发起/刷新附近扫描(读缓存;绝不阻塞主循环) */
n = net_bt_scan(items, S_NET_MAX);
if (n == 0) {
long t = (long)now_ms();
if (g_bt_scan_ms && t - g_bt_scan_ms < 6000)
snprintf(back[0], sizeof back[0], "扫描中...");
else
snprintf(back[0], sizeof back[0], "未发现附近设备");
txt[0] = back[0]; n = 1; items[0].id = -1;
}
else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; }
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
break;
case NET_LIST_ABOUT:
s_about_open(); /* M2.5x 系统设置「关于本机」:只读设备信息弹窗 */
return; /* 不上网/蓝牙缓存刷新(信息已现场读取) */
default:
return;
}
s_net_refresh_kick(); /* 异步刷新;run_settings_page 主循环 pump 逐步骤填充 */
}