Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/net.c
T
MeowOS e6f0b665e3 refactor(launcher): 拆分 meow_launcherd.c(9778行)→meow_launcherd/ 多文件(agent-149)
- 14 模块 .c + meow_launcher_internal.h(642): util/font_theme/fb_compositor/settings_page/power_menu/notif_center/app_launcher/main_menu/audio/backlight/net/keys/about/statusbar; meow_launcherd.c 已删
- 按行逐字搬移(0-diff), 118函数提升非static+47组extern(内部头), #if0退役区不编入(源码留.pre_split.bak)
- 关于本机小改: 作者 AskaEth/17 + info/item[13]防越界 + s_dd_modal 超高滚动(clamp/触摸滚动/音量键跟随/裁剪, 仅超高启用)
- build_launcher.sh 编译列表→模块; 文档/注释引用 meow_launcherd.c→模块/目录
- 编译0warning/-Wall -Wextra告警集与原文一致(77条无新增)/qemu-mipsel冒烟一致(--scan/--sysdump); launcher=c0799357(505KB; md5因多文件重编译与原文异,非回归依据)
- 备份 meow_launcherd.c.pre_split.bak/meow_launcherd.pre_split.bak; M2.69(0.7.64)已推卡并用户验证OK
2026-08-30 21:38:30 +08:00

721 lines
32 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 刷新) */
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 名(如 "<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 带超时。 */
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):首行为表头,其余每行 "<id>\t<ssid>\t<bssid>\t<flag>"。
* 填 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):首行列头,数据行形如
* <bssid> <freq> <signal> [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 <MAC> <Name>"。
* 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 <MAC> <Name>",含已配对+未配对发现,去重)。 */
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 <MAC> <Name>",去重;含已配对
* 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。
* 返回项数(≤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 <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 (*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 <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;
}
/* 网络信息缓存(绘制路径只读,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();
}