/* 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 刷新) */ static int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 popen 带超时) */ /* ===== 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; } /* 十六进制字符值('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 名(如 "")原样通过。 */ 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 带超时。 */ static 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):首行为表头,其余每行 "\t\t\t"。 * 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN),返回项数(≤max)。 */ static 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++; 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); n++; } } line = eol ? eol + 1 : NULL; } free(o); return n; } /* WiFi 扫描触发(异步:wpa_cli scan 只是通知 supplicant 发起扫描,立即返回)。 * 结果稍后经 net_wifi_scan() 读。 */ static 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):首行列头,数据行形如 * [flags..] [ssid...] * flags 恒以 '[' 起;SSID 为 flags 后的整段余量(可含空格;为空=隐藏网络)。 * M2.40:同一 AP 常出现多条(不同频段/空 SSID/重名),按 BSSID 去重——每个 BSSID 只留一条, * 优先采用含完整 SSID 的行(后到且有 SSID 则覆盖先到的空 SSID 行),隐藏网络显示 "(隐藏网络)"。 * 填 out[i].id=-1;name 存 SSID(或隐藏标记)。返回项数(≤max,且 ≤S_NET_MAX)。 */ static 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); /* 去重:同一 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; 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 "。 * hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */ static 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 后的名字) */ if (*nm) snprintf(out[n].name, sizeof out[n].name, "%s", nm); else { int ml = (int)(mac - p); if (ml > 47) ml = 47; memcpy(out[n].name, p, (size_t)ml); out[n].name[ml] = 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 ",含已配对+未配对发现,去重)。 */ static long g_bt_scan_ms = 0; /* 最近一次发起扫描的 now_ms();0=从未 */ static 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 ",去重;含已配对 * 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。 * 返回项数(≤max,且 ≤S_NET_MAX)。 */ static 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 ";发现会话里的 "[NEW] Device "。 * 跳过 [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 (*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。 */ static 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 "(如 " < 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 ",取首个)。 * 定长 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; } /* 网络信息缓存(绘制路径只读,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; /* 刷新网络信息缓存(进入设置页 / 网络动作后调用;含一次 WiFi 扫描触发) */ void s_net_refresh(void) { 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, "未连接"); } net_wifi_scan_trigger(); /* 异步发起扫描,稍后附近/计数更接近真实 */ net_bt_scan_trigger(); /* M2.40:异步发起蓝牙附近扫描(写 /tmp 缓存,不阻塞) */ bt_role_refresh(1); /* M2.48:进入设置/网络动作时立即重探蓝牙角色,本行/状态栏最新 */ meow_net_t tmp[S_NET_MAX]; g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX); g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX); g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); /* 附近(已配对+扫描发现) */ } /* 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; } snprintf(buf, len, "-"); return 0; } /* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。 * 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。 */ 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) system("hciconfig hci0 up 2>/dev/null"); else system("hciconfig hci0 down 2>/dev/null"); } /* 进入设置页时把 network.wifi_enabled/bluetooth_enabled 同步为真实硬件态, * 让开关行反映 reality(仅同步内存 + 刷新缓存;不落盘 —— s_commit 的 save 才真正写)。 * settings 未 init/取值失败则不动(保默认)。 */ 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); } 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); } s_net_refresh(); } /* 打开 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(); }