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

+102
View File
@@ -0,0 +1,102 @@
/* about.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
/* M2.5x 系统设置「关于本机」:复用 s_dd_modal(与「WiFi连接状态」弹窗同款)列只读设备信息,
* 点卡片外/返回即关。信息源:settings system.hostname / g_meowos_version(=/etc/meowos_version) /
* uname -r / /proc/cpuinfo / /proc/meminfo / df / wpa_cli status / hciconfig / fb 分辨率+状态栏高。
* 只展示安全信息(不含任何密钥/凭证);实时读,短超时不阻塞主循环;行数 ≤12 适配下拉视口。
* 说明:launcher/bootmenu/watchdog/meow_v 未单独暴露版本号,统一以 /etc/meowos_version 的
* MeowOS 版本表示(与固件打包同源);无独立固件版本文件。 */
void s_about_open(void)
{
/* M2.5x 关于本机:信息行上限 12→13(新增「作者」行后近乎占满;超出由 s_dd_modal 滚动展示)。 */
char info[13][96];
const char *item[13];
int n = 0;
char host[64];
char *o;
/* 设备名(settings system.hostname;缺省 meowos) */
if (meow_settings_get_str("system.hostname", host, sizeof host) != 0 || !host[0])
snprintf(host, sizeof host, "meowos");
snprintf(info[n++], sizeof info[0], "设备名: %s", host);
/* MeowOS / 固件版本(/etc/meowos_version;与固件打包同源) */
snprintf(info[n++], sizeof info[0], "MeowOS: v%s", g_meowos_version);
/* 作者(2026-08-30 用户要求:关于本机加作者一行) */
snprintf(info[n++], sizeof info[0], "作者: AskaEth / 17");
/* 内核版本 */
o = popen_read_tmo(400, "PATH=/bin:/sbin:/usr/bin:/usr/sbin uname -r 2>/dev/null");
if (o) { for (char *p = o; *p; p++) if (*p == '\n' || *p == '\r') { *p = 0; break; }
snprintf(info[n++], sizeof info[0], "内核: %s", o[0] ? o : "-"); free(o); }
else snprintf(info[n++], sizeof info[0], "内核: -");
/* CPU(/proc/cpuinfo:Processor/CPU model/Hardware/system type 任取首非空行) */
o = popen_read_tmo(500,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin awk -F: '/(model name|cpu model|system type|machine|hardware)/{print $2; exit}' /proc/cpuinfo 2>/dev/null");
if (o) { char *q = o; for (char *p = o; *p; p++) if (*p == '\n' || *p == '\r') { *p = 0; break; }
while (*q == ' ' || *q == '\t') q++;
snprintf(info[n++], sizeof info[0], "CPU: %s", q[0] ? q : "-"); free(o); }
else snprintf(info[n++], sizeof info[0], "CPU: -");
/* 内存(/proc/meminfo MemTotal / MemAvailable,KB→MB) */
long mtot = 0, mavail = 0;
o = popen_read_tmo(500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin awk '/^MemTotal:/{print $2}/^MemAvailable:/{print $2}' /proc/meminfo 2>/dev/null");
if (o) { if (sscanf(o, "%ld%ld", &mtot, &mavail) < 2) mavail = 0; free(o); }
if (mtot > 0) snprintf(info[n++], sizeof info[0], "内存: %ld/%ld MB", mavail / 1024, mtot / 1024);
else snprintf(info[n++], sizeof info[0], "内存: -");
/* 板载 /usr/data:df -k → 可用/总量(MB) */
long dfree = 0, dtot = 0;
o = popen_read_tmo(600, "PATH=/bin:/sbin:/usr/bin:/usr/sbin df -k /usr/data 2>/dev/null | awk 'NR==2{print $4, $2}'");
if (o) { if (sscanf(o, "%ld%ld", &dfree, &dtot) < 2) dtot = 0; free(o); }
if (dtot > 0) snprintf(info[n++], sizeof info[0], "板载: %ld/%ld MB", dfree / 1024, dtot / 1024);
else snprintf(info[n++], sizeof info[0], "板载: -");
/* TF 卡 /usr/data/mnt/sd_0:卡在(/proc/mounts 含该挂载点)则 df,否则「无卡」 */
if (card_present()) {
long sfree = 0, stot = 0;
o = popen_read_tmo(600, "PATH=/bin:/sbin:/usr/bin:/usr/sbin df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'");
if (o) { if (sscanf(o, "%ld%ld", &sfree, &stot) < 2) stot = 0; free(o); }
if (stot > 0) snprintf(info[n++], sizeof info[0], "TF卡: %ld/%ld MB", sfree / 1024, stot / 1024);
else snprintf(info[n++], sizeof info[0], "TF卡: -");
} else {
snprintf(info[n++], sizeof info[0], "TF卡: 无卡");
}
/* 当前屏幕分辨率 + 状态栏高 */
snprintf(info[n++], sizeof info[0], "屏幕: %dx%d 状态栏%dpx", fb_xres, fb_yres, (int)SB_BAR_H);
/* WiFi:连上则 SSID(decode)+ 本机IP + 本机MAC;未连接显示「未连接」 */
if (wlan_connected()) {
char ss[64]; net_wifi_ssid(ss, sizeof ss);
snprintf(info[n++], sizeof info[0], "WiFi: %s", ss[0] ? ss : "已连接");
o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null");
char ip[32] = "", mac[24] = "";
if (o) {
char *p = o;
while (p && *p) {
char *e = strchr(p, '\n'); if (e) *e = 0;
if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11);
else if (!strncmp(p, "address=", 8)) snprintf(mac, sizeof mac, "%s", p + 8);
p = e ? e + 1 : NULL;
}
free(o);
}
if (ip[0]) snprintf(info[n++], sizeof info[0], "本机IP: %s", ip);
if (mac[0]) snprintf(info[n++], sizeof info[0], "本机MAC: %s", mac);
} else {
snprintf(info[n++], sizeof info[0], "WiFi: 未连接");
}
/* 蓝牙:hci_powered 决定开/关 */
snprintf(info[n++], sizeof info[0], "蓝牙: %s", hci_powered() ? "开" : "关");
for (int i = 0; i < n && i < 13; i++) item[i] = info[i];
s_dd_modal("关于本机", item, n, -1); /* cur=-1:只读展示,无预勾选 */
}
File diff suppressed because it is too large. Load diff
+224
View File
@@ -0,0 +1,224 @@
/* audio.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
static int amixer_vol(int numid)
{
char cmd[96];
snprintf(cmd, sizeof cmd, "amixer -c 0 cget numid=%d 2>/dev/null", numid);
FILE *p = popen(cmd, "r");
if (!p) return -1;
char buf[128];
int v = -1;
while (fgets(buf, sizeof buf, p)) {
/* ": values=N" line — the other "values=1" on the type line is the
* element's value COUNT, not the actual volume */
char *s = strstr(buf, ": values=");
if (s) { v = atoi(s + 9); break; }
}
pclose(p);
return v;
}
static void amixer_set(int numid, int v)
{
if (v < 0) return;
char cmd[96];
snprintf(cmd, sizeof cmd, "amixer -c 0 cset numid=%d %d >/dev/null 2>&1",
numid, v);
system(cmd);
}
struct vol_snap vol_snapshot(void)
{
struct vol_snap s = { amixer_vol(1), amixer_vol(2) };
return s;
}
void vol_restore(struct vol_snap s)
{
logmsg("volume restore -> L=%d R=%d", s.l, s.r);
amixer_set(1, s.l);
amixer_set(2, s.r);
}
int settings_volume = MEOW_VOL_DEFAULT; /* 当前音量 0-100(= 写入 DAC 的映射值) */
int settings_volume_max = MEOW_VOL_MAX_DEFAULT; /* 最大音量上限 clamp */
int settings_volume_step = MEOW_VOL_STEP_DEFAULT; /* 跳步 */
/* M2.17/M2.16:把 meow_settings 当前值同步到本模块的缓存全局。读取方
* (DAC/状态栏/息屏/刷新率调度)统一从缓存取,避免散读 backend。 */
void settings_sync_cache_from_backend(void)
{
int v;
if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v;
if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v;
if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v;
if (meow_settings_get_enum("display.refresh_rate", &v) == 0) /* names "60,30,15" */
refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60;
if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v;
if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v;
if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v;
if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v;
if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v;
}
/* meow_settings 变更回调:set_*(如音量键)后同步缓存全局,保证既有引用
* settings_volume 等变量的代码与 backend 单点一致(SYSTEM_SETTINGS §3)。 */
void settings_sync_cache_listener(const char *key)
{
if (!strcmp(key, "audio.volume")) { int v; if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v; }
else if (!strcmp(key, "audio.volume_max")) { int v; if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v; }
else if (!strcmp(key, "audio.volume_step")) { int v; if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v; }
else if (!strcmp(key, "display.refresh_rate")) { int v; if (meow_settings_get_enum("display.refresh_rate", &v) == 0) refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60; }
else if (!strcmp(key, "display.refresh_always_max")) { int v; if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v; }
else if (!strcmp(key, "display.screen_off_timeout")) { int v; if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v; }
else if (!strcmp(key, "display.screen_off_mode")) { int v; if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v; }
else if (!strcmp(key, "power.longpress_menu_ms")) { int v; if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v; }
else if (!strcmp(key, "power.forceoff_ms")) { int v; if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v; }
}
/* 启动加载(M2.11/M2.24):统一由 meow_settings 后端读 settings.conf;
* 此处仅注册监听(一次性)+ 把 backend 当前值同步到缓存全局。重载(应用期
* watchdog 可能改音量)等价于 meow_settings_init() + 手动同步。 */
int settings_cache_registered = 0; /* M2.24:监听只注册一次 */
/* ---- MeowOS 真实 DAC 音量(CS43198,ANALYSIS §18.39)----
* HibyOS 播放器经 ioctl 直写 /dev/snd/controlC0 的 "Left/Right Playback
* Volume"(numid 1/2,0-255)→ codec 驱动 cs43198_vol_*_put → I2C 直写 CS43198
* 寄存器(0x9000-0x9002/0x7000-0x7001,双片 L/R)。我们复刻同路径(amixer
* cset = SNDRV_CTL_IOCTL_ELEM_WRITE),0-100 → 0-255 映射直写两声道。
* 读回不可用(驱动 get 为 dummy 恒 0;/dev/i2c-3 被驱动占用),故 meow_vol_get()
* 返回我们写入的跟踪值(= 真实 DAC 映射值)。 */
void meow_dac_vol_write(int pct)
{
int dac = pct * 255 / 100;
if (dac < 0) dac = 0;
if (dac > 255) dac = 255;
char cmd[160];
snprintf(cmd, sizeof cmd,
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
dac, dac);
system(cmd);
logmsg("DAC vol -> L/R=%d (pct %d)", dac, pct);
}
static unsigned long long vol_dac_last_ms = 0;
void meow_dac_vol_write_throttled(void)
{
unsigned long long now = now_ms();
if (now - vol_dac_last_ms < VOL_DRAG_DAC_MS) return; /* 节流窗口内跳过,等抬手统一写 */
vol_dac_last_ms = now;
meow_dac_vol_write(settings_volume);
}
/* 抬手/点击提交连 save 时调用:无条件直写一次真实 DAC(绕过节流,保证最终音量落 DAC/落盘)。 */
void meow_dac_vol_flush(void)
{
vol_dac_last_ms = now_ms();
meow_dac_vol_write(settings_volume);
}
/* clamp 集中:0..MEOW_VOL_MAX,再 clamp 到上限 settings_volume_max。 */
int vol_clamp(int v)
{
if (v < 0) v = 0;
if (v > MEOW_VOL_MAX) v = MEOW_VOL_MAX;
if (v > settings_volume_max) v = settings_volume_max;
return v;
}
static void settings_volume_set(int v)
{
v = vol_clamp(v); /* 0..MEOW_VOL_MAX,再 clamp 到上限 */
meow_settings_set_int("audio.volume", v); /* clamp 集中在 backend;listener 同步缓存 */
meow_dac_vol_write(settings_volume); /* 每次变更即直写真实 DAC(CS43198) */
meow_settings_save(); /* 单点变更即落盘 */
}
int settings_volume_get(void) { return settings_volume; }
int vol_ramp_dir = 0; /* +1 上 / -1 下 / 0 停 */
static int vol_ramp_target = -1; /* 目标值;-1=无 */
static unsigned long long vol_ramp_next_at = 0;
static unsigned long long vol_ramp_press_at = 0; /* M2.18 Bug 4:本方向键按下时刻(长按判据) */
static void vol_ramp_begin(int delta)
{
int target = vol_clamp(settings_volume + delta);
if (target == settings_volume) return;
vol_ramp_dir = (target > settings_volume) ? 1 : -1;
vol_ramp_target = target;
vol_ramp_press_at = now_ms(); /* M2.18 Bug 4:记录该方向键按下时刻,供 CLICK_MS 长按判定 */
vol_ramp_next_at = 0; /* 立即迈第一步,给即时反馈 */
}
/* 每主循环 tick 调;key_held=该方向按键是否仍按住(长按续爬)。返回 1=仍在渐变。
* M2.18 Bug 4 语义:单击(<CLICK_MS=400ms) 到 target 即停(只跳 step 格);长按(≥400ms) 才
* 续段连续爬。普通按压力 50-150ms 远大于 VOL_RAMP_MS(15ms),旧逻辑在到 target 后只要
* key_held=1 就续段 → 单击被当成长按,连爬 6~7 格(「点一下跳好几格」)。CLICK_MS 此前只
* 被 kd_tick 状态机使用,vol_ramp 路径从未接上。 */
int vol_ramp_tick(int key_held)
{
if (vol_ramp_dir == 0) return 0;
unsigned long long now = now_ms();
if (now < vol_ramp_next_at) return 1;
vol_ramp_next_at = now + VOL_RAMP_MS;
/* 先看是否已到「当前目标值」(settings_volume 等于 target,而非候选 v)——
* 因为一旦在 target 再取 v=settings_volume+dir 便越过 target,导致 hold_wait 失效继续爬。 */
if (settings_volume == vol_ramp_target) {
if (!key_held) { /* 松开 → 一段完成,结束(单击到 target 即停,只跳 step 格) */
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save(); return 0;
}
if (now - vol_ramp_press_at < CLICK_MS) {
/* hold_wait:已到 target 但未到长按阈值 → 停在 target 不爬(保持待命不清 dir,
* 等长按激活或松开);若直接续段,长按第一次到 target 就断了。 */
return 1;
}
/* 长按激活:续段(在 target 基础上再 +step) */
vol_ramp_target = vol_clamp(vol_ramp_target + vol_ramp_dir * settings_volume_step);
if (vol_ramp_target == settings_volume) { /* 续段到上限无进展 → 结束 */
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save(); return 0;
}
}
int v = vol_clamp(settings_volume + vol_ramp_dir);
if (v == settings_volume) { /* 已达上下限 */
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save(); return 0;
}
meow_settings_set_int("audio.volume", v); /* 更新 backend 内存 + 同步缓存(不逐 1% 落盘) */
meow_dac_vol_write(settings_volume); /* 每 1% 写一次真实 DAC(settings_volume 已同步) */
return 1;
}
/* 步进调节(±settings_volume_step)→ 起线性渐变(1% 爬,见上)。 */
void settings_volume_adj(int delta) { vol_ramp_begin(delta); }
/* MeowOS 音量 API:get 返回当前(= 写入 DAC 的映射值);set 写真实 DAC + 持久化。 */
static void meow_vol_set(int pct) { settings_volume_set(pct); }
static int meow_vol_get(void) { return settings_volume; }
+130
View File
@@ -0,0 +1,130 @@
/* backlight.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
/* ---- MeowOS 面板自持(M2.6:播放器已死,MeowOS 拥有面板/背光,无拔河)----
* 电源键按时长三分:单击(短按松开)= 息屏/亮屏;3s=电源菜单;8s=强制关机。
* 息屏 = 亮度0 + FBIOBLANK(4)(面板下电,同时挂起 goodix 触摸,无误触);
* 亮屏 = FBIOBLANK(0) + 恢复亮度。 */
int meow_screen_on = 1;
int meow_saved_brightness = 60;
int meow_auto_slept = 0; /* M2.17:当前是否「超时自动息屏」置位(手动电源息屏不置位) */
/* brightness save/restore (defined below, used by run_menu) */
int saved_brightness = -1;
/* M2.53:MeowOS 主屏接管时把亮度强制压回用户设置值(display.brightness,经
* meow_settings_brightness_clamped() —— 下限 brightness_min 默认 1、上限 100),
* 覆盖 HibyOS 播放器写入 sysfs 的背光下限(16/25)。仅应在「MeowOS 真正接管画面」
* (播放器已死、主屏常驻,即 run_menu 的 had_player==0)时调用;播放器会话期间的
* 覆盖菜单(had_player==1)不去抢亮度——尊重播放器背光(§红线)。 */
void apply_user_brightness(void)
{
int v = meow_settings_brightness_clamped();
if (v <= 0) v = 60; /* 防御:clamp 后不可能 ≤0,仅兜底 */
write_brightness(v);
g_sys.brightness = v;
logmsg("apply user brightness (MeowOS takeover) -> %d", v);
}
/* ---- backlight save/restore ---- */
int read_brightness(void)
{
int fd = open(BACKLIGHT, O_RDONLY);
if (fd < 0) return -1;
char buf[16];
int n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) return -1;
buf[n] = 0;
return atoi(buf);
}
static void write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
if (fd < 0) return;
int l = strlen(val);
if (write(fd, val, l) < 0)
logmsg("write %s <- %s: %s", path, val, strerror(errno));
close(fd);
}
void write_brightness(int val)
{
char buf[8];
int l = snprintf(buf, sizeof buf, "%d\n", val);
int fd = open(BACKLIGHT, O_WRONLY);
if (fd < 0) { logmsg("backlight open fail: %s", strerror(errno)); return; }
if (write(fd, buf, l) < 0) logmsg("backlight write %d: %s", val, strerror(errno));
close(fd);
}
/* Bring the panel up for the menu, but ONLY when it is actually dark:
* brightness 0 (the player's own screen-off/standby writes brightness 0
* then FBIOBLANK(4); on this device bl_power stays 0, so brightness==0 is
* the reliable indicator — keep the bl_power check as a belt-and-suspenders
* for other states). A normal brightness is left completely untouched so
* the menu never yanks the brightness up (Bug A).
*
* CRITICAL (2026-08-28 hotfix): we NEVER touch FBIOBLANK here — the panel
* blank state belongs to the player's display state machine. fb_open()
* already unblanks for the menu (the menu must be visible); forcibly
* unblanking while the player thinks the screen is blanked leaves the
* player's state machine inconsistent (it stops drawing, UI freezes —
* this is the Bug B keeper deadlock, do not reintroduce). */
void ensure_backlight(void)
{
int cur = read_brightness();
int blp = read_file_int(BACKLIGHT_POWER);
if (cur > 0 && blp <= 0) {
logmsg("backlight: screen on (%d), leave untouched", cur);
return;
}
int target = (saved_brightness > 0) ? saved_brightness : 60;
logmsg("backlight off (cur=%d bl_power=%d) -> %d", cur, blp, target);
if (cur <= 0) write_brightness(target);
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
}
/* Restore the backlight after the player resumes (app exit / SIGTERM).
* Minimal duty after the 2026-08-28 hotfix:
* - if the menu opened while the player had blanked the panel (screen
* timeout), re-blank it (brightness 0 + FBIOBLANK(4)) so the physical
* panel matches the player's display state machine — no frozen frame,
* and the player still owns the blank state;
* - otherwise only write brightness back when it reads 0, and never
* unblank (unblanking behind the player's back wedges its display state
* machine — see ensure_backlight). A normal brightness is left
* completely untouched (Bug A: no forced brightness raise). */
void restore_backlight(void)
{
if (menu_opened_blank) {
logmsg("restore: panel was blanked at menu open, re-blank (0 + FBIOBLANK 4)");
write_brightness(0);
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 4); close(fbfd); }
menu_opened_blank = 0;
return;
}
int cur = read_brightness();
int blp = read_file_int(BACKLIGHT_POWER);
if (cur <= 0 || blp > 0) {
int target = (saved_brightness > 0) ? saved_brightness : 60;
logmsg("restore backlight (cur=%d bl_power=%d) -> %d (saved=%d)",
cur, blp, target, saved_brightness);
if (cur <= 0) write_brightness(target);
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
}
}
+10 -2
View File
@@ -12,14 +12,22 @@
# - MEOWOS_VERSION 0.7.21 → 0.7.23(M2.28)。
#
# Build(在 patch_v2/meow_launcherd 下):
# zig-mipsel-cc -I../sdk -Os -static meow_launcherd.c meow_touch.c meow_settings.c meow_notif.c ../sdk/meow_ui.a <sysroot>/lib/libz.a -lpthread -o meow_launcherd
# zig-mipsel-cc -I../sdk -Os -static main_menu.c statusbar.c fb_compositor.c settings_page.c \
# power_menu.c notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
# font_theme.c keys.c util.c meow_touch.c meow_settings.c meow_notif.c \
# ../sdk/meow_ui.a <sysroot>/lib/libz.a -lpthread -o meow_launcherd
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
[ -f "../sdk/meow_ui.a" ] || { echo "!! 缺 ../sdk/meow_ui.a,先跑 sdk/build_sdk.sh"; exit 1; }
"$CC" -I"../sdk" -Os -static meow_launcherd.c meow_touch.c meow_settings.c meow_notif.c "../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
"$CC" -I"../sdk" -Os -static \
main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \
notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
font_theme.c keys.c util.c \
meow_touch.c meow_settings.c meow_notif.c \
"../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
echo "== meow_launcherd =="
ls -la meow_launcherd
md5sum meow_launcherd
+222
View File
@@ -0,0 +1,222 @@
/* fb_compositor.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
unsigned char *fb; /* 绘制目标指针 = SDK 的 back_buf(A-1/A-2:launcher 与 app 统一渲染栈) */
static unsigned char *fb_mem = NULL; /* 真实 mmap 帧缓冲(播放器帧备份/恢复源 + app 帧读回 + 双缓冲 yoffset 用) */
int fb_xres, fb_yres, fb_line, fb_bpp;
int fb_fd = -1;
static unsigned int fb_yres_virtual = 0; /* virtual height (double-buffer = 2*yres) */
static unsigned int fb_active_yoffset = 0; /* scanline of the displayed buffer (0 or yres) */
static int fb_orig_yoffset = -1; /* player's yoffset at menu open (saved) */
static unsigned char *fb_player_backup = NULL;
static size_t fb_player_backup_len = 0;
/* ---- A-1/A-2:launcher 用 SDK 渲染栈 ----
* fb 指向 SDK 的 back_buf(meow_ui_back_buf()),所有绘制(put_px/meow_font/dim_screen/
* draw_menu/statusbar/电源菜单/确认框)画进 SDK 的离屏;呈现统一走 meow_ui_flush()(= SDK present,
* 内含 vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自建 back_buf/meow_present/帧率门控。
* fb_mem 仍保留 launcher 自有的全虚拟 mmap:仅用于播放器帧备份/恢复/双缓冲 yoffset 切换
* (fb_backup_player/fb_norm_buf0/fb_restore_player/fb_pan_buf0_force)与 app 帧读回
* (pm_capture_app_frame)——这些是「显示 buffer/页码切换」,与「present 上屏」不同层,不动。 */
int back_buf_ok = 0; /* fb 是否指向 SDK back_buf(1=可经 SDK present 上屏) */
int fb_open(void)
{
int fd = open(FB_DEV, O_RDWR);
if (fd < 0) { logmsg("fb open failed: %s", strerror(errno)); return -1; }
unsigned char vinfo[160], finfo[68];
memset(vinfo, 0, sizeof vinfo);
memset(finfo, 0, sizeof finfo);
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 ||
ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) {
logmsg("fb ioctl failed"); close(fd); return -1;
}
fb_xres = *(uint32_t *)(vinfo + 0);
fb_yres = *(uint32_t *)(vinfo + 4);
fb_yres_virtual = *(uint32_t *)(vinfo + 12);
fb_active_yoffset = *(uint32_t *)(vinfo + 20);
fb_bpp = *(uint32_t *)(vinfo + 24);
fb_line = *(uint32_t *)(finfo + 44);
if (!fb_line) fb_line = fb_xres * (fb_bpp / 8);
/* v6.6: fb0 is DOUBLE-BUFFERED (verified yres_virtual=1440=2*720,
* smem_len=2 frames, ypanstep=1). The displayed buffer is selected by
* yoffset (FBIOPAN_DISPLAY 0x4606 verified to flip it). Map the FULL
* virtual framebuffer so we can read/write both buffers for the
* backup/restore below; mmapping only one frame would let the menu
* land on the buffer the panel is NOT showing. */
if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* single buffer */
fb_mem = mmap(NULL, (size_t)fb_line * fb_yres_virtual,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb_mem == MAP_FAILED) {
logmsg("fb mmap failed: %s (xres=%d yres=%d bpp=%d line=%d size=%d)",
strerror(errno), fb_xres, fb_yres, fb_bpp, fb_line,
fb_line * fb_yres_virtual);
close(fd);
return -1;
}
fb_fd = fd;
/* A-1/A-2:launcher 用 SDK 渲染栈 —— fb 指向 SDK 的 back_buf(统一离屏),present 统一走
* meow_ui_flush()(SDK present:vsync + 帧率门控 + 原子 blit 脏区)。不再自建 back_buf。
* fb_mem(本函数 mmap 的全虚拟 fb)仅保留给播放器帧备份/恢复 + yoffset 切换 + app 帧读回。 */
fb = meow_ui_back_buf();
if (fb) {
back_buf_ok = 1;
meow_dirty_full();
logmsg("fb: %dx%d bpp=%d line=%d vres=%u yoffset=%u (A-1/A-2 SDK back_buf, meow_ui_flush)",
fb_xres, fb_yres, fb_bpp, fb_line, fb_yres_virtual, fb_active_yoffset);
} else {
fb = fb_mem; back_buf_ok = 0;
logmsg("fb: no SDK back_buf -> direct fb write (present disabled)");
}
/* NOTE: the panel is NOT unblanked here anymore — the player may still
* be running at this point (stop_player() comes after fb_open) and its
* screen-off state machine would re-blank the panel behind the menu.
* unblank_panel() runs after stop_player() (v6.2 hotfix) so the frozen
* player cannot re-blank and the menu stays visible. */
return fd;
}
/* ---- double-buffer handling (v6.6) --------------------------------
* fb0 is double-buffered and the DISPLAYED buffer is chosen by fb_var's
* yoffset (FBIOPAN_DISPLAY flips it). The player page-flips via yoffset (its
* power/screen-off redraw leaves yoffset on the back buffer), so the menu
* MUST be drawn to the buffer the panel actually shows — otherwise the menu
* is invisible and the screen freezes on the player's last frame (the v6.5
* bug). Strategy: snapshot the player's displayed frame into a heap backup,
* pan the display to buffer0 (what the launcher and all apps draw to — apps
* like hello.c mmap exactly one frame at offset 0 and never pan), draw the
* menu/app-list to buffer0, and on session end copy the backup back to the
* player's buffer and pan to the original yoffset so a SIGSTOPped player that
* does NOT redraw (event-driven, ANALYSIS §18.15) still shows its own frame. */
/* Snapshot the player's currently-displayed frame into a heap backup. */
void fb_backup_player(void)
{
if (fb_yres_virtual <= fb_yres) { fb_orig_yoffset = -1; return; } /* single buffer */
if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0;
fb_orig_yoffset = (int)fb_active_yoffset;
fb_player_backup_len = (size_t)fb_line * fb_yres;
fb_player_backup = malloc(fb_player_backup_len);
if (!fb_player_backup) {
logmsg("fb backup malloc failed (len=%zu)", fb_player_backup_len);
fb_orig_yoffset = -1;
fb_player_backup_len = 0;
return;
}
memcpy(fb_player_backup, fb_mem + (size_t)fb_active_yoffset * fb_line,
fb_player_backup_len);
logmsg("fb backup: player frame @ yoffset=%u (buffer %u) -> heap",
fb_active_yoffset,
fb_yres ? fb_active_yoffset / fb_yres : 0);
}
/* Pan the display to buffer0 (yoffset=0). buffer0 is what this launcher and
* every app draw to (apps mmap one frame at offset 0 and never pan), so the
* panel must show buffer0 for the menu and apps to be visible. */
void fb_norm_buf0(void)
{
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
if (fb_active_yoffset == 0) return; /* already showing buffer0 */
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return;
*(uint32_t *)(v + 16) = 0; /* xoffset */
*(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */
ioctl(fb_fd, FBIOPAN_DISPLAY, v);
fb_active_yoffset = 0;
logmsg("fb pan -> buffer0 (yoffset=0)");
}
/* M2.8 Bug 3-residual 根因:fb_norm_buf0 在缓存 fb_active_yoffset==0 时短路提前返回、
* 不再重发 FBIOPAN_DISPLAY。但 FBIOBLANK(4) 下电 -> FBIOBLANK(0) 上电后,LCDC 的扫描
* 地址可能已复位到非 buffer0 / 失效位置,而缓存仍是 0 —— 于是真正的 re-pan 从未发出,
* 唤醒后仍是黑屏/旧帧。本函数无条件重发 pan 到 buffer0(同 yoffset 的重复 pan 是硬件
* no-op,无频闪),供唤醒/重绘路径调用,确保重画的界面一定落在被显示的缓冲。 */
void fb_pan_buf0_force(void)
{
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return; /* read current vinfo */
*(uint32_t *)(v + 16) = 0; /* xoffset */
*(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */
ioctl(fb_fd, FBIOPAN_DISPLAY, v); /* force-issue, even if cached 0 */
fb_active_yoffset = 0;
logmsg("fb force-pan -> buffer0 (yoffset=0)");
}
/* Restore the player's frame to its buffer and pan back to the original
* yoffset. Called at menu/session end (BACK TO PLAYER / SIGTERM) so a frozen
* player that never redraws shows its own frame again. Idempotent. */
void fb_restore_player(void)
{
int restore_y = fb_orig_yoffset;
if (fb_fd >= 0 && restore_y >= 0 && fb_player_backup) {
memcpy(fb_mem + (size_t)restore_y * fb_line, fb_player_backup,
fb_player_backup_len);
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) == 0) {
*(uint32_t *)(v + 16) = 0;
*(uint32_t *)(v + 20) = (uint32_t)restore_y;
if (ioctl(fb_fd, FBIOPAN_DISPLAY, v) < 0)
logmsg("fb restore pan failed: %s", strerror(errno));
}
logmsg("fb restore: player frame -> yoffset=%d (buffer %d)",
restore_y, fb_yres ? restore_y / fb_yres : 0);
}
if (fb_player_backup) { free(fb_player_backup); fb_player_backup = NULL; }
fb_orig_yoffset = -1;
fb_player_backup_len = 0;
}
/* 把当前显示 buffer(buffer0)逐像素压暗(半透明遮罩模拟) */
void dim_screen(void)
{
for (int y = 0; y < fb_yres; y++) {
unsigned char *row = fb + (size_t)y * fb_line;
for (int x = 0; x < fb_xres; x++) {
unsigned char *p = row + x * (fb_bpp / 8);
if (fb_bpp == 16) {
uint16_t c = *(uint16_t *)p;
int r = (((c >> 11) & 0x1F) * 3) / 5;
int g = (((c >> 5) & 0x3F) * 3) / 5;
int b = ((c & 0x1F) * 3) / 5;
*(uint16_t *)p = (uint16_t)((r << 11) | (g << 5) | b);
}
}
}
}
/* A-0:电源菜单背景 = app 当前帧。从 fb0 buffer0(yoffset=0,app 永不 pan)读回 app
* SIGSTOP 后的最后一帧 → back_buf,再经 dim_screen 压暗作菜单背景,替代旧的「压暗
* launcher 应用列表底」(Bug:应用内长按电源,背景错显示成应用列表)。
* 参考 fb_backup_player 的读回思路,但目标是 app 帧而非播放器帧(播放器帧在其自身
* yoffset,app 帧固定 buffer0)。MeowOS 主界面(无 app 运行)时 buffer0 = launcher
* 列表帧,读回即现有「压暗列表底」效果,无回归。 */
void pm_capture_app_frame(void)
{
if (!fb_mem || !back_buf_ok || !fb) return; /* 无真实 fb / 直写模式:维持现状 */
memcpy(fb, fb_mem, (size_t)fb_line * (size_t)fb_yres); /* fb = SDK back_buf */
meow_dirty_full(); /* 新背景整帧呈现 */
logmsg("pm: menu bg = app frame (fb0 buffer0)");
}
+732
View File
@@ -0,0 +1,732 @@
/* font_theme.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
/* M2.6 字重结构:卡内字体 = <卡根>/MeowOS/fonts/<family>/<weight>/(三件套)。
* 每个 preset = 一套 <family>/<weight>(如 misans/regular)。值 = 卡根相对路径
* MeowOS/fonts/<family>/<weight>/font.png;family/weight 单列供设置页「family→字重」子菜单
* 分组与 meow_font_effective_paths_tiered 按 family+weight 选库。 */
const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */
const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签(含字重+档位大小标记) */
/* M2.5x 字体档位:每套的 charset 档("common"/"gbk"/"full",取自 font.json,缺省 "common")
* 与 glyph_count(信息用)。供档位分层装载(meow_font_effective_paths_tiered)与设置页标注。 */
static const char *font_preset_tiers[FONT_PRESETS_MAX];
static int font_preset_glyphs[FONT_PRESETS_MAX];
const char *font_preset_fams[FONT_PRESETS_MAX]; /* family slug(目录一级) */
const char *font_preset_wts[FONT_PRESETS_MAX]; /* weight slug(目录二级) */
const char *font_preset_names[FONT_PRESETS_MAX]; /* font.json name(family 显示名) */
static char font_preset_val_buf[FONT_PRESETS_MAX][320]; /* 值持久存储(指向上面指针数组) */
static char font_preset_label_buf[FONT_PRESETS_MAX][96]; /* 标签持久存储(含字重+档位标记) */
static char font_preset_tier_buf[FONT_PRESETS_MAX][8]; /* charset 持久存储 */
static char font_preset_fam_buf[FONT_PRESETS_MAX][64]; /* family 持久存储 */
static char font_preset_wt_buf[FONT_PRESETS_MAX][32]; /* weight 持久存储 */
static char font_preset_name_buf[FONT_PRESETS_MAX][64]; /* name 持久存储 */
int font_presets_n = 0; /* 运行时动态计数(默认=1,仅"默认字体") */
int mfont_ready = 0;
int mfont_forced = 0; /* font.file 变更→强制真实(un)load,跳过 SDK 复用捷径 */
/* fb_open 后调用:设置字库绘制目标为当前双缓冲(buffer0 起点 = fb 偏移 0) */
void meow_font_bind_fb(void)
{
meow_font_set_fb(fb_xres, fb_yres, fb_line);
}
/* 把 font.file 存的「卡根相对/绝对」路径解析为设备绝对路径。
* 相对路径以 /usr/data/mnt/sd_0/ 为卡根(与 MEOWAPPS 同源);绝对路径原样。
* 返回 0 成功,-1 参数无效。 */
int meow_font_resolve_path(const char *ref, char *out, int olen)
{
if (!ref || !ref[0]) return -1;
if (ref[0] == '/') { snprintf(out, olen, "%s", ref); return 0; }
snprintf(out, olen, "/usr/data/mnt/sd_0/%s", ref);
return 0;
}
/* 解析 font.file 指向的 png + 派生同目录 idx(font.png → font.idx)。
* 仅当指向的 png 存在(access R_OK)返回 0;否则 -1(交由回退链)。
* 返回 0:out_png/out_idx 已填设备绝对路径。 */
static int meow_font_resolve_selected(char *out_png, int png_len, char *out_idx, int idx_len)
{
char ref[MEOW_SET_MAX_LEN];
if (meow_settings_get_str("font.file", ref, sizeof ref) != 0) return -1;
if (meow_font_resolve_path(ref, out_png, png_len) != 0) return -1;
if (access(out_png, R_OK) != 0) return -1; /* 指向不存在 → 回退卡默认/固件 */
snprintf(out_idx, idx_len, "%s", out_png); /* 同目录 font.idx(标准三件套布局) */
size_t n = strlen(out_idx);
if (n > 4 && !strcmp(out_idx + n - 4, ".png")) {
out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd'; out_idx[n-1] = 'x'; out_idx[n] = 0;
}
return 0;
}
/* 尝试加载字库;成功返回 1。优先级(M2.5x,font.load_full 已接入):
* 0) 生效档 = font.load_full ? gbk : common(launcher 自身 UI 跟随系统大字库开关);
* 1) 生效字体(tier 解析,tier0 即 font.file 所选;相对路径按卡根解析);
* 2) 卡 MeowOS/fonts/default/regular/font.png(默认字库);
* 3) 固件 /usr/share/meowos/font/font.png。
* A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。
* 当字体指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload;
* 当字体指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。
* mfont_forced=1(font.file/font.load_full 变更触发)时无条件真实 reload,跳过上述捷径。
* 全失败 → mfont_ready=0,ui_draw 回退 5x7 点阵(系统不死)。 */
int meow_font_ensure(void)
{
if (mfont_ready && !mfont_forced) return 1;
mfont_forced = 0;
char fpng[400], fidx[400], fam[96];
int tier = meow_font_effective_tier(MEOW_FONT_TIER_COMMON); /* launcher 自身 UI 跟随 load_full */
int have_sel = (meow_font_effective_paths_tiered(tier, fpng, sizeof fpng,
fidx, sizeof fidx, fam, sizeof fam) == 0);
/* SDK 已加载且所选字库为「默认卡字库」(或未设置)→ 复用,避免重复解码。 */
if (meow_gfx_mfont_ready() && (!have_sel || !strcmp(fpng, UI_FONT_PNG_CARD))) {
mfont_ready = 1;
logmsg("meow_font: reuse sdk-loaded font");
return 1;
}
/* 生效字体优先(tier=common 时即 font.file;load_full → 大字库 gbk 档)。 */
if (have_sel && meow_font_load(fpng, fidx) == 0) {
mfont_ready = 1;
logmsg("meow_font: loaded %s (%d/%d sizes), tier=%d", fpng,
meow_font_has_size(28), meow_font_has_size(20), tier);
return 1;
}
if (have_sel)
logmsg("meow_font: selected font %s missing, fallback default", fpng); /* 系统日志 */
/* 卡默认 → 固件。 */
if (meow_font_load(UI_FONT_PNG_CARD, UI_FONT_IDX_CARD) == 0 ||
meow_font_load(UI_FONT_PNG_SYS, UI_FONT_IDX_SYS) == 0) {
mfont_ready = 1;
logmsg("meow_font: loaded fallback (%d/%d sizes)",
meow_font_has_size(28), meow_font_has_size(20));
return 1;
}
logmsg("meow_font: all font sources missing, using 5x7 bitmap"); /* WARN:字库不可用 */
return 0;
}
/* 从 MeowOS/fonts/<family>/<weight>/font.png 绝对路径提 family(第一级目录名)。
* 如 .../misans/regular/font.png → "misans"、.../default/regular/font.png → "default";
* 结构异常/找不到前缀 → 回退 "default"。 */
static void font_family_from_png(const char *png, char *out, int olen)
{
const char *prefix = "/MeowOS/fonts/";
const char *p = png ? strstr(png, prefix) : NULL;
if (!p) { snprintf(out, olen, "default"); return; }
const char *q = p + strlen(prefix);
const char *slash = strchr(q, '/');
int n = slash ? (int)(slash - q) : (int)strlen(q);
if (n <= 0 || n >= olen) { snprintf(out, olen, "default"); return; }
memcpy(out, q, (size_t)n); out[n] = 0;
}
/* 同路径同时提取 family + weight(两级目录名)。weight 缺省 "regular"。 */
void font_family_weight_from_png(const char *png, char *fam, int flen, char *wt, int wlen)
{
const char *prefix = "/MeowOS/fonts/";
const char *p = png ? strstr(png, prefix) : NULL;
if (!p) { snprintf(fam, flen, "default"); snprintf(wt, wlen, "regular"); return; }
const char *q = p + strlen(prefix);
const char *s1 = strchr(q, '/');
if (!s1) { snprintf(fam, flen, "%s", q); snprintf(wt, wlen, "regular"); return; }
int nf = (int)(s1 - q);
if (nf > 0 && nf < flen) { memcpy(fam, q, (size_t)nf); fam[nf] = 0; }
else snprintf(fam, flen, "default");
const char *s2 = strchr(s1 + 1, '/');
int nw = s2 ? (int)(s2 - (s1 + 1)) : (int)strlen(s1 + 1);
if (nw > 0 && nw < wlen) { memcpy(wt, s1 + 1, (size_t)nw); wt[nw] = 0; }
else snprintf(wt, wlen, "regular");
}
/* 求「当前生效字体」的 png/idx 绝对路径 + family,供 launch_app 注入 MEOW_FONT_PNG/IDX/APP。
* 优先级(与 meow_font_ensure 一致):font.file(resolve_selected)→ 卡默认 MeowOS/fonts/default。
* family 取自 MeowOS/fonts/<family>/<weight>/font.png 的第一级目录名;无法判定则 "default"。
* 返回 0 成功(out_png/out_idx/out_family 已填),-1 无卡/无可用字体(调用方跳过注入,
* SDK 自身会回退卡默认→固件→点阵)。 */
static int meow_font_effective_paths(char *out_png, int plen, char *out_idx, int ilen,
char *out_family, int flen)
{
char fpng[400], fidx[400];
if (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0) {
snprintf(out_png, plen, "%s", fpng);
snprintf(out_idx, ilen, "%s", fidx);
font_family_from_png(fpng, out_family, flen);
return 0;
}
/* 回退卡默认 MeowOS/fonts/default */
if (access(UI_FONT_PNG_CARD, R_OK) == 0) {
snprintf(out_png, plen, "%s", UI_FONT_PNG_CARD);
snprintf(out_idx, ilen, "%s", UI_FONT_IDX_CARD);
snprintf(out_family, flen, "default");
return 0;
}
return -1;
}
/* 档位 → font.json charset 字符串名(common/gbk/full)。 */
static const char *font_tier_charset(int tier)
{
if (tier >= MEOW_FONT_TIER_FULL) return "full";
if (tier >= MEOW_FONT_TIER_GBK) return "gbk";
return "common";
}
/* 生效档:app 声明档(0/1/2,越界 clamp)合并「加载大字库」开关(font.load_full)。
* 开 → 强制 ≥GBK(gbk≈20k 档大字库);关(默认)→ 保持 app 声明(缺省 common≈4k)。
* 断言:0<=返回<=2。 */
int meow_font_effective_tier(int app_tier)
{
if (app_tier < MEOW_FONT_TIER_COMMON) app_tier = MEOW_FONT_TIER_COMMON;
if (app_tier > MEOW_FONT_TIER_FULL) app_tier = MEOW_FONT_TIER_FULL;
int lf = 0;
if (meow_settings_get_switch("font.load_full", &lf) == 0 && lf)
if (app_tier < MEOW_FONT_TIER_GBK) app_tier = MEOW_FONT_TIER_GBK;
return app_tier;
}
/* 【tier 选择入口,M2.5x 已实现】按字体档位取「当前生效字体」png/idx,供 launch_app 注入
* MEOW_FONT_PNG/IDX/APP(配合 MEOW_FONT_TIER)。口径(MEOWOS_SPEC_REF §5.2/§5.7.8):
* - 生效档 = max(app 声明档, font.load_full ? GBK : COMMON):系统「加载大字库」开 →
* 强制 ≥gbk;
* - tier<=common → 走既有 meow_font_effective_paths(font.file/卡默认/固件);
* - tier>0 → 在已扫描的 MeowOS/fonts/<family>/<weight>/ 里找 charset 匹配(gbk 请求接受
* gbk/full;full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回(family = 一级目录名);
* - M2.6 字重:优先选「与用户当前 font.file 同 family(其次同 weight)」的大字库,避免
* 大字库跳到另一家字体;找不到匹配 → 回退 meow_font_effective_paths(避免点到固件小库)。
* 返回 0 成功(out_png/out_idx/out_family 已填),-1 无可用。 */
int meow_font_effective_paths_tiered(int tier, char *out_png, int plen,
char *out_idx, int ilen, char *out_family, int flen)
{
int eff = meow_font_effective_tier(tier); /* 合并 font.load_full:开 → 强制 ≥GBK */
if (eff <= MEOW_FONT_TIER_COMMON)
return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen);
/* 大字库:从扫描结果选 charset 匹配的一套。
* 打分(档位主导 ×100):gbk 请求 → gbk=200 > full=100;full 请求 → full=200 > gbk=100;
* + 同 family +30(其次同 weight +10,M2.6)仅作同档位内 tie-break:大字库沿用用户所选
* family,但绝不因「同 family」而把 common 小库越档选成大字库。 */
const char *want = font_tier_charset(eff);
char curfam[64] = "", curwt[32] = "";
{ /* 用户当前 font.file 所选 family/weight(无法判定 → 空,不参与加成) */
char fpng[400], fidx[400];
if (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0)
font_family_weight_from_png(fpng, curfam, sizeof curfam, curwt, sizeof curwt);
}
char ref[320];
int found = -1, best_score = 0;
for (int i = 0; i < font_presets_n; i++) {
const char *cs = font_preset_tiers[i] ? font_preset_tiers[i] : "common";
/* 档位优先(×100 主导):gbk 请求 → gbk=200 > full=100;full 请求 → full=200 > gbk=100。
* M2.6:同 family +30、同 weight +10 仅作同档位内 tie-break(不越档选小库)。 */
int score = 0;
if (!strcmp(cs, want)) score = 200;
else if (!strcmp(cs, "full") || !strcmp(cs, "gbk")) score = 100; /* 降级 */
if (curfam[0] && font_preset_fams[i] && !strcmp(font_preset_fams[i], curfam)) score += 30;
if (curfam[0] && curwt[0] && font_preset_wts[i] && !strcmp(font_preset_wts[i], curwt)) score += 10;
if (score > best_score) { best_score = score; found = i; }
}
if (found >= 0)
snprintf(ref, sizeof ref, "%s", font_presets[found]);
if (found >= 0) {
/* 相对路径按卡根解析为设备绝对路径(与 meow_font_effective_paths 同源)。 */
if (meow_font_resolve_path(ref, out_png, plen) != 0) return -1;
if (access(out_png, R_OK) != 0) goto fallback;
snprintf(out_idx, ilen, "%s", out_png);
size_t n = strlen(out_idx);
if (n > 4 && !strcmp(out_idx + n - 4, ".png")) {
out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd';
out_idx[n-1] = 'x'; out_idx[n] = 0;
}
font_family_from_png(out_png, out_family, flen);
return 0;
}
fallback:
/* 无匹配大字库/不可读 → 回退全局(font.file 所选;其可能是大字库或默认)。 */
return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen);
}
/* M2.5x 多字体:从 settings 读 display.multi_font + 6 类 font.class_*,把各自相对路径按卡根
* 解析为绝对路径,去重装载到 meow_font 槽位(同 family 去重 & ≤4 套上限由
* meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效;
* 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及
* 设置页变更 display.multi_font / font.class_* / font.file。 */
void meow_font_reload_multifont(void)
{
static const char *cls_keys[MEOW_FCLS_COUNT] = {
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
"font.class_latin", "font.class_latin_sym", "font.class_other" };
int on = 0;
meow_settings_get_switch("display.multi_font", &on);
char gpng[400], gidx[400], gfam[96];
/* 全局跟生效档(合并 font.load_full → 大字库),与 meow_font_ensure 同源,避免槽0实际装载与
* 此处理解不一致(多字体去重以全局 png 为基准)。 */
if (meow_font_effective_paths_tiered(meow_font_effective_tier(MEOW_FONT_TIER_COMMON),
gpng, sizeof gpng, gidx, sizeof gidx, gfam, sizeof gfam) != 0)
return; /* 无全局字体:无法多字体 */
/* 各类候选(静态缓冲保活;config_multifont 同步读取,不持指针) */
static char cpng[MEOW_FCLS_COUNT][400], cidx[MEOW_FCLS_COUNT][400];
meow_font_class_cfg cfg[MEOW_FCLS_COUNT];
memset(cfg, 0, sizeof cfg);
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
char ref[MEOW_SET_MAX_LEN];
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) == 0 && ref[0]) {
char abs[400];
if (meow_font_resolve_path(ref, abs, sizeof abs) == 0 && access(abs, R_OK) == 0) {
snprintf(cpng[c], sizeof cpng[c], "%s", abs);
snprintf(cidx[c], sizeof cidx[c], "%s", abs);
size_t n = strlen(cidx[c]);
if (n > 4 && !strcmp(cidx[c] + n - 4, ".png")) {
cidx[c][n-4]='.'; cidx[c][n-3]='i'; cidx[c][n-2]='d'; cidx[c][n-1]='x'; cidx[c][n]=0;
}
cfg[c].png = cpng[c]; cfg[c].idx = cidx[c];
} else {
/* M2.58:类别字体配置了但缺失 → 该类别回退全局,并记系统 WARN。 */
logmsg("multi_font: class %s font %s missing, fallback global", cls_keys[c], ref);
}
}
}
int distinct = 0, truncated = 0;
if (meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated) != 0)
logmsg("multi_font: config failed -> single-font");
else if (truncated)
logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct);
else if (on)
logmsg("multi_font: enabled, %d distinct fonts (global=%s)", distinct, gfam);
}
/* ================================================================== */
/* M2.5x 多字体库扫描 + M2.6 字重结构 / preset 动态化 */
/* 字体目录 = <卡根>/MeowOS/fonts/<family>/<weight>/,每套含三件套: */
/* font.png(预渲染灰度 PNG,bit depth 4|8)+ font.idx(MFNI v2 索引) */
/* + font.json(元数据 + 完整性校验)。缺任一件 → 该套不进列表 + 日志。 */
/* font.json schema(定版): */
/* { */
/* "family": "家族标识(同目录名/family)", // 信息用,不参与校验 */
/* "name": "下拉显示名", // 缺则回退目录名 */
/* "weight": "regular", // 字重 slug(缺省 regular) */
/* "glyph_count": 10000, // 信息用 */
/* "charset": "common|gbk|full", // 档位(M2.5x,驱动按需加载) */
/* "size_list": [28,20], // 信息用 */
/* "bit_depth": 4, // 信息用(4|8) */
/* "sha256_png": "<64hex>", // font.png 校验 */
/* "sha256_idx": "<64hex>", // font.idx 校验 */
/* "source": "...", // 可选:来源 */
/* "license": "..." // 可选:许可 */
/* } */
/* 校验:sha256 不匹配仅 WARN 不硬失败(仍可选);缺三件套或 font.json */
/* 解析不出 name → 跳过 + 日志。仅启动/进设置页扫描一次(勿放热路径)。 */
/* ================================================================== */
/* 计算文件 sha256(走 busybox/宿主 sha256sum;输出小写 64hex)。返回 0 成功。 */
static int font_sha256_file(const char *path, char *out, int olen)
{
out[0] = 0;
/* 单引号转义防 shell 注入(路径含 ' 时安全;转义后超长则截断 → sha256sum 失败走 WARN) */
char esc[1024]; int j = 0;
for (const char *p = path; *p && j < (int)sizeof esc - 4; ) {
if (*p == '\'') { memcpy(esc + j, "'\\''", 4); j += 4; p++; }
else esc[j++] = *p++;
}
esc[j] = 0;
char cmd[1080];
snprintf(cmd, sizeof cmd, "sha256sum '%s' 2>/dev/null", esc);
FILE *f = popen(cmd, "r");
if (!f) return -1;
char line[160];
int ok = -1;
if (fgets(line, sizeof line, f)) {
int i = 0, k = 0;
while (line[i] == ' ' || line[i] == '\t') i++; /* 跳前导空白 */
while (line[i] && k < 64) {
char c = line[i++];
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
out[k++] = c;
else break;
}
out[k] = 0;
ok = (k == 64) ? 0 : -1;
}
pclose(f);
return ok;
}
/* 比较两个 sha256 十六进制串(大小写不敏感)。均为 64hex 才比对,否则视为不匹配。 */
static int font_sha_eq(const char *a, const char *b)
{
if (!a || !b || strlen(a) < 64 || strlen(b) < 64) return 0;
for (int i = 0; i < 64; i++) {
char ca = a[i], cb = b[i];
if (ca >= 'A' && ca <= 'F') ca += 32;
if (cb >= 'A' && cb <= 'F') cb += 32;
if (ca != cb) return 0;
}
return 1;
}
/* 解析 font.json:提取 name / sha256_png / sha256_idx / weight(M2.6,缺省 "regular")。
* 文件缺失或解析不出则留空(name 空 → 调用方回退目录名;weight 空 → 回退 "regular")。 */
static void meow_font_json_probe(const char *json_path, char *name, int nlen,
char *sha_png, int slen, char *sha_idx, int xlen,
char *charset, int clen, int *glyph_count,
char *weight, int wlen)
{
name[0] = 0; sha_png[0] = 0; sha_idx[0] = 0;
if (charset) charset[0] = 0;
if (glyph_count) *glyph_count = 0;
if (weight) weight[0] = 0;
FILE *f = fopen(json_path, "r");
if (!f) return;
char buf[256];
while (fgets(buf, sizeof buf, f)) {
if (!name[0]) json_str(buf, "name", name, nlen);
if (!sha_png[0]) json_str(buf, "sha256_png", sha_png, slen);
if (!sha_idx[0]) json_str(buf, "sha256_idx", sha_idx, xlen);
/* M2.5x 字体档位:charset(common|gbk|full)+ glyph_count(信息用,驱动档位分层/标注)。 */
if (charset && !charset[0]) {
char v[8] = "";
if (json_str(buf, "charset", v, sizeof v) > 0 && v[0])
snprintf(charset, clen, "%s", v);
}
if (glyph_count && *glyph_count == 0) {
char v[16] = "";
if (json_str(buf, "glyph_count", v, sizeof v) > 0 && v[0])
*glyph_count = atoi(v);
}
/* M2.6 字重:weight(regular|bold|italic|…),缺省 "regular"。 */
if (weight && !weight[0]) {
char v[32] = "";
if (json_str(buf, "weight", v, sizeof v) > 0 && v[0])
snprintf(weight, wlen, "%s", v);
}
}
fclose(f);
}
/* 字重 slug → 显示名(首字母大写、连字符/下划线转空格:regular→Regular、bold→Bold、italic→Italic)。 */
static void font_weight_display(const char *weight, char *out, int olen)
{
const char *w = (weight && weight[0]) ? weight : "regular";
out[0] = 0;
int o = 0, cap = 1;
for (const char *p = w; *p && o < olen - 1; p++) {
char c = *p;
if (c == '_' || c == '-') { if (o < olen - 1) out[o++] = ' '; cap = 1; continue; }
out[o++] = cap ? (char)((c >= 'a' && c <= 'z') ? c - 32 : c) : c;
cap = 0;
}
out[o] = 0;
}
/* 档位大小标记(设置页字体选项标注):common≈4k / gbk≈20k / full。
* 由 font.json 的 charset + glyph_count 派生;缺 charset 回退 common。 */
static void font_tier_marker(const char *charset, int glyph_count, char *out, int olen)
{
const char *tier = (charset && charset[0]) ? charset : "common";
if (!strcmp(tier, "full")) { snprintf(out, olen, "full"); return; }
if (glyph_count > 0)
snprintf(out, olen, "%s≈%dk", tier, (glyph_count + 500) / 1000);
else
snprintf(out, olen, "%s", tier);
}
/* 探测一个 <卡根>/MeowOS/fonts/<family>/<weight> 套件是否合法:三件套齐全 → 解析 label+档位+字重。
* 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。
* 输出:label=显示名 `<name> <Weight>(common≈4k)`(如 `MiSans Regular(common≈4k)`);
* charset/glyph_count=档位信息;wtout=weight slug(缺省 "regular")。 */
static int meow_font_probe_weight(const char *dirpath, const char *family, const char *weight,
char *label, int llen,
char *charset, int clen, int *glyph_count,
char *wtout, int wtlen, char *nameout, int namelen)
{
char ppath[400], ipath[400], jpath[400];
snprintf(ppath, sizeof ppath, "%s/font.png", dirpath);
snprintf(ipath, sizeof ipath, "%s/font.idx", dirpath);
snprintf(jpath, sizeof jpath, "%s/font.json", dirpath);
if (access(ppath, R_OK) != 0 || access(ipath, R_OK) != 0 || access(jpath, R_OK) != 0) {
logmsg("fonts: skip '%s/%s' (缺 font.png/font.idx/font.json)", family, weight);
return 0;
}
char name[64] = "", jsha_png[80] = "", jsha_idx[80] = "";
char cs[8] = ""; int gc = 0; char wts[32] = "";
meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png,
jsha_idx, sizeof jsha_idx, cs, sizeof cs, &gc, wts, sizeof wts);
const char *wt = wts[0] ? wts : (weight ? weight : "regular");
if (wtout) snprintf(wtout, wtlen, "%s", wt);
if (nameout) snprintf(nameout, namelen, "%s", name[0] ? name : family);
/* 显示名 = `<name> <Weight>(common≈4k)`(字重缺省 Regular) */
char wd[24]; font_weight_display(wt, wd, sizeof wd);
char marker[24];
font_tier_marker(cs, gc, marker, sizeof marker);
snprintf(label, llen, "%s %s(%s)", name[0] ? name : family, wd, marker);
if (charset) snprintf(charset, clen, "%s", cs[0] ? cs : "common");
if (glyph_count) *glyph_count = gc;
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */
char apng[80] = "", aidx[80] = "";
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
logmsg("fonts: '%s/%s' sha256(png) 不可用,跳过校验", family, wt);
else if (jsha_png[0] && !font_sha_eq(apng, jsha_png))
logmsg("fonts: '%s/%s' WARN sha256_png 不匹配(仍可选)", family, wt);
if (font_sha256_file(ipath, aidx, sizeof aidx) != 0)
logmsg("fonts: '%s/%s' sha256(idx) 不可用,跳过校验", family, wt);
else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx))
logmsg("fonts: '%s/%s' WARN sha256_idx 不匹配(仍可选)", family, wt);
logmsg("fonts: add '%s/%s' (%s) charset=%s glyph_count=%d", family, wt, label,
cs[0]?cs:"common", gc);
return 1;
}
/* M2.58 字体索引:CSV 字段写入(始终双引号包裹,内嵌双引号加倍)。返回写入长度。 */
static int font_index_csvq(char *dst, int olen, const char *s)
{
int o = 0;
if (olen <= 0) return 0;
if (o < olen) dst[o++] = '"';
for (; *s && o < olen - 3; s++) {
if (*s == '"') dst[o++] = '"';
if (o < olen - 2) dst[o++] = *s;
}
if (o < olen) dst[o++] = '"';
dst[o] = 0;
return o;
}
/* 把当前 font_presets* 扫描结果落盘为 <card_root>/MeowOS/config/fonts/index(覆盖)。
* 每套一条 `"family","weight","path","label","tier",glyph_count`;path=卡根相对 font.png。
* 目录不存在则 mkdir -p;无卡(mkdir/fopen 失败)→ 静默放弃(仅内存默认项)。
* 该文件是 SDK meow_font_get_available_count()/enumerate() 的持久数据源(规格 §5.7)。 */
static void meow_font_write_index(const char *card_root)
{
if (!card_root || !card_root[0]) return;
char t1[360], t2[380], base[400], path[420];
snprintf(t1, sizeof t1, "%s/MeowOS", card_root);
snprintf(t2, sizeof t2, "%s/MeowOS/config", card_root);
snprintf(base, sizeof base, "%s/MeowOS/config/fonts", card_root);
/* 无卡(card_root 父目录不存在 → ENOENT)→ 静默放弃:仅内存默认项,不刷系统日志。 */
if (mkdir(t1, 0755) != 0 && errno != EEXIST) {
if (errno != ENOENT) logmsg("fonts: index mkdir fail %s", t1);
return;
}
if (mkdir(t2, 0755) != 0 && errno != EEXIST) { logmsg("fonts: index mkdir fail %s", t2); return; }
if (mkdir(base, 0755) != 0 && errno != EEXIST) { logmsg("fonts: index mkdir fail %s", base); return; }
snprintf(path, sizeof path, "%s/index", base);
FILE *f = fopen(path, "w");
if (!f) { logmsg("fonts: index fopen fail %s", path); return; }
char line[1024];
for (int i = 0; i < font_presets_n; i++) {
const char *fam = font_preset_fams[i] ? font_preset_fams[i] : "default";
const char *wt = font_preset_wts[i] ? font_preset_wts[i] : "regular";
const char *val = font_presets[i] ? font_presets[i] : "MeowOS/fonts/default/regular/font.png";
const char *lbl = font_preset_labels[i] ? font_preset_labels[i] : fam;
const char *tier = font_preset_tiers[i] ? font_preset_tiers[i] : "common";
int o = 0;
o = font_index_csvq(line, sizeof line, fam);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, wt);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, val);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, lbl);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, tier);
line[o++] = ','; line[o] = 0;
snprintf(line + o, sizeof line - (size_t)o, "%d\n", font_preset_glyphs[i]);
fputs(line, f);
}
fclose(f);
logmsg("fonts: index written (%d entries) -> %s", font_presets_n, path);
}
/* 扫描 <card_root>/MeowOS/fonts/ 下两级 <family>/<weight>/,重建字体 preset 列表。
* 每个 (family,weight) 组合一套(如 misans/regular、misans/bold);family 级 'default' 由
* 索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX
* (1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。
*
* M2.58 释放语义(规格 §5.7 需求 4「扫→写 index→释放(或按需读 index)」结论):
* - 持久来源 = <card_root>/MeowOS/config/fonts/index(扫描后由 meow_font_write_index 落盘)。
* SDK 只读该文件,从不依赖本内存数组。
* - font_presets* / *_buf 均为「静态、有界(FONT_PRESETS_MAX=17)、非堆」数组:无内存泄漏,
* 故无运行期 free;「释放」含义 = 两个扫描点(启动 / 进设置页)重建 + 每套固定上限防膨胀。
* - 设置字体列表(font.file / font.class_* 下拉、family→字重子菜单)在本视图内显示时用
* 该数组;离开字体列表视图(run_settings_page 退出)无需显式 free(静态数组),下次
* 进设置/重扫自动重建。
* - ⚠️ 未做「启动后即清空数组」:meow_font_effective_paths_tiered(launch_app 大字库档位
* 选库)与 meow_font_reload_multifont 在运行期持续读 font_presets_n,清空会造成大字库
* 档位退化(red line「保持档位/多字体行为」)。如需「零驻留」,须把这几个核心路径改为
* 直接读 index(较大重构,超出本批边界,留待后续)。 */
void meow_font_scan_presets_root(const char *card_root)
{
font_presets_n = 0;
font_presets[0] = "MeowOS/fonts/default/regular/font.png"; /* 默认字库 = MeowOS/fonts/default/regular */
font_preset_labels[0] = font_preset_label_buf[0]; /* 指向缓冲(运行时派生含字重+大小标记) */
font_preset_fams[0] = font_preset_fam_buf[0];
font_preset_wts[0] = font_preset_wt_buf[0];
font_preset_names[0] = font_preset_name_buf[0];
/* 默认套也读 font.json 档位+字重(存在则跟真实档,缺则 common/regular);供分层装载/标注。 */
{
char jp[400], cs[8] = ""; int gc = 0;
char nm0[64] = "", sp0[80] = "", si0[80] = "", wt0[32] = "";
snprintf(jp, sizeof jp, "%s/%s/default/regular/font.json", card_root, MEOW_FONTS_CFG);
meow_font_json_probe(jp, nm0, sizeof nm0, sp0, sizeof sp0, si0, sizeof si0,
cs, sizeof cs, &gc, wt0, sizeof wt0);
char mk[24]; font_tier_marker(cs, gc, mk, sizeof mk);
char wd[24]; font_weight_display(wt0[0] ? wt0 : "regular", wd, sizeof wd);
snprintf(font_preset_label_buf[0], sizeof font_preset_label_buf[0], "默认字体 %s(%s)", wd, mk);
snprintf(font_preset_tier_buf[0], sizeof font_preset_tier_buf[0], "%s", cs[0]?cs:"common");
snprintf(font_preset_fam_buf[0], sizeof font_preset_fam_buf[0], "default");
snprintf(font_preset_wt_buf[0], sizeof font_preset_wt_buf[0], "%s", wt0[0]?wt0:"regular");
snprintf(font_preset_name_buf[0], sizeof font_preset_name_buf[0], "默认字体");
font_preset_tiers[0] = font_preset_tier_buf[0];
font_preset_glyphs[0] = gc;
}
font_presets_n = 1;
char root[340];
snprintf(root, sizeof root, "%s/%s", card_root, MEOW_FONTS_CFG);
DIR *d = opendir(root);
if (d) { /* 无卡/无字库目录 → 跳过扫描,仅默认 */
struct dirent *de;
while ((de = readdir(d)) && font_presets_n < FONT_PRESETS_MAX) {
if (de->d_name[0] == '.') continue;
if (!strcmp(de->d_name, "default")) continue; /* 默认字体由索引 0 单独处理 */
char fampath[400];
snprintf(fampath, sizeof fampath, "%s/%s", root, de->d_name);
DIR *w = opendir(fampath); /* 一级:<family>/ */
if (!w) continue;
struct dirent *we;
while ((we = readdir(w)) && font_presets_n < FONT_PRESETS_MAX) {
if (we->d_name[0] == '.') continue;
char dirpath[400], label[96], cs[8] = ""; int gc = 0; char wt[32] = "", nm[64] = "";
snprintf(dirpath, sizeof dirpath, "%s/%s", fampath, we->d_name);
if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, label, sizeof label,
cs, sizeof cs, &gc, wt, sizeof wt, nm, sizeof nm) == 0)
continue; /* 缺组件/损坏 → 跳过该套 */
int i = font_presets_n;
snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i],
"%s/%s/%s/font.png", MEOW_FONTS_CFG, de->d_name, we->d_name);
snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label);
snprintf(font_preset_tier_buf[i], sizeof font_preset_tier_buf[i], "%s", cs[0]?cs:"common");
snprintf(font_preset_fam_buf[i], sizeof font_preset_fam_buf[i], "%s", de->d_name);
snprintf(font_preset_wt_buf[i], sizeof font_preset_wt_buf[i], "%s", wt);
snprintf(font_preset_name_buf[i], sizeof font_preset_name_buf[i], "%s", nm);
font_presets[i] = font_preset_val_buf[i];
font_preset_labels[i] = font_preset_label_buf[i];
font_preset_tiers[i] = font_preset_tier_buf[i];
font_preset_fams[i] = font_preset_fam_buf[i];
font_preset_wts[i] = font_preset_wt_buf[i];
font_preset_names[i] = font_preset_name_buf[i];
font_preset_glyphs[i] = gc;
font_presets_n++;
}
closedir(w);
}
closedir(d);
}
/* M2.58:扫描结果落盘为 <card_root>/MeowOS/config/fonts/index(含默认行 index0),
* 作为 SDK meow_font_get_available_count()/enumerate() 的持久数据源;无卡时仅默认 1 行,
* 由 meow_font_write_index 内部 mkdir-fail 静默放弃(此路径不刷屏)。 */
meow_font_write_index(card_root);
}
/* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。 */
void meow_font_scan_presets(void)
{
meow_font_scan_presets_root("/usr/data/mnt/sd_0");
}
/* MD3 tonal palette(565 参考值见 MD3_UI_SPEC §1;此处存 24-bit RGB,#hex) */
static const meow_ui_theme_t meow_ui_theme_dark = {
0x141824, 0x1E2436, 0x263038, 0x4A6A9A, 0xFFFFFF,
0x9AA4C8, 0x2A2E38, 0xE57373, 0x4A6A9A
};
static const meow_ui_theme_t meow_ui_theme_light = {
0xE8EAF0, 0xD5D9E4, 0xCFD4E0, 0x2A5A8A, 0x1A1A2A,
0x4A4F5A, 0x9AA4C8, 0xB3261E, 0x2A5A8A
};
/* 获取当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用 */
static int s_now_min_of_day(void)
{
time_t t = time(NULL);
struct tm tm;
localtime_r(&t, &tm);
return tm.tm_hour * 60 + tm.tm_min;
}
/* 深色时段判定(纯逻辑,便于单测):now_min ∈ [start, end) → 深色。
* - start==end 视为未配置,返回 -1(由调用方决定回退策略)。
* - start<end:非跨天,如 720→1080 = 12:00→18:00,now ∈ [720,1080) 为深色。
* - start>end:跨午夜,如 1140→420 = 19:00→次日07:00,now∈[1140,1440)∪[0,420) 为深色。
* - 边界:end 半开(恰好 end 分回到浅色),start 闭(恰好 start 分进入深色)。 */
static int theme_should_dark(int now_min, int start, int end)
{
if (start == end) return -1; /* 未配置 */
if (start < end)
return (now_min >= start && now_min < end) ? 1 : 0;
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
}
/* 当前主题(dark/light 两套;接 display.theme_mode + display.theme)。
* manual(mode==THEME_MODE_MANUAL):直读 display.theme(dark/light),保持原有行为。
* auto(mode==THEME_MODE_AUTO):读深色时段两键 + 当前本地分钟;start==end 未配置 →
* 回退 manual(读 display.theme,避免误判全天深色)。
* 读取失败一律回退 dark。 */
const meow_ui_theme_t *s_ui_theme(void)
{
int mode = THEME_MODE_MANUAL;
if (meow_settings_get_enum("display.theme_mode", &mode) != 0) mode = THEME_MODE_MANUAL;
int v = 0; /* display.theme(manual)当前索引 */
int have_manual = (meow_settings_get_enum("display.theme", &v) == 0);
if (mode == THEME_MODE_AUTO) {
int start = 0, end = 0;
meow_settings_get_int("display.theme_dark_start", &start);
meow_settings_get_int("display.theme_dark_end", &end);
int d = theme_should_dark(s_now_min_of_day(), start, end);
if (d < 0) { /* 未配置:回退 manual(display.theme) */
if (have_manual && v == 1) return &meow_ui_theme_light;
return &meow_ui_theme_dark;
}
return d ? &meow_ui_theme_dark : &meow_ui_theme_light;
}
/* manual:照旧读 display.theme */
if (have_manual && v == 1) return &meow_ui_theme_light;
return &meow_ui_theme_dark;
}
+204
View File
@@ -0,0 +1,204 @@
/* keys.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
int kd_tick(struct kd_state *s, int raw)
{
unsigned long long now = now_ms();
int ev = KD_NONE;
if (raw != s->raw) {
if (s->raw == 0 && raw != 0) { /* 任何跳变都重记稳定窗口(含原始 raw 变化) */ }
s->raw = raw; s->since = now;
}
int want = s->raw;
if (want != s->stable) {
if (now - s->since >= KEY_DEBOUNCE_MS) {
s->stable = want;
if (s->stable && !s->down) { s->down = 1; s->down_at = now; s->longpressed = 0; ev = KD_DOWN; }
else if (!s->stable && s->down) { s->down = 0; ev = KD_UP; }
}
}
if (s->down && !s->longpressed && now - s->down_at >= CLICK_MS) s->longpressed = 1;
return ev;
}
/* Unblank the panel AFTER the player is frozen: with the player SIGSTOPped
* it can no longer re-blank, so the menu (drawn on fb0) stays visible even
* when the menu opened while the player's screen timeout had blanked the
* panel. The blank state still belongs to the player's state machine — on
* session end restore_backlight() re-blanks when menu_opened_blank, so the
* player resumes into a consistent (blanked) state. */
void unblank_panel(void)
{
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); }
logmsg("panel unblanked (player frozen)");
}
void meow_panel_off(void)
{
meow_saved_brightness = read_brightness();
if (meow_saved_brightness <= 0) meow_saved_brightness = 60;
write_brightness(0);
if (screen_off_mode == 0) {
/* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */
int f = open(FB_DEV, O_RDWR);
if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); }
} else {
/* black:仅背光 0 全黑(面板仍上电,触摸可唤醒) */
}
meow_screen_on = 0;
logmsg("meow panel OFF (mode=%s brightness 0%s)",
screen_off_mode ? "black" : "panel",
screen_off_mode ? "" : " + blank 4");
}
void meow_panel_on(void)
{
int f = open(FB_DEV, O_RDWR);
if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); }
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60);
meow_screen_on = 1;
/* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在
* 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位
* 到非 buffer0 / 失效位置,缓存仍是 0 导致真正的 re-pan 从未发出 —— M2.7 只加 fb_norm_buf0
* 仍黑屏的原因。主界面都画在 buffer0(mmap 偏移 0),此处强制 re-pan 保证唤醒后落在 buffer0。
* 当前界面内容的重绘由调用方(run_menu 醒屏分支)经 meow_repaint_surface 完成。 */
fb_pan_buf0_force();
logmsg("meow panel ON (blank 0 + brightness %d)", meow_saved_brightness);
}
void meow_toggle_panel(void) { if (meow_screen_on) meow_panel_off(); else meow_panel_on(); }
int kp_open(struct keypoll *kp, const char *dev)
{
memset(kp, 0, sizeof *kp);
kp->fd = open(dev, O_RDONLY | O_NONBLOCK);
if (kp->fd < 0) { logmsg("open %s: %s", dev, strerror(errno)); return -1; }
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) {
logmsg("EVIOCGKEY %s: %s", dev, strerror(errno));
close(kp->fd); return -1;
}
memcpy(kp->prev, kp->cur, sizeof kp->cur);
kp->ok = 1;
logmsg("keypoll %s ready", dev);
return 0;
}
/* 关闭 keypoll(设置页/下拉框等自开设备用;幂等,ok=0 时不动) */
void kp_close(struct keypoll *kp)
{
if (kp->ok && kp->fd >= 0) close(kp->fd);
kp->ok = 0;
}
/* Poll device key state; call cb(code, is_down) for each edge.
* Returns 1 if any edge. */
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
void *ctx)
{
if (!kp->ok) return 0;
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) return 0;
int changed = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp->cur[w] == kp->prev[w]) continue;
for (int b = 0; b < 32; b++) {
int c = (kp->cur[w] >> b) & 1;
int p = (kp->prev[w] >> b) & 1;
if (c != p) {
int code = w * 32 + b;
changed = 1;
if (cb) cb(code, c, ctx);
}
}
kp->prev[w] = kp->cur[w];
}
return changed;
}
int kp_is_down(const struct keypoll *k, int code)
{
return (k->cur[code / 32] >> (code % 32)) & 1;
}
int hotkey_mode = HOTKEY_MODE_PWRVOL;
int seq_codes[MAX_SEQ_LEN];
int seq_len = 0;
int seq_gap_ms = 600;
static int key_name_code(const char *n)
{
if (!strcmp(n, "volup")) return KEY_VOLUMEUP;
if (!strcmp(n, "voldown")) return KEY_VOLUMEDOWN;
if (!strcmp(n, "play")) return KEY_PLAYPAUSE;
if (!strcmp(n, "next")) return KEY_NEXT;
if (!strcmp(n, "previous")) return KEY_PREVIOUSSONG;
if (!strcmp(n, "power")) return KEY_POWER;
return -1;
}
void load_config(void)
{
menu_bg = 0x000000;
hotkey_mode = HOTKEY_MODE_PWRVOL; /* default: hold power + VOL_DOWN tap */
seq_len = 0;
seq_gap_ms = 600;
char path[256];
snprintf(path, sizeof path, "%s/meow.conf", MEOWAPPS);
FILE *f = fopen(path, "r");
if (f) {
char buf[128];
while (fgets(buf, sizeof buf, f)) {
if (!strncmp(buf, "bg_color=", 9)) {
unsigned int v;
if (sscanf(buf + 9, "%x", &v) == 1) menu_bg = v & 0xFFFFFF;
} else if (!strncmp(buf, "hotkey=", 7)) {
char *p = buf + 7;
while (*p == ' ' || *p == '\t') p++;
if (!strncmp(p, "seq", 3)) hotkey_mode = HOTKEY_MODE_SEQ;
else hotkey_mode = HOTKEY_MODE_PWRVOL;
} else if (!strncmp(buf, "hotkey_seq=", 11)) {
char *p = buf + 11;
seq_len = 0;
/* BUGFIX: the outer loop must consume the EOL — with
* *p staying at '\n' the empty-name branch never advanced p
* and the daemon spun forever (found on-device: a hotkey_seq
* line wedged load_config in a busy loop). */
while (*p && *p != '\n' && *p != '\r' && seq_len < MAX_SEQ_LEN) {
char name[16];
int k = 0;
while (*p && *p != ',' && *p != '\n' && *p != '\r' && k < 15)
name[k++] = *p++;
name[k] = 0;
if (k >= 15) /* malformed long name: skip to separator/EOL */
while (*p && *p != ',' && *p != '\n' && *p != '\r') p++;
if (*p == ',') p++;
int c = key_name_code(name);
if (c > 0) seq_codes[seq_len++] = c;
}
} else if (!strncmp(buf, "hotkey_seq_gap=", 15)) {
int g = atoi(buf + 15);
if (g >= 200 && g <= 2000) seq_gap_ms = g;
}
}
fclose(f);
}
if (seq_len == 0) { seq_codes[0] = KEY_VOLUMEUP; seq_len = 3; } /* seq fallback volup x3 */
if (hotkey_mode == HOTKEY_MODE_PWRVOL)
logmsg("config: bg_color=%06x hotkey=power_voldown (window %d-%dms)",
menu_bg, HK_PWRWIN_MIN, HK_PWRWIN_MAX);
else
logmsg("config: bg_color=%06x hotkey=seq (%d taps of code %d gap=%dms)",
menu_bg, seq_len, seq_codes[0], seq_gap_ms);
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -2,7 +2,7 @@
# meow_card_path.sh - TF 卡 MeowOS/opt/bin(外围 CLI 迁卡后)在卡在时前插 PATH;无卡跳过不崩。
# 安装到 rootfs /etc/profile.d/(由 /etc/profile 对登录 shell source)。
# 使 SSH 登录会话可直接用卡上工具(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd)。
# MeowOS 启动器把该目录前插进自启子进程的 PATH(见 meow_launcherd.c);本文件覆盖 SSH 会话。
# MeowOS 启动器把该目录前插进自启子进程的 PATH(见 meow_launcherd/ app_launcher.c launch_app);本文件覆盖 SSH 会话。
if [ -d /usr/data/mnt/sd_0/MeowOS/opt/bin ]; then
export PATH="/usr/data/mnt/sd_0/MeowOS/opt/bin:$PATH"
fi
@@ -0,0 +1,642 @@
/* meow_launcher_internal.h — MeowOS launcher 拆分模块共享内部头(LAUNCHER_SPLIT_PLAN). */
/* 只收“被 2 个以上模块引用”的声明;单一模块自含的不上此头。逻辑与原 meow_launcherd.c 0-diff。*/
#ifndef MEOW_LAUNCHER_INTERNAL_H
#define MEOW_LAUNCHER_INTERNAL_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <dirent.h>
#include <stdint.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <poll.h> /* 0-warning:musl 把 <sys/poll.h> 重定向到 <poll.h>,用后者消除 -W#warnings */
#include "meow_touch.h" /* M2.25:Launcher 统一触摸接口(电源菜单/确认框复用) */
#include "pm_icons.h" /* M2.32:Material Symbols rounded 图标(电源菜单圆钮,alpha 位图+主题色) */
#include "sb_icons.h" /* MD3 状态栏/通知中心图标(Material Symbols rounded,32x32 单通道 alpha) */
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
#include <pthread.h> /* M0:IPC 线程 */
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
#include <sys/socket.h> /* M0:AF_UNIX IPC */
#include <sys/un.h> /* M0:sockaddr_un */
#include <linux/input.h>
#include "meow_font.h" /* M2.6: 预渲染位图字体(agent-28 交付;对象已打包进 meow_ui.a,launcher 不再单独链 meow_font.a) */
#include "meow_ipc.h" /* M0+M1:IPC 协议帧/消息/负载定(与 meow_ipc 工具同源) */
#include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */
#include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */
#include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */
#include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */
#define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL, PLAY, NEXT */
#define DEV_TOUCH "/dev/input/event1" /* goodix-ts (not grabbed, shareable) */
#define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs"
#define LOG_DIR_CARD "/usr/data/mnt/sd_0/MeowOS/logs" /* TF card logs(系统约定:系统文件放 MeowOS/ 下) */
#define PID_PATH "/usr/data/meow_launcherd.pid"
#define WATCHDOG1 "/usr/data/meow_watchdog"
#define WATCHDOG2 "/usr/bin/meow_watchdog"
#define POWMENU_MARKER "/tmp/meow_power_menu"
#define POWMENU_ACTIVE "/tmp/meow_power_menu_active"
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
#define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power"
#define FB_DEV "/dev/fb0"
#define MOUNTS "/proc/mounts"
#ifndef KEY_POWER
#define KEY_POWER 116
#endif
#ifndef KEY_PREVIOUSSONG
#define KEY_PREVIOUSSONG 165
#endif
#ifndef KEY_VOLUMEDOWN
#define KEY_VOLUMEDOWN 114
#endif
#ifndef KEY_VOLUMEUP
#define KEY_VOLUMEUP 115
#endif
#ifndef KEY_PLAYPAUSE
#define KEY_PLAYPAUSE 164
#endif
#ifndef KEY_NEXT
#define KEY_NEXT 163
#endif
#define SEQ_DEFAULT_TAPS 3 /* default hotkey: VOL_UP x3 quick taps */
#define POLL_MS 10
#define MEOWOS_VERSION "dev" /* M2.50:版本号唯一来源 = /etc/meowos_version(运行时 load_meowos_version 读);本宏仅作文件缺失时占位,不再当实际版本,升级只改 /etc/meowos_version */
#define MAX_APPS 8
#define TOAST_MS 1500
#define LOG_SIZE_LIMIT (1u << 30) /* 1 GB total on the TF card */
#define MEOW_MAX_ACTS 16
#define MEOW_MAX_ROUTES 8
#define MEOW_MAX_SVCS 8
#define BTN_TOUCH 0x14a
#define ABS_X 0x00
#define ABS_Y 0x01
#define ABS_MT_POSITION_X 0x35
#define ABS_MT_POSITION_Y 0x36
#ifndef EV_KEY
#define EV_KEY 1
#endif
#ifndef EV_ABS
#define EV_ABS 3
#endif
#ifndef EVIOCGKEY
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len)
#endif
#define KEYBUF_WORDS 32 /* 1024 bits, covers KEY_MAX 767 */
#define BOOT_BUF_TMP "/tmp/meow_boot.log"
#define BOOT_BUF_MAX (256 * 1024)
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOGET_FSCREENINFO 0x4602
#define FBIOPAN_DISPLAY 0x4606
#define FBIOBLANK 0x4611
#define FBIO_WAITFORVSYNC 0x4620
#define KEY_DEBOUNCE_MS 40 /* 键值须稳定此毫秒数才认可(抗 ADC 抖动) */
#define CLICK_MS 400 /* 按压 <此毫秒 = 单击一次;≥ = 长按(连续) */
#define SCREEN_OFF_TIMEOUT_DEFAULT 30
#define SCREEN_OFF_MODE_DEFAULT 0 /* 0=panel, 1=black */
#define PWR_MENU_MS_DEFAULT 3000
#define PWR_FORCE_MS_DEFAULT 8000
#define REFRESH_RATE_CFG_DEFAULT 60
#define UI_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.png"
#define UI_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.idx"
#define UI_FONT_PNG_SYS "/usr/share/meowos/font/font.png"
#define UI_FONT_IDX_SYS "/usr/share/meowos/font/font.idx"
#define MEOW_FONTS_CFG "MeowOS/fonts"
#define FONT_PRESETS_MAX 17 /* 1 默认 + ≤16 卡内字体(防爆) */
#define ROW_H 52
#define ROW_TOP 96
#define EXIT_ROW 640
#define TOAST_Y 590
#define HOTKEY_MODE_PWRVOL 0
#define HOTKEY_MODE_SEQ 1
#define HK_PWRWIN_MIN 60 /* min power-hold time before VOL_DOWN counts */
#define HK_PWRWIN_MAX 2500 /* max power-hold window; beyond this no trig */
#define MAX_SEQ_LEN 6
#define MEOW_VOL_MAX 100 /* 硬上限 0-100 */
#define MEOW_VOL_DEFAULT 60 /* 默认音量 */
#define MEOW_VOL_STEP_DEFAULT 1 /* 默认跳步 1%(线性渐变;可配 audio.volume_step 1-5) */
#define MEOW_VOL_STEP_MAX 5 /* 跳步上限(SYSTEM_SETTINGS.md:最大 5) */
#define MEOW_VOL_MAX_DEFAULT 100 /* 默认最大音量上限(尊重上限;可配 audio.volume_max) */
#define VOL_DRAG_DAC_MS 60 /* 滑条拖动期间两次真实 DAC 写入的最短墙钟间隔 */
#define VOL_RAMP_MS 15 /* 每 1% 步进的最短墙钟间隔 */
#define BT_ROLE_OFF 0 /* 关 */
#define BT_ROLE_ON_NO_CONN 1 /* 开未连接 */
#define BT_ROLE_MASTER 2 /* 主模式:连了外围(如耳机) */
#define BT_ROLE_SLAVE 3 /* 从模式:被远端接入,设备自身当耳机 */
#define BT_ROLE_UNKNOWN (-1) /* 判定失败(调用方按实际态兜底) */
#define MEOWOS_VERSION_FILE "/etc/meowos_version"
#define MEOWOS_VERSION_DEF MEOWOS_VERSION /* 兜底默认 = 编译期宏;运行时读 /etc/meowos_version 覆盖 */
#define WIFI_LVL_UNKNOWN (-1000) /* wifi_signal_level 读取失败哨兵(低于任何真实 dBm) */
#define SB_BAR_Y 0
#define SB_BAR_H 28 /* rows 0..27;分隔线在 y=28 不在此(40→28,用户定) */
#define SB_FONT 20 /* 状态栏字体 28→20(28px 栏放不下 28 号;垂直居中锚点见 sb_text_y) */
#define MEOW_SBAR_ANIM_MS 200 /* App 前台全屏/收合切换的顶条「上移出屏」过渡时长(ms,agent-146) */
#define THEME_MODE_MANUAL 0 /* display.theme_mode 枚举索引 0 = 手动 */
#define THEME_MODE_AUTO 1 /* display.theme_mode 枚举索引 1 = 时段自动 */
#define SB_TGAP 6 /* 图标右缘 ↔ 文字间隙 */
#define SB_XGAP 10 /* 相邻两项(右→左)间隙 */
#define SB_ICON_INK 16 /* 主图标目标 ink 高(px):24→16(28px 栏等比缩放,留上下边距) */
#define SB_CLK_MARGIN 14 /* 右簇左缘相对时钟右缘须留出的安全间距 */
#define SB_ICON_CY 14
#define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */
#define NCP_DRAG_ENTER_SLOP 16
#define NCP_DRAG_THR_PCT 40
#define TAP_ARM_DELAY_MS 300
#define PM_NUM 5
#define PM_R 50 /* 圆钮半径(直径 100) */
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
#define PM_GAPY 140 /* 相邻行圆心 y 间距 */
#define PM_CARD_X0 40 /* 圆角卡片左 */
#define PM_CARD_W 400 /* 圆角卡片宽(40..440,居中) */
#define PM_CARD_CX (PM_CARD_X0 + PM_CARD_W / 2) /* 卡片水平中心 240 */
#define PM_CARD_Y0 115 /* 圆角卡片顶 */
#define PM_CARD_H 505 /* 圆角卡片高(115..620) */
#define PM_CARD_R 26 /* 圆角卡片圆角 */
#define LINUX_REBOOT_MAGIC1 0xfee1dead
#define LINUX_REBOOT_MAGIC2 672274793 /* 0x28121969 */
#define LINUX_REBOOT_CMD_RESTART 0x01234567
#define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc
#define NET_LIST_WIFI_SSID 1 /* WiFi 当前连接(只读信息;点开显示详情) */
#define NET_LIST_WIFI_SAVED 2 /* WiFi 已保存网络(下拉,可 connect) */
#define NET_LIST_WIFI_SCAN 3 /* 附近WiFi:WiFi 扫描结果(按 BSSID 去重,只读展示) */
#define NET_LIST_BT_NEARBY 4 /* 蓝牙附近设备(已配对 + 扫描发现;下拉,只读展示) */
#define NET_LIST_BT_MODE 5 /* 蓝牙当前连接模式(只读信息;对应"蓝牙模式"行,s_net_get 显示) */
#define NET_LIST_ABOUT 6 /* 系统设置「关于本机」:只读设备信息弹窗(复用 s_dd_modal) */
#define S_NET_MAX 8 /* 网络下拉一次最多展示项(屏幕高度限制) */
#define BG_PRESETS_N 3
#define HK_PRESETS_N 2
#define SEQ_PRESETS_N 2
#define OUT_PRESETS_N 1
#define S_TIME_TIERS_N ((int)(sizeof s_time_tiers / sizeof s_time_tiers[0]))
#define SMENU_N ((int)(sizeof SMENU / sizeof SMENU[0]))
#define SROW_H 58 /* M2.37:行高 52→58,便于触摸点选 */
#define SET_CARD_X 8
#define SET_CARD_W (fb_xres - 16)
#define SET_CARD_Y 92
#define SET_CARD_H 536 /* 卡片底 628;视口高 = 536-16 = 520 = 10×52 */
#define SET_CONTENT_X (SET_CARD_X + 12)
#define SET_CONTENT_W (SET_CARD_W - 24)
#define SET_CONTENT_Y (SET_CARD_Y + 8)
#define SET_CONTENT_H (SET_CARD_H - 16)
#define SET_CONTENT_BOT (SET_CONTENT_Y + SET_CONTENT_H)
#define S_SCROLL_SLOP 40 /* px:触摸纵向位移超此值 → 判为列表拖动滚动(非 tap;tap 判定 ady<60) */
#define S_SLIDER_X (SET_CARD_X + 16)
#define S_SLIDER_W (SET_CONTENT_W)
#define NCP_TOP SB_BAR_H /* 28:面板顶缘(状态栏下) */
#define NCP_GRAB_Y 32
#define NCP_TITLE_Y 46
#define NCP_CHIP_Y 86
#define NCP_CHIP_H 44
#define NCP_CHIP_W 70
#define NCP_CHIP_GAP 8
#define NCP_CHIP_N 5 /* ⑥:快捷 chip 扩充为 5 个(+电源菜单) */
#define NCP_SLIDER_X 20
#define NCP_SLIDER_W (fb_xres - 40)
#define NCP_SL1_Y 162 /* 亮度滑条 track 顶(label 在 -26 =136) */
#define NCP_SL2_Y 214 /* 音量滑条 track 顶(label 在 -26 =188) */
#define NCP_DIV_Y 258
#define NCP_DND_Y 264 /* ⑥:勿扰面板内开关带顶缘(分隔线下方) */
#define NCP_DND_H 40 /* 勿扰开关带高(label+状态+s_draw_switch 48×28) */
#define NCP_LIST_TOP 308 /* 通知列表顶缘(原 266 → 下移 42 给勿扰带) */
#define NCP_LIST_BOT 694
#define NCP_ROW_H 92
#define NCP_ROW_X 12
#define NCP_ROW_W (fb_xres - 24)
#define NCP_ICON_X 26
#define NCP_TEXT_X 72
#define NCP_TEXT_W (NCP_ROW_W - (NCP_TEXT_X - NCP_ROW_X) - 10)
#define NCP_FOOT_Y 700
#define NCP_MAX_ROWS 24 /* 面板一次缓冲的通知行上限(防御) */
#define NCP_SCROLL_REBOUND_TOL 48 /* px:列表拖到顶/底仍继续拖过此容差 → 判回弹(关闭语义),
避免在边界小幅抖动即误关(> S_SCROLL_SLOP=40) */
#define NCP_SWIPE_SLOP 24 /* px:行内水平位移达此值且横向主导+左移 → 进行左滑(>阈值) */
#define NCP_SWIPE_RATE 3 /* px/ms:行左滑落位动画线性速率(回弹/滑出) */
#define NCP_SWIPE_DELETE_FRAC 40 /* %:行宽百分比阈值(│off│>=row_w*40/100 → 删) */
#define NCP_SWIPE_DELETE_MIN 110 /* px:绝对像素阈值(│off│>=110 → 删;OR 语义,窄行由 frac 起作用) */
#define NCP_DND_MIN_URGENCY 2
#define NCP_DND_SPLIT_X (fb_xres / 2) /* 勿扰/清除带分割中线(左右对称) */
#define NCP_ANIM_FIX 1024 /* 进度 p 的定点分母(1/1024 精度) */
#define NCP_ANIM_MS 180 /* 单程打开/关闭时长(ms,真机可调) */
#define MEOW_IPC_RING 16 /* 无锁环深度(单生产者=IPC线程,单消费者=主循环) */
#define MEOW_IPC_CONNS 10 /* 同时接入连接上限(backlog 4 + 余量) */
#define MEOW_IPC_BACKLOG 4
#define MEOW_SBAR_MODE_FILE "/tmp/meowapp_sbar"
/* ---- 共享结构体/枚举(来自 meow_launcherd.c,拆分后统一放置)---- */
struct kd_state { int raw, stable, down; int longpressed; unsigned long long since, down_at; };
/* ---- M2.16 按键防抖:键值须稳定 KEY_DEBOUNCE_MS 才认可,防 ADC 抖动("点一下跳 5-6 下")。
* 返回值:0=无事件;KD_DOWN=稳定按下边沿;KD_UP=稳定松开边沿(下沿后计数)。
* s->down=稳定按住;s->longpressed=按住已 ≥CLICK_MS(长按)。 */
enum { KD_NONE = 0, KD_DOWN = 1, KD_UP = 2 };
/* ================================================================== */
/* key state polling (EVIOCGKEY) */
/* ================================================================== */
struct keypoll {
int fd;
unsigned long cur[KEYBUF_WORDS];
unsigned long prev[KEYBUF_WORDS];
int ok;
};
/* ================================================================== */
/* app scanning (MeowAPPs spec) */
/* ================================================================== */
/* 路由行:route.<act>=<chain>。act=route 键后缀(activity_id 的 app 内部分),
* chain=根屏→目标屏的 activity_id 序列(';' 分隔),链长=逻辑深度。 */
struct meow_route {
char act[16]; /* route 键后缀,如 "nowplaying" */
char chain[MEOW_CHAIN_MAX]; /* 如 "home;search;nowplaying" */
int keep_awake; /* M2.17:该 activity 常亮(app.json awake.<id>=1 / route.<id>.keep_awake=1) */
int fullscreen; /* 活动级全屏声名(app.json route.<id>.fullscreen=1;M3 活动切换时消费) */
};
/* 服务行:service.<svc>=mode:restart:max:window_ms(M1 仅解析存储,M4 保活) */
struct meow_svc {
char name[16];
int mode; /* 0=keepalive 1=ondemand(预留) */
int restart; /* 1=退避重启 */
int max; /* max_restart 熔断阈值 */
int window_ms; /* 重启窗口 */
};
struct app {
char id[MEOW_APP_ID_MAX]; /* M2.52:app_id=app.json 的 package(权威,缺省回退卡上目录名 de->d_name),供 MEOW_LOG_APP 注入 */
char package[MEOW_APP_ID_MAX]; /* 安卓化:app.json 的 package 字段(反域名权威标识;目录名=package),scan_apps 据此定 app_id */
char name[17]; /* ASCII display name */
char exec[129]; /* absolute path */
int ok; /* exec exists and X_OK */
/* ---- 阶段 D · M1:app.json 路由/框架/服务/图标(scan_apps 解析)---- */
char framework; /* '1'=framework=true(看门狗返回发 SIGUSR1) */
char immersive; /* '1'=immersive=true(隐藏虚拟返回键,返回手势归 SDK) */
char fullscreen; /* '1'=app.json fullscreen=true(注入 MEOW_FULLSCREEN=1,SDK 预先设全屏) */
char fonts; /* 字库档:0=默认4K常用 1=大字库GBK 2=Full(app.json "fonts";注入 MEOW_FONT_TIER) */
char icon[129]; /* icon= 路径 */
int nroute; /* route.<id> 行数 */
struct meow_route routes[MEOW_MAX_ROUTES];
int nsvc;
struct meow_svc svcs[MEOW_MAX_SVCS];
};
/* Activity 路由注册表项:app_id 全局唯一的运行时路由层,由 scan_apps(静态 app.json)
* 与 MSG_ACT_DECLARE/HELLO(运行时)并集填充;单一写者=主循环,无需锁。 */
struct meow_acct {
char app_id[MEOW_APP_ID_MAX];
char name[17];
char exec[129];
int ok;
char framework;
char icon[129];
int nroute;
struct meow_route routes[MEOW_MAX_ROUTES];
int nsvc;
struct meow_svc svcs[MEOW_MAX_SVCS];
int declared; /* 是否经 HELLO/ACT_DECLARE 运行时声明过 */
};
/* ---- volume save/restore ---- */
/* The player sets volume via ioctl on /dev/snd/controlC0 writing the
* CS43198 "Left/Right Playback Volume" controls (§11.4). We snapshot the
* same controls through amixer so a hotkey sequence's volume side effects
* can be restored silently (the player itself never reads these; next key
* press it writes its own value again — acceptable). */
struct vol_snap { int l, r; };
/* ================================================================== */
/* 系统状态统一快照(A:进 MeowOS 全量重抓 + 每进必刷) */
/* 主循环 10ms 热键轮询绝不读系统态;全量 meow_sys_refresh() 仅在进 */
/* MeowOS 时调一次;1s 轻刷 meow_sys_light() 只更新时钟/电量/WiFi, */
/* 并约 5s 节流探测一次蓝牙角色(M2.48)。 */
/* 状态栏/后续设置/面板统一读此快照,单点一致,避免重复读 amixer/sysfs。*/
/* ================================================================== */
typedef struct meow_sys_state {
int batt; /* 容量 % 0-100,-1 未知 */
int charging; /* 1 充电 0 放电 */
int batt_mv; /* 电压 mV */
int wifi_on; /* M2.48:1 网卡 up(射频开)0 down(关则不显示图标) */
int wifi_connected;/* M2.48:1 已关联 AP(operstate up)0 未连(区分「网卡up未连」→WIFI_NO) */
int wifi_level; /* M2.66+:信号 dBm(-1000=未知);状态栏 WiFi 分档(强/中/弱),喂给 SDK */
char wifi_ssid[33];
char wifi_ip[16];
int bt_on; /* 1 蓝牙上电 0 关 */
int bt_seen; /* hci 节点存在 1/0(无模块不放图标) */
int bt_role; /* M2.48:蓝牙角色(bt_role() 判定缓存):BT_ROLE_* */
int vol; /* 系统软件音量 0-100(settings) */
int brightness; /* sysfs */
long data_free_kb; /* /data 剩余 KB */
int sd_mounted; /* TF 卡已挂载 */
long sd_free_kb; long sd_total_kb;
int mode; /* 1 meow 0 hiby(调用方) */
int napps; /* 应用数(调用方) */
char ver[16];
} meow_sys_state;
/* ---- 主题(MD3 color roles,dark/light 两套)----
* M2.44:上移到本处(状态栏绘制之前),使状态栏/设置页共用同一套主题色板。 */
typedef struct meow_ui_theme {
uint32_t surface; /* 全局背景 */
uint32_t surface_variant; /* 卡片/列表底 */
uint32_t surface_container; /* 控件容器 */
uint32_t primary; /* 高亮/选中/accent */
uint32_t on_surface; /* 主文字 */
uint32_t on_surface_variant; /* 次文字 */
uint32_t outline; /* 描边/分隔线 */
uint32_t error; /* 错误提示 */
uint32_t surface_tint; /* 选中态叠色 */
} meow_ui_theme_t;
/* ================================================================== */
/* M2.36 阶段 B2b:实体系统设置页(MD3 风格) */
/* - 入口:应用列表「系统设置」builtin 行(draw_menu 中位于 Exit 行上 */
/* 方;触摸命中 run_menu 分发到此 run_settings_page)。 */
/* - 结构:分组列表(显示/电源/开机/输入/字体主题/通知/日志/音频), */
/* 全屏单张 surface_variant 圆角卡片,组标题 on_surface_variant 小字。*/
/* - 控件:开关(SC_SWITCH)/滑条(SC_SLIDER)/选择器(SC_ENUM)/预设路径 */
/* (SC_PRESET)。所有读写仅走 meow_settings_getXXX / setXXX 接口; */
/* meow_settings_save(),绝不直接改文件(SYSTEM_SETTINGS §4)。 */
/* ================================================================== */
/* M2.36 阶段 B2b:实体系统设置页(MD3 风格) */
/* - 入口:应用列表「系统设置」builtin 行(draw_menu 中位于 Exit 行上 */
/* 方;触摸命中 run_menu 分发到此 run_settings_page)。 */
/* - 结构:分组列表(显示/电源/开机/输入/字体主题/通知/日志/音频), */
/* 全屏单张 surface_variant 圆角卡片,组标题 on_surface_variant 小字。*/
/* - 控件:开关(SC_SWITCH)/滑条(SC_SLIDER)/选择器(SC_ENUM)/预设路径 */
/* (SC_PRESET)。所有读写仅走 meow_settings_getXXX / setXXX 接口; */
/* meow_settings_save(),绝不直接改文件(SYSTEM_SETTINGS §4)。 */
/* - 主题:MD3 color roles(dark/light 两套,接 display.theme),集中 */
/* 下表,禁止散落硬编码色值(MD3_UI_SPEC §1)。 */
/* - 交互:音量键=调系统音量 / 电源键=系统级语义(单击=息屏/亮屏、长按=电源菜单、
8s=强关;不再返回)/ 播放键=抓住(不返回)/ 退出仅靠触摸(底部返回 + 左缘左滑)。*/
/* 选中行 tonal 高亮(meow_ui_fill_rect_alpha),滑条拖动实时 set_*。 */
/* 静态展示不主动刷新,仅交互时经 meow_dirty 区标脏 + blit 局部。 */
/* ================================================================== */
/* 主题(MD3 color roles)定义已上移到本文件前方状态栏处(M2.44)。 */
/* ---- 控件类型 ---- */
typedef enum { SC_HEADER, SC_SWITCH, SC_SLIDER, SC_ENUM, SC_PRESET, SC_INFO, SC_LIST } sctrl_t;
/* ---- 一行描述(label+key+控件);key=NULL 表示分组标题行 ---- */
typedef struct {
const char *label;
const char *key;
sctrl_t ctrl;
int min, max, step; /* SC_SLIDER 取值域/步进 */
const char *const *presets; /* SC_PRESET 选项名 */
const char *const *preset_labels; /* SC_PRESET 选项中文标签 */
int preset_n;
int net_list; /* SC_INFO/SC_LIST 网络信息类别(NET_LIST_*),0=无 */
} srow_t;
/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作) */
typedef struct { int id; char name[48]; } meow_net_t;
/* chip 索引(⑥:第 5 个 = 电源菜单,点击唤起既有系统电源菜单 show_power_menu) */
enum { NCP_CHIP_WIFI=0, NCP_CHIP_BT=1, NCP_CHIP_THEME=2, NCP_CHIP_BRIGHT=3,
NCP_CHIP_POWER=4, NCP_CHIP_COUNT=5 };
/* 通知行快照(面板读取字段 + 缓存的 icon 句柄)。 */
typedef struct {
uint32_t id;
char app_id[MEOW_NOTIF_APP_ID_MAX];
char summary[MEOW_NOTIF_SUMMARY_MAX];
char body[MEOW_NOTIF_BODY_MAX];
char icon[MEOW_NOTIF_ICON_MAX];
char target[MEOW_NOTIF_ACTIVITY_MAX];
uint64_t created_ms;
int progress;
uint8_t urgency;
meow_img *img; /* 缓存图标(meow_img_load_file 结果;NULL=回退首字符圆点) */
} ncp_row_t;
/* ---- ⑧ 一键清除(批量):先转储留档 → 再清空显示(红线:保留 ongoing/media/progress)---- */
typedef struct { uint32_t ids[NCP_MAX_ROWS]; int n; } ncp_clear_ctx_t;
/* 入队消息:类型 + 请求 id + 客户端 fd(回包用)+ 负载拷贝(≤CARGO) */
struct meow_ipc_msg {
uint16_t type;
uint16_t plen;
uint32_t req_id;
int fd;
uint8_t payload[MEOW_IPC_CARGO];
};
/* 每连接字节流帧组装状态(TCP 流语义下逐字节缓冲,跨 poll 保留) */
struct meow_ipc_conn {
int fd;
uint8_t hdr[MEOW_IPC_HDR_LEN];
int hdr_n;
uint8_t pay[MEOW_IPC_MAX];
uint32_t pay_tot;
uint32_t pay_n;
};
/* ---- 跨模块共享变量(extern)---- */
extern int back_buf_ok;
extern unsigned char *fb;
extern int fb_xres, fb_yres, fb_line, fb_bpp;
extern int fb_fd;
extern const char *font_preset_fams[FONT_PRESETS_MAX];
extern const char *font_preset_labels[FONT_PRESETS_MAX];
extern const char *font_preset_names[FONT_PRESETS_MAX];
extern const char *font_preset_wts[FONT_PRESETS_MAX];
extern const char *font_presets[FONT_PRESETS_MAX];
extern int font_presets_n;
extern char g_meowos_version[16];
extern int g_naccts;
extern int g_sb_icon_cy;
extern int g_sb_mode_last;
extern meow_sys_state g_sys;
extern int hotkey_mode;
extern volatile int ipc_shutdown;
extern char ipc_sock_path[MEOW_IPC_SOCK_LEN];
extern char log_dir[256];
extern int log_fd;
extern char log_file[256];
extern uint32_t menu_bg;
extern int menu_opened_blank;
extern int meow_auto_slept;
extern int meow_mode;
extern int meow_saved_brightness;
extern int meow_screen_on;
extern int mfont_forced;
extern int mfont_ready;
extern int player_stopped;
extern const int pm_cx[PM_NUM];
extern int power_forceoff_ms;
extern int power_longpress_menu_ms;
extern int refresh_always_max;
extern int refresh_rate_cfg;
extern int saved_brightness;
extern int screen_off_mode;
extern int screen_off_timeout;
extern int seq_codes[MAX_SEQ_LEN];
extern int seq_gap_ms;
extern int seq_len;
extern int settings_cache_registered;
extern int settings_volume;
extern int settings_volume_max;
extern int settings_volume_step;
extern volatile sig_atomic_t sigterm_flag;
extern int sim_no_exec;
extern int sim_power_pick;
extern int vol_ramp_dir;
/* ---- 跨模块共享函数原型 ---- */
void apply_user_brightness(void);
void bt_role_refresh(int force);
int card_present(void);
int chain_depth(const char *c);
int confirm_reboot_player(void);
void cont_player(void);
void dim_screen(void);
void drain_input(void);
void draw_clock(void);
void draw_statusbar(void);
void draw_text(int x, int y, const char *s, int scale, uint32_t color);
void ensure_backlight(void);
int ensure_card_mounted(void);
void ensure_wifi(void);
void fb_backup_player(void);
void fb_norm_buf0(void);
int fb_open(void);
void fb_pan_buf0_force(void);
void fb_restore_player(void);
int fg_keep_awake(void);
void fill_circle(int cx, int cy, int r, uint32_t color);
void fill_rect(int x0, int y0, int w, int h, uint32_t color);
void fill_round_rect(int x0, int y0, int w, int h, int r, uint32_t color);
int find_hiby_pid(void);
void font_family_weight_from_png(const char *png, char *fam, int flen, char *wt, int wlen);
int hci_powered(void);
unsigned long long idle_ms_since(unsigned long long t, unsigned long long last);
int is_meow_mode(void);
int json_str(const char *buf, const char *key, char *out, int olen);
int kd_tick(struct kd_state *s, int raw);
void kp_close(struct keypoll *kp);
int kp_is_down(const struct keypoll *k, int code);
int kp_open(struct keypoll *kp, const char *dev);
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
void *ctx);
int launch_app(struct app *a);
void load_config(void);
void log_cleanup_old(void);
int log_open_card(void);
void log_poll_card(void);
void logmsg(const char *fmt, ...);
void meow_acct_declare(const char *app_id, const char *entries, int count);
struct meow_acct *meow_acct_find(const char *app_id);
void meow_acct_hello(const char *app_id, uint32_t version, uint32_t flags);
void meow_dac_vol_flush(void);
void meow_dac_vol_write(int pct);
void meow_dac_vol_write_throttled(void);
void meow_dirty_add(int x, int y, int w, int h);
void meow_dirty_full(void);
void meow_fg_act_back(void);
void meow_fg_act_set(const char *app_id, const char *act);
void meow_font_bind_fb(void);
int meow_font_effective_paths_tiered(int tier, char *out_png, int plen,
char *out_idx, int ilen, char *out_family, int flen);
int meow_font_effective_tier(int app_tier);
int meow_font_ensure(void);
void meow_font_reload_multifont(void);
int meow_font_resolve_path(const char *ref, char *out, int olen);
void meow_font_scan_presets(void);
void meow_font_scan_presets_root(const char *card_root);
int meow_ipc_init(void);
void meow_ipc_process(void);
void meow_panel_off(void);
void meow_panel_on(void);
void meow_parse_app_json(FILE *f, struct app *a);
int meow_pkg_export(const char *pkg, const char *out_path);
int meow_pkg_install(const char *mpk_path);
void meow_sys_light(void);
void meow_sys_refresh(void);
void meow_toggle_panel(void);
void net_wifi_ssid(char *buf, int len);
unsigned long long now_ms(void);
void paint_sb_clock(void);
void paint_sb_right(void);
void pm_capture_app_frame(void);
char *popen_read_tmo(int tmo_ms, const char *cmd);
int proc_by_comm(const char *target);
void put_px(int x, int y, uint32_t color);
int read_brightness(void);
int read_file_int(const char *path);
int resolve_chain(const char *app_id, const char *activity, char *out, int *depth);
void restore_backlight(void);
uint16_t rgb565(uint32_t c);
void run_notif_center(struct app *apps, int napps, int *want_launch,
int live_open, int lox, int loy);
void run_settings_page(void);
void s_about_open(void);
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur);
void s_draw_chevron(int x, int y, uint32_t color);
void s_draw_chevron_left(int x, int y, uint32_t color);
void s_draw_slider(int x, int y, int w, int val, int mn, int mx,
const meow_ui_theme_t *th);
void s_draw_switch(int x, int y, int on, const meow_ui_theme_t *th);
int s_net_get(const srow_t *r, char *buf, int len);
void s_net_open(const srow_t *r);
void s_net_refresh(void);
void s_net_sync_from_sys(void);
void s_net_toggle_bt(int on);
void s_net_toggle_wifi(int on);
const meow_ui_theme_t *s_ui_theme(void);
void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h);
int scan_apps(struct app apps[MAX_APPS]);
int sd_device_present(void);
int sd_mounted(void);
void settings_load(void);
void settings_sync_cache_from_backend(void);
void settings_sync_cache_listener(const char *key);
void settings_volume_adj(int delta);
int settings_volume_get(void);
int show_power_menu(void);
int stop_player(void);
void sys_do_reboot(int cmd);
void ui_draw(int x, int y, const char *s, uint32_t color, int size);
int ui_width(const char *s, int size);
void unblank_panel(void);
int vol_clamp(int v);
int vol_ramp_tick(int key_held);
void vol_restore(struct vol_snap s);
struct vol_snap vol_snapshot(void);
int wlan_connected(void);
int wlan_radio_up(void);
void write_brightness(int val);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_LAUNCHER_INTERNAL_H */
Binary file not shown.
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -14,7 +14,7 @@
* - 转储钩子:meow_notif_dump_now()(关机前转储所有未上卡);
* meow_notif_before_poweroff()(dump + 清板载)。
*
* 空洞实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg/now_ms),配额经
* 空洞实现风格:本文件自包含(不依赖 launcher 主模块的 logmsg/now_ms —— 二者现归 util.c),配额经
* meow_settings(B2a)读 notify.categories/category_quota/media_quota(键缺省用默认值),
* 文件 I/O 失败仅打点,绝不崩。注释风格中文 +「M2.a 说明」。
*/
+3 -2
View File
@@ -12,8 +12,9 @@
* - 原生 IPC 通道接入点(MSG_NOTIFY→ACK / MSG_NOTIFY_DISMISS→ACK;MSG_NOTIFY_LIST
* 为守护→App 下发,本批提供序列化 helper,UI(M5) 接入)。
*
* 空洞实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg/now_ms),
* 配额从 meow_settings 读(B2a;键缺省时用本文件默认值)。注释风格中文 +「M2.a 说明」。
* 空洞实现风格:本文件自包含(不依赖 launcher 主模块的 logmsg/now_ms —— 二者现归 util.c,可经
* meow_launcher_internal.h 引用),配额从 meow_settings 读(B2a;键缺省时用本文件默认值)。
* 注释风格中文 +「M2.a 说明」。
*/
#ifndef MEOW_NOTIF_H
#define MEOW_NOTIF_H
+4 -4
View File
@@ -1,8 +1,8 @@
/* meow_notif_panel.h — 通知中心面板的纯逻辑(header-only,宿主可单测)。
*
* 定位:把通知中心面板里「无副作用、可独立判定」的部分抽成 static inline,
* 供 launcher(meow_launcherd.c 内嵌面板)与宿主单测(test_notif_panel.c)共用同一份实现,
* 避免两端漂移;绘制/触摸循环等依赖 launcher static 符号的部分留在 meow_launcherd.c 内。
* 供 launcher(notif_center.c 内嵌面板)与宿主单测(test_notif_panel.c)共用同一份实现,
* 避免两端漂移;绘制/触摸循环等依赖 launcher static 符号的部分留在 notif_center.c 内。
*
* 不依赖 launcher 头 / SDK 头,仅 <stdint.h> <string.h> <stdio.h> <time.h> —— 纯宿主可编。
*/
@@ -160,7 +160,7 @@ static inline int mnp_scroll_rebound(int target, int max, int tol)
/* ---- 开关平移动画(状态时间轴,纯逻辑,可单测)----
* 通知中心面板「打开/关闭滑入/收回」= 面板内容整体做 y-offset 平移(参照 SDK §5.2 快照 blit 平移)。
* 状态:progress p ∈ [0, fix],0=全隐藏(面板内容在面板区上缘之外),fix=到位(offset_y=0)。
* 本组函数只做标量时间轴/裁切计算,不碰 fb/几何——供 launcher(meow_launcherd.c 内嵌面板)与
* 本组函数只做标量时间轴/裁切计算,不碰 fb/几何——供 launcher(notif_center.c 内嵌面板)与
* 宿主单测(test_notif_panel.c)共用同一份实现,避免两端漂移。 */
/* 进度 p([0,fix])→ 面板内容 y-offset(px,∈[-ph,0])。
@@ -260,7 +260,7 @@ static inline int mnp_clearable(int ongoing, int media, int progress)
* 把面板打开/关闭从「触发式时间轴过渡」升级为「手指拖动实时跟手」:progress∈[0,fix],
* reveal(面板当前露出高度,∈[0,ph])随手指位移连续映射;松手按阈值锚定补到位/收回。
* 本组函数只做标量:reveal→progress / seed+delta clamp / 松手锚定,不碰 fb/几何——
* 供 launcher(meow_launcherd.c 内嵌面板)与宿主单测(test_notif_panel.c)共用同一份实现。 */
* 供 launcher(notif_center.c 内嵌面板)与宿主单测(test_notif_panel.c)共用同一份实现。 */
/* 跟手 reveal→progress:reveal∈[0,ph](面板露出高度)→ p∈[0,fix]。线性、方向无关。
* 与 mnp_anim_off 互逆(off = -ph + reveal;p = fix*reveal/ph)。 */
+1 -1
View File
@@ -10,7 +10,7 @@
* - 落盘:meow_settings_save()(写板载 settings.conf,保留 "# meow-settings 1" 头)。
* 为避免 UBIFS 高频写,调用方在低频点(如音量渐变结束)才落盘一次。
*
* 空实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg),文件 I/O 失败仅打点,
* 空实现风格:本文件自包含(不依赖 launcher 主模块的 logmsg —— 现归 util.c),文件 I/O 失败仅打点,
* 绝不崩(内存继续用,符合 SYSTEM_SETTINGS.md §8「无卡/无板载用内存缓存」)。
*/
#include <stdio.h>
+1 -1
View File
@@ -6,7 +6,7 @@
*
* 设计规范:SYSTEM_SETTINGS.md §1/§2/§3 + MEOWOS_API.md §2。
* 返回码:MEOWOS_API.md §1(MEOW_OK=0,负值错误码)。
* 注释风格:与 meow_launcherd.c 一致(中文注释 + 「M2.x 说明」格式)。
* 注释风格:与 meow_launcherd 拆分层源码注释一致(中文注释 + 「M2.x 说明」格式)。
*
* 阶段 B2a 只建后端 + 核心 API;实体设置页(B2b)/Web 面板(C)后续复用。
*/
+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();
}
File diff suppressed because it is too large. Load diff
+406
View File
@@ -0,0 +1,406 @@
/* power_menu.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530 }; /* 圆心 y(行距 140) */
static const char *pm_labels_cn[PM_NUM] = { "关机", "重启", "回HiBy", "升级", "取消" };
static const char *pm_labels_en[PM_NUM] = { "POWER OFF", "RESTART", "HIBYOS", "UPGRADE", "CANCEL" };
/* 电源菜单圆钮图标:从 pm_icons.h 的 56×56 alpha 位图 blit 到 fb(以圆钮中心 cx,cy 为锚居中)。
* alpha 按 col(RGB565)映射:图标前景色随选中/未选中主题色变化(日夜间由 pm_theme_* 提供)。
* dst_px 为 fb16bpp 写入:新像素 = col 前景与背景(btn bg)按 alpha 混合。 */
static void blit_pm_icon(int idx, int cx, int cy, uint16_t col)
{
const unsigned char *src = pm_icons[idx].data;
int S = PM_ICON_SIZE;
int x0 = cx - S / 2, y0 = cy - S / 2;
for (int y = 0; y < S; y++) {
int yy = y0 + y;
if (yy < 0 || yy >= fb_yres) continue;
uint16_t *row = (uint16_t *)fb + yy * (fb_line / 2);
for (int x = 0; x < S; x++) {
int a = src[y * S + x];
if (a < 8) continue; /* 透明像素跳过 */
int xx = x0 + x;
if (xx < 0 || xx >= fb_xres) continue;
/* 前景 col 已含所需亮度;此处仅做软边 alpha 混合到背景,避免锯齿 */
uint16_t bg = row[xx];
if (a >= 240) { row[xx] = col; continue; }
int a4 = a >> 4; /* 0..15 */
int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4;
int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4;
int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4;
row[xx] = (uint16_t)((r << 11) | (g << 5) | b);
}
}
}
/* 绘制电源菜单圆钮 i(MD3 圆形:外描边+内填充+圆内 Material 图标;选中高亮)。
* 圆内放 Material Symbols 位图,文字标签放圆下(Android 12 式:icon 上、label 下)。 */
static void draw_pm_button(int i, int selected)
{
int cx = pm_cx[i], cy = pm_cy[i];
/* 主题(日夜间由 pm_theme_* 决定;默认取 dark,后续接设置) */
const pm_theme_t *th = &pm_theme_dark;
/* 先铺一块比任何选中圈都大的卡片底色圆,盖掉上一态残留的选中描边(问题3:
* 焦点移走后 PM_R+2..PM_R+6 之间残留亮圈。以 0x1C2438 卡片色清底)。 */
fill_circle(cx, cy, PM_R + 10, 0x1C2438);
/* MD3 无边框高亮:选中=未选中底色稍亮填充(无描边圈),仅底色区分焦点;避免圆形描边残留。 */
if (selected)
fill_circle(cx, cy, PM_R, th->btn_sel_bg);
else
fill_circle(cx, cy, PM_R, th->btn_nor_bg);
/* 圆内 Material 图标(alpha 位图 + 主题前景色) */
blit_pm_icon(i, cx, cy, selected ? th->icon_sel : th->icon_nor);
uint32_t col = selected ? 0xFFFFFF : 0xC8D0E8;
/* 圆下文字标签(真字体/中文 或 点阵 ASCII 回退) */
int ly = cy + PM_R + 8;
if (mfont_ready) {
const char *lab = pm_labels_cn[i];
int lw = meow_font_width_text(lab, 18);
meow_font_drawtext((uint16_t *)fb, cx - lw / 2, ly, lab, rgb565(col), 18);
} else {
const char *lab = pm_labels_en[i];
int lw = (int)strlen(lab) * 5;
draw_text(cx - lw / 2, ly + 1, lab, 1, col);
}
}
void sys_do_reboot(int cmd)
{
/* M2a:正常电源事件(关机/重启)触发清除板载通知数据之前,先转储所有未上卡通知
* (NOTIFICATION_SYSTEM.md §8)。meow_notif_before_poweroff 幂等安全(未 init 直接返回)。 */
meow_notif_before_poweroff();
sync(); /* 先落盘,避免数据丢失 */
logmsg("sys_do_reboot cmd=0x%x", cmd);
syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, NULL);
logmsg("sys_do_reboot returned: %s", strerror(errno)); /* 正常不会返回 */
for (;;) usleep(1000); /* 防御:永不返回 */
}
/* 回HibyOS:写板载 /data/meow_boot_default=1(开机默认权威标记,O_TRUNC 单值、不写 TF 卡),
* 再 reboot 回选单并默认 HibyOS。meow.conf 不再写/承载 boot_default(bootmenu 以板载标记为准)。 */
static void set_boot_default_hiby(void)
{
int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { write(fd, "1\n", 2); close(fd); logmsg("pm: /data/meow_boot_default=1"); }
}
/* 电源菜单(M2.6):2×2 网格,dirty 绘制消频闪;输入修复:
* - 触摸按下捕获坐标、抬起命中按钮即确认(同 run_menu MT 防抖);
* - 音量上/下导航;播放=确认;电源新按(松开长按后)=确认当前钮(非取消);
* - 取消=选「取消」钮;8s 持续按住=强制关机(绕过)。
* 返回 1=采取动作,0=取消。 */
int show_power_menu(void)
{
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } /* 亮起面板 */
pm_capture_app_frame(); /* A-0:菜单背景 = app 当前帧(fb0 buffer0),再压暗绘制其上 */
dim_screen();
/* M2.8 Bug 3-residual:若电源菜单是在「息屏」态打开(按住 3s),此处 FBIOBLANK(0)
* 刚上电,LCDC 扫描地址可能非 buffer0;强制 re-pan 让压暗的应用列表 + 菜单落在被显示的
* 缓冲,否则菜单画在非显示缓冲上不可见。 */
fb_pan_buf0_force();
meow_screen_on = 1; /* 菜单打开即视为亮屏 */
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
int sel = PM_NUM - 1; /* 默认取消(安全) */
/* M2.30 MD3:居中圆角卡片背景 + 标题居中 + 圆钮网格。卡片白色柔和描边/深色底。 */
fill_round_rect(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H, PM_CARD_R, 0x141824);
fill_round_rect(PM_CARD_X0 + 2, PM_CARD_Y0 + 2, PM_CARD_W - 4, PM_CARD_H - 4, PM_CARD_R - 2, 0x1C2438);
/* 卡片顶部标题,水平居中 */
char title[32];
snprintf(title, sizeof title, "POWER MENU");
int title_y = PM_CARD_Y0 + 26; /* 标题在卡片内顶部(卡片顶 115,标题基线 ~141) */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2, title_y, title, rgb565(0xFFFFFF), 20);
else draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, 0xFFFFFF);
/* 底部提示行(卡片下方,非圆钮所需) */
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text("音量导航 电源确认", 18)) / 2, hint_y, "音量导航 电源确认", rgb565(0x777799), 18);
else draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2, hint_y + 1, "VOLUME NAV POWER OK", 1, 0x777799);
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
/* 触摸状态(按下→抬起):统一 meow_touch 接口(goodix 时序免疫,原点由首个有效 ABS 锁) */
meow_touch mt;
mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0;
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE) ? 1 : 0;
int act = -1;
unsigned long long pm_t0 = now_ms();
unsigned long long pm_last_interact = pm_t0; /* M2.33:菜单空闲自动关闭基准(任何互动刷新) */
int pm_init = 0, last_sel = -1;
/* M2.10 Bug:电源菜单 8s 强关(按住电源≥8s=绕过强关)会覆盖用户的菜单操作 —— 用户按住
* 电源打开菜单后(尤其仍按着电源)点选「回到HibyOS/重启」等按钮,8s 一到先触发强关 → 关机
* 而非执行所选动作。修法:一旦用户与菜单互动(音量导航/触摸/播放/电源确认),置 pm_interacted=1
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
* 8s=强关」。*/
int pm_interacted = 0;
for (;;) {
/* dirty 绘制:首次全画;此后仅选中变化才重画受影响两钮(消频闪)→ 标脏 + present。 */
if (!pm_init) {
for (int i = 0; i < PM_NUM; i++) draw_pm_button(i, i == sel);
pm_init = 1; last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present(vsync+帧率门控+blit 脏=全帧,含压暗列表+标题+按钮) */
} else if (sel != last_sel) {
draw_pm_button(last_sel, 0);
draw_pm_button(sel, 1);
/* M2.30 圆形:dirty 区 = 圆(圆心±半径)+ 圆下文字(半径+偏移+行高),四周留 10px */
int dr = (PM_R + 6) + 10, dtxt = PM_R + 26 + 10;
meow_dirty_add(pm_cx[last_sel] - dr, pm_cy[last_sel] - dr, dr * 2, dr + dtxt);
meow_dirty_add(pm_cx[sel] - dr, pm_cy[sel] - dr, dr * 2, dr + dtxt);
last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present,blit 两钮区 */
}
if (sim_power_pick >= 0 && now_ms() - pm_t0 >= 2000) act = sim_power_pick;
int vold = 0, volu = 0, play = 0, pwr = 0;
if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL);
vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); }
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
if (vold && !prev_vold) { sel = (sel + 1) % PM_NUM; pm_interacted = 1; pm_last_interact = now_ms(); }
if (volu && !prev_volu) { sel = (sel - 1 + PM_NUM) % PM_NUM; pm_interacted = 1; pm_last_interact = now_ms(); }
/* M2.19:播放键不再作确认(让位给全局媒体控制)。电源菜单的物理确认
* 已由电源键负责(下行),播放键删除,重复确认移除。 */
(void)play;
if (pwr && !prev_pwr) { act = sel; pm_interacted = 1; pm_last_interact = now_ms(); } /* 电源新按=确认(非取消) */
prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr;
/* 触摸:按下(meow_touch 原点锁定,goodix 时序免疫),抬起按按钮命中确认 */
if (tfd >= 0) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !mt.down) {
mt_down(&mt);
pm_interacted = 1;
pm_last_interact = now_ms();
} else if (!val && mt.down) {
pm_interacted = 1;
pm_last_interact = now_ms();
int dx, dy;
/* M2.25:抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动(返回0),
* 不做确认;否则有效 tap,按下原点命中的按钮即确认。 */
if (mt_up(&mt, &dx, &dy)) {
/* M2.30 圆形命中:按下原点距圆心 <= PM_R+6 即命中 */
for (int i = 0; i < PM_NUM; i++) {
int ddx = mt.down_x - pm_cx[i], ddy = mt.down_y - pm_cy[i];
int rr = (PM_R + 6) * (PM_R + 6);
if (ddx * ddx + ddy * ddy <= rr) { sel = i; act = i; break; }
}
}
}
}
}
}
}
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); }
else { logmsg("pm: POWER HOLD 8s -> force poweroff"); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
return 1;
}
/* M2.33 菜单空闲超时自动关闭:菜单受 MeowOS screen_off_timeout 管理(系统界面不豁免)。
* 超过超时无任何互动 → 关闭菜单(视为取消)并自动息屏。目的是:息屏后单击电源亮屏时,
* 电源按下信号不会命中菜单按钮(菜单已关),杜绝误触关机/执行菜单动作。 */
if (screen_off_timeout > 0 &&
(unsigned long long)(now_ms() - pm_last_interact) >= (unsigned long long)screen_off_timeout * 1000) {
logmsg("pm: idle %ds -> close menu + auto screen-off (act=cancel)", screen_off_timeout);
meow_panel_off();
meow_auto_slept = 1;
act = 4; /* 取消(关菜单) */
break;
}
if (act >= 0) break;
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与 syscall 类型逐项核对:
* act==4(取消) -> 只关菜单,绝不关机(return 0,永不到 reboot);
* act==0(关机) -> 才 poweroff;act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才 reboot。
* 只有 act 为 0/1/2/3 才可能执行 sys_do_reboot;power 键本身绝不触发关机。 */
logmsg("pm: act=%d -> %s", act,
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式" : "取消");
if (act == 4) { logmsg("pm: cancel (close menu only)"); return 0; }
if (act == 0) {
logmsg("pm: POWER OFF");
if (!sim_no_exec) {
/* 先显关机 logo(cmd_jpeg_display 是独立二进制,能正常显图),再直连 poweroff */
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin cmd_jpeg_display /etc/shutdown_logo.jpeg >/dev/null 2>&1");
sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF);
}
} else if (act == 1) {
logmsg("pm: REBOOT");
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
} else if (act == 2) {
logmsg("pm: RETURN TO HIBYOS");
set_boot_default_hiby();
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
} else if (act == 3) {
/* M2.29 升级模式:写 mtd5=ota:kernel2(bootmode.sh Recovery = flash_erase + nandwrite)
* 再 reboot —— uboot 读 mtd5=ota:kernel2 引导 kernel2(升级模式,自动刷 TF 卡
* r3proii.upt,双进度条,跑完重启进新固件)。已验证此链路可靠。 */
logmsg("pm: ENTER UPGRADE");
if (!sim_no_exec) {
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/bootmode.sh Recovery >/dev/null 2>&1");
sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
}
}
return 1;
}
/* M2.6 G「Reboot to player」:MeowOS 常驻模式点「返回播放器」时,播放器已死,弹确认框
* 「返回播放器需重启,现在重启?[是/否]」。是=写选单默认 HibyOS + reboot;否=留在 MeowOS。
* VOL=是/否 切换;PLAY=确认;POWER=取消;触摸点 是/否 确认。风格同电源菜单。
* 返回 1=是(已 reboot,通常不回),0=否/取消(调用方重绘应用列表)。 */
/* 「返回播放器」确认框「是/否」按钮(左右),选中高亮。
* M2.9 Bug 4:改为 dirty 绘制 —— 仅 sel 变化时重画两钮(旧钮取消高亮 + 新钮高亮),
* 不随主循环每 10ms 全刷(原实现每帧重画同一内容,与 LCD 刷新竞争产生频闪)。 */
static void draw_cr_button(int sel, int i)
{
int x0 = (i == 0) ? 80 : 280, y0 = 420, w = 120, h = 60;
fill_round_rect(x0, y0, w, h, 12, (i == sel) ? 0x2A3A6A : 0x1A1A2A);
const char *lbl = (i == 0) ? "是" : "否";
if (mfont_ready) {
int lw = meow_font_width_text(lbl, 20);
meow_font_drawtext((uint16_t *)fb, x0 + w / 2 - lw / 2, y0 + 30 - meow_font_line_height(20) / 2,
lbl, rgb565((i == sel) ? 0xFFFFFF : 0xC8D0E8), 20);
} else {
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, (i == sel) ? 0xFFFFFF : 0xC8D0E8);
}
}
int confirm_reboot_player(void)
{
dim_screen();
/* M2.8 Bug 3-residual:确认框同电源菜单 —— 若打开前刚经历息屏/blank,LCDC 扫描地址可能
* 非 buffer0;强制 re-pan 让「压暗列表 + 是/否」落在被显示的缓冲。 */
fb_pan_buf0_force();
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320, "返回播放器需重启,现在重启?", rgb565(0xFFFFFF), 20);
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, 0xFFFFFF);
meow_touch mt;
mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0;
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE) ? 1 : 0;
int sel = 1; /* 默认「否」:不误重启 */
int act = -1;
int cr_init = 0, cr_last_sel = -1; /* M2.9 Bug 4:dirty——仅 sel 变化才重画两钮 */
/* 标题/按键提示:打开时画一次(原实现把它们放主循环每 10ms 全刷,与 LCD 刷新竞争 →
* 「是/否」钮频闪;改为只在打开/状态变化时绘制)。标题在前面已画一次,这里画提示行。 */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560, "音量切换 电源取消", rgb565(0x666688), 20);
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, 0x666688);
meow_dirty_full(); /* M2.16:压暗列表+标题+提示作为一整帧呈现 */
for (;;) {
/* dirty 绘制:首次全画两钮;此后仅 sel 变化才重画(消频闪)→ 标脏 + present。 */
if (!cr_init) {
for (int i = 0; i < 2; i++) draw_cr_button(sel, i);
cr_init = 1; cr_last_sel = sel;
meow_ui_flush();
} else if (sel != cr_last_sel) {
draw_cr_button(sel, cr_last_sel); /* 旧选中:取消高亮 */
draw_cr_button(sel, sel); /* 新选中:高亮 */
meow_dirty_add((cr_last_sel == 0 ? 80 : 280) - 10, 412, 140, 76);
meow_dirty_add((sel == 0 ? 80 : 280) - 10, 412, 140, 76);
cr_last_sel = sel;
meow_ui_flush();
}
int vold = 0, volu = 0, play = 0, pwr = 0;
if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL);
vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); }
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
if (vold && !prev_vold) sel = (sel + 1) % 2;
if (volu && !prev_volu) sel = (sel - 1 + 2) % 2;
/* M2.19:播放键不再作「是/确认」(让位给全局媒体控制)。确认框的确认
* 走触摸点选(下方 tfd 命中),电源键=取消(下行);播放键删除。 */
(void)play;
if (pwr && !prev_pwr) act = 3; /* 电源=取消 */
prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr;
if (tfd >= 0) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !mt.down) {
mt_down(&mt);
} else if (!val && mt.down) {
int dx, dy;
if (mt_up(&mt, &dx, &dy)) {
for (int i = 0; i < 2; i++) {
int x0 = (i == 0) ? 80 : 280;
/* 命中「是/否」钮:x∈[x0-8, x0+128], y∈[412, 488](含端点) */
if (mt_in(&mt, x0 - 8, 412, 137, 77))
{ sel = i; act = i; break; }
}
}
}
}
}
}
}
if (act >= 0) break;
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
if (act == 0) {
logmsg("reboot-to-player: YES -> default HibyOS + reboot");
set_boot_default_hiby();
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
return 1;
}
logmsg("reboot-to-player: cancel");
return 0;
}
File diff suppressed because it is too large. Load diff
+111
View File
@@ -0,0 +1,111 @@
/* statusbar.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
int g_sb_icon_cy = SB_ICON_CY;
/* 状态栏文字垂直锚 y(行首升线顶):(栏高-行高)/2 把文字垂直居中于 28px 栏;
* 下限 1 防极端行高(>栏高)时算出负 y 溢出顶端(不越界优先)。 */
/* 状态栏 MD3 图标 blit:把 sb_icons.h 的 32x32 单通道 alpha 位图画进状态栏(fb = SDK back_buf)。
* x = 图标槽左缘(槽宽 SB_ICON_SIZE);fg = 24-bit RGB 前景色(主题 on_surface / on_surface_variant)。
* M2.46-2 视觉对齐(音量图标小一圈/下边距、图标与文字垂直中心):
* - 先求该位图 ink 边界(阈值 32,忽略近透明抗锯齿);按 ink 边界在槽内水平居中,并把 ink 垂直
* 中心对齐到全局 g_sb_icon_cy(==状态栏文字中心),→ 所有图标共用同一光学中线,不再「偏下」。
* - ink_h>0 时对 ink 做等比缩放(绘制时最近邻采样,源位图不变)到该 ink 高:主图标统一用
* SB_ICON_INK,音量(volume_up ink 较矮)随之放大到与电量/亮度一致 → 不显「小一圈」。
* - ink_h==0 保持位图自然 ink 尺寸(仅居中):用于 WiFi 强弱档——较短 ink(少一格)本身即
* 「信号弱」语义,不可被等比拉大。
* 像素 = 前景与背景按 alpha 软边混合(a<8 跳过透明;a>=240 直写前景;其余按 a>>4 在 565 空间
* 插值),与 blit_pm_icon 同构——暗/亮主题下图标随状态栏底色一起换色。 */
void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h)
{
const unsigned char *src = sb_icons[icon_idx].data;
uint16_t col = rgb565(fg);
/* 1) ink 边界(含空图兜底) */
int minx = SB_ICON_SIZE, miny = SB_ICON_SIZE, maxx = -1, maxy = -1;
for (int i = 0; i < SB_ICON_SIZE * SB_ICON_SIZE; i++) {
if (src[i] >= 32) {
int xx = i % SB_ICON_SIZE, yy = i / SB_ICON_SIZE;
if (xx < minx) minx = xx; if (xx > maxx) maxx = xx;
if (yy < miny) miny = yy; if (yy > maxy) maxy = yy;
}
}
if (maxx < minx || maxy < miny) { minx = miny = 0; maxx = maxy = SB_ICON_SIZE - 1; }
int ink_w = maxx - minx + 1, ink_h_n = maxy - miny + 1;
int t_h = (ink_h > 0) ? ink_h : ink_h_n; /* 目标 ink 高 */
int t_w = ink_w * t_h / ink_h_n; if (t_w < 1) t_w = 1;
if (t_w > SB_ICON_SIZE * 2) t_w = SB_ICON_SIZE * 2; /* 防御:等比极端上限 */
/* 2) 目标框左上:水平居中于 32px 槽;垂直中心 = g_sb_icon_cy */
int x0 = x + (SB_ICON_SIZE - t_w) / 2;
int y0 = g_sb_icon_cy - t_h / 2;
/* 3) 最近邻采样映射:目标 (dx,dy) ← 源 ink (minx+dx*ink_w/t_w, miny+dy*ink_h_n/t_h) */
for (int dy = 0; dy < t_h; dy++) {
int ty = y0 + dy;
if (ty < 0 || ty >= fb_yres) continue;
int sy = miny + (dy * ink_h_n) / t_h;
uint16_t *row = (uint16_t *)fb + ty * (fb_line / 2);
for (int dx = 0; dx < t_w; dx++) {
int tx = x0 + dx;
if (tx < 0 || tx >= fb_xres) continue;
int sx = minx + (dx * ink_w) / t_w;
int a = src[sy * SB_ICON_SIZE + sx];
if (a < 8) continue; /* 透明像素跳过 */
uint16_t bg = row[tx];
if (a >= 240) { row[tx] = col; continue; }
int a4 = a >> 4; /* 0..15 */
int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4;
int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4;
int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4;
row[tx] = (uint16_t)((r << 11) | (g << 5) | b);
}
}
}
/* ---- Stage-One 状态栏收敛:launcher 不再自绘状态栏,统一由 SDK 全局状态栏绘制 ----
* launcher 系统界面(主界面/设置页/电源菜单/通知中心)顶部 28px 状态栏改由 SDK 的
* meow_ui_sys_statusbar_render()(present() 时合成,z 最顶)绘制。launcher 侧只保留三个
* 入口(paint_sb_clock/paint_sb_right/draw_statusbar,调用点遍布主界面/设置页/通知中心/
* 音量/亮度/主题变更/1s 时钟),其职责从「自绘」改为「把权威状态喂进 SDK + 标脏顶条」。
* 依据(devdocs/plan/LAUNCHER_SDK_REWRITE.md):
* - launcher 不跑 SDK 事件循环 → SDK 的 meow_ui_sys_refresh() 不在 launcher 触发,数据
* 必须由 launcher 喂。g_sys(meow_sys_refresh/light + 事件路径维护)是权威源,尤其
* 亮度/音量(写入方=launcher)。
* - SDK 主题是缓存的 → 喂前 meow_theme_reload() 逼重读 settings,状态栏跟手 launcher 换肤。
* - 标脏顶条 (0,0,xres,28) → present() 时 SDK 重画顶条;SDK 只在「数据变(force)/顶条被触及/
* 收合过渡」才重画,天然满足「状态栏只在变化时重绘」。 */
static void sb_sdk_refresh(void)
{
meow_theme_reload(); /* SDK 状态栏主题随 launcher 换肤(重读 settings) */
meow_ui_feed f;
f.batt = g_sys.batt;
f.charging = g_sys.charging;
f.wifi_on = g_sys.wifi_on;
f.wifi_connected = g_sys.wifi_connected;
f.wifi_level = g_sys.wifi_level;
f.bt_seen = g_sys.bt_seen;
f.bt_on = g_sys.bt_on;
f.vol = g_sys.vol;
f.brightness = g_sys.brightness;
meow_ui_sys_feed(&f); /* 权威快照喂入 + 置 force(顶条必重画) */
meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 标脏顶条 → present() 时 SDK 重画 */
}
/* 三个入口(调用点不动):现在驱动 SDK 全局状态栏(唯一绘制者),不再自绘。 */
void paint_sb_clock(void) { sb_sdk_refresh(); }
void paint_sb_right(void) { sb_sdk_refresh(); }
void draw_statusbar(void) { sb_sdk_refresh(); } /* 对齐原语义:整条状态栏 → SDK 绘制 */
/* 上次读到的 App 顶条模式(用于在 launch_app waitpid 循环里判「模式新变 → 立即叠一次」)。
* 高整文件作用域;每次启动 App 前重置为 -1(强制首帧同步一次)。 */
int g_sb_mode_last = -1;
Binary file not shown.
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -35,7 +35,7 @@
#define MEOW_FONT_PNG_SYS "/usr/share/meowos/font/font.png"
#define MEOW_FONT_IDX_SYS "/usr/share/meowos/font/font.idx"
/* 默认字库(卡 MeowOS/fonts/default/regular,M2.6 字重结构)与固件内置之外,launcher 会把
* 「当前生效字体」经环境变量下发给 app(见 meow_launcherd.c launch_app / meow_font_effective_paths):
* 「当前生效字体」经环境变量下发给 app(见 meow_launcherd/app_launcher.c `launch_app` / font_theme.c `meow_font_effective_paths`):
* MEOW_FONT_PNG / MEOW_FONT_IDX = 绝对路径;MEOW_FONT_APP = <family> 或 "default"。
* SDK load_font 优先读 env(匹配系统 font.file),无则回退卡默认 → 固件。 */
@@ -879,8 +879,8 @@ static int load_font(void)
}
/* M2.5x 多字体:读 launcher 注入的环境变量,构建 6 字符类 → 字库的映射(同 family 去重、
* ≤4 套上限由 meow_font_config_multifont 统一处理)。launcher 注入(见 meow_launcherd.c
* launch_app):MEOW_MULTI_FONT=0/1;MEOW_FONT_PNG/IDX=全局(保留);每类
* ≤4 套上限由 meow_font_config_multifont 统一处理)。launcher 注入(见 meow_launcherd/app_launcher.c
* `launch_app`):MEOW_MULTI_FONT=0/1;MEOW_FONT_PNG/IDX=全局(保留);每类
* MEOW_FONT_CLS_<X>_PNG/IDX(命中则覆盖全局;未注入 = 该类跟随全局)。
* 仅当全局字库已装载(g_mfont_ready)才配置;关/无 env → 单字体,零额外开销。 */
static void meow_gfx_load_multifont(void)
+1 -1
View File
@@ -14,7 +14,7 @@
* (launcher 是背光/音量写入方)。故此 API 让 launcher 把权威快照喂进 SDK 的系统状态,
* 喂入即置状态栏 force,下一次 present() 重画顶条。SDK 自身 meow_ui_sys_refresh() 仍按
* 同源兜底(供 App 流程 meow_ui_run 使用)。只填 SDK 快照所需最小字段,与 launcher 的
* meow_sys_state(meow_launcherd.c)字段名交叉映射,二者是不同 TU 的异名类型,故用此
* meow_sys_state(util.c)字段名交叉映射,二者是不同 TU 的异名类型,故用此
* 专用喂入结构,避免 launcher 引入 meow_ui_sys.h 造成 typedef 重定义。 */
typedef struct meow_ui_feed {
int batt; /* 电量 %,-1 未知 */
+1 -1
View File
@@ -5,7 +5,7 @@
* 1. 纯函数(clamp / 键读取)不依赖 meow_ui 其它子系统,宿主 gcc 直接可测
* (test_meow_ui_vol.c),无需链整库/stub。
* 2. 把「音量从 settings 读数并 clamp 到 volume_max」这一语义钉死在单点,
* 与 launcher 的 vol_clamp(meow_launcherd.c)口径对齐,杜绝两端音量漂移。
* 与 launcher 的 vol_clamp(audio.c)口径对齐,杜绝两端音量漂移。
*
* 数据源:/usr/data/meow/settings.conf 扁平 key=value。launcher meow_settings.c
* 落盘为 "key = value"(带空格),本解析兼容无空格/多空格,与 launcher 读循环
+1 -1
View File
@@ -15,7 +15,7 @@
#define MEOW_UI_VOL_MAX_DEF 100
/* 纯函数:把软件音量夹到 [0, MEOW_UI_VOL_MAX],再夹到 volume_max 软上限。
* 语义与 launcher 的 vol_clamp(meow_launcherd.c)一致:
* 语义与 launcher 的 vol_clamp(audio.c)一致:
* - 硬上限 100(0-100 口径);
* - 尊重 audio.volume_max(用户把上限调小则音量被夹到该值);
* - volume_max 异常(<0)兜底为 0(不进负值)。