- wifi_page/bt_page 查询/操作改走 netmgrd IPC(netmgr_ipc.c 客户端: socket /usr/data/meow_net.sock,HELLO 鉴权 → WIFI_CTL/BT_CTL → RESULT)。 - WiFi 新网络录入:扫描结果点选 →(加密网络)SDK IME 输密码 → connect → 结果反馈 → 刷新已存网络;开放网络直连;当前连接详情含断开。 - 双轨过渡:netmgrd 不可达(NETMGR_UNREACH)回退 net.c 直连 wpa_cli/hciconfig (保功能),页面显示「网络管理服务未运行(直连模式)」降级占位(不崩)。 - BT 仅迁 on/off/status/paired(netmgrd BT_CTL 无连接/配对子命令,其余直连)。 - net.c net_wifi_scan 补 sec 字段(wifi_page.c 补 meow_net_t.sec)。 - 测试:+test_netmgr_ipc.c(假 socket 帧:扫描渲染/连接成功失败两态/降级占位)。
799 lines
36 KiB
C
799 lines
36 KiB
C
/* 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;
|
||
}
|
||
|
||
|
||
/* 十六进制字符值('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;
|
||
|
||
|
||
/* 刷新网络信息缓存(进入设置页 / 网络动作后调用;含一次 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;
|
||
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 */
|
||
}
|
||
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();
|
||
}
|
||
|