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
This commit is contained in:
MeowOS committed 2026-08-30 21:38:30 +08:00
1 parent 1481ae00c4
commit e6f0b665e3
33 files changed
+9798 -9804

No files matched your search

+720
View File
@@ -0,0 +1,720 @@
/* 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();
}