diff --git a/FRAMEWORK_DESIGN.md b/FRAMEWORK_DESIGN.md index 4610aa3..8cc4441 100644 --- a/FRAMEWORK_DESIGN.md +++ b/FRAMEWORK_DESIGN.md @@ -199,7 +199,7 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最 关闭后回 App(非回 run_menu)。 - **App 崩溃/卡死**:launcher 会话子循环周期用 `waitpid(pid,&st,WNOHANG)`/`kill(pid,0)` 探测;App 死→立即结束会话 (不 SIGCONT 死进程、不恢复死帧),走既有**统一应用退出路径**归位;launcher 不崩(`kill(pid,0)` 存活检测 - 与 watchdog 让位语义同 `meow_launcherd.c` ~8533-8555)。 + 与 watchdog 让位语义同 `util.c`(IPC 服务端 `ipc_thread`/`ipc_dispatch`)。 - **全屏/沉浸不冲突**:全屏/收合时 SDK 顶部热区缩小为 `MEOW_SBAR_HOTZONE_FS=4`(或提示线高),**仍保留顶部 一处下拉入口**(`meow_ui_statusbar_hotzone()`),spec-ref §3.1/A-13/§5.1 已定「全屏下顶部下滑仍开通知中心」。 - **与 M3 关系**:本方案是 M3「前台会话」的一个切面(App 前台会话+跨进程叠加)。**本方案先行**(握手+叠加会话+ diff --git a/GESTURE_BACK_DESIGN.md b/GESTURE_BACK_DESIGN.md index 1d2dec1..e890896 100644 --- a/GESTURE_BACK_DESIGN.md +++ b/GESTURE_BACK_DESIGN.md @@ -109,7 +109,7 @@ | `patch_v2/sdk/meow_ui_gfx.c` | 新增 `meow_ui_swipe_hint(x)` 顶部横条绘制 | | `patch_v2/sdk/meow_ui_widget.c`(如有输入/绘制分发) | 分发 `on_swipe_progress` | | `patch_v2/meow_watchdog/meow_watchdog.c` | 删除 `paint_button()` 返回键绘制/按钮命中;保留兜底 | -| `patch_v2/meow_launcherd/meow_launcherd.c` | 解析 app.json `immersive` → 写 `/tmp/meowapp_state` | +| `patch_v2/meow_launcherd/app_launcher.c`(`meow_write_app_state`/`launch_app`) | 解析 app.json `immersive` → 写 `/tmp/meowapp_state` | | `patch_v2/hello_app/hello.c` | 删 `on_key(KEY_POWER)→exit`;声明 immersive;处理 `on_swipe_progress` 画条 | | `patch_v2/apps_assets/apps/os.meow.hello/app.json` | 加 `"immersive":1` | | `APPS.md` / `MEOW_APP_SDK.md` | 已写 §3.4.1 规范;SDK 文档补 `on_swipe_progress`/`meow_ui_swipe_hint` | diff --git a/KEY_CONTROL_DESIGN.md b/KEY_CONTROL_DESIGN.md index 7a5b53e..160fc63 100644 --- a/KEY_CONTROL_DESIGN.md +++ b/KEY_CONTROL_DESIGN.md @@ -61,7 +61,7 @@ watchdog 主循环(`for(;;)`)加按键轮询(`read /dev/input/event0/event | 文件 | 改动 | |---|---| | `patch_v2/meow_watchdog/meow_watchdog.c` | 主循环加按键轮询:电源(单击息屏/长按菜单/8s关机)、音量(调音量)、播放/上下曲(抓住);长按电源 → 唤醒 launcher | -| `patch_v2/meow_launcherd/meow_launcherd.c` | 应用运行时(waitpid 中)能被 eventfd 唤醒处理电源菜单;复用现有电源菜单/panel/音量逻辑 | +| `patch_v2/meow_launcherd/app_launcher.c`(`launch_app` waitpid 循环) | 应用运行时(waitpid 中)能被 eventfd 唤醒处理电源菜单;复用现有电源菜单/panel/音量逻辑 | | `patch_v2/sdk/meow_ui_input.c` | 不接管物理键语义(仅触摸 tap/swipe),排除误读事件1以外的键 | | `APPS.md §3.4.1` | 已写规范(应用禁映射物理键)——保持 | | `FRAMEWORK_DESIGN.md` | 阶段 D 媒体会话:播放/上下曲路由到活跃会话(附录 B) | @@ -87,7 +87,7 @@ watchdog 主循环(`for(;;)`)加按键轮询(`read /dev/input/event0/event 完成项(相对本蓝本): - ✅ 电源 单击=息屏/亮屏:`meow_watchdog.c handle_power`(沿用 M2.22,短按<3s)。 -- ✅ 电源 长按≥3s=电源菜单:`meow_watchdog.c wd_power_tick` 写 `/tmp/meow_power_menu` → `meow_launcherd.c` 应用 waitpid 轮询消费 → `SIGSTOP app` + 写 `/tmp/meow_power_menu_active` → `show_power_menu()`(launcher 一处绘制)→ 关菜单清标记 → `SIGCONT app`。方案 A。 +- ✅ 电源 长按≥3s=电源菜单:`meow_watchdog.c wd_power_tick` 写 `/tmp/meow_power_menu` → `app_launcher.c`(`launch_app` waitpid 循环)消费 → `SIGSTOP app` + 写 `/tmp/meow_power_menu_active` → `power_menu.c show_power_menu()`(launcher 一处绘制)→ 关菜单清标记 → `SIGCONT app`。方案 A。 - ✅ 音量±:`meow_watchdog.c wd_vol_adj` 自实现(写 CS43198 amixer numid=1/2 + 持久化 `/usr/data/meow/settings.conf` 与 launcher 同源);launcher `launch_app` 末尾 `settings_load()` 刷新。 - ✅ 播放/上下曲(164/165/163/19):`meow_watchdog.c` 捕获+日志+消费,不路由;SDK `meow_ui_input.c` 吞噬全部物理键,应用绝不碰。 - ✅ 应用内电源菜单弹出中断恢复:`show_power_menu` 复用(含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)。 diff --git a/MEOW_APP_SDK.md b/MEOW_APP_SDK.md index 5588056..1f327ec 100644 --- a/MEOW_APP_SDK.md +++ b/MEOW_APP_SDK.md @@ -287,7 +287,7 @@ int meow_page_view_current(const meow_page_view *pv); /* 当前页 */ ### 5.4 设计模板 A① 分组设置表单 [源:2026-08-30] -> **参照**:`patch_v2/meow_launcherd/meow_launcherd.c` 设置页(`SMENU` 表 + `srow_t` + `draw_settings_page`/`run_settings_page`)。**性质**:SDK 设计模板,供 App/设置页按需选用;`meow_ui.h` 现状控件最小集(按钮/滑条/开关/列表,§5)只提供单控件,本模板是把它们**组织成「分组设置表单」的结构范型**。API 为「草案/待定案」,不实现。 +> **参照**:`patch_v2/meow_launcherd/settings_page.c` 设置页(`SMENU` 表 + `srow_t` + `draw_settings_page`/`run_settings_page`)。**性质**:SDK 设计模板,供 App/设置页按需选用;`meow_ui.h` 现状控件最小集(按钮/滑条/开关/列表,§5)只提供单控件,本模板是把它们**组织成「分组设置表单」的结构范型**。API 为「草案/待定案」,不实现。 **摘要**:一页设置表单 = 若干**分组标题行** + 若干**设置行**。每行 `label + 控件`,控件类型决定取值方式——开关 / 滑条 / 下拉(枚举·预设)/ 子菜单 / 信息 / 列表。数据源 = **setting 键**(经 `meow_settings_*` 读写,见 spec-ref §4),UI 只绑定键、不直接触存储文件。 @@ -389,7 +389,7 @@ int meow_ui_listview_hit(meow_widget *lv, int x, int y); /* 命中行; ### 5.6 设计模板 B③ 弹窗/对话框 [源:2026-08-30] -> **参照**:`meow_launcherd.c` `s_dd_modal()`(WiFi 列表 / 关于本机 / 字体子菜单共用同一模态)。**性质**:SDK 设计模板,覆盖 **确认/选择/输入/信息** 四种;模态/非模态;返回/电源键关闭;遮罩。`meow_ui.h` 无通用弹窗,本模板为「草案/待定案」。 +> **参照**:`settings_page.c` `s_dd_modal()`(WiFi 列表 / 关于本机 / 字体子菜单共用同一模态)。**性质**:SDK 设计模板,覆盖 **确认/选择/输入/信息** 四种;模态/非模态;返回/电源键关闭;遮罩。`meow_ui.h` 无通用弹窗,本模板为「草案/待定案」。 **摘要**:在应用之上弹出**居中卡片**(遮罩压暗 + 圆角卡片 + 标题 + 内容 + 操作),用于打断当前流程的一次性决策。按用途分四型:确认(是/否)、选择(单选列表,可复用 A② 数据源)、输入(复用 IME §5.1)、信息(只读展示,如「关于本机」)。 @@ -883,7 +883,7 @@ void meow_theme_remove_listener(meow_theme_listener cb); | MIUIx | dark | MIUIx 深色系一套(草案) | 随 MIUIx 深色默认 | | MIUIx | light | MIUIx 浅色系一套(草案) | 随 MIUIx 浅色默认 | -> **修复语义**:此前 `display.background`(`black`/`blue`/`green`)只对 launcher 主页生效、只有深色、浅色不搭(launcher `meow_launcherd.c` `s_apply_setting` 的 `display.background` 分支 / `bg_presets`——`blue→0x0A1428`、`green→0x0A2814`、`else→0x000000`)。本定案将背景改为「style×mode 派生选项集」:**每种背景配色定义深/浅两套 surface/bg**(§8.1.4),随 `display.theme` 切换 → 深浅都搭、全界面(状态栏/未声明背景 App/主页)同步。 +> **修复语义**:此前 `display.background`(`black`/`blue`/`green`)只对 launcher 主页生效、只有深色、浅色不搭(launcher `settings_page.c s_commit` 的 `display.background` 分支 / `bg_presets`——`blue→0x0A1428`、`green→0x0A2814`、`else→0x000000`)。本定案将背景改为「style×mode 派生选项集」:**每种背景配色定义深/浅两套 surface/bg**(§8.1.4),随 `display.theme` 切换 → 深浅都搭、全界面(状态栏/未声明背景 App/主页)同步。 #### 8.1.2 SDK 接口(一键切换) @@ -902,7 +902,7 @@ const meow_theme *meow_theme_get(void); - **设置页联动**:设置页 `ui.style` → `meow_ui_set_style` → 框架**全局刷新**(状态栏 SDK 全局 / 未声明背景的 App / launcher 主页,§8.1.4)。App 在 `on_tick` 据通知重绘(`meow_ui_flush_region`,沿用 `meow_theme_listener` 机制)。 - **明暗联动**:`display.theme` / `display.theme_mode`(auto)变化仍走现有 `meow_theme_mode` + `meow_theme_listener`(§8 现状),仅「同一 style 下」的浅/深背景随切换自动换。 -- **命名对齐(与 `meow_ui.h` / launcher 现状)**:SDK **App 侧**结构体权威名 = **`meow_theme`**(既有 `meow_theme_get()`),**不是** `meow_ui_theme`;launcher 内部另有独立结构体 `meow_ui_theme_t`(`meow_launcherd.c`,仅供状态栏/设置页,**与 App 侧无关、不暴露**)。故本节接口统一用 `meow_theme` / `meow_theme_get`;若实现期另起 `meow_ui_theme()`/`meow_ui_theme_t` 作同义别名亦可,视为与 `meow_theme` 等价(待定案)。 +- **命名对齐(与 `meow_ui.h` / launcher 现状)**:SDK **App 侧**结构体权威名 = **`meow_theme`**(既有 `meow_theme_get()`),**不是** `meow_ui_theme`;launcher 内部另有独立结构体 `meow_ui_theme_t`(`font_theme.c`,仅供状态栏/设置页,**与 App 侧无关、不暴露**)。故本节接口统一用 `meow_theme` / `meow_theme_get`;若实现期另起 `meow_ui_theme()`/`meow_ui_theme_t` 作同义别名亦可,视为与 `meow_theme` 等价(待定案)。 - **状态栏图标随 `ui.style` 换集**:状态栏右侧图标集与风格族绑定(MD3=默认图标、MIUIx=另套基础);`meow_ui_set_style` 时框架**一并切换状态栏图标集**(触发全局刷新,见 §8.1.4),颜色随 `meow_theme`(style×mode)日/夜(详见 §3.1「同一个状态栏」正确闭环)。 #### 8.1.3 两套风格参数(草案值) diff --git a/patch_v2/meow_launcherd/about.c b/patch_v2/meow_launcherd/about.c new file mode 100644 index 0000000..4e80be4 --- /dev/null +++ b/patch_v2/meow_launcherd/about.c @@ -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:只读展示,无预勾选 */ +} + diff --git a/patch_v2/meow_launcherd/app_launcher.c b/patch_v2/meow_launcherd/app_launcher.c new file mode 100644 index 0000000..44bae0a --- /dev/null +++ b/patch_v2/meow_launcherd/app_launcher.c @@ -0,0 +1,1006 @@ +/* app_launcher.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ +/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ +#include "meow_launcher_internal.h" + + +static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */ + +int g_naccts = 0; + + +/* ---- 阶段 D · M1:路由注册表辅助(主循环单写者,无锁)---- */ +struct meow_acct *meow_acct_find(const char *app_id) +{ + if (!app_id) return NULL; + for (int i = 0; i < g_naccts; i++) + if (!strcmp(g_accts[i].app_id, app_id)) return &g_accts[i]; + return NULL; +} + + +/* 取(或在末尾新建)app_id 对应的注册表项。返回 NULL 当满。 */ +static struct meow_acct *meow_acct_upsert(const char *app_id) +{ + if (!app_id || !app_id[0]) return NULL; + struct meow_acct *a = meow_acct_find(app_id); + if (a) return a; + if (g_naccts >= MEOW_MAX_ACTS) { logmsg("act: registry full, drop %s", app_id); return NULL; } + a = &g_accts[g_naccts++]; + memset(a, 0, sizeof *a); + strncpy(a->app_id, app_id, MEOW_APP_ID_MAX - 1); + return a; +} + + +/* 由 scan_apps 调用:把 app.json 静态路由整体同步进注册表(扫描即权威,替换)。 */ +static void meow_acct_sync(const char *app_id, const struct app *ap) +{ + if (!ap) return; + struct meow_acct *a = meow_acct_upsert(app_id); + if (!a) return; + a->framework = ap->framework; + strncpy(a->icon, ap->icon, 128); + strncpy(a->name, ap->name, 16); + strncpy(a->exec, ap->exec, 128); + a->ok = ap->ok; + a->nroute = ap->nroute > MEOW_MAX_ROUTES ? MEOW_MAX_ROUTES : ap->nroute; + for (int i = 0; i < a->nroute; i++) a->routes[i] = ap->routes[i]; + a->nsvc = ap->nsvc > MEOW_MAX_SVCS ? MEOW_MAX_SVCS : ap->nsvc; + for (int i = 0; i < a->nsvc; i++) a->svcs[i] = ap->svcs[i]; +} + + +/* 运行时 HELLO:声明 app 存在(补入注册表),记录声明源。 */ +void meow_acct_hello(const char *app_id, uint32_t version, uint32_t flags) +{ + struct meow_acct *a = meow_acct_upsert(app_id); + if (a) { a->declared = 1; (void)version; (void)flags; } + logmsg("act: hello app=%s ver=%u flags=%u", app_id, version, flags); +} + + +/* 运行时 ACT_DECLARE:并集覆盖静态路由。entries=count 个 NUL 结尾 "id=chain" 串。 */ +void meow_acct_declare(const char *app_id, const char *entries, int count) +{ + struct meow_acct *a = meow_acct_upsert(app_id); + if (!a || !entries || count <= 0) return; + a->declared = 1; + const char *p = entries; + for (int k = 0; k < count && *p; k++) { + const char *eq = strchr(p, '='); + if (eq && eq > p) { + int idlen = (int)(eq - p); + if (idlen >= 16) idlen = 15; + char id[16]; + memcpy(id, p, idlen); id[idlen] = 0; + const char *chain = eq + 1; + int ri = -1; + for (int i = 0; i < a->nroute; i++) + if (!strcmp(a->routes[i].act, id)) { ri = i; break; } + if (ri < 0) { + if (a->nroute >= MEOW_MAX_ROUTES) { p += strlen(p) + 1; continue; } + ri = a->nroute++; + } + strncpy(a->routes[ri].act, id, 15); + strncpy(a->routes[ri].chain, chain, MEOW_CHAIN_MAX - 1); + } + p += strlen(p) + 1; + } +} + + +/* 链路深度 = ';' 分隔段数(根屏→目标屏的逻辑链长)。 */ +int chain_depth(const char *c) +{ + if (!c || !*c) return 0; + int d = 1; + for (; *c; c++) if (*c == ';') d++; + return d; +} + + +/* 解析 activity 链路:填 out_chain(root..target)与 depth。 + * 返回 0=成功;MEOW_ERR_NOENT=app 未注册 / activity 未注册。 + * 规则(FRAMEWORK_DESIGN §2):route 缺失(app 未声明任何路由)→ chain=activity、深度 1 兜底; + * 声明了路由但 activity 非 route 键 → MEOW_ERR_NOENT(activity 未注册)。 */ +int resolve_chain(const char *app_id, const char *activity, char *out, int *depth) +{ + if (!out || !depth) return MEOW_ERR_INVAL; + struct meow_acct *a = meow_acct_find(app_id); + if (!a) return MEOW_ERR_NOENT; + if (a->nroute == 0) { + strncpy(out, activity, MEOW_CHAIN_MAX - 1); + *depth = 1; + return MEOW_OK; + } + for (int i = 0; i < a->nroute; i++) { + if (!strcmp(a->routes[i].act, activity)) { + strncpy(out, a->routes[i].chain, MEOW_CHAIN_MAX - 1); + *depth = chain_depth(a->routes[i].chain); + return MEOW_OK; + } + } + return MEOW_ERR_NOENT; +} + + +/* ---- M2.17 keep_awake:前台当前顶层 activity 追踪 ---- + * 前台应用经 IPC MSG_ACT_FOCUS/BACK 上报其当前顶层 activity;fg_keep_awake() 依注册表判断该 + * activity 是否声明 keep_awake(app.json awake.=1 / route..keep_awake=1;运行时同类)。 + * 主界面(无上报/空)= 非 keep_awake(主页空闲照常超时息屏)。单一写者=主循环(IPC 排空处)。 */ +static char g_fg_app[MEOW_APP_ID_MAX] = ""; + +static char g_fg_act[MEOW_ACT_MAX] = ""; + +void meow_fg_act_set(const char *app_id, const char *act) +{ + if (!app_id) return; + strncpy(g_fg_app, app_id, MEOW_APP_ID_MAX - 1); + g_fg_app[MEOW_APP_ID_MAX - 1] = 0; + if (act) { strncpy(g_fg_act, act, MEOW_ACT_MAX - 1); g_fg_act[MEOW_ACT_MAX - 1] = 0; } + else g_fg_act[0] = 0; + logmsg("keep_awake: fg_act app=%s act=%s", g_fg_app, g_fg_act); +} + +void meow_fg_act_back(void) +{ + g_fg_app[0] = 0; + g_fg_act[0] = 0; + logmsg("keep_awake: fg_act back to home (cleared)"); +} + +/* 前台当前 activity 是否 keep_awake(无上报/未注册/未声明=0,即照常超时息屏)。 */ +int fg_keep_awake(void) +{ + if (!g_fg_app[0] || !g_fg_act[0]) return 0; + struct meow_acct *a = meow_acct_find(g_fg_app); + if (!a) return 0; + for (int i = 0; i < a->nroute; i++) + if (!strcmp(a->routes[i].act, g_fg_act)) return a->routes[i].keep_awake; + return 0; +} + + +/* 确保联网:wlan0 未 up 且尚无 wpa_supplicant 在跑 → 调用 /usr/bin/wifi_up.sh 连保存网络。 + * 幂等:连上 / 已有 wpa_supplicant 则不动;每轮询重试,不阻塞主界面。 + * M2.39:尊重 network.wifi_enabled —— 用户在设置页关 WiFi(=0)则不自动拉起(否则与 + * 手动关冲突);取值失败(未 init)保默认 1=允许自动连网。 */ +void ensure_wifi(void) +{ + int enabled = 1; + if (meow_settings_get_bool("network.wifi_enabled", &enabled) == 0 && !enabled) return; + if (wlan_connected()) return; + if (proc_by_comm("wpa_supplicant")) return; + logmsg("wifi not up -> /usr/bin/wifi_up.sh"); + system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh"); +} + + +/* 解析一个 app.json 流 → 填充 struct app 的 name/exec/icon/framework/routes/svcs。 + * 极简:不做 JSON 校验,仅匹配 key 与路由/服务扁平行(JSON 或扁平皆可)。 + * 不设 name/exec(由调用方从返回的 app 再复制),成功判定由调用方做。 */ +void meow_parse_app_json(FILE *f, struct app *a) +{ + char buf[256]; + char name[17] = "", exec[129] = "", icon[129] = ""; + char package[MEOW_APP_ID_MAX] = ""; + int fram = 0, immersive = 0; + int fullscreen = 0; /* app.json fullscreen=(注入 MEOW_FULLSCREEN env,SDK 预先设全屏) */ + int fonts = 0, fonts_set = 0; /* app.json "fonts":<档>(0=默认4K常用 1=大字库GBK 2=Full;缺省 0) */ + struct app tmp; + memset(&tmp, 0, sizeof tmp); + while (fgets(buf, sizeof buf, f)) { + if (!name[0]) json_str(buf, "name", name, sizeof name); + if (!exec[0]) json_str(buf, "exec", exec, sizeof exec); + if (!icon[0]) json_str(buf, "icon", icon, sizeof icon); + if (!package[0]) json_str(buf, "package", package, sizeof package); /* 安卓化权威 app_id */ + /* route.=(activity 路由,M1) */ + char *rp = strstr(buf, "route."); + if (rp && tmp.nroute < MEOW_MAX_ROUTES) { + rp += 6; /* 跳过 "route." */ + int idl = 0; char id[32] = ""; /* 读满整键(识别 ".fullscreen" 后缀,cap 31) */ + while (*rp && *rp != '"' && *rp != ':' && *rp != '=' && idl < 31) + id[idl++] = *rp++; + id[idl] = 0; + if (idl > 0) { + char *vp = rp; + while (*vp && *vp != ':' && *vp != '=') vp++; + if (*vp == ':' || *vp == '=') vp++; + while (*vp == ' ' || *vp == '\t') vp++; + if (*vp == '"') vp++; + /* route..fullscreen=:活动级全屏声明(M1 顺带;M3 活动切换时消费)。 + * 命中既有 route. 置 fullscreen;不新建 route(避免 id 被截断误建 "…fulls")。 */ + if (idl >= 11 && !strcmp(id + idl - 11, ".fullscreen")) { + id[idl - 11] = 0; /* 去掉 ".fullscreen" 还原 act */ + char v[8] = ""; int k = 0; + while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' && + *vp != '\r' && k < 7) v[k++] = *vp++; + v[k] = 0; + int on = (!strcmp(v, "1") || !strcmp(v, "true") || !strcmp(v, "yes")); + for (int i = 0; i < tmp.nroute; i++) + if (!strcmp(tmp.routes[i].act, id)) { tmp.routes[i].fullscreen = on; break; } + } else { + int vl = 0; char chain[MEOW_CHAIN_MAX] = ""; + while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' && + *vp != '\r' && vl < MEOW_CHAIN_MAX - 1) + chain[vl++] = *vp++; + chain[vl] = 0; + while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t')) + chain[--vl] = 0; + strncpy(tmp.routes[tmp.nroute].act, id, 15); + tmp.routes[tmp.nroute].act[15] = 0; /* 显式收尾(原依赖 id 读长 ≤15) */ + strncpy(tmp.routes[tmp.nroute].chain, chain, MEOW_CHAIN_MAX - 1); + tmp.nroute++; + } + } + } + /* awake.=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。 + * 命中的既有 route. 置 keep_awake;无 route 则新建单 activity 路由。 */ + char *ap = strstr(buf, "awake."); + if (ap && tmp.nroute < MEOW_MAX_ROUTES) { + ap += 6; /* 跳过 "awake." */ + int al = 0; char aid[16] = ""; + while (*ap && *ap != '"' && *ap != ':' && *ap != '=' && al < 15) + aid[al++] = *ap++; + aid[al] = 0; + if (al > 0) { + char *vp = ap; + while (*vp && *vp != ':' && *vp != '=') vp++; + if (*vp == ':' || *vp == '=') vp++; + while (*vp == ' ' || *vp == '\t') vp++; + if (*vp == '"') vp++; + int on = atoi(vp); + int ri = -1; + for (int i = 0; i < tmp.nroute; i++) + if (!strcmp(tmp.routes[i].act, aid)) { ri = i; break; } + if (ri < 0) { + ri = tmp.nroute++; + strncpy(tmp.routes[ri].act, aid, 15); + strncpy(tmp.routes[ri].chain, aid, MEOW_CHAIN_MAX - 1); /* 单 activity 兜底链路 */ + } + tmp.routes[ri].keep_awake = (on != 0); + } + } + /* framework=true(看门狗返回发 SIGUSR1,M1 解析存储) */ + if (!fram && strstr(buf, "framework")) { + char v[8] = ""; + if (json_str(buf, "framework", v, sizeof v) > 0) { + if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")) + fram = 1; + } + } + /* immersive=true(隐藏虚拟返回键、返回手势移交 SDK;写 /tmp/meowapp_state) */ + if (!immersive && strstr(buf, "immersive")) { + char v[8] = ""; + if (json_str(buf, "immersive", v, sizeof v) > 0) { + if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")) + immersive = 1; + } + } + /* fullscreen=true(app 级:注入 MEOW_FULLSCREEN=1,SDK 端 meow_ui_init 预先设全屏)。 + * 与 immersive 是两回事:fullscreen 隐藏状态栏/沉浸,immersive 关虚拟返回键。 */ + if (!fullscreen && strstr(buf, "fullscreen")) { + char v[8] = ""; + if (json_str(buf, "fullscreen", v, sizeof v) > 0) { + if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")) + fullscreen = 1; + } + } + /* "fonts":<档>(字库档:0=默认4K常用 1=大字库GBK 2=Full;可选,缺省 0)。 + * launcher 注入 MEOW_FONT_TIER;大字库装载由后续 font 代理实现。 */ + if (!fonts_set && strstr(buf, "fonts")) { + char v[8] = ""; + if (json_str(buf, "fonts", v, sizeof v) > 0) { + if (!strcmp(v, "common") || !strcmp(v, "0")) fonts = 0; + else if (!strcmp(v, "gbk") || !strcmp(v, "1")) fonts = 1; + else if (!strcmp(v, "full") || !strcmp(v, "2")) fonts = 2; + else fonts = atoi(v); /* 数字兜底(越界收敛到 [0,2]) */ + if (fonts < 0) fonts = 0; + if (fonts > 2) fonts = 2; + fonts_set = 1; + } + } + /* service.=mode:restart:max:window_ms(M1 解析存储,保活 M4) */ + char *sp = strstr(buf, "service."); + if (sp && tmp.nsvc < MEOW_MAX_SVCS) { + sp += 8; /* 跳过 "service." */ + int sl = 0; char nm[16] = ""; + while (*sp && *sp != '"' && *sp != ':' && *sp != '=' && sl < 15) + nm[sl++] = *sp++; + nm[sl] = 0; + if (sl > 0) { + char *vp = sp; + while (*vp && *vp != ':' && *vp != '=') vp++; + if (*vp == ':' || *vp == '=') vp++; + while (*vp == ' ' || *vp == '\t') vp++; + if (*vp == '"') vp++; + int m = 0, r = 0, mx = 0, w = 0; + sscanf(vp, "%d:%d:%d:%d", &m, &r, &mx, &w); + strncpy(tmp.svcs[tmp.nsvc].name, nm, 15); + tmp.svcs[tmp.nsvc].mode = m; + tmp.svcs[tmp.nsvc].restart = r; + tmp.svcs[tmp.nsvc].max = mx; + tmp.svcs[tmp.nsvc].window_ms = w; + tmp.nsvc++; + } + } + } + strncpy(a->name, name, 16); + strncpy(a->exec, exec, 128); + strncpy(a->package, package, sizeof a->package - 1); + a->framework = fram ? '1' : 0; + a->immersive = immersive ? '1' : 0; + a->fullscreen = fullscreen ? '1' : 0; /* 注入 MEOW_FULLSCREEN env */ + a->fonts = (char)(fonts < 0 ? 0 : (fonts > 2 ? 2 : fonts)); /* 注入 MEOW_FONT_TIER env */ + strncpy(a->icon, icon, 128); + a->nroute = tmp.nroute; + if (tmp.nroute) memcpy(a->routes, tmp.routes, sizeof(a->routes[0]) * (size_t)tmp.nroute); + a->nsvc = tmp.nsvc; + if (tmp.nsvc) memcpy(a->svcs, tmp.svcs, sizeof(a->svcs[0]) * (size_t)tmp.nsvc); +} + + +int scan_apps(struct app apps[MAX_APPS]) +{ + memset(apps, 0, sizeof(struct app) * MAX_APPS); + if (!card_present()) return -1; /* state 1 */ + + DIR *d = opendir(MEOWAPPS); + if (!d) return -2; /* state 2 */ + int n = 0; + struct dirent *de; + while ((de = readdir(d)) && n < MAX_APPS) { + if (de->d_name[0] == '.') continue; + char jpath[192]; + snprintf(jpath, sizeof jpath, "%s/%s/app.json", MEOWAPPS, de->d_name); + FILE *f = fopen(jpath, "r"); + if (!f) continue; + struct app a0; + memset(&a0, 0, sizeof a0); + meow_parse_app_json(f, &a0); + fclose(f); + if (!a0.name[0] || !a0.exec[0]) continue; /* corrupt -> skip */ + struct app *a = &apps[n]; + memset(a, 0, sizeof *a); + memcpy(a, &a0, sizeof a0); + a->ok = (access(a->exec, X_OK) == 0); + /* 安卓化:app_id=app.json 的 package(权威),缺省回退目录名 de->d_name; + * 两者都存在且不同则 WARN(正常情况二者相等)。launch_app 注入 MEOW_LOG_APP 用 */ + const char *aid = a0.package[0] ? a0.package : de->d_name; + strncpy(a->id, aid, sizeof a->id - 1); + if (a0.package[0] && strcmp(a0.package, de->d_name) != 0) + logmsg("WARN: app '%s' package '%s' != dir '%s'", a0.name, a0.package, de->d_name); + /* M1 路由注册表同步(app_id = a->id,即 package 优先) */ + meow_acct_sync(a->id, a); + n++; + } + closedir(d); + return n; /* 0 -> state 2 */ +} + + +/* ================================================================== */ +/* hiby_player management */ +/* ================================================================== */ +int find_hiby_pid(void) +{ + /* scan /proc/N/exe for /usr/bin/hiby_player */ + DIR *d = opendir("/proc"); + if (!d) return 0; + struct dirent *de; + int pid = 0; + while ((de = readdir(d))) { + if (de->d_name[0] < '0' || de->d_name[0] > '9') continue; + char path[64], exe[256]; + snprintf(path, sizeof path, "/proc/%s/exe", de->d_name); + ssize_t n = readlink(path, exe, sizeof exe - 1); + if (n <= 0) continue; + exe[n] = 0; + if (!strcmp(exe, "/usr/bin/hiby_player")) { pid = atoi(de->d_name); break; } + } + closedir(d); + return pid; +} + + +/* ---- MeowOS 常驻主界面判定(M2.2 修复批次)---- + * Launcher 被 MeowOS 拉起(bootmenu MeowOS 分支 exec,或热键切 MeowOS 后 exec)时, + * 播放器已死(find_hiby_pid()==0)。此时 launcher == MeowOS 主界面常驻:必须自绘 + * fb0(画 buffer0 + FBIOPAN yoffset=0),绝不走旧「备份播放器帧/冻结/SIGCONT」路径, + * 绝不遗留/恢复播放器死帧。仅当存在存活播放器(会话覆盖场景)才保留旧菜单覆盖语义。 */ +int meow_mode = 0; + + +int is_meow_mode(void) +{ + return find_hiby_pid() <= 0; +} + + +int player_stopped = 0; + +static int player_pid = 0; + + +static int proc_is_stopped(int pid) +{ + char path[64], line[512]; + snprintf(path, sizeof path, "/proc/%d/stat", pid); + int fd = open(path, O_RDONLY); + if (fd < 0) return 0; + ssize_t n = read(fd, line, sizeof line - 1); + close(fd); + if (n <= 0) return 0; + line[n] = 0; + /* comm may contain spaces; find last ')' */ + char *p = strrchr(line, ')'); + if (!p || p + 2 >= line + n) return 0; + return p[2] == 'T'; +} + + +int stop_player(void) +{ + player_pid = find_hiby_pid(); + if (!player_pid) { logmsg("hiby_player not found, menu without stop"); return 0; } + if (kill(player_pid, SIGSTOP) < 0) { + logmsg("SIGSTOP %d failed: %s", player_pid, strerror(errno)); + return -1; + } + /* wait for state T */ + for (int i = 0; i < 100; i++) { + if (proc_is_stopped(player_pid)) break; + usleep(10000); + } + player_stopped = proc_is_stopped(player_pid); + logmsg("hiby_player pid=%d stopped=%d", player_pid, player_stopped); + return player_stopped ? 0 : -1; +} + + +void cont_player(void) +{ + if (player_stopped && player_pid > 0) { + kill(player_pid, SIGCONT); + logmsg("SIGCONT %d", player_pid); + } + player_stopped = 0; +} + + +/* ================================================================== */ +/* M2.12 MeowAPPs 包数据目录 + .mpk 接口 */ +/* exFAT 无符号链接 → 数据目录用环境变量(MEOW_DATA_DIR/CACHE_DIR) */ +/* 传址给应用,不做 symlink(APPS.md 附录 v2 修订)。 */ +/* ================================================================== */ + +/* 由 exec 绝对路径取包名:/usr/data/mnt/sd_0/MeowAPPs// → */ +static int meow_pkg_name(const char *exec, char *out, int n) +{ + if (!exec || strncmp(exec, MEOWAPPS, strlen(MEOWAPPS)) != 0) { if (n>0) out[0]=0; return -1; } + const char *p = exec + strlen(MEOWAPPS); + if (*p != '/') { if (n>0) out[0]=0; return -1; } + p++; + const char *end = strchr(p, '/'); + int len = end ? (int)(end - p) : (int)strlen(p); + if (len <= 0 || len >= n) { if (n>0) out[0]=0; return -1; } + memcpy(out, p, len); out[len] = 0; + return 0; +} + + +/* 启动应用前准备数据目录:建 MeowData//{data,cache}(如缺)。幂等。 + * 失败只 log(MeowData 所在卡不存在/只读),绝不崩 —— 零卡依赖:应用需按 + * APPS.md 规范容忍无数据目录。返回 0=就绪,-1=pkg 不在卡上/不能判名。 */ +static int meow_app_prepare(const char *exec) +{ + char pkg[64]; + if (meow_pkg_name(exec, pkg, sizeof pkg) != 0) return -1; + if (!sd_device_present()) return -1; /* 卡不在,跳过(应用仍可跑非卡资源) */ + char cmd[320]; + snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' " + "'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg); + int rc = system(cmd); + logmsg("app_prepare: pkg=%s rc=%d", pkg, rc); + return rc == 0 ? 0 : -1; +} + + +/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs//。 + * 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 .mpk。 + * 同名先备份;解压后校验 app.json;随后数据 init(MeowData//{data,cache})。 + * 返回 0 ok / 负=错误。 */ +int meow_pkg_install(const char *mpk_path) +{ + if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; } + if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; } + if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; } + /* 包名 = 文件名茎(去掉 .mpk) */ + const char *bn = strrchr(mpk_path, '/'); + bn = bn ? bn + 1 : mpk_path; + const char *dot = strrchr(bn, '.'); + char pkg[64]; + if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need .mpk"); return -1; } + int len = (int)(dot - bn); + if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; } + memcpy(pkg, bn, len); pkg[len] = 0; + /* 目标 MeowAPPs//;同名先备份(.bak_),再解压,再校验 app.json */ + char dst[256], bak[256], cmd[700]; + snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg); + snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid()); + /* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */ + snprintf(cmd, sizeof cmd, + "rm -rf '%s' 2>/dev/null; " + "[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; " + "mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && " + "[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst); + if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; } + /* 数据 init:MeowData//{data,cache} */ + snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' " + "'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg); + system(cmd); + logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak); + return 0; +} + + +/* .mpk 导出:打包 MeowAPPs//(数据目录本就由 env 指向 MeowData,MeowAPPs/ 下无 + * data/cache 实体,直接全打包)→ .mpk。返回 0 ok。 */ +int meow_pkg_export(const char *pkg, const char *out_path) +{ + if (!pkg || !out_path || strpbrk(pkg, " ;`$'\"&|<>/" ) || strpbrk(out_path, " ;`$'\"&|<>")) { + logmsg("pkg_export: unsafe path"); return -2; + } + if (!sd_mounted()) { logmsg("pkg_export: card not mounted"); return -1; } + char src[256], dst[256]; + snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg); + if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; } + snprintf(dst, sizeof dst, "%s", out_path); + /* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */ + char cmd[512]; + snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && " + "mv '%s.tmp' '%s'", src, dst, dst, dst); + if (system(cmd) != 0) { logmsg("pkg_export: tar/gzip fail"); return -1; } + logmsg("pkg_export: %s -> %s", pkg, dst); + return 0; +} + + +/* returns 0 on success (app ran and exited), -1 on launch failure */ +/* GESTURE_BACK §2.4:把 app.json 的 immersive 声明写进 /tmp/meowapp_state, + * watchdog 据此决定是否画虚拟返回键 / 走 immersive 手势(watchdog 每 100ms 读一次)。 + * immersive=1→"1";0→"0";<0→清空(应用退出,恢复默认非沉浸)。 */ +static void meow_write_app_state(int immersive) +{ + if (immersive < 0) { unlink("/tmp/meowapp_state"); return; } + int fd = open("/tmp/meowapp_state", O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return; + if (write(fd, immersive ? "1" : "0", 1) < 0) { /* ignore write error */ } + close(fd); +} + + +/* 读 App 顶条模式:0=展开 1=收合 2=隐藏;-1=无文件/读取失败(按展开处理,默认安全)。 */ +static int app_sbar_mode_read(void) +{ + int fd = open(MEOW_SBAR_MODE_FILE, O_RDONLY); + if (fd < 0) return -1; + char ch[8]; + int n = read(fd, ch, sizeof ch - 1); + close(fd); + if (n <= 0) return -1; + if (ch[0] == '1') return 1; + if (ch[0] == '2') return 2; + return 0; +} + + +static void app_overlay_statusbar(void) +{ + if (!back_buf_ok || !fb) return; /* 无 SDK back_buf(直写模式):无法叠加,跳过 */ + int mode = app_sbar_mode_read(); + if (mode == 2) { /* 全屏隐藏(immersive,inset=0):launcher 不叠顶条 */ + meow_ui_set_fullscreen(1); + meow_ui_statusbar_set_collapsed(0); + return; /* 无内容可叠:交 App 全屏(其 present inset0 已写满屏) */ + } + if (mode == 1) { /* 收合:SDK 切收合(上移过渡 + 提示线),无时钟/图标/值 */ + meow_ui_set_fullscreen(0); + meow_ui_statusbar_set_collapsed(1); + meow_ui_statusbar_animate(MEOW_SBAR_ANIM_MS); /* 平滑上移/恢复过渡时长 */ + } else { /* 展开(默认/无文件):整条 28px */ + meow_ui_set_fullscreen(0); + meow_ui_statusbar_set_collapsed(0); + } + meow_sys_light(); /* 刷新 g_sys(电池/WiFi/音量/蓝牙;唯一权威源) */ + 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(顶条必重画) */ + /* 撤销 set_collapsed 可能引发的「整帧脏」:launcher 在 App 上叠加只应 blit 顶条,绝不能整帧 blit + * (否则会把自身 back_buf 的 stale 菜单内容盖到 App 内容上,属灾难性回归——agent-146)。 + * 随后只标「心跳」脏(顶条左上角 1px)唤醒 present();实际 blit 区由 SDK 渲染在 present() 内标脏决定 + * (render_off 画 [0, 28-off) 可见条高 / 收合稳定画 [0,4) 提示线 / 展开画 [0,28) 整条),故上移时 + * 只 blit 可见条高、其余行保留 App 内容 —— 平滑上收无残留、不黑框。 */ + meow_gfx_dirty_reset(); + meow_dirty_add(0, SB_BAR_Y, 1, 1); + meow_ui_flush(); /* SDK present:vsync + 原子 blit(区随渲染标脏) */ +} + +/* (definition follows) */ + +int launch_app(struct app *a) +{ + logmsg("launch: %s (%s)", a->name, a->exec); + if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; } + + /* M2.12:启动前准备 MeowData//{data,cache}(幂等;卡不在/失败只 log 不崩)。 + * 应用经环境变量 MEOW_DATA_DIR/MEOW_CACHE_DIR 定位数据(exFAT 无 symlink)。 */ + meow_app_prepare(a->exec); + + /* save the brightness the player had before the menu/app session, then + * drain input queues so the app starts clean */ + saved_brightness = read_brightness(); + if (saved_brightness <= 0) saved_brightness = 60; /* standby/unknown */ + logmsg("saved brightness = %d", saved_brightness); + drain_input(); + + /* Bug 3: distinguish a REAL exec failure from a clean app exit. The child + * holds the write end of a CLOEXEC pipe; if execve() fails it writes the + * errno and _exit(127), if it succeeds the fd closes on exec so the parent + * read() returns EOF. Parent blocks on read() until the child either execs + * (success) or reports the error (failure); only then do we spawn the + * watchdog and waitpid — so an exec failure never leaves an orphan watchdog + * and never masquerades as "app ran and exited". */ + int execpipe[2]; + if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return -1; } + fcntl(execpipe[1], F_SETFD, FD_CLOEXEC); + + pid_t pid = fork(); + if (pid < 0) { + close(execpipe[0]); close(execpipe[1]); + logmsg("fork failed: %s", strerror(errno)); return -1; + } + if (pid == 0) { + /* child: exec the app. M2.12 注入 MEOW_LOG_DIR + MEOW_DATA_DIR/MEOW_CACHE_DIR + * (exFAT 无 symlink,数据目录用 env 传给应用)。PATH 继承 launcher(含卡上 opt/bin + * 若在),卡不在则回退普通 PATH。M2.52 追加注入 MEOW_LOG_APP(app_id)+MEOW_LOG_LEVEL + * (读全局 log.level),使 SDK meow_log() 按 app 分离写 apps/.log 并做级别过滤。 */ + close(execpipe[0]); + char *argv[] = { a->exec, NULL }; + char env_path[320], env_log[320], env_data[320], env_cache[320]; + char env_app[MEOW_APP_ID_MAX + 16], env_lvl[48]; + char env_fpng[400], env_fidx[400], env_fapp[96]; + char env_mf[24]; + char env_tier[24]; + char env_fs[20]; + char env_cls[MEOW_FCLS_COUNT][2][400]; + char *envp[40]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),tier(1),class(≤12),fullscreen(1),data,cache */ + int ne = 0; + const char *cp = getenv("PATH"); + snprintf(env_path, sizeof env_path, "PATH=%s", cp ? cp : "/usr/bin:/bin"); + envp[ne++] = env_path; + envp[ne++] = "HOME=/root"; + if (log_dir[0]) { snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", log_dir); envp[ne++] = env_log; } + /* M2.52:MEOW_LOG_APP=app_id(=app.json.package,缺省回退目录名;与注册表一致;无 id 则跳过,SDK 兜底 "app") */ + if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; } + /* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3), + * SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */ + { + static const char *lvl_name[] = { "ERROR", "WARN", "INFO", "DEBUG" }; + int lv = 2; /* 默认 info */ + meow_settings_get_enum("log.level", &lv); + if (lv < 0 || lv > 3) lv = 2; + snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]); + envp[ne++] = env_lvl; + } + /* M2.x:注入「当前生效字体」= 系统 font.file(launcher 已 load 的那套), + * SDK meow_ui_gfx.load_font 优先读 MEOW_FONT_PNG/IDX,app 未自带字库(§5.7 load_handle + * 未实现)时跟随系统字体,不再硬编码 MeowAPPs/fonts。无卡/无可用字体则跳过注入, + * SDK 自行回退卡默认→固件→点阵。 */ + { + static const char *cls_env_png[MEOW_FCLS_COUNT] = { + "MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_NUM_PNG", + "MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_OTHER_PNG" }; + static const char *cls_env_idx[MEOW_FCLS_COUNT] = { + "MEOW_FONT_CLS_CJK_IDX", "MEOW_FONT_CLS_CJK_SYM_IDX", "MEOW_FONT_CLS_NUM_IDX", + "MEOW_FONT_CLS_LATIN_IDX", "MEOW_FONT_CLS_LATIN_SYM_IDX", "MEOW_FONT_CLS_OTHER_IDX" }; + 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" }; + char fpng[400], fidx[400], fam[96]; + int have_global; + int mf_on = 0; + meow_settings_get_switch("display.multi_font", &mf_on); + snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0); + envp[ne++] = env_mf; + /* M2.5x:字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入,并合并 + * 「加载大字库」开关(font.load_full → 强制 ≥GBK)得**生效档**;0=默认4K常用, + * 1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档并按档装载(见 + * meow_font_effective_paths_tiered;真正装载由 meow_font_reload_tier 实现)。 */ + int eff_tier = meow_font_effective_tier(a->fonts); + snprintf(env_tier, sizeof env_tier, "MEOW_FONT_TIER=%d", eff_tier); + envp[ne++] = env_tier; + have_global = (meow_font_effective_paths_tiered(eff_tier, fpng, sizeof fpng, + fidx, sizeof fidx, fam, sizeof fam) == 0); + if (have_global) { + snprintf(env_fpng, sizeof env_fpng, "MEOW_FONT_PNG=%s", fpng); + snprintf(env_fidx, sizeof env_fidx, "MEOW_FONT_IDX=%s", fidx); + snprintf(env_fapp, sizeof env_fapp, "MEOW_FONT_APP=%s", fam); + envp[ne++] = env_fpng; + envp[ne++] = env_fidx; + envp[ne++] = env_fapp; + logmsg("launch: font env %s/%s (family=%s)", fpng, fidx, fam); + } + /* M2.5x 多字体:每类注入 png/idx(命中则覆盖全局;类值为空=跟随全局,不注入)。 + * SDK 读这些 env 构建多字体映射;同 family 去重由 SDK 侧 meow_font_config_multifont 处理。 */ + if (mf_on) + for (int c = 0; c < MEOW_FCLS_COUNT; c++) { + char ref[MEOW_SET_MAX_LEN], abs[400], idxabs[400]; + if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) != 0 || !ref[0]) continue; + if (meow_font_resolve_path(ref, abs, sizeof abs) != 0 || access(abs, R_OK) != 0) continue; + snprintf(idxabs, sizeof idxabs, "%s", abs); + size_t n4 = strlen(idxabs); + if (n4 > 4 && !strcmp(idxabs + n4 - 4, ".png")) { + idxabs[n4-4]='.'; idxabs[n4-3]='i'; idxabs[n4-2]='d'; idxabs[n4-1]='x'; idxabs[n4]=0; + } + snprintf(env_cls[c][0], sizeof env_cls[c][0], "%s=%s", cls_env_png[c], abs); + snprintf(env_cls[c][1], sizeof env_cls[c][1], "%s=%s", cls_env_idx[c], idxabs); + envp[ne++] = env_cls[c][0]; + envp[ne++] = env_cls[c][1]; + } + } + /* 全屏声明:app.json "fullscreen"(app 级)→ 注入 MEOW_FULLSCREEN=1,SDK 端 + * meow_ui_init 据此预先设全屏。基座只接 app 级;活动级 route..fullscreen + * (meow_route.fullscreen)在 M3 活动切换时由后续代理消费(此处暂不下发)。 + * 未声明则不注入(SDK 默认非全屏)。 */ + if (a->fullscreen) { + snprintf(env_fs, sizeof env_fs, "MEOW_FULLSCREEN=1"); + envp[ne++] = env_fs; + } + char pkg[64]; + if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) { + snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg); + snprintf(env_cache, sizeof env_cache, "MEOW_CACHE_DIR=/usr/data/mnt/sd_0/MeowData/%s/cache", pkg); + envp[ne++] = env_data; + envp[ne++] = env_cache; + } + envp[ne] = NULL; + execve(a->exec, argv, envp); + int e = errno; + (void)write(execpipe[1], &e, sizeof e); /* reached only on exec failure */ + _exit(127); + } + close(execpipe[1]); + int exe_err = 0; + ssize_t er = read(execpipe[0], &exe_err, sizeof exe_err); + close(execpipe[0]); + if (er > 0) { + logmsg("launch FAILED execve(%s): %s (errno %d)", a->exec, + strerror(exe_err), exe_err); + int st = 0; + waitpid(pid, &st, 0); /* reap the failed child */ + return -1; + } + + /* GESTURE_BACK §2.4:应用启动即写 immersive 状态,watchdog 依此隐藏/保留虚拟返回键 */ + meow_write_app_state(a->immersive == '1' ? 1 : 0); + + /* watchdog for virtual back key / gestures */ + char wd1[192], wd2[192], pidstr[16]; + pid_t wpid = 0; + snprintf(pidstr, sizeof pidstr, "%d", pid); + if (access(WATCHDOG1, X_OK) == 0) + snprintf(wd1, sizeof wd1, "%s", WATCHDOG1); + else if (access(WATCHDOG2, X_OK) == 0) + snprintf(wd1, sizeof wd1, "%s", WATCHDOG2); + else + wd1[0] = 0; + if (wd1[0]) { + wpid = fork(); + if (wpid == 0) { + /* pass the same boot log file + dir so all MeowAPPs components + * log to one place */ + char *argv[] = { wd1, pidstr, NULL }; + char *envp[] = { "PATH=/usr/bin:/bin", NULL, NULL, NULL }; + char file_env[320], dir_env[256]; + if (log_file[0]) { + snprintf(file_env, sizeof file_env, "MEOW_LOG_FILE=%s", log_file); + envp[1] = file_env; + } + if (log_dir[0]) { + snprintf(dir_env, sizeof dir_env, "MEOW_LOG_DIR=%s", log_dir); + envp[2] = dir_env; + } + execve(wd1, argv, envp); + _exit(0); + } + if (wpid > 0) logmsg("watchdog %s pid=%d", wd1, wpid); + } + + /* wait for app; in-app kill = VOL_DOWN x3 quick taps (the old + * PREVIOUS+VOL_UP hold combo is retired — the PREVIOUS key's GPIO + * never fires on this unit, §18.12). Volume is snapped on the first + * tap and restored when the sequence completes. */ + int status = 0; + int app_prev_vold = 0; + int app_seq_hits = 0; + unsigned long long app_seq_last = 0; + struct vol_snap app_vol0 = { -1, -1 }; + /* M2.11 D:应用二进制/资源若在卡上,卡被拔可能 SIGBUS 崩。启动前记录,运行期监测卡移除。 + * (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */ + int app_on_card = (strncmp(a->exec, MEOWAPPS, strlen(MEOWAPPS)) == 0); + if (app_on_card) logmsg("app on card (%s): monitoring card removal during run", a->exec); + struct keypoll kp_power, kp_keys; + kp_open(&kp_power, DEV_POWER); + kp_open(&kp_keys, DEV_KEYS); + /* M2.17 前台应用会话:系统仍统一控制息屏(应用不自管)。launcher 无法看到应用内触摸, + * 故以「可物理键」为 idle 信号;前台当前 activity(经 IPC MSG_ACT_FOCUS 上报)若 keep_awake + * 则不解屏(fg_keep_awake)。唤醒时应用随其输入重绘,launcher 仅 fb_pan_buf0_force 兜底 + * (M2.8 唤醒重绘路径的首步)。见 §18.46。 */ + unsigned long long app_last_input = now_ms(); + unsigned long save_pow[KEYBUF_WORDS], save_keys[KEYBUF_WORDS]; + memcpy(save_pow, kp_power.cur, sizeof save_pow); + memcpy(save_keys, kp_keys.cur, sizeof save_keys); + /* 「同一个状态栏」闭环:App 前台状态栏由 launcher 常驻叠加(顶条 28px、非模态、不 SIGSTOP App)。 + * 立即叠一次(App 首帧前顶条就有),其后每 ~1s 重画(时钟每秒变 + 数据新鲜)。app 级 + * fullscreen 声明(沉浸,整条隐藏)时不叠。agent-146:读 App 顶条模式(meowapp_sbar),App 运行时 + * 全屏/收合 → launcher 顶条切收合提示线(无时钟/图标/值),热区事件与渲染分离。 */ + unsigned long long last_sb_overlay_ms = 0; + g_sb_mode_last = -1; /* 强制首帧同步(进入新 App 会话清零,避免沿用上一 App 的缓存态) */ + unlink(MEOW_SBAR_MODE_FILE); /* 清可能残留的上一 App 顶条模式(崩溃/被杀未清),防新 App 误收合 */ + if (!a->fullscreen) app_overlay_statusbar(); + /* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。 */ + unlink(POWMENU_MARKER); + unlink(POWMENU_ACTIVE); + for (;;) { + int r = waitpid(pid, &status, WNOHANG); + if (r == pid) break; + if (r < 0 && errno != EINTR) break; + if (sigterm_flag) { logmsg("sigterm during app"); kill(pid, SIGTERM); break; } + /* M2.11 D:卡被拔(设备节点消失)且前台应用在卡上 → 主动 SIGTERM 干净退出, + * 避免应用等 SIGBUS 崩。回菜单走正常应用退出路径。 */ + if (app_on_card && !sd_device_present()) { + logmsg("hotplug: card removed during app run -> SIGTERM app "); + kill(pid, SIGTERM); + usleep(300000); + if (waitpid(pid, &status, WNOHANG) != pid) { kill(pid, SIGKILL); waitpid(pid, &status, 0); } + break; + } + /* in-app kill: VOL_DOWN x3 quick taps */ + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + int vold = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; + } + /* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏; + * 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。 */ + { + int key_edge = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) + if (kp_power.cur[w] != save_pow[w] || kp_keys.cur[w] != save_keys[w]) { key_edge = 1; break; } + if (key_edge) { + memcpy(save_pow, kp_power.cur, sizeof save_pow); + memcpy(save_keys, kp_keys.cur, sizeof save_keys); + app_last_input = now_ms(); + if (meow_auto_slept) { + logmsg("auto screen-off (app): key wake"); + meow_panel_on(); + meow_auto_slept = 0; + fb_pan_buf0_force(); + } + } + if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) { + if (now_ms() - app_last_input >= (unsigned long long)screen_off_timeout * 1000) { + meow_panel_off(); + meow_auto_slept = 1; + logmsg("auto screen-off (app %s) after %ds idle (mode=%s keep_awake=%d)", + a->name, screen_off_timeout, screen_off_mode ? "black" : "panel", + fg_keep_awake()); + } + } + } + if (vold && !app_prev_vold) { + unsigned long long nw = now_ms(); + if (app_seq_hits == 0) app_vol0 = vol_snapshot(); + else if (nw - app_seq_last > (unsigned)seq_gap_ms) { + app_seq_hits = 0; + app_vol0 = vol_snapshot(); + } + app_seq_hits++; + app_seq_last = nw; + if (app_seq_hits >= 3) { + logmsg("kill seq: VOL_DOWN x3 -> SIGTERM app"); + vol_restore(app_vol0); + kill(pid, SIGTERM); + usleep(500000); + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + break; + } + } + app_prev_vold = vold; + if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2) + app_seq_hits = 0; + /* M2.24 全面按键接管:watchdog 检测到电源长按≥3s 写 /tmp/meow_power_menu → 此处消费: + * SIGSTOP 应用冻结 → 写 powmenu_active 让 watchdog 让位 → 统一电源菜单(launcher 一处绘制, + * 复用 show_power_menu,含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)→ 菜单关 → + * 清 active → SIGCONT 应用续跑。 */ + if (access(POWMENU_MARKER, F_OK) == 0) { + unlink(POWMENU_MARKER); + logmsg("watchdog: power menu request (app %s) -> SIGSTOP + menu", a->name); + kill(pid, SIGSTOP); + int mfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (mfd >= 0) { write(mfd, "1", 1); close(mfd); } + int pm_ret = show_power_menu(); + unlink(POWMENU_ACTIVE); + kill(pid, SIGCONT); + logmsg("watchdog: power menu closed (ret=%d) -> SIGCONT app", pm_ret); + /* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge + * (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */ + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + memcpy(save_pow, kp_power.cur, sizeof save_pow); + memcpy(save_keys, kp_keys.cur, sizeof save_keys); + app_last_input = now_ms(); /* M2.17:菜单交互即输入,重置应用会话自动息屏计时 */ + if (pm_ret != 0) break; /* 关机/重启/回HibyOS:系统通常已 reboot,防御性退出 */ + } + /* 「同一个状态栏」闭环:常驻顶条叠加(agent-146 全屏收合接入)。 + * - 展开态每 ~1s 慢刷(时钟/数据新鲜;仅标脏顶条上屏,不碰 App 内容); + * - App 顶条模式新变(进入/退出全屏收合)→ 立即叠一次(即时切换,消除「半秒闪」); + * - 收合/恢复过渡期(meow_ui_statusbar_animating)每 ~30ms 快刷驱动平滑上移出屏。 */ + if (!a->fullscreen) { + unsigned long long now2 = now_ms(); + int m = app_sbar_mode_read(); + if (m != g_sb_mode_last) { + g_sb_mode_last = m; + last_sb_overlay_ms = now2; + app_overlay_statusbar(); + } else if (meow_ui_statusbar_animating()) { + if (now2 - last_sb_overlay_ms >= 30) { + last_sb_overlay_ms = now2; + app_overlay_statusbar(); + } + } else if (now2 - last_sb_overlay_ms >= 1000) { + last_sb_overlay_ms = now2; + app_overlay_statusbar(); + } + } + /* 顶条收合/恢复过渡期把循环睡得短些(~20ms),驱动更平滑的上移出屏;稳定后回 100ms。 */ + usleep(meow_ui_statusbar_animating() ? 20000 : 100000); + } + if (wpid > 0) { + /* reap the watchdog: SIGTERM then wait up to 2s (it exits when the + * app dies; bounded so we never hang if it is stuck) */ + kill(wpid, SIGTERM); + unsigned long long wstart = now_ms(); + while (now_ms() - wstart < 2000) { + if (waitpid(wpid, NULL, WNOHANG) == wpid) break; + usleep(50000); + } + } + meow_write_app_state(-1); /* GESTURE_BACK §2.4:应用退出,清 immersive 状态 */ + /* agent-146:App 退出 → 清顶条模式文件 + 复位 launcher SDK 收合/全屏态(菜单恢复整条 28px)。 + * 模式文件由 App 进程 SDK 写;此处 launcher 在退出时清理,防下一 App 读到残留 '1'(收合) 误收合。 */ + unlink(MEOW_SBAR_MODE_FILE); + g_sb_mode_last = -1; + meow_ui_statusbar_set_collapsed(0); + meow_ui_statusbar_animate(0); /* 回菜单:顶条恢复整条,瞬切(不做上移动画) */ + meow_ui_set_fullscreen(0); + meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 菜单状态栏需重画整条(恢复展开) */ + /* M2.24:应用运行期音量由 watchdog 写(settings.conf 同源)。返回菜单前 re-load, + * 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */ + settings_load(); + logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status)); + return 0; +} + diff --git a/patch_v2/meow_launcherd/audio.c b/patch_v2/meow_launcherd/audio.c new file mode 100644 index 0000000..1eff816 --- /dev/null +++ b/patch_v2/meow_launcherd/audio.c @@ -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 语义:单击( %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"); + } +} + diff --git a/patch_v2/meow_launcherd/build_launcher.sh b/patch_v2/meow_launcherd/build_launcher.sh index a419c71..0fa3405 100755 --- a/patch_v2/meow_launcherd/build_launcher.sh +++ b/patch_v2/meow_launcherd/build_launcher.sh @@ -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 /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 /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 diff --git a/patch_v2/meow_launcherd/fb_compositor.c b/patch_v2/meow_launcherd/fb_compositor.c new file mode 100644 index 0000000..593cc61 --- /dev/null +++ b/patch_v2/meow_launcherd/fb_compositor.c @@ -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)"); +} + diff --git a/patch_v2/meow_launcherd/font_theme.c b/patch_v2/meow_launcherd/font_theme.c new file mode 100644 index 0000000..23ff6ac --- /dev/null +++ b/patch_v2/meow_launcherd/font_theme.c @@ -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///(三件套)。 + * 每个 preset = 一套 /(如 misans/regular)。值 = 卡根相对路径 + * MeowOS/fonts///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///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///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/// 里找 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///,每套含三件套: */ +/* 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// 套件是否合法:三件套齐全 → 解析 label+档位+字重。 + * 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 + * 输出:label=显示名 ` (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); + /* 显示名 = ` (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* 扫描结果落盘为 /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); +} + + +/* 扫描 /MeowOS/fonts/ 下两级 //,重建字体 preset 列表。 + * 每个 (family,weight) 组合一套(如 misans/regular、misans/bold);family 级 'default' 由 + * 索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX + * (1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。 + * + * M2.58 释放语义(规格 §5.7 需求 4「扫→写 index→释放(或按需读 index)」结论): + * - 持久来源 = /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); /* 一级:/ */ + 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:扫描结果落盘为 /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(由调用方决定回退策略)。 + * - startend:跨午夜,如 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; +} + diff --git a/patch_v2/meow_launcherd/keys.c b/patch_v2/meow_launcherd/keys.c new file mode 100644 index 0000000..3c22771 --- /dev/null +++ b/patch_v2/meow_launcherd/keys.c @@ -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); +} + diff --git a/patch_v2/meow_launcherd/main_menu.c b/patch_v2/meow_launcherd/main_menu.c new file mode 100644 index 0000000..58e8e42 --- /dev/null +++ b/patch_v2/meow_launcherd/main_menu.c @@ -0,0 +1,1480 @@ +/* main_menu.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ +/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ +#include "meow_launcher_internal.h" + + +/* set by run_menu: the player's panel was blanked (screen timeout) when the + * menu opened. fb_open() unblanks it so the menu is visible; on session end + * restore_backlight() re-blanks (brightness 0 + FBIOBLANK(4)) to put the + * physical panel back into the state the player's display state machine + * assumes — otherwise the player resumes believing the panel is blanked + * while the panel is actually lit (frozen frame). Blanking is idempotent + * and matches what the player itself did, so it cannot wedge the player. */ +int menu_opened_blank = 0; + + +/* ---- config (MeowAPPs/meow.conf): pure black background by default ---- */ +uint32_t menu_bg = 0x000000; + + +static int touch_fd = -1; + +static int t_last_x = -1, t_last_y = -1; + +static int t_down = 0; + +static int t_down_x = 0, t_down_y = 0; + +/* M2.23 手势原点锁定:BTN_TOUCH=1 只标「已按下」,坐标(goodix)不保证先于按下边沿, + * 故 t_down_x/y 延迟到「本次按下之后」的首个有效 ABS 才锁定,避免把上一手势残留 + * (-1/旧坐标) 当本次原点。t_down_valid=0 表示本次尚未锁到有效坐标。 */ +static int t_down_valid = 0; + +/* ⑨ 顶部热区「跟手下拉」进入通知中心的 per-gesture latch:进入后置 1,本次手势不再二次进入; + * 由下次 touch-down 复位。防止「拖动中切入 run_notif_center 后返回」把同次手势再判成打开/滑动。 */ +static int menu_live_open_pending = 0; + +/* Bug 2/4 (2026-08-28): after an app exits we return to the menu, but touch + * events that were generated DURING the app run (the goodix ghost touch the + * v6.4 watchdog guards against, plus the back-tap/press that actually exited + * the app) are still queued on the launcher's touch fd and get replayed as + * fresh menu taps. A replayed tap on an app row relaunches the app (symptom: + * BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO + * PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen + * flashes back to the player — Bug 4). We drain the queue immediately after + * the app exits and ignore touches for a short cooldown that covers the + * measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */ +static unsigned long long menu_touch_cooldown_until = 0; + +static void menu_drain_touch(void) +{ + if (touch_fd >= 0) { + unsigned char ev[16]; + while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } + } +} + +static void menu_arm_touch(long ms) +{ + menu_drain_touch(); + menu_touch_cooldown_until = now_ms() + ms; +} + + +/* Drain the touch queue + arm the post-app cooldown (call right after the + * app exits, before the menu resumes polling touch). The drain always runs so + * touches queued by the launch-attempt tail are cleared; the cooldown only + * arms when an app actually ran (app_ran) so a failed launch doesn't lock the + * menu out for 2.5s. */ +static void menu_after_app_exit(int app_ran) +{ + menu_drain_touch(); + /* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。 + * 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短, + * 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */ + if (app_ran) menu_touch_cooldown_until = now_ms() + 1200; +} + + +/* Header block (status bar + MEOWAPPS title + thin divider) covering the + * region y=0..58, kept separate from the app-list body so the ~1s clock tick + * (draw_clock) never redraws or tears the list. draw_menu fills the whole + * background then calls draw_header_content. The menu is pre-drawn before + * fb_norm_buf0 so the initial pan shows a complete frame. */ +static void draw_header_content(void) +{ + draw_statusbar(); + /* M2.6 标题真字体(28px;回退点阵3x) */ + if (mfont_ready) { + meow_font_drawtext((uint16_t *)fb, 24, 48, "MeowOS 应用", rgb565(0xFFE066), 28); + fill_rect(0, 48 + meow_font_line_height(28) + 2, fb_xres, 2, 0x3A3A5C); + } else { + draw_text(24, 28, "MEOWAPPS", 3, 0xFFE066); + fill_rect(0, 56, fb_xres, 2, 0x3A3A5C); + } +} + + +/* 每秒一次刷新状态栏时钟(顶部 HH:MM:SS,唯一每秒都变的值)。Stage-One 状态栏收敛后, + * launcher 不再自绘状态栏——draw_clock 只经 paint_sb_clock() → sb_sdk_refresh() 把权威 + * 状态喂进 SDK 全局状态栏 + 标脏顶条,由 SDK present() 顶条重绘(时钟/电量/音量等一并刷新)。 + * 绝不动应用列表(仅顶条 28px 脏),故列表不重绘/不撕裂;无 amixer/sysfs 每帧开销。 */ +void draw_clock(void) +{ + paint_sb_clock(); /* → sb_sdk_refresh():喂 SDK + 标脏顶条,present() 时 SDK 重绘状态栏 */ +} + + +/* M2.36 右向小箭头(进入/下拉提示)。点阵后备字库(5x7)缺 '>' 字形(glyph_index 落到空 + * 格),故用像素画一个 4x7 的 '>';顶点在 (x,y) 指向右。设置页选择器/设置项行复用。 */ +void s_draw_chevron(int x, int y, uint32_t color) +{ + for (int i = 0; i < 4; i++) { + put_px(x - i, y - i, color); + put_px(x - i, y + i, color); + } +} + + +/* M2.39 左向小箭头(返回提示)。点阵后备字库同样缺 '<' 字形('‹' 会渲染成方框), + * 故用像素画一个指向左的 4x7 '<';顶点在 (x,y)。设置页底部「返回」/任何左向提示复用。 */ +void s_draw_chevron_left(int x, int y, uint32_t color) +{ + for (int i = 0; i < 4; i++) { + put_px(x + i, y - i, color); + put_px(x + i, y + i, color); + } +} + + +static void draw_menu(struct app apps[MAX_APPS], int napps, int state, + int cursor, int toast_ms, const char *toast) +{ + fill_rect(0, 0, fb_xres, fb_yres, menu_bg); + draw_header_content(); + + char line[64]; + int y = ROW_TOP; + if (state == 1) { + if (mfont_ready) { ui_draw(30, 200, "没有存储卡", 0xFFFFFF, 20); ui_draw(30, 234, "请插入TF卡后重开菜单", 0x9FB8D9, 20); } + else { draw_text(30, 200, "NO STORAGE CARD", 2, 0xFFFFFF); draw_text(30, 240, "INSERT TF CARD", 2, 0x9FB8D9); draw_text(30, 272, "THEN RE-OPEN MENU", 2, 0x9FB8D9); } + } else if (state == 2 || napps == 0) { + if (mfont_ready) { ui_draw(30, 200, "没有找到应用", 0xFFFFFF, 20); ui_draw(30, 234, "请放入应用到", 0x9FB8D9, 20); ui_draw(30, 268, "MeowAPPs//app.json", 0x9FB8D9, 20); } + else { draw_text(30, 200, "NO APPS FOUND", 2, 0xFFFFFF); draw_text(30, 240, "PUT APP IN", 2, 0x9FB8D9); draw_text(30, 272, "MeowAPPs//app.json", 2, 0x9FB8D9); } + } else { + for (int i = 0; i < napps; i++) { + int sel = (i == cursor); + if (sel) { + fill_rect(8, y - 4, fb_xres - 16, ROW_H - 8, 0x2A3A6A); + snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name); + ui_draw(24, y, line, 0xFFFFFF, 20); + if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0xFF6B6B, 20); + } else { + snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name); + ui_draw(24, y, line, 0xC8D0E8, 20); + if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0x8A4A4A, 20); + } + y += ROW_H; + } + } + + /* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前; + * 不依赖 .mpk(图标/文案就地绘制)。音量导航可把光标移到此处(cursor==napps), + * 触摸点击该行进入设置页(run_menu 分发到 run_settings_page)。 */ + { + int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */ + int selset = (cursor == napps); /* 光标已移到设置行 */ + fill_rect(8, sy - 4, fb_xres - 16, ROW_H - 8, selset ? 0x2A3A6A : 0x12142A); + if (mfont_ready) ui_draw(24, sy, "系统设置", selset ? 0xFFFFFF : 0xC8D0E8, 20); + else draw_text(24, sy, "SETTINGS", 2, selset ? 0xFFFFFF : 0xC8D0E8); + s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, 0x9FB8D9); /* 右向进入提示 */ + } + + /* exit row */ + fill_rect(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 0x1A1A3A); + /* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */ + if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", 0x8FBFD9, 20); + else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, 0x8FBFD9); + + /* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。屏幕高 fb_yres=720,字号 20 真实行高 + * meow_font_line_height(20)=27(ascent 21 + descent 6);原 y=700 使字底 = 700+27 = 727 + * > 720,溢出 7px ≈ 1/5 字符高被裁。改为 y = fb_yres - 行高 - 底部留白。非 mfont 回退 + * 分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */ + #define FOOTY_PAD 8 + /* dev version, bottom-right: single source MEOWOS_VERSION. */ + { + char ver[32]; + snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); + int foot_y = fb_yres - meow_font_line_height(20) - FOOTY_PAD; + ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, 0x666688, 20); + } + + if (toast_ms > 0 && toast && toast[0]) { + fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, 0x2A0A0A); + ui_draw(30, TOAST_Y, toast, 0xFF8080, 20); + } +} + + +/* ================================================================== */ +/* M2.3A 电源菜单(Android 11 样式):长按电源 3s 弹出;8s 强制关机 */ +/* 半透明深色遮罩 + 居中 5 个大按钮(2 列×3 行,ASCII 点阵,不耦合字体) */ +/* 与「单击=息屏」「电源+音量减=切MeowOS」按电源按住时长区分。 */ +/* ================================================================== */ +/* M2.3A sim 钩子(电源菜单验证,物理留用户): + * --sim-power-hold N —— 主循环 N ms 后强制 pwr=1(模拟按住电源),驱动 + * 3s 弹电源菜单 / 8s 强制关机两条路径。 + * --sim-power-pick N —— 电源菜单打开 2s 后自动选中按钮 N(0..4;M2.29 起 5 钮)。 + * --sim-no-exec —— 免执行:电源动作(关机/重启/回HibyOS)/8s强关仅打 log, + * 不真正 poweroff/reboot(安全 sim 用)。 */ +static long sim_power_hold_ms = 0; + +int sim_power_pick = -1; + +int sim_no_exec = 0; + + +void fill_circle(int cx, int cy, int r, uint32_t color) +{ + for (int dy = -r; dy <= r; dy++) + for (int dx = -r; dx <= r; dx++) + if (dx * dx + dy * dy <= r * r) + put_px(cx + dx, cy + dy, color); +} + + +/* 圆角矩形 */ +void fill_round_rect(int x0, int y0, int w, int h, int r, uint32_t color) +{ + fill_rect(x0 + r, y0, w - 2 * r, h, color); + fill_rect(x0, y0 + r, w, h - 2 * r, color); + fill_circle(x0 + r, y0 + r, r, color); + fill_circle(x0 + w - r, y0 + r, r, color); + fill_circle(x0 + r, y0 + h - r, r, color); + fill_circle(x0 + w - r, y0 + h - r, r, color); +} + + + +/* returns: 0 = stay in menu, 1 = exit menu (BACK TO PLAYER / sim-exit), + * 2 = launch request */ + +/* M2.8 Bug 3-residual「统一重绘」:把当前 MeowOS 界面内容整体重绘到 buffer0 并强制 pan 到 + * buffer0。息屏唤醒(电源单击亮屏)时调用 —— 唤醒不是 draw 事件:FBIOBLANK(4) 下电 -> + * FBIOBLANK(0) 上电后,LCDC 指向的内容可能已失效/为空,必须把当前真实界面(应用列表主界面, + * 含「没有存储卡/没有找到应用」态)重新画一遍再强制 re-pan,而不是仅翻页到陈旧/空缓冲。 + * signature 传入当前界面状态(由 run_menu 持有),不把应用列表局部状态全局化。 */ +static void meow_repaint_surface(struct app apps[MAX_APPS], int napps, int state, int cursor) +{ + /* M2.11 E:记录唤醒上下文(时刻/亮度/当前 screen_on/待 yoffset),便于定位偶发「唤醒后黑屏 + * 又自愈」的竞态。 */ + int pre_bri = read_brightness(); + int pre_screen = meow_screen_on; + logmsg("wake repaint: pre bri=%d screen_on=%d (state=%d napps=%d cursor=%d)", + pre_bri, pre_screen, state, napps, cursor); + draw_menu(apps, napps, state, cursor, 0, ""); /* 完整重绘当前视图到 back_buf */ + fb_pan_buf0_force(); /* 确保落在被显示的缓冲(buffer0) */ + meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit) */ + int post_bri = read_brightness(); + logmsg("wake repaint done: post bri=%d (after force-pan buffer0)", post_bri); +} + + +/* set by on_sigterm; run_menu breaks on it so the menu closes cleanly and + * the main loop restores the player (must be declared before run_menu) */ +volatile sig_atomic_t sigterm_flag = 0; + + +/* M2.2 sim 钩子:--sim-exit-tap N —— 菜单打开 N ms 后模拟「点 BACK TO PLAYER 行」, + * 用于验证 MeowOS 模式下 BACK TO PLAYER 显示需重启提示(而非恢复死帧)。 */ +static long sim_exit_tap_ms = 0; + +static int sim_exit_tap_done = 0; + + +/* real menu loop */ +static void run_menu(struct app apps[MAX_APPS], int napps, int state, + long sim_tap_ms, long sim_exit_ms) +{ + int cursor = 0; + unsigned long long t0 = now_ms(); + int toast_ms = 0; + char toast[64] = ""; + int want_exit = 0; + /* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。 + * meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */ + unsigned long long last_input_ms = now_ms(); + int any_input = 0; + int fbfd = -1; + int had_player = 0; /* M2.2:菜单打开时是否存在存活播放器 */ + long sim_tap_done = 0, sim_exit_done = 0; + long meow_last_poll = 0; /* M2.2:MeowOS 后台拔营(卡/网)轮询时间戳 */ + + load_config(); /* meow.conf on the card: bg_color etc. */ + + /* A-1/A-2:launcher 用 SDK 渲染栈 —— 初始化 SDK(自建 back_buf/读几何/载字库), + * 并把刷新率档位/恒满速开关交给 SDK present 帧率门控(去中心化:launcher 不再自带一套)。 + * set_statusbar(1)(Stage-One 状态栏收敛):launcher 系统界面(主界面/设置页/电源菜单/ + * 通知中心)顶部状态栏统一由 SDK 全局状态栏绘制(MUI_SBAR_H=28),launcher 不再自绘 + * (draw_statusbar/sb_redraw 退役,改由 sb_sdk_refresh() 喂权威状态 + 标脏顶条驱动)。 + * 与 App 同一实现/同一主题/同日/夜。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ + meow_ui_init(NULL); + meow_ui_set_statusbar(1); + /* 「同一个状态栏」闭环(FRAMEWORK_DESIGN §10.2 / MEOW_APP_SDK §3.1):SDK 全局状态栏 + * 仅系统进程(launcher)渲染。launcher 是系统 UI 渲染宿主 → 置 system_mode=1 让 SDK 渲染顶条 + * /提示线;App 进程默认 0(不渲染、只 inset,由 launcher 叠加)。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ + meow_ui_sys_set_system_mode(1); + meow_ui_set_refresh_rate(refresh_rate_cfg); + meow_ui_set_refresh_always_max(refresh_always_max); + + /* record the brightness BEFORE we touch it, so any menu exit can + * restore the player's pre-menu value (0 = player standby -> default 60) */ + int raw_brightness = read_brightness(); + saved_brightness = (raw_brightness > 0) ? raw_brightness : 60; + logmsg("menu saved brightness = %d", saved_brightness); + /* remember whether the player had blanked the panel: fb_open() below + * unblanks so the menu is visible, and restore_backlight() re-blanks + * on exit to keep the player's display state machine consistent */ + menu_opened_blank = (raw_brightness <= 0); + + fbfd = fb_open(); + meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */ + /* M2.6 加载字库(卡上 MeowOS/fonts/default/,卡片已挂载则该步成功;未挂载由下方轮询补载) */ + meow_font_ensure(); + meow_font_reload_multifont(); /* M2.5x 多字体(读 settings display.multi_font + 6 类;关=单字体) */ + /* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份 + * 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才 + * 冻结 + 备份,退出时恢复其帧。 */ + had_player = (find_hiby_pid() > 0); + meow_mode = had_player ? 0 : 1; /* 权威判定:每次按播放器是否存活重置 */ + /* M2.11 A:每次进 MeowOS(热键切换/开机选 MeowOS/覆盖菜单)都全量重抓系统状态快照, + * HibyOS 里改过(蓝牙/WiFi/音量/亮度等)也同步。主循环 10ms 绝不读系统态。 */ + meow_sys_refresh(); + g_sys.mode = meow_mode; + g_sys.napps = napps; + if (had_player) { + stop_player(); /* freeze the player so the menu owns fb0 */ + } else { + logmsg("meow mode: hiby_player dead, self-drawn home (no stop/backup)"); + } + ensure_backlight(); /* only if the panel is dark, never yank brightness */ + unblank_panel(); /* player frozen: it can no longer re-blank */ + /* M2.53:MeowOS 主屏接管(播放器已死,had_player==0)时强制把亮度压回用户 + * display.brightness 设定值。HibyOS 播放器运行/恢复画面会把它自己的背光下限 + * (16/25)写进 sysfs;若以它为「当前亮度」,用户设的低亮度(<25)会被覆盖。 + * 覆盖菜单(had_player==1,播放器会话中)不压回——播电影时亮度归播放器管。 */ + if (!had_player) apply_user_brightness(); + if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */ + draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */ + fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */ + meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit 到 fb0) */ + + /* touch: event reads (event1 is not grabbed) */ + touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno)); + /* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */ + menu_arm_touch(TAP_ARM_DELAY_MS); + + /* key polling */ + struct keypoll kp_power, kp_keys; + kp_open(&kp_power, DEV_POWER); + kp_open(&kp_keys, DEV_KEYS); + /* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */ + struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; + memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); + + logmsg("menu open: napps=%d state=%d cursor=%d", + napps, state, cursor); + + /* key edge baselines: sample the initial state so keys already held + * when the menu opens (e.g. the hotkey release tail) do not fire + * edges; kp.prev cannot be used for this because kp_poll() syncs it + * to cur before we compare */ + int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; + } + /* M2.3A 电源按住计时:≥8s 强制关机;≥3s 弹电源菜单;与单击息屏/热键按时长区分 */ + unsigned long long pwr_hold_at = 0; + int pwr_menu_state = 0; + /* M2.18 Bug 2: 一次性标志——本次电源按下已触发「自动息屏唤醒」时置位,其释放 + * 只唤醒不 toggle(防同一次按键被唤醒路径与单单击 toggle 各消费一次 → 闪黑)。 */ + int pwr_woke = 0; + /* M2.34: 一次性标志——电源菜单刚关闭(无论确认/取消/超时)。返回主循环后,本次 + * 电源按下-松开不得被 3130「松开→单击」路径误判为「电源单击」→ toggle 息屏,否则: + * ①菜单点关机/重启:关机/重启画面被息屏覆盖;②焦点在取消键单击电源:取消同时熄屏。 + * 清除时机:电源松开(3130 松开分支末尾)或下一次电源按下边沿。 */ + int pwr_suppress_click = 0; + + /* The menu was already drawn into buffer0 above (pre-drawn before the pan + * to buffer0), so a redraw is only needed when something actually changes: + * cursor movement (input), app exit/enter, toast start/end, or a future + * notification-badge change. NEVER on a periodic timer — see the big + * comment on the redraw block below. */ + int dirty = 0; /* 结构变化(app 进出/state/唤醒/热键…)→ 全帧重画 */ + int need_menu_paint = 0; /* M2.16:需 draw_menu 全画 back_buf(光标/toast/test 变化) */ + int last_cursor = 0; /* pre-drawn frame showed cursor 0 */ + int toast_active = 0; /* tracks toast_ms>0 for start/end redraw edges */ + long last_status_tick = 0; /* ~1s status-bar-only refresh (clock) */ + + for (;;) { + unsigned long long t = now_ms(); + long el = (long)(t - t0); + any_input = 0; /* M2.17:本圈是否有输入(重置 auto screen-off 计时) */ + + /* SIGTERM during the menu: close it cleanly (restore + resume happen + * in the hotkey block / main loop after we return) */ + if (sigterm_flag) { logmsg("menu: sigterm, closing"); want_exit = 1; break; } + + /* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */ + meow_ipc_process(); + meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ + + /* sim hooks */ + if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) { + logmsg("SIM TAP row 0"); + sim_tap_done = 1; + if (napps > 0 && state == 0) { + /* fall through: select row 0 */ + int ret = launch_app(&apps[0]); + /* app exited: RETURN TO THE MENU (v6.3) — closing the menu + * here would leave the app's last frame (e.g. BYE) on fb0, + * and the player only redraws on input events (none arrive, + * injection is not delivered), so the screen would stay + * dark until a key press. Staying in the menu keeps the + * menu visible; BACK TO PLAYER then resumes the player. + * Bug 2/3/4: a queued touch from the app run must NOT be + * replayed as a fresh menu tap (relaunch / flash-to-player), + * and the "CANNOT START" toast only fires on a real launch + * failure (ret != 0), never after a clean app exit. */ + menu_after_app_exit(ret == 0); + last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ + dirty = 1; /* app wrote buffer0 during its run -> redraw menu */ + if (ret != 0) { + snprintf(toast, sizeof toast, "CANNOT START: %s", apps[0].name); + toast_ms = TOAST_MS; + } + } else { + snprintf(toast, sizeof toast, "NO APPS"); + toast_ms = TOAST_MS; + } + } + if (sim_exit_ms > 0 && !sim_exit_done && el >= sim_exit_ms) { + logmsg("SIM EXIT"); + sim_exit_done = 1; + want_exit = 1; + break; + } + /* M2.6 sim:点 BACK TO PLAYER 行 → MeowOS 模式弹「需重启」确认框并留下; + * 会话覆盖模式则正常退出。 */ + if (sim_exit_tap_ms > 0 && !sim_exit_tap_done && el >= sim_exit_tap_ms) { + logmsg("SIM TAP exit row"); + sim_exit_tap_done = 1; + if (meow_mode) { + int rb = confirm_reboot_player(); + menu_arm_touch(TAP_ARM_DELAY_MS); + dirty = 1; + logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb); + } else { + want_exit = 1; + break; + } + } + + /* M2.2 MeowOS 系统拔营:无播放器时由 launcher 接管官方交给 + * hiby_player/sys_server 的两件事 —— TF 卡挂载 + WiFi 联网。 + * 后台每 2s 重试,不阻塞主界面;卡片就绪后重扫应用列表并就地刷新。 + * 仅 meow 常驻模式需要(HibyOS 覆盖模式播放器仍在,/或其管理)。 + * M2.11 C:TF 卡热插拔最佳努力 —— 拔出检测(节点消失→无卡)、 + * 插入检测(节点出现→自动挂载+重扫+重载字库)。不阻塞、不崩。 */ + if (meow_mode && (long)(t - t0) - meow_last_poll >= 2000) { + meow_last_poll = (long)(t - t0); + ensure_wifi(); + if (!sd_device_present()) { + /* 卡被拔出:设备节点消失 → 无卡态。停止扫描;留 state=1(NO STORAGE CARD)。 + * 若此前是正常态,切到无卡并重绘。不杀正在跑的应用(其由 D 的监测处理)。 */ + if (state == 0 || state == 2) { state = 1; dirty = 1; logmsg("hotplug: card removed -> state=1"); } + g_sys.sd_mounted = 0; + log_poll_card(); /* 卡日志不可用时回退 /tmp 缓冲 */ + } else if (ensure_card_mounted()) { + /* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */ + int was_nocard = (state == 1); + if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); } + if (mfont_ready) meow_font_reload_multifont(); /* M2.5x:卡挂载后重配多字体(此前无全局可配) */ + if (was_nocard || state != 0 || napps == 0) { + struct app fresh[MAX_APPS]; + int fn = scan_apps(fresh); + int fstate = (fn < 0) ? (fn == -1 ? 1 : 2) : (fn == 0 ? 2 : 0); + if (fstate == 0 && fn > 0) { + for (int i = 0; i < fn; i++) apps[i] = fresh[i]; + napps = fn; state = 0; + logmsg("hotplug: card ready -> %d apps loaded", napps); + dirty = 1; + } else if (fstate != state) { + state = fstate; + logmsg("hotplug: state -> %d", state); + dirty = 1; + } + } + g_sys.sd_mounted = 1; + log_poll_card(); + } else if (!sd_mounted()) { + /* 卡在但未挂载(正挂载/失败):保持当前显示不误判;不崩。 */ + g_sys.sd_mounted = 0; + } + } + + /* keys: poll and handle edges. Power is NOT handled here anymore + * (v6.5): inside the menu the power key is a pure screen-off/on + * toggle handled by sys_server/kernel (the player being SIGSTOPped + * does not affect it) — the launcher must NOT touch brightness/panel + * or exit the menu on it. Exit is only via the BACK TO PLAYER touch + * row (below) or sim-exit. */ + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + int vold = 0, volu = 0, play = 0, pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; + } + if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */ + /* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */ + int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */ + (void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */ + /* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。 + * 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */ + (void)kd_tick(&kd_vold, vold); + (void)kd_tick(&kd_volu, volu); + (void)kd_tick(&kd_play, play); + (void)kd_tick(&kd_pwr, pwr); + vold = kd_vold.stable; volu = kd_volu.stable; + play = kd_play.stable; pwr = kd_pwr.stable; + /* M2.6 电源语义(MeowOS 自持面板,无播放器拔河): + * 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */ + if (meow_mode) { + if (pwr && !m_prev_pwr) { + pwr_hold_at = t; pwr_menu_state = 0; any_input = 1; + pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */ + /* M2.18 Bug 2: 若这次按下正好落在「自动息屏」态(auto_slept 且屏已关), + * 本次电源键只做唤醒(下方通用醒屏分支 meow_panel_on()),其释放不再走 + * 单单击 toggle —— 否则同一次按下先被唤醒点亮、又在释放时被 toggle 关掉 + * (症状:自动息屏后点电源「闪一下黑、再点才亮」)。 */ + if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; + } + if (pwr && pwr_hold_at) { + /* M2.10/M2.11 B:8s 强关只在该「按住」从未打开过电源菜单时才触发 + * (!pwr_menu_state)。一旦打开过菜单(用户已互动/取消),run_menu 的 + * 8s 强关永远不在此 hold 上再触发 —— 由菜单自身的「按当前项确认」/取消 + * 决定动作。避免「按住电源开菜单->确认取消->仍按住->run_menu 8s 强关」残留。 + * M2.37:阈值接 power.forceoff_ms(默认 8000),强关保持 > 弹菜单上限。 */ + if (t - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { + if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } + else { logmsg("pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } + m_prev_pwr = pwr; continue; + } else if (t - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { + pwr_menu_state = 1; + logmsg("pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); + show_power_menu(); /* 动作在函数内 system(); 取消则续循环 */ + /* M2.31:菜单打开期间主循环阻塞,last_input_ms 不会刷新;若停留超过自动息屏 + * 超时,取消返回后会被误判「空闲」而立即熄屏(用户点取消瞬间黑屏)。 + * 菜单返回即视为「刚有输入」,重置自动息屏计时。 */ + last_input_ms = now_ms(); + /* M2.11 B:菜单已关闭(取消)。清空按住计时与菜单态:仍按住/随后松开的电源 + * 按「新按下」处理,绝不把已互动的 hold 再累计到 8s 强关。 */ + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; + pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */ + dirty = 1; /* 取消后重绘应用列表 */ + menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */ + /* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。 + * 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」, + * 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at, + * 随后稳定值转 0 时又被误判「单击」→ meow_toggle_panel 把面板关掉(黑屏且 + * 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw + * 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw + * 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */ + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + continue; + } + } + if (!pwr && m_prev_pwr) { + /* 松开:短按(<3s 且菜单未开)→ 单击 = 息屏/亮屏切换 */ + if (pwr_suppress_click) { + /* M2.34:本次电源按下来自「电源菜单内确认/取消」,其松开不得 toggle 息屏 + * (否则关机/重启画面被息屏覆盖,或焦点在取消键单击电源导致熄屏)。 */ + logmsg("pm: power release after power-menu -> no toggle (suppress)"); + } else if (pwr_hold_at && (t - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { + logmsg("pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); + meow_toggle_panel(); + if (meow_screen_on) { + /* M2.8 Bug 3-residual:唤醒不是 draw 事件,LCDC 在 FBIOBLANK(4)->(0) + * 后指向的内容可能已失效/为空;必须把当前界面(应用列表)整体重绘到 + * buffer0 并强制 re-pan,而非仅翻页/仅 dirty 重绘(M2.7 仍黑屏根因)。 */ + meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ + last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟(防 t 过期回绕) */ + meow_repaint_surface(apps, napps, state, cursor); + } else { + dirty = 1; /* 息屏:面板已黑,留 dirty 供下次亮屏重绘 */ + last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟 */ + } + } else if (pwr_woke) { + /* M2.18 Bug 2:本次电源按下已由自动息屏唤醒分支消费,释放只记录、不 toggle。 */ + logmsg("pm: power release after auto screen-off wake -> no toggle (wake only)"); + } + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; + pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */ + } + m_prev_pwr = pwr; + } + /* volume/play edges against the launcher-maintained baselines + * (previously dead code: prev was re-seeded from cur each pass, so + * no edge could ever fire and the menu keys could not navigate or + * launch). + * M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp), + * 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */ + if (meow_mode) { + if (vold && !m_prev_vold) { + any_input = 1; + settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */ + logmsg("volume DOWN <- ramp step=%d", settings_volume_step); + } else if (volu && !m_prev_volu) { + any_input = 1; + settings_volume_adj(+settings_volume_step); + logmsg("volume UP <- ramp step=%d", settings_volume_step); + } + } else { + /* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。 + * 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */ + if (vold && !m_prev_vold && napps > 0 && state == 0) { + any_input = 1; + cursor++; + if (cursor > napps) cursor = 0; /* napps=设置行 */ + logmsg("menu cursor -> %d", cursor); + } + if (volu && !m_prev_volu && napps > 0 && state == 0) { + any_input = 1; + cursor--; + if (cursor < 0) cursor = napps; /* 回绕到设置行 */ + logmsg("menu cursor -> %d", cursor); + } + } + /* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */ + if (meow_mode && vol_ramp_dir != 0) { + int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */ + int going = vol_ramp_tick(held); + g_sys.vol = settings_volume_get(); + paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */ + if (!going) logmsg("volume ramp end -> %d", g_sys.vol); + } + /* M2.19:播放键不再是「应用列表的确认/打开」——它让位给全局媒体控制 + * (播放/暂停,路由到当前活跃媒体会话)。菜单/列表的确认只走触摸点选 + * (见下方 touch 段),物理确认交给电源键(power 菜单/确认框),播放键 + * 在 MeowOS 主循环里不再触发 launch_app。媒体预留:后续媒体会话接管, + * 此处仅作 any_input(any_input 仍用于重置自动息屏计时)。(原播放键=打开 + * 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */ + if (play && !m_prev_play) { + any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */ + logmsg("play key: media-reserved (no menu confirm)"); + } + m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; + + /* touch (event reads) */ + if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */ + struct pollfd pfd = { touch_fd, POLLIN, 0 }; + if (poll(&pfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(touch_fd, ev, sizeof ev) == 16) { + uint16_t type = *(uint16_t *)(ev + 8); + uint16_t 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) t_last_x = val; + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) t_last_y = val; + /* M2.23 原点锁定:goodix 事件时序不保证 ABS 与 BTN_TOUCH 先后, + * 按下边沿只标 t_down,原点延迟到「本次按下之后」的首个有效坐标才锁定, + * 杜绝把上一手势残留(-1/旧坐标)当本次手势起点。 */ + if (t_down && !t_down_valid && t_last_x >= 0 && t_last_y >= 0) { + t_down_x = t_last_x; t_down_y = t_last_y; t_down_valid = 1; + } + /* ⑨ 顶部热区「跟手下拉」打开通知中心(拖动中即进,真跟手): + * 原点锁定 + 起点在顶部热区(y= 0 && t_down_y < SB_BAR_H && + (t_last_y - t_down_y) >= NCP_DRAG_ENTER_SLOP) { + menu_live_open_pending = 1; /* 本次手势仅触发一次 */ + int loy = t_down_y; + logmsg("menu: top-edge live drag-down -> notification center (dy=%d)", + t_last_y - loy); + t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */ + menu_arm_touch(TAP_ARM_DELAY_MS); + int want_launch = -1; + run_notif_center(apps, napps, &want_launch, 1, t_down_x, loy); + /* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */ + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + last_input_ms = now_ms(); + menu_arm_touch(800); + dirty = 1; + if (want_launch >= 0 && want_launch < napps) { + logmsg("menu: notification launch app idx=%d", want_launch); + int ret = launch_app(&apps[want_launch]); + menu_after_app_exit(ret == 0); + last_input_ms = now_ms(); + dirty = 1; + if (ret != 0) { + snprintf(toast, sizeof toast, "CANNOT START: %s", + apps[want_launch].name); + toast_ms = TOAST_MS; + } + } + continue; /* 本圈不再按 tap/其余处理(剩余事件因 t_down=0 成 no-op) */ + } + } else if (type == EV_KEY && code == BTN_TOUCH) { + if (val && !t_down) { + /* Bug 2/4: ignore touches inside the post-app + * cooldown (goodix ghost + back-tap replay). */ + if (now_ms() < menu_touch_cooldown_until) { + /* M2.18 排查乱跳:记录被冷却丢弃的按下(坐标 + 剩余冷却), + * 便于定位「概率进 Hello」是否 ghost/重放越过冷却。 */ + logmsg("menu touch dropped by cooldown (at %d,%d, rem %llu ms)", + t_last_x, t_last_y, + menu_touch_cooldown_until - now_ms()); + } else { + any_input = 1; /* M2.17:触摸重置自动息屏计时 */ + t_down = 1; + t_down_valid = 0; /* 原点待首个有效坐标锁定(防残留 -1/旧值) */ + t_last_x = -1; t_last_y = -1; /* 本次以「无坐标」起步(防跨手势残留) */ + menu_live_open_pending = 0; /* ⑨ 新触摸:复位 live-open latch */ + } + } + else if (!val && t_down) { + t_down = 0; + if (!t_down_valid) { + /* 本次按下从未锁到有效坐标(坐标滞后/ghost 空按下):丢本次。 */ + logmsg("menu tap dropped: no valid origin (last %d,%d)", + t_last_x, t_last_y); + t_down_x = t_down_y = -1; + } else { + int tx = t_down_x, ty = t_down_y; + int dx = t_last_x - tx, dy = t_last_y - ty; + int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; + t_down_valid = 0; + logmsg("menu tap at %d,%d (up %d,%d, d=(%d,%d))", tx, ty, + t_last_x, t_last_y, dx, dy); + /* 阶段B:顶部热区下滑打开通知中心。判定在「滑动」分支内先于左缘返回—— + * 左缘(down_x notification center (release edge, fallback)"); + menu_arm_touch(TAP_ARM_DELAY_MS); + int want_launch = -1; + run_notif_center(apps, napps, &want_launch, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */ + /* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */ + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + last_input_ms = now_ms(); + menu_arm_touch(800); + dirty = 1; + /* 点击通知命中可启动 app → 走标准启动链路(与触摸行同路径)。 */ + if (want_launch >= 0 && want_launch < napps) { + logmsg("menu: notification launch app idx=%d", want_launch); + int ret = launch_app(&apps[want_launch]); + menu_after_app_exit(ret == 0); + last_input_ms = now_ms(); + dirty = 1; + if (ret != 0) { + snprintf(toast, sizeof toast, "CANNOT START: %s", + apps[want_launch].name); + toast_ms = TOAST_MS; + } + } + continue; /* 本圈不再按 tap/其余处理 */ + } + /* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、 + * 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */ + if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) { + logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy); + } else if (tx >= 0 && ty >= 0) { + if (ty >= EXIT_ROW - 4 && ty < EXIT_ROW + ROW_H - 8) { + logmsg("menu: touch exit row"); + if (meow_mode) { + /* M2.6 G:播放器已死,弹出「返回播放器需重启」确认框。 + * 是=写选单默认HibyOS + reboot;否=留在 MeowOS。 + * 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */ + logmsg("meow mode: BACK TO PLAYER -> confirm reboot"); + int rb = confirm_reboot_player(); + menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */ + dirty = 1; + if (rb) { logmsg("reboot-to-player: yes, rebooting"); } + /* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */ + } else { + want_exit = 1; + break; + } + } + /* M2.36 B2b:系统设置行(位于 Exit 行上方,y ∈ [588-4, 588+52-8))。 + * 命中 → 进设置页。阻塞式 run_settings_page 返回后,重播 kd_* 并以 + * 当前 raw 对齐 m_prev_*(M2.25 黑屏修复同款),杜绝「设置页内按电源 + * 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */ + else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) { + logmsg("menu: touch settings row -> settings page"); + menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */ + run_settings_page(); + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + last_input_ms = now_ms(); /* 回主界面即视为输入 */ + menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */ + dirty = 1; /* 全帧重画应用列表 */ + } + /* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与 + * 应用行动作非互斥。应用很多(napps*ROW_H 上探到 EXIT 带)时, + * 一个 tap 会同时命中退出行(<~684)与应用行带,触发确认重启又启动 + * 应用(或直接 break 走人)→ 概率乱跳。退出行优先,命中即不再 + * 落到应用行。普通小列表两区间不重叠,行为不变。 */ + else if (ty >= ROW_TOP && ty < ROW_TOP + napps * ROW_H && napps > 0 && state == 0) { + int idx = (ty - ROW_TOP) / ROW_H; + if (idx < napps) { + logmsg("menu: touch row %d", idx); + int ret = launch_app(&apps[idx]); + /* app exited: return to menu (v6.3). + * menu_after_app_exit() drains queued + * app-run touches so they are not + * replayed as row/tap gestures. */ + menu_after_app_exit(ret == 0); + last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ + dirty = 1; /* app wrote buffer0 -> redraw menu */ + if (ret != 0) { + snprintf(toast, sizeof toast, "CANNOT START: %s", apps[idx].name); + toast_ms = TOAST_MS; + } + } + } + } + t_down_x = t_down_y = -1; /* 复位,下一手势从零开始 */ + } + } + } + } + if (want_exit) break; + } + } + + /* ---- M2.17 超时自动息屏(主界面;主循环 10ms 低频检查,不阻塞)---- + * 任何输入重置 last_input;空闲 > screen_off_timeout(0=常亮)且未在 keep_awake 页 → 息屏。 + * 自动息屏后任意输入唤醒(电源单击路径已在 meow_mode 块处理;这里处理其他输入/触摸)。 + * black 模式:面板仍上电、触摸未挂起,故在屏下额外探测一次 BTN_TOUCH 按下 → 触发唤醒 + * (不处理点击;唤醒后下一圈正常触摸处理)。panel 模式触摸已被 FBIOBLANK(4) 挂起,仅按键唤醒。 */ + if (meow_auto_slept && !meow_screen_on && screen_off_mode == 1 && touch_fd >= 0) { + struct pollfd spfd = { touch_fd, POLLIN, 0 }; + if (poll(&spfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(touch_fd, ev, sizeof ev) == 16) { + uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_KEY && code == BTN_TOUCH && val) any_input = 1; + } + } + } + /* M2.18 Bug 1: 用 now_ms() 而非本圈顶部捕获的 t —— launch_app() 阻塞期间 t 已过期 + * (停在 app 启动前小数),若仍写 t 会把 last_input 倒拨到 app 启动时刻;写 now_ms() + * 才是「输入发生」的真实墙钟。配合下方 idle_ms_since 的回绕保护,杜绝误判空闲。 */ + if (any_input) last_input_ms = now_ms(); + if (meow_auto_slept && any_input && !meow_screen_on) { + logmsg("auto screen-off: input wake (meow_mode=%d keep_awake=%d)", meow_mode, fg_keep_awake()); + meow_panel_on(); + meow_auto_slept = 0; + last_input_ms = now_ms(); + meow_repaint_surface(apps, napps, state, cursor); /* M2.8 唤醒重绘 */ + } + if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) { + /* M2.18 Bug 1: 用 idle_ms_since 取差,杜绝 t= (unsigned long long)screen_off_timeout * 1000) { + meow_panel_off(); + meow_auto_slept = 1; + logmsg("auto screen-off after %ds idle (mode=%s keep_awake=%d, t=%llu last=%llu)", + screen_off_timeout, screen_off_mode ? "black" : "panel", + fg_keep_awake(), t, last_input_ms); + } + } + + /* Redraw only when something actually changed: cursor movement (an + * input event), app exit/enter (which rewrote buffer0), toast + * start/end, or a future notification-badge change. NEVER on a + * periodic timer. + * + * v6.8 BUG (the ~3fps list flicker): the v6.8 "500ms safety redraw" + * rewrote the ENTIRE menu into the live-displayed buffer0 every 500ms + * (the v6.8 fix made last_redraw=el so the timer finally fired), plus + * draw_statusbar()'s amixer popen on every redraw. An in-place full + * rewrite of the buffer the LCDC is scanning produces visible tearing/ + * pulsing on the app list, and the slow amixer popen stalls the loop. + * Full-redraw in place is only correct right after a REAL change; the + * steady-state list must not be touched. */ + /* M2.16:只在真变化时标脏(精确到格子),绝无"动一下就全屏重绘"。 + * 光标移动 → 旧/新两行;toast 起止 → toast 区;其余结构变化 → dirty=1(全帧)。 + * Stage-One 状态栏收敛:音量渐变/时钟只经 paint_sb_right/draw_clock → sb_sdk_refresh() + * 把权威状态喂进 SDK 全局状态栏 + 标脏顶条(0,0,xres,28),由 SDK present() 顶条重绘。 */ + if (cursor != last_cursor) { + /* M2.36 B2b:光标可能在应用行(0..napps-1)或「系统设置」行(napps,位于 Exit 行上方)。 + * 分别标脏对应行 rect(设置行固定 y=EXIT_ROW-ROW_H,与 napps 无关)。 */ + if (cursor == napps || last_cursor == napps) + meow_dirty_add(8, EXIT_ROW - ROW_H - 4, fb_xres - 16, ROW_H - 8); + if (state == 0 && napps > 0) { + if (last_cursor >= 0 && last_cursor < napps) + meow_dirty_add(8, ROW_TOP + last_cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8); + if (cursor >= 0 && cursor < napps) + meow_dirty_add(8, ROW_TOP + cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8); + } + last_cursor = cursor; + need_menu_paint = 1; + } + if (toast_active != (toast_ms > 0)) { /* toast start/end */ + toast_active = (toast_ms > 0); + meow_dirty_add(20, TOAST_Y - 6, fb_xres - 40, 40); + need_menu_paint = 1; + } + if (dirty) { /* 结构变化 → 全帧重画 + 全屏 blit */ + meow_dirty_full(); + need_menu_paint = 1; + dirty = 0; + } + if (need_menu_paint) { + draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */ + need_menu_paint = 0; + } + /* A-1/A-2:呈现统一走 SDK meow_ui_flush()(内含帧率门控;无脏返回,门控未达期保留脏 + * 待下一轮)。fb_norm_buf0 确保 LCDC 显示 buffer0(yoffset 非 0 才 pan,短路无开销)。 + * 不再用 launcher 的 g_ndirty/g_dirty_full 做呈现门——SDK present 自会 no-op 于空脏。 */ + fb_norm_buf0(); /* Bug 3:确保 LCDC 显示 buffer0 */ + meow_ui_flush(); /* M2.16 无闪烁 + M2.16 帧率门控归口 SDK */ + if (toast_ms > 0) { + toast_ms -= POLL_MS; + if (toast_ms < 0) toast_ms = 0; + } + /* Clock refresh ~1s: redraw ONLY the clock digits (and the right info + * cell if battery/wifi changed), never the list. */ + if (el - last_status_tick >= 1000) { + last_status_tick = el; + meow_sys_light(); /* 同步电量/WiFi/音量进 g_sys */ + draw_clock(); /* Stage-One:喂 SDK 全局状态栏 + 标脏顶条;SDK present() 重绘 */ + fb_norm_buf0(); + meow_ui_flush(); /* blit 状态栏整条,不动列表 */ + } + + usleep(POLL_MS * 1000); + } + + if (touch_fd >= 0) close(touch_fd); + /* v6.6: restore the player's frame + pan back BEFORE releasing fb (the + * player is event-driven and won't redraw on its own — see §18.15). + * M2.2:仅当存在存活播放器才恢复其帧;MeowOS 常驻主界面(无播放器)绝不 + * 恢复死帧——屏幕保持自绘的应用列表(buffer0),不遗留播放器残帧。 */ + if (had_player) fb_restore_player(); + if (fbfd >= 0) { if (fb_fd == fbfd) fb_fd = -1; close(fbfd); } + logmsg("menu close (exit=%d meow_mode=%d had_player=%d)", want_exit, meow_mode, had_player); +} + + +/* ================================================================== */ +/* app launch */ +/* ================================================================== */ +static void on_sigterm(int s) { (void)s; sigterm_flag = 1; } + + +/* Drain pending events from all input devices (non-blocking read to EAGAIN). + * Called right before launching an app so residual events (the tap/key that + * selected the row, the hotkey combo's release tail) are consumed before the + * app opens its own fds. Note: events generated AFTER the drain still reach + * the app (multicast); the app therefore also drains itself at startup and + * only exits on a fresh press edge (see hello.c). */ +void drain_input(void) +{ + for (int i = 0; i < 4; i++) { + char dev[32]; + snprintf(dev, sizeof dev, "/dev/input/event%d", i); + int fd = open(dev, O_RDONLY | O_NONBLOCK); + if (fd < 0) continue; + unsigned char ev[16]; + while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } + close(fd); + } +} + + +/* ================================================================== */ +/* main */ +/* ================================================================== */ +static void usage(void) +{ + fprintf(stderr, + "usage: meow_launcherd [--sim-hotkey N] [--sim-tap N] [--sim-exit N]\n" + " [--sim-exit-tap N] [--sim-power-hold N]\n" + " [--sim-power-pick N] [--sim-no-exec]\n" + " [--dumpkeys N] [--scan] [--sysdump]\n" + " [--pkg-install ] [--pkg-export ]\n" + " [--ipc-only N] [--ipc-sock ] [--appinfo ]\n" + " [--fontscan ]\n"); +} + + +int main(int argc, char **argv) +{ + long sim_hotkey = 0, sim_tap = 0, sim_exit = 0, dumpkeys = 0; + int do_scan = 0, do_sysdump = 0, sim_vol = 0; + long ipc_only_ms = 0; + const char *appinfo_path = NULL; + const char *fontscan_root = NULL; /* --fontscan :宿主/qemu 测试字形扫描 */ + const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]); + else if (!strcmp(argv[i], "--sim-tap") && i + 1 < argc) sim_tap = atol(argv[++i]); + else if (!strcmp(argv[i], "--sim-exit") && i + 1 < argc) sim_exit = atol(argv[++i]); + else if (!strcmp(argv[i], "--sim-exit-tap") && i + 1 < argc) sim_exit_tap_ms = atol(argv[++i]); + else if (!strcmp(argv[i], "--sim-power-hold") && i + 1 < argc) sim_power_hold_ms = atol(argv[++i]); + else if (!strcmp(argv[i], "--sim-power-pick") && i + 1 < argc) sim_power_pick = atoi(argv[++i]); + else if (!strcmp(argv[i], "--sim-no-exec")) sim_no_exec = 1; + else if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]); + else if (!strcmp(argv[i], "--scan")) do_scan = 1; + else if (!strcmp(argv[i], "--sysdump")) do_sysdump = 1; + else if (!strcmp(argv[i], "--sim-vol") && i + 1 < argc) sim_vol = atoi(argv[++i]); + else if (!strcmp(argv[i], "--ipc-only") && i + 1 < argc) ipc_only_ms = atol(argv[++i]); + else if (!strcmp(argv[i], "--ipc-sock") && i + 1 < argc) { + snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]); + } + else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i]; + else if (!strcmp(argv[i], "--fontscan") && i + 1 < argc) fontscan_root = argv[++i]; + else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i]; + else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; } + else { usage(); return 1; } + } + + /* unified log: TF card (or /tmp boot buffer when the card is absent) */ + log_cleanup_old(); + if (log_open_card() == 0) + fprintf(stderr, "log file: %s\n", log_file); + else + fprintf(stderr, "log: no TF card, using /tmp boot buffer (%s)\n", + BOOT_BUF_TMP); + logmsg("=== meow_launcherd start (sim_hotkey=%ld sim_tap=%ld sim_exit=%ld dumpkeys=%ld scan=%d) ===", + sim_hotkey, sim_tap, sim_exit, dumpkeys, do_scan); + + load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per + * menu open so edits apply without a daemon restart) */ + settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */ + + /* M2.6:启动扫描一次 TF 卡 MeowOS/fonts///,填充「字体文件」下拉 preset。 + * 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */ + meow_font_scan_presets(); + + if (fontscan_root) { + /* M2.6 测试钩子:扫描指定卡根下的 MeowOS/fonts/// 并打印 preset(qemu/宿主可测)。 + * 用于验证 font.json 解析(含 weight)/三件套/缺件跳过/sha 校验 + preset 动态化逻辑。 */ + meow_font_scan_presets_root(fontscan_root); + printf("fonts=%d\n", font_presets_n); + for (int i = 0; i < font_presets_n; i++) + printf(" %d %s -> %s (fam=%s wt=%s)\n", i, font_preset_labels[i], font_presets[i], + font_preset_fams[i], font_preset_wts[i] ? font_preset_wts[i] : "regular"); + return 0; + } + + if (sim_vol > 0) { + /* M2.14 sim:验证线性渐变——step=1/2/5 下设定音量按 1% 逐级爬升/回落, + * 每 1% 经 meow_dac_vol_write 写一次真实 DAC,打点进 log(stderr)。 */ + settings_volume = 60; /* 固定起点,便于对比 */ + settings_volume_step = (sim_vol > 5) ? 5 : (sim_vol < 1 ? 1 : sim_vol); + int base = settings_volume; + /* UP */ + settings_volume_adj(+settings_volume_step); + for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } + int up = settings_volume; + /* DOWN */ + settings_volume_adj(-settings_volume_step); + for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } + int down = settings_volume; + printf("sim-vol: step=%d base=%d up=%d down=%d (each 1%% logged 'DAC vol -> L/R=')\n", + settings_volume_step, base, up, down); + /* 长按:held=1 持续延段爬升 → 连续渐变到上限 clamp */ + settings_volume = 60; + settings_volume_adj(+settings_volume_step); + for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(1); usleep(20000); } + printf("sim-vol-held: step=%d long-press top=%d (expect clamp 100)\n", + settings_volume_step, settings_volume); + return 0; + } + + if (pkg_install_mpk) { + /* M2.12 .mpk 安装接口(CLI 钩子;UI 后期)。安全、离开主循环。 */ + int r = meow_pkg_install(pkg_install_mpk); + printf("pkg_install %s -> %d\n", pkg_install_mpk, r); + return r; + } + if (pkg_export_name && pkg_export_out) { + /* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */ + int r = meow_pkg_export(pkg_export_name, pkg_export_out); + printf("pkg_export %s -> %s (%d)\n", pkg_export_name, pkg_export_out, r); + return r; + } + + if (do_sysdump) { + /* M2.11 测试钩子:安全打印系统状态快照(不碰 fb/输入设备,可在常驻 launcher + * 旁运行验证 A/E 的 BT/WiFi/电量/音量/亮度抓取),然后退出。 */ + meow_sys_refresh(); + printf("batt=%d%% chg=%d mv=%d\n", g_sys.batt, g_sys.charging, g_sys.batt_mv); + printf("wifi_on=%d ip=%s ssid=%s\n", g_sys.wifi_on, g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", + g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-"); + printf("bt_seen=%d bt_on=%d\n", g_sys.bt_seen, g_sys.bt_on); + printf("vol=%d brightness=%d\n", g_sys.vol, g_sys.brightness); + printf("data_free_kb=%ld sd_mounted=%d sd_free=%ld sd_total=%ld\n", + g_sys.data_free_kb, g_sys.sd_mounted, g_sys.sd_free_kb, g_sys.sd_total_kb); + printf("mode=%d napps=%d ver=%s\n", g_sys.mode, g_sys.napps, g_sys.ver); + printf("render: eff_rate=%d cfg=%d always_max=%d frames=%llu\n", + meow_ui_effective_rate(), refresh_rate_cfg, refresh_always_max, + meow_ui_frames_presented()); + return 0; + } + + if (do_scan) { + struct app apps[MAX_APPS]; + int n = scan_apps(apps); + printf("state=%d napps=%d\n", n < 0 ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0), n < 0 ? 0 : n); + for (int i = 0; i < n && n > 0; i++) + printf(" %d %s -> %s ok=%d\n", i, apps[i].name, apps[i].exec, apps[i].ok); + return 0; + } + + if (appinfo_path) { + /* 阶段 D · M1 debug:解析单个 app.json 并打印路由/框架/服务/图标(qemu 可测静态路由) */ + FILE *f = fopen(appinfo_path, "r"); + if (!f) { printf("appinfo: cannot open %s\n", appinfo_path); return 1; } + struct app a; + memset(&a, 0, sizeof a); + meow_parse_app_json(f, &a); + fclose(f); + printf("name=%s\n", a.name[0] ? a.name : "(missing)"); + printf("exec=%s\n", a.exec[0] ? a.exec : "(missing)"); + printf("ok=%d\n", (a.exec[0] && access(a.exec, X_OK) == 0) ? 1 : 0); + printf("framework=%d\n", a.framework ? 1 : 0); + printf("icon=%s\n", a.icon[0] ? a.icon : "(none)"); + printf("routes=%d\n", a.nroute); + for (int i = 0; i < a.nroute; i++) + printf(" route.%s -> %s (depth=%d keep_awake=%d)\n", a.routes[i].act, a.routes[i].chain, + chain_depth(a.routes[i].chain), a.routes[i].keep_awake); + printf("services=%d\n", a.nsvc); + for (int i = 0; i < a.nsvc; i++) + printf(" service.%s -> mode:%d restart:%d max:%d window_ms:%d\n", + a.svcs[i].name, a.svcs[i].mode, a.svcs[i].restart, + a.svcs[i].max, a.svcs[i].window_ms); + return 0; + } + + if (ipc_only_ms > 0) { + /* 阶段 D · M0/M1 sim:仅跑 IPC 骨架(免 fb/输入设备),供 qemu 下用 meow_ipc + * 验证 HELLO→ACK、ACT_QUERY→链路。无卡时 /tmp 缓冲承载日志。 */ + fprintf(stderr, "ipc-only: socket=%s\n", ipc_sock_path); + int rc = meow_ipc_init(); + if (rc != 0) { fprintf(stderr, "ipc-only: IPC init failed, exiting\n"); return 1; } + unsigned long long s0 = now_ms(); + while (!sigterm_flag && now_ms() - s0 < (unsigned long long)ipc_only_ms) { + meow_ipc_process(); + meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ + usleep(POLL_MS * 1000); + } + ipc_shutdown = 1; + unlink(ipc_sock_path); + logmsg("=== meow_launcherd ipc-only exit ==="); + return 0; + } + + /* pid file */ + int pfd = open(PID_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (pfd >= 0) { + char pbuf[16]; + int l = snprintf(pbuf, sizeof pbuf, "%d\n", getpid()); + write(pfd, pbuf, l); + close(pfd); + } + + signal(SIGTERM, on_sigterm); + signal(SIGINT, on_sigterm); + signal(SIGHUP, SIG_IGN); + + /* 阶段 D · M0:启动 IPC 线程(AF_UNIX /usr/data/meow_ipc.sock)。幂等;失败只 log, + * 主循环仍跑(10ms 热键轮询不受影响)。线程创建晚于进程但有独立生命周期。 */ + meow_ipc_init(); + + struct keypoll kp_power, kp_keys; + /* retry opening the input devices (they may not exist yet at boot) */ + for (int tries = 0; tries < 30; tries++) { + if (kp_open(&kp_power, DEV_POWER) == 0 && + kp_open(&kp_keys, DEV_KEYS) == 0) + break; + logmsg("input devices not ready, retry %d/30 in 1s", tries + 1); + log_poll_card(); /* flush boot buffer even if input devices lag */ + sleep(1); + } + if (!kp_power.ok || !kp_keys.ok) + logmsg("WARNING: input devices unavailable, launcher blind"); + + /* ===== MeowOS 常驻主界面(M2.2 修复批次)===== + * 播放器已死(find_hiby_pid()==0:bootmenu MeowOS 分支 exec、或热键切 MeowOS 后 exec, + * 二者皆此语境)时,launcher == MeowOS 主界面常驻: + * - 启动即扫描并自绘应用列目录(run_menu 画 buffer0 + FBIOPAN yoffset=0), + * 绝不等到首次热键才画 —— 修复「切到 MeowOS 后屏幕停留在播放器最后一帧」; + * - 无存活播放器 → run_menu 内不 SIGSTOP / 不备份 / 不 SIGCONT 死播放器、不恢复死帧; + * - BACK TO PLAYER → 显示「需重启」提示并留在主界面(不渲染死帧); + * - 仅 SIGTERM 退出(rcS/SSH 关停),退出时屏幕保持应用列表主界面(无残帧)。 + * 若此时竟有存活播放器(不应用于 MeowOS,防御性),回落到下面的旧覆盖路径。 */ + meow_mode = is_meow_mode(); + /* M2.12:卡在则把 /usr/data/mnt/sd_0/MeowOS/opt/bin 前插 PATH(外围 CLI 已迁卡, + * 按需可用;卡不在跳过,绝不崩)。SSH 会话另由 rootfs /etc/profile.d/meow_card_path.sh 处理。 */ + if (ensure_card_mounted() || sd_mounted()) { + setenv("PATH", "/usr/data/mnt/sd_0/MeowOS/opt/bin:/usr/bin:/bin:/usr/sbin:/sbin", 1); + logmsg("PATH: card MeowOS/opt/bin prepended"); + } else { + logmsg("PATH: no card, MeowOS/opt/bin skipped"); + } + if (meow_mode) { + /* M2.2 拔营:启动即尝试挂 TF 卡 + 联网(非阻塞);失败由 run_menu 后台周期重试。 */ + ensure_card_mounted(); + ensure_wifi(); + struct app apps[MAX_APPS]; + int n = scan_apps(apps); + int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); + int napps = n < 0 ? 0 : n; + logmsg("MeowOS mode: hiby_player NOT running -> resident app-list home " + "(state=%d napps=%d), drain-to-home immediately", state, napps); + run_menu(apps, napps, state, sim_tap, sim_exit); + logmsg("=== meow_launcherd exit (MeowOS mode) ==="); + if (log_fd >= 0) { close(log_fd); log_fd = -1; } + ipc_shutdown = 1; unlink(ipc_sock_path); + unlink(PID_PATH); + return 0; + } + + /* hotkey state: power_voldown (default) + legacy seq */ + int prev_pwr = 0, prev_prev = 0; + int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_nxt = 0; + int pwr_hold_armed = 0; /* power held, within trigger window */ + unsigned long long pwr_down_at = 0; + int seq_key_prev = 0; /* the configured seq key, edge-tracked */ + int seq_hits = 0; + unsigned long long seq_last_at = 0; + struct vol_snap seq_vol0 = { -1, -1 }; + + long sim_hk_done = 0; + unsigned long long t0 = now_ms(); + int log_tick = 0; /* periodic card-log retry (~1s) while no card log */ + + if (hotkey_mode == HOTKEY_MODE_PWRVOL) + logmsg("ready, hotkey = power_voldown (hold power, tap VOL_DOWN)"); + else + logmsg("ready, hotkey = %d taps of key code %d (gap %dms)", + seq_len, seq_codes[0], seq_gap_ms); + + while (!sigterm_flag) { + unsigned long long t = now_ms(); + long el = (long)(t - t0); + + /* 阶段 D · M0:排空 IPC 队列(非阻塞,零延迟;热键轮询 10ms 不受 socket 阻塞) */ + meow_ipc_process(); + meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ + + /* sample current key states */ + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + int pwr = 0, prev = 0, vold = 0, volu = 0, play = 0, nxt = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; + if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; + if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) nxt = 1; + } + + /* sim hotkey: emulate the configured hotkey. + * power_voldown (default): HOLD power from N, VOL_DOWN tap at N+800 + * (within the 60-2500ms window) — this drives pwr/vold so the + * real detection below sees the same DOWN edges a physical press + * would, and fires the menu at ~N+800. + * seq: 3 quick VOL_UP taps at N, N+300, N+600. */ + if (sim_hotkey > 0 && !sim_hk_done) { + if (hotkey_mode == HOTKEY_MODE_PWRVOL) { + if (el >= sim_hotkey && el < sim_hotkey + 3000) pwr = 1; + if (el >= sim_hotkey + 800 && el < sim_hotkey + 950) vold = 1; + } else { + if (el >= sim_hotkey && el < sim_hotkey + 150) volu = 1; + if (el >= sim_hotkey + 300 && el < sim_hotkey + 450) volu = 1; + if (el >= sim_hotkey + 600 && el < sim_hotkey + 750) volu = 1; + if (el >= sim_hotkey + 750) sim_hk_done = 1; + } + } + + if (vold != prev_vold) logmsg("key VOL_DOWN %s", vold ? "DOWN" : "UP"); + if (volu != prev_volu) logmsg("key VOL_UP %s", volu ? "DOWN" : "UP"); + if (play != prev_play) logmsg("key PLAY %s", play ? "DOWN" : "UP"); + if (nxt != prev_nxt) logmsg("key NEXT %s", nxt ? "DOWN" : "UP"); + if (pwr != prev_pwr) logmsg("key POWER %s", pwr ? "DOWN" : "UP"); + if (prev != prev_prev) logmsg("key PREVIOUS %s", prev ? "DOWN" : "UP"); + + /* hotkey trigger (v6.5). + * DEFAULT — power_voldown: the launcher arms on the power DOWN edge + * and fires when a VOL_DOWN DOWN edge lands while power is still + * held, 60-2500ms after the power press. The player sees the power + * press (its screen-off flow starts — accepted side effect) and the + * VOL_DOWN tap (volume -1 — accepted). The menu open runs the + * verified unblank + bring-backlight-only-if-dark logic, so a + * power-initiated screen-off is undone for the menu. Power release + * or window expiry disarms; a VOL_DOWN before 60ms or after 2500ms + * is ignored (a plain volume press while holding power). + * LEGACY — seq: N quick taps of one key within seq_gap_ms opens the + * menu (single/double presses stay ordinary volume keys); the taps + * raise volume, snapshotted and restored on trigger. */ + if (pwr && !prev_pwr && hotkey_mode == HOTKEY_MODE_PWRVOL) { + pwr_hold_armed = 1; + pwr_down_at = t; + logmsg("hotkey: power DOWN, arm hold-window"); + } + if (!pwr && prev_pwr) pwr_hold_armed = 0; /* release: disarm */ + + int hotkey_fired = 0; + if (hotkey_mode == HOTKEY_MODE_PWRVOL) { + /* disarm once the hold window has passed (power still held) */ + if (pwr_hold_armed && pwr && t - pwr_down_at > HK_PWRWIN_MAX) + pwr_hold_armed = 0; + if (pwr_hold_armed && pwr && vold && !prev_vold) { + long since = (long)(t - pwr_down_at); + if (since >= HK_PWRWIN_MIN && since <= HK_PWRWIN_MAX) { + logmsg("HOTKEY: power held + VOL_DOWN tap (%ldms) -> menu", + since); + hotkey_fired = 1; + } else { + logmsg("hotkey: POWER+VOL_DOWN ignored (%ldms outside window)", + since); + } + } + } else { + int seq_key = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_power.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; + if (kp_keys.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; + } + if (seq_key && !seq_key_prev) { /* DOWN edge */ + if (seq_hits == 0) seq_vol0 = vol_snapshot(); + else if (t - seq_last_at > (unsigned)seq_gap_ms) { + seq_hits = 0; /* gap too long: restart */ + seq_vol0 = vol_snapshot(); + } + seq_hits++; + seq_last_at = t; + logmsg("seq tap %d/%d", seq_hits, seq_len); + if (seq_hits >= seq_len) { + logmsg("SEQ HOTKEY: %d taps of key %d -> menu", + seq_len, seq_codes[0]); + vol_restore(seq_vol0); + seq_hits = 0; + hotkey_fired = 1; + } + } else if (!seq_key && seq_key_prev) { /* UP edge: nothing */ + } + seq_key_prev = seq_key; + if (seq_hits > 0 && t - seq_last_at > (unsigned)seq_gap_ms * 2) + seq_hits = 0; + } + + if (hotkey_fired) { + struct app apps[MAX_APPS]; + int n = scan_apps(apps); + int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); + int napps = n < 0 ? 0 : n; + run_menu(apps, napps, state, sim_tap, sim_exit); + /* restore the player's panel state FIRST (re-blank if the menu + * opened while blanked), THEN let it resume — so the player never + * sees a panel that contradicts its display state machine + * (v6.2 hotfix) */ + restore_backlight(); + cont_player(); + sim_hk_done = 1; + pwr_hold_armed = 0; + /* absorb current state as baseline for the menu keys */ + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + prev_vold = prev_volu = prev_play = prev_nxt = 0; + prev_pwr = prev_prev = 0; + seq_key_prev = 0; + seq_hits = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) prev_vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) prev_volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) prev_play = 1; + if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) prev_nxt = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) prev_pwr = 1; + if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev_prev = 1; + } + /* the menu just proved the card is present: flush */ + log_poll_card(); + continue; + } + + prev_vold = vold; prev_volu = volu; prev_play = play; prev_nxt = nxt; + prev_pwr = pwr; prev_prev = prev; + + if (dumpkeys > 0 && el >= dumpkeys) { + logmsg("dumpkeys duration reached, exit"); + break; + } + + /* card may have mounted after S90: open the card log and move the + * /tmp boot buffer into it (no menu needed to see boot logs) */ + if (log_fd < 0 && (++log_tick % 100) == 0) log_poll_card(); + + /* NOTE: the Bug B backlight keeper was REMOVED (2026-08-28 hotfix). + * It restored brightness + FBIOBLANK(0) after the player's bkl_time + * screen timeout, which unblanked the panel behind the player's back + * and froze its display state machine (UI gone, keys dead — device + * log: keeper at 40.9s, player wedged). The player owns the panel + * blank state; the launcher never touches fb0/backlight outside the + * menu/app sessions anymore. The player's own "背光时间" timeout is + * expected stock behaviour (set it to 常亮 in the player settings UI + * if unwanted). */ + + usleep(POLL_MS * 1000); + } + + /* If the player is still stopped we never made it through the menu path's + * restore+resume (e.g. SIGKILL-free SIGTERM race): restore the panel state + * before resuming. When the player is already resumed the menu path did it, + * and re-running restore_backlight here would write brightness onto a panel + * the player still believes is blanked — leave it alone. */ + if (player_stopped) { fb_restore_player(); restore_backlight(); } + cont_player(); + logmsg("=== meow_launcherd exit ==="); + if (log_fd >= 0) { close(log_fd); log_fd = -1; } + ipc_shutdown = 1; unlink(ipc_sock_path); + unlink(PID_PATH); + return 0; +} + diff --git a/patch_v2/meow_launcherd/meow_card_path.sh b/patch_v2/meow_launcherd/meow_card_path.sh index f5256ed..9a12fc6 100644 --- a/patch_v2/meow_launcherd/meow_card_path.sh +++ b/patch_v2/meow_launcherd/meow_card_path.sh @@ -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 diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h new file mode 100644 index 0000000..80e01ab --- /dev/null +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* 0-warning:musl 把 重定向到 ,用后者消除 -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 +#include +#include /* Bug 2:直连 reboot(2) syscall 用 */ +#include /* M0:IPC 线程 */ +#include /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */ +#include /* M0:AF_UNIX IPC */ +#include /* M0:sockaddr_un */ +#include +#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=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.=1 / route..keep_awake=1) */ + int fullscreen; /* 活动级全屏声名(app.json route..fullscreen=1;M3 活动切换时消费) */ +}; + + +/* 服务行:service.=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. 行数 */ + 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 */ diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 63730e1..4426663 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/meow_launcherd.c b/patch_v2/meow_launcherd/meow_launcherd.c deleted file mode 100644 index 39b3590..0000000 --- a/patch_v2/meow_launcherd/meow_launcherd.c +++ /dev/null @@ -1,9778 +0,0 @@ -/* meow_launcherd.c — R3 Pro II Plan B zero-patch app launcher daemon. - * - * M2.53 亮度接管修正(开机/切到 MeowOS 亮度被重置为 25/16): - * - 根因:HibyOS 播放器运行/恢复画面会把它自己的背光下限(16/25)写进 sysfs, - * MeowOS 主屏接管时又以 read_brightness() 读到的该值当「当前亮度」 - * (saved_brightness / meow_saved_brightness),从不重应用用户 display.brightness, - * 故用户设的低亮度(<25,最低到 1)被覆盖。 - * - 修法:run_menu 主屏接管(had_player==0,播放器已死=真接管)时调 - * apply_user_brightness():读 settings display.brightness(经新导出的 - * meow_settings_brightness_clamped(),clamp 下限 brightness_min 默认 1、上限 100) - * → write_brightness 强写回,覆盖播放器残留的 16/25。 - * - 边界:播放器会话期间的覆盖菜单(had_player==1)不压回——播电影时亮度归播放器管; - * ensure_backlight/restore_backlight 仍用 saved_brightness(覆盖会话=播放器真实值), - * 不改为用户值,避免菜单关闭瞬间抢播放器背光。 - * - * M2.46-2 状态栏 5 问题(2026-08-29): - * ① 亮度图标主题色(on_surface_variant 灰 → on_surface 主色)+ 拖动亮度滑条时状态栏百分比实时跟: - * 根因 s_commit brightness 分支只 write_brightness 未同步 g_sys.brightness/未重绘;补 - * g_sys.brightness=v + paint_sb_right()(与随后 meow_sys_refresh 从 sysfs 回读一致不打架)。 - * ② 右簇音/电顺序互换:最右由「音量」改「电量」(绘制顺序先电量后音量,右→左:电→音→蓝牙→WiFi→亮度)。 - * ③ 图标视觉对齐:sb_blit_icon 按 ink 边界居中 + 主图标等比等大(SB_ICON_INK=16),音量不再「小一圈/ - * 下边距」;ink 垂直中心对齐状态栏文字中心(g_sb_icon_cy=SB_ICON_CY=14,见 sb_text_y),图文同中线。 - * ④ WiFi 图标随信号分档:新增 wifi_signal_level() 读 /proc/net/wireless wlan0 level(dBm), - * ≥-60 强 / [-60,-75) 中 / <-75 弱,选 SB_ICON_WIFI_STRONG/MEDIUM/WEAK(新位图追加进 sb_icons.h); - * 强弱档保留自然 ink(少一格即语义),满格/强档走等大;wifi_on 为假不画(保持现状)。 - * (⑤ 数字随亮度刷新已并入①。) - * - * M2a 通知存储 + 原生 IPC(2026-08-29,阶段 D 链开端): - * - 新增 meow_notif.h/.c(NOTIFICATION_SYSTEM.md §4/§7/§8):通知结构对齐 freedesktop - * + MeowOS 扩展(target_activity/progress);内存配额(每 App ≤ notify.categories 类型、 - * 每类型活跃 ≤ notify.category_quota、媒体每 App ≤ notify.media_quota,系统事件作 - * app_id="system" 同走配额);TTL 10s 转储钩子(§8,TF 落盘 M2c 后置,预留 notif_dump_cb); - * 满丢该类型最旧条(ring 语义);ONGOING 常驻(转储后仍保留,服务死随清)。 - * - meow_settings 默认表新增 notify.categories/category_quota/media_quota(B2a 后端)。 - * - IPC:ipc_dispatch 新增 MSG_NOTIFY→MSG_NOTIFY_ACK、MSG_NOTIFY_DISMISS→MSG_ACK、 - * MSG_NOTIFY_PROGRESS→MSG_ACK(实时进度仅改内存活跃槽,不落盘); - * MSG_NOTIFY_LIST 为守护→App 下行(M5 接入)。meow_ipc 测试钩子新增 notify/dismiss 子命令。 - * - meow_notif_before_poweroff() 挂入 sys_do_reboot():正常电源事件前先转储未上卡通知再清板载。 - * - 通知模型补全(2026-08-30,用户定案):SDK→IPC→存储→dump 全链路补 urgency(0-3)/ - * icon(空→按 app_id 回退该 app 默认图标);progress 实时仅改内存(MSG_NOTIFY_PROGRESS), - * TF 快照只在创建/结束(100% 或 dismiss)(meow_notif_dump_tf 写当时值)。 - * - * B2a 统一系统设置后端(2026-08-29): - * - 新增 meow_settings.h/.c(SYSTEM_SETTINGS.md §3/MEOWOS_API §2):全部系统设置 - * 抽象为「名称/类型/默认/取值域」扁平表,单点存板载 /usr/data/meow/settings.conf; - * clamp 集中 set,变更即广播 listener,提供 get/set/枚举/list/导入导出。 - * - settings_load()(手写解析 settings.conf)→ meow_settings_init() + 缓存同步; - * settings_save_volume() → meow_settings_save();settings_volume 等全局仅作缓存, - * 由 meow_settings 的 set 回调/加载后同步,DAC/状态栏/息屏/刷新率调度照旧引用。 - * 音量渐变(1% 爬)仍只在渐变结束落盘一次,不逐 1% 写盘(UBIFS 低频)。 - * - 本批只建后端 + 迁移 audio/display 已有设置;实体设置页(B2b)/Web 面板(C)后复用。 - * - * M2.39 设置页第二批 7 问题(2026-08-29,版本 0.7.34): - * ① 亮度滑条 1/4 卡 25:设备 settings.conf 残留旧 brightness_min=25(覆盖默认 1)致 - * clamp 回 25。默认表维持 1;加载时把恰好为旧默认 25 的 brightness_min 迁移为 1 并落盘, - * 亮度滑条可调到「极暗但可见」的 1(防黑屏下限仍由 brightness_min 控)。滑条上限 101→100 - * (101 闪屏)。③ 子菜单值刷新核验:set 后 after_modal 整帧重绘,s_display_value 用 get_enum/ - * get_str 重读,值即时更新(已核验,无需额外改动)。 - * ④ 设置页音量键不再当光标移动(用户约束)——改调系统音量(vol_ramp_begin/tick 线性渐变写 - * DAC + 刷状态栏 VOL);仅 run_settings_page 移除,电菜单/应用列表的系统级音量导航不动; - * 选择器子模态内部仍保留音量键=移动选项高亮(属「选值」另一语义)。 - * ⑤ 新增「网络」分组:WiFi 开关(network.wifi_enabled)+已连接 SSID+已保存网络(下拉 connect - * wpa_cli select_network)+附近设备(WiFi scan/scan_results)+蓝牙开关(network.bluetooth_enabled) - * +已配对设备;读取全部经带超时 popen(wpa_cli/bluetoothctl),扫描异步触发,绝不阻塞主循环; - * ensure_wifi() 尊重 wifi_enabled(关=0 不再自动连网)。⑥ 设置页支持左缘右滑返回(复用共享 - * 判定 meow_edge_back(),与 SDK 全局左缘返回同一语义)。⑦ 底部「返回」改用像素左向箭头+「返回」, - * 点阵回退字库缺 '‹' 字形不再渲染方框。 - * - * M2.40 设置页网络/子菜单 5 问题(2026-08-29,版本沿 0.7.34): - * ① 子菜单点值无反应(严重):s_dd_modal 触摸解析用裸 ddx/ddy 在按下边沿锁原点,踩 goodix - * 「BTN_TOUCH 先于 ABS」时序 → 锁到 -1/上一手势残留 → 点选项被判「点卡片外=取消」→ 菜单 - * 关闭无变化。改为抬手用抬手时刻最新 abs 坐标判定(与 meow_touch mt_up 同源),点值真正生效。 - * ② WiFi 扫描把 MAC 当独立设备:net_wifi_scan 按 BSSID 去重(同 BSSID 只留一条,优先具名, - * 后到且有 SSID 覆盖先到空 SSID 行),隐藏网络显示「(隐藏网络)」,不再与具名条目重复。 - * ③ 已保存/已连接中文 SSID 乱码:wpa_cli list_networks/status 对非 ASCII 输出 \xNN 转义文字, - * 新增 wpa_decode_ssid() 还原成真实 UTF-8 字节存入 name。 - * ④ 网络分组命名/顺序:SMENU「附近设备」→「附近WiFi」(其顺序本就正确);蓝牙「蓝牙设备」改 - * 读附近扫描发现(已配对+未配对),新增 net_bt_scan_trigger()/net_bt_scan():后台有界驱动 - * bluetoothctl(scan on→sleep 4→scan off→devices→quit)写 /tmp 缓存,system() 尾部 & 立即 - * 返回,绝不死循环;扫描中显示「扫描中...」,无缓存退回已配对列表。 - * ⑤ WiFi 扫描触发本就立即返回(wpa_cli scan),net_wifi_scan 读超时 1800→2500ms(留扫描出结果余量)。 - * - * M2.41 B2b 主题三方式(手动深色/手动浅色/时段自动,2026-08-29,版本 0.7.38): - * - 新增设置键 display.theme_mode(enum manual/auto,默认 manual)+ 深色时段两键 - * display.theme_dark_start/end(int 分钟 0-1439,默认 0);display.theme 保留为手动深浅。 - * - s_ui_theme:manual=直读 display.theme(原行为不变);auto=读时段两键+本地分钟,按 - * [start,end) 判定(start>end 跨午夜;start==end 视为未配置 → 回退 manual 读 display.theme)。 - * - 设置页「主题」行改绑 display.theme_mode(手动/时段自动),新增「主题深浅」(display.theme) - * 与「深色时段开始/结束」两行(SC_SLIDER 0-1439,step 15,显示 HH:MM;start==end 显示「未设置」)。 - * - * M2.29 电源菜单加「升级模式」按钮(2026-08-29,版本 0.7.24): - * - 电源菜单 PM_NUM 4→5,新增「升级模式」按钮(index 3):动作 = - * system("/usr/bin/bootmode.sh Recovery")(写 mtd5=ota:kernel2)+ sys_do_reboot(RESTART), - * uboot 读 mtd5=ota:kernel2 引导 kernel2 升级模式(自动刷 TF 卡 r3proii.upt)。 - * - 布局 2×2 → 2 列×3 行(行 y 300/420/540),取消仍默认选中(index 4,右下=安全项)。 - * - MEOWOS_VERSION 0.7.23 → 0.7.24。 - * - * M2.25 Launcher 统一触摸接口 meow_touch(2026-08-29,版本 0.7.20): - * - 新增 meow_touch.h/.c:轻量、不依赖 SDK 的触摸手势解析(goodix 时序免疫:BTN_TOUCH=1 - * 不锁原点,等按下后首个有效 ABS 才锁;抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动)。 - * - 电源菜单 show_power_menu / 确认框 confirm_reboot_player 触摸解析迁到 meow_touch: - * 修「确认框/电源菜单点两下」—— 原来两者各自独立解析,按下边沿用当时 t_last(可能 - * 残留 -1/旧坐标)锁原点 → 抬手命中失败;现统一原点锁定,点一下即确认。 - * - 黑屏修复:电源菜单关闭(尤其触摸点「取消」)后,kd_* 防抖状态因阻塞式 - * show_power_menu() 冻结,返回后 stable 与 raw 错配 → 误判电源「单击」→ meow_toggle_panel - * 关掉面板(黑屏且难唤醒)。菜单关闭后 memset 重播 kd_* + 以当前 raw 对齐 m_prev_*。 - * - 应用运行期电源菜单(M2.24 watchdog 路径)关闭后重播 save_pow/save_keys 防误判 key_edge。 - * - 应用列表(run_menu 内联解析,M2.23 已修)保留内联(含 cooldown/滑动日志/行带判定), - * 语义与 meow_touch 一致。 - * - 版本 0.7.19 → 0.7.20。 - * M2.24 全面按键接管(2026-08-29,版本 0.7.19): - * - launcher 应用运行期(waitpid 轮询)消费 watchdog 的电源长按菜单请求 - * (/tmp/meow_power_menu):SIGSTOP app → 写 /tmp/meow_power_menu_active(watchdog 让位) - * → 弹统一电源菜单 show_power_menu(复用,一处绘制)→ 关菜单 → SIGCONT app。 - * 电源单击=息屏/亮屏、音量=调音量、播放/上下曲=抓消费由 watchdog 在应用期统一处理; - * launcher 的 run_menu 菜单期语义不变(KEY_CONTROL_DESIGN §3 统一监听点)。 - * - launch_app 末尾 settings_load():应用期 watchdog 可能改音量(settings.conf 同源), - * 返回菜单前 re-load,保证菜单音量与真实 DAC 一致。 - * - 版本 0.7.18 → 0.7.19。 - * M2.23 菜单触摸判定修复(2026-08-29,版本 0.7.18): - * - 手势原点锁定:BTN_TOUCH=1 只标「已按下」,t_down_x/y 延迟到本次按下后首个有效坐标才锁, - * 杜绝上一手势残留(-1/旧坐标)当手势起点(修「按下坐标=-1」)。 - * - 滑动不启动:抬手位移 >= MENU_SWIPE_THRESH(65) 判为滑动,不 launch、不做行判定 - * (修「位移 85 被当 tap 命中行」;为将来左右滑切屏预留,本轮只判滑动)。 - * - 行判定收紧:仅当 tap(位移<阈值)且 ty 落进行带才 launch_app;行带外 tap(空白)不触发。 - * - 版本 0.7.17 → 0.7.18。 - * M2.16 无闪烁渲染 + 刷新率调度 + 按键防抖/单击长按(2026-08-29,版本 0.7.11): - * - 无闪烁渲染地基:RAM 离屏后备缓冲(现由 SDK 提供),所有绘制 - * (put_px/meow_font/dim_screen/draw_menu/statusbar/电源菜单/确认框)一律画进 SDK back_buf; - * 呈现统一走 SDK meow_ui_flush() = FBIO_WAITFORVSYNC 等垂直同步 + 一次 memcpy(dirty 区或全帧) - * 到 fb0(buffer0, yoffset=0)。fb 从不被渐进绘制、绝无中间态/无撕裂。空闲无 dirty → 不画不拷。 - * - 脏区追踪 meow_dirty_add(x,y,w,h)(桥进 SDK 脏追踪):矩形合并集(相交吸收为 bbox, - * 超限转全屏);状态栏时钟/右侧信息格只在"那个格"变化时标脏(paint_sb_clock/paint_sb_right)。 - * - 刷新率调度:display.refresh_rate(60/30/15,默认 60,屏幕 62Hz 锁 60 不逼瓶颈) + - * display.refresh_always_max(1=恒 max 不降档)。A-1/A-2 起该帧率门控+自适应降档 governor - * 归口 SDK present(meow_ui_set_refresh_rate/set_refresh_always_max),launcher 不再自带一套。 - * - A-1/A-2(0.7.23):launcher 与 app 统一渲染栈(SDK back_buf + meow_ui_flush present + 帧率门控), - * launcher 不再自建 back_buf/meow_present/refresh_governor;播放器 fb 双缓冲交互(fb_backup_player/ - * fb_norm_buf0/FBIOPAN/yoffset)保留——与上屏 present 分属不同层。 - * - 按键防抖 + 单击/长按:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认(抗 ADC 抖动"点一下跳5-6下"); - * 按压 =path / framework=true / - * service.=mode:restart:max:window_ms / icon;scan_apps 解析入 g_accts 注册表; - * MSG_ACT_DECLARE(运行时并集覆盖)/ACT_QUERY/ACT_RESULT;resolve_chain(app,activity) - * 返回 [root..target] 链路与深度(route 缺失→深度1兜底;activity 未注册→MEOW_ERR_NOENT)。 - * - 本阶段不做:通知存储(M2)、导航栈/launch_activity(M3)、UI(阶段 B)。播放器不动。 - * 测试钩子:meow_ipc(patch_v2/meow_launcherd/meow_ipc.c,与 evtool 同定位)+ - * launcher --ipc-only N --ipc-sock (qemu sim 免 fb/输入)。 - * - * M2.14 音量渐变批次(2026-08-29,版本 0.7.9): - * - audio.volume_step 默认 2 → **1**(SYSTEM_SETTINGS.md:默认 1,可配 1-5,clamp 上限 5)。 - * - 线性渐变:settings_volume_adj(±step) 不再一次跳 N%,改起 vol_ramp_begin/tick 状态机—— - * 每主循环 tick 按 **1%** 递增爬向目标(VOL_RAMP_MS=15ms 墙钟节流,不阻塞主循环), - * 每 1% 写一次 meow_dac_vol_write(真实 DAC);step>1 时不突兀、step=1 一步即达。 - * - 长按持续渐变:到目标若按键仍按住则再延一段(vol_ramp_target += dir*step)直到松开/到上限; - * 连按=每次边沿叠加继续。落盘只在渐变结束一次。状态栏 VOL 随渐变刷新(显示爬升值/最终值)。 - * MEOWOS_VERSION 0.7.8 → 0.7.9。 - * - * M2.13 真实 DAC 音量批次(2026-08-28,版本 0.7.8): - * - 软件占位音量 → 真实 CS43198 DAC 音量:settings_volume_set/adj 每次变更即经 - * amixer cset(SNDRV_CTL_IOCTL_ELEM_WRITE)直写 /dev/snd/controlC0 的 - * "Left/Right Playback Volume"(numid 1/2,0-255 映射 0-100)到双片 CS43198。 - * 新增 meow_dac_vol_write()/meow_vol_get()/meow_vol_set();进 MeowOS 时 - * meow_sys_refresh 同步 DAC 至当前档。尊重上限:audio.volume_max(默认 100)clamp; - * 跳步更细 audio.volume_step(默认 2,可配回 5)。 - * - 读回局限(§18.39):codec 驱动音量 get 为 dummy 恒 0(amixer cget 死镜像成因), - * /dev/i2c-3 被驱动占用(i2cget busy),故 meow_vol_get() 返回我方写入跟踪值。 - * MEOWOS_VERSION 0.7.7 → 0.7.8。 - * - * M2.12 迁移批次(2026-08-28,版本 0.7.7): - * - 外围 CLI(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd)自 rootfs /usr/bin 迁至 - * TF 卡 MeowOS/opt/bin/(Tools 皆静态 MIPS,无 .so 依赖);MeowOS 启动时卡在则 PATH - * 前插 /usr/data/mnt/sd_0/MeowOS/opt/bin(卡不在跳过)。固件 rootfs 瘦身(目标 ≤42MB)。 - * - MeowAPPs 数据目录(exFAT 无符号链接 → 用环境变量,不做 symlink):launch_app 启动前 - * 调 meow_app_prepare(exec) 建 MeowData//{data,cache}(如缺),并向子进程注入 - * MEOW_DATA_DIR/MEOW_CACHE_DIR(与 MEOW_LOG_DIR 同族)。新增 .mpk 安装/导出接口 - * meow_pkg_install/export + CLI 钩子 --pkg-install/--pkg-export(本期接口,UI 后期)。 - * - 零卡依赖审查:settings 在 /data、字库/应用/日志/opt 均可缺省回退;拔卡不死、插卡自建。 - * MEOWOS_VERSION 0.7.6 → 0.7.7。 - * - * M2.11 修复批次(2026-08-28,版本 0.7.6): - * - A 系统状态全量重抓:新增 meow_sys_state + meow_sys_refresh(),每次进 MeowOS - * 重抓 电量/WiFi/蓝牙/音量/亮度/存储/系统;主循环 10ms 热键轮询绝不读系统态; - * 全量进系统一次 + 1s 轻刷时钟/电量/WiFi;状态栏统一读快照(不再 amixer 死镜像)。 - * - B 电源菜单「按电源键」触发关机:电源=按当前项确认;默认光标改「取消」(安全项, - * 避免未导航按电源即关机);取消→只关菜单绝不关机;关机/重启/回HibyOS→按对应项才执行; - * 菜单里按电源键本身绝不触发 poweroff。菜单返回后清 pwr_hold_at/pwr_menu_state, - * 杜绝「按住电源开菜单→确认→仍按住→run_menu 8s 强关」残留覆盖。 - * - C TF 卡热插拔最佳努力:拔出检测(/proc/mounts + /dev/mmcblk0 消失)→ 标记无卡、 - * 停止扫描、优雅中止不崩;插入检测 → 自动挂载 + 重扫 MeowAPPs + 重载字库 + 日志迁回; - * 写日志/挂载前确认,失败回退内存;扫描遍历安全终止防死循环。 - * - D 运行中应用卡拔:前台 MeowAPP 二进制/资源在卡上,卡被拔 → 启动器主动 SIGTERM - * 应用(干净退出,避免等 SIGBUS 崩),显示「卡已移除,应用已结束」,回菜单。 - * - E:音量键调软件音量(settings audio.volume 0-100 步进5 clamp,持久化); - * 蓝牙读实际 powered(关=灰/不显示);WiFi 读实际(operstate);唤醒偶发加日志。 - * MEOWOS_VERSION 0.7.5 → 0.7.6。 - * - * M2.10 修复批次(2026-08-28): - * - Bug:电源菜单「回到 HibyOS」点一下就关机。根因:电源菜单 8s 强关(按住电源≥8s 绕过)与 - * 用户点选按钮竞争 —— 8s 检查在触摸处理之前,用户按住电源开菜单后(仍按着)点「回到HibyOS/ - * 重启」,8s 一到先触发强关(poweroff)而非执行所选动作。修法:用户一旦与菜单互动(音量导航/ - * 触摸/播放/电源确认)置 pm_interacted=1,8s 强关取消;8s 强关移到输入处理之后。另复核 - * 「回到HibyOS」动作即 set_boot_default_hiby()+sys_do_reboot(RESTART)(RESTART 已真机确认会 - * 重启,非关机)。版本 0.7.5。 - * - * M2.9 修复批次(2026-08-28): - * - Bug 2:电源菜单 POWER MENU 标题 y=300 与 2×2 按钮(pm_y0 顶缘 320)重叠被遮, - * 标题上移到 y=150(标题在上、按钮在下,不重叠)。 - * - Bug 4:「返回播放器」确认框是/否按钮主循环内每 10ms 全刷 → 频闪;改为 dirty 绘制 - * (仅 sel 变化才重画两钮),提示行移出循环只画一次。 - * - 复核电源菜单动作直连 syscall(SYS_reboot)(M2.7 已改,本批确认路径一致)。 - * 版本 0.7.4。 - * - * M2.8 修复批次(2026-08-28):Bug 3 残留 —— MeowOS 主屏用电源键息屏后再按电源, - * 只有背光无画面(M2.7 加了 fb_norm_buf0 仍黑)。根因:fb_norm_buf0 在缓存 - * fb_active_yoffset==0 时短路不再重发 FBIOPAN_DISPLAY,而 FBIOBLANK(4)->(0) 后 LCDC - * 扫描地址可能已复位到非 buffer0/失效位置,缓存仍是 0 导致真正的 re-pan 从未发出; - * 且唤醒不是 draw 事件,buffer0 内容可能为空。修法:新增 fb_pan_buf0_force(无条件 - * 重发 pan)+ meow_repaint_surface(唤醒时把当前界面整体重绘到 buffer0 再强制 pan), - * 电源菜单/确认框也在「息屏态打开」时强制 re-pan。版本 0.7.3。 - * - * M2.2 修复批次(2026-08-28): - * - MeowOS 常驻主界面:启动即自绘应用列表主界面(buffer0+pan0,不等热键), - * 绝不遗留播放器死帧;播放器已死时不备份/不冻结/不 SIGCONT、不恢复死帧。 - * - BACK TO PLAYER(播放器已死)→ 显示「重启后返回播放器」提示并留在主界面。 - * - MeowOS 系统拔营:无播放器时由 launcher 接管 TF 卡挂载(/dev/mmcblk0 -> - * /usr/data/mnt/sd_0, exfat)与 WiFi 联网(/usr/bin/wifi_up.sh),后台周期重试。 - * - * Design (see ANALYSIS.md §18): - * - PASSIVE multi-reader: never EVIOCGRABs, never injects events. - * Physical key events multicast to all evdev readers on this kernel, - * but injected events (write()) update device state yet are NEVER - * delivered to any client (vendor input-subsystem quirk, verified). - * => keys are read via EVIOCGKEY state polling (immune to any grab, - * exact mechanism the player itself uses for the power key), touch is - * read via normal event reads on event1 (not grabbed by the player). - * - Hotkey (v6.5): HOLD power, then a SINGLE VOL_DOWN tap 60-2500ms after - * the power-down edge opens the menu. The player sees the power press (its - * screen-off flow starts — accepted side effect; the menu open undoes it - * via the verified unblank + bring-backlight-only-if-dark logic) and the - * VOL_DOWN tap (volume -1 — accepted). Legendary sequence mode - * (N quick taps of one key, default VOL_UP x3) remains available via - * meow.conf hotkey=seq; the seq taps raise volume, snapshotted and - * restored on trigger. A two-key chord on event2 is impossible (its four - * keys share ONE ADC channel), and the PREVIOUS key's GPIO (PC28) never - * fires on this unit (§18.12). Configurable via MeowAPPs/meow.conf: - * hotkey=power_voldown|seq / hotkey_seq=volup,volup,volup / - * hotkey_seq_gap=600. Inside an app, VOL_DOWN x3 quick taps force-kill - * the app (volume restored too). - * - Menu: SIGSTOP hiby_player, bring the backlight back ONLY if the panel - * is dark (brightness 0 / blanked — the player's own screen timeout or - * standby); a normal brightness is left untouched (Bug A: the old - * force_backlight() yanked the brightness to 101 on every menu open), - * draw app list on fb0 (scan /usr/data/mnt/sd_0/MeowAPPs//app.json, - * 5 edge cases), navigate with touch or keys, launch app via - * fork/execve/waitpid, spawn meow_watchdog for the virtual back - * key, then SIGCONT. - * - Double-buffer (v6.6): fb0 is double-buffered — FBIOGET_VSCREENINFO - * reports yres_virtual=1440 (2x720), smem_len=1382400 (exactly 2 - * frames), ypanstep=1 (verified on device), and the DISPLAYED buffer is - * selected by fb_var.yoffset via FBIOPAN_DISPLAY (0x4606, verified to - * flip 0<->720). The player's power/screen-off redraw leaves yoffset on - * a back buffer, so the menu drawn ONLY at mmap offset 0 (buffer0) can - * land on the buffer the panel is NOT showing -> menu invisible, screen - * frozen on the player's last frame (the v6.5 bug). Fix: at menu open - * snapshot the player's displayed frame (the buffer at the current - * yoffset) into a heap backup, pan the display to buffer0 (what this - * code AND every app draw to — apps 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 + pan to the original - * yoffset so a SIGSTOPped player that does not redraw (event-driven, - * §18.15) still shows its own frame. fb is mapped over the FULL virtual - * framebuffer (both frames), not just one. - * - Backlight discipline (2026-08-28 hotfix): the launcher NEVER touches - * FBIOBLANK outside the menu. The player owns the panel blank state: - * the bkl_time ("背光时间") screen timeout writes brightness 0 + - * FBIOBLANK(4) and the player stops drawing while it believes the panel - * is blanked. The old backlight keeper restored brightness + - * FBIOBLANK(0) behind its back, which wedged the player's display state - * machine (UI frozen, keys dead — device log: keeper fired at 40.9s - * post-boot, player wedged). Do NOT reintroduce unblanking outside the - * menu. Inside the menu: unblank_panel() runs AFTER stop_player() so - * the frozen player cannot re-blank behind the menu (v6.2 hotfix — - * unblanking while the player was still running let it re-blank, so the - * menu was drawn on a black panel); on session end restore_backlight() - * re-blanks first (if the menu opened while blanked) and only then the - * player resumes, keeping its state machine consistent. ensure_backlight - * / restore_backlight only ever write brightness (harmless; ignored - * while the panel is blanked) and bl_power. - * - Player watchdog: SIGSTOP does not exit the process, so the - * hiby_player.sh "exit -> reboot" watchdog never triggers. - * - UI: pure black background by default (configurable via - * MeowAPPs/meow.conf bg_color=RRGGBB), slim 20px status bar with - * clock / battery / wifi. - * - Logging: unified to the TF card MeowOS/logs/meow_.log - * (boot timestamp from wall-clock minus uptime); when the card is absent - * (S90 starts before the card mounts) logs go to a bounded /tmp boot - * buffer (/tmp/meow_boot.log, recoverable over SSH, survives daemon - * restarts) and are moved into the card log automatically once the card - * mounts (periodic retry in the main loop — no menu needed). NEVER write - * onboard storage, which is nearly full. Watchdog gets - * MEOW_LOG_FILE, apps get MEOW_LOG_DIR via the environment. - * - * Static MIPS32r2 musl build: - * ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -static meow_launcherd.c -o meow_launcherd - */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include /* 0-warning:musl 把 重定向到 ,用后者消除 -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 -#include -#include /* Bug 2:直连 reboot(2) syscall 用 */ -#include /* M0:IPC 线程 */ -#include /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */ -#include /* M0:AF_UNIX IPC */ -#include /* M0:sockaddr_un */ -#include -#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,可单测) */ - -/* ---- paths ---- */ -#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" -/* M2.24 全面按键接管:watchdog(应用运行期)检测到电源长按≥3s 写此标记,launcher 在 - * waitpid 轮询中消费 → SIGSTOP app + 弹统一电源菜单;菜单期 launcher 写 powmenu_active - * 让 watchdog 让位,菜单关后清除再 SIGCONT app。 */ -#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" - -/* ---- key codes ---- */ -#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 - -/* ---- tuning ---- */ -#define SEQ_DEFAULT_TAPS 3 /* default hotkey: VOL_UP x3 quick taps */ -#define POLL_MS 10 -/* Single source of truth for the MeowOS build version. The on-screen label - * ("MeowOS v dev", bottom-right of the app list) and the - * etc/meowos_version file that build_fw_apps.sh greps from this line must both - * agree — change the version HERE only, never in two places. */ -#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 */ -/* 阶段 D · M1:Activity 路由注册表(g_accts)容量。route=app.json/声明的路由行, - * svc=service. 行(M1 仅解析存储,保活 M4 实现)。 */ -#define MEOW_MAX_ACTS 16 -#define MEOW_MAX_ROUTES 8 -#define MEOW_MAX_SVCS 8 - -/* ---- event codes / ioctls ---- */ -#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 */ - -/* ================================================================== */ -/* unified logging: TF card MeowOS/logs/meow_.log */ -/* - no TF card at startup (S90 runs before the card mounts) -> */ -/* bounded /tmp boot buffer (/tmp/meow_boot.log): recoverable */ -/* over SSH, survives daemon restarts within the boot session */ -/* - log_poll_card() (called periodically in the main loop) opens */ -/* the card log as soon as the card mounts and moves the /tmp */ -/* buffer into it — no menu needed */ -/* - NEVER write onboard storage (nearly full) */ -/* ================================================================== */ -#define BOOT_BUF_TMP "/tmp/meow_boot.log" -#define BOOT_BUF_MAX (256 * 1024) -static int log_fd = -1; /* file log fd on the TF card */ -static char log_dir[256] = ""; /* "MeowOS/logs" on card, or "" */ -static char log_file[256] = ""; /* full path of the boot log file */ - -static int card_available(void) -{ - /* TF card mounted? */ - FILE *f = fopen(MOUNTS, "r"); - if (!f) return 0; - char line[256]; - int found = 0; - while (fgets(line, sizeof line, f)) { - if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; } - } - fclose(f); - if (!found) return 0; - /* writable logs dir? */ - mkdir(LOG_DIR_CARD, 0755); - return access(LOG_DIR_CARD, W_OK) == 0; -} - -static void boot_timestamp(char *out, size_t n) -{ - /* reuse the timestamp cached for this boot session (/tmp is cleared on - * reboot) so repeated launcher starts in one boot share one log file */ - int fd = open("/tmp/meow_bootts", O_RDONLY); - if (fd >= 0) { - int r = read(fd, out, (int)n - 1); - close(fd); - if (r > 0) { out[r] = 0; return; } - } - time_t now = time(NULL); - time_t up = 0; - fd = open("/proc/uptime", O_RDONLY); - if (fd >= 0) { - char buf[64]; - int r = read(fd, buf, sizeof buf - 1); - close(fd); - if (r > 0) { - buf[r] = 0; - char *p = strchr(buf, ' '); - if (p) *p = 0; - up = (time_t)atof(buf); - } - } - time_t boot = (time_t)((double)now - (double)up + 0.5); - struct tm tm; - localtime_r(&boot, &tm); - strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm); - /* cache for the rest of this boot session */ - fd = open("/tmp/meow_bootts", O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd >= 0) { - write(fd, out, strlen(out)); - close(fd); - } -} - -static unsigned long long now_ms(void) -{ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; -} - -/* M2.18 Bug 1: 无符号回绕保护的「空闲时长」。 - * launch_app()/confirm_reboot_player() 是阻塞调用,主循环 for(;;) 顶部的 t=now_ms() - * 在 app 运行期间被卡住而停在启动前旧值;app 退出后 last_input_ms 被置为 app 退出 - * 时刻(较大值)。若该圈 any_input==0(如触摸释放单独落在下一圈、音量/播放边沿发生在 - * 更早圈),line「if (any_input) last_input_ms = t」不执行 → 检查处 t < last_input_ms, - * (unsigned long long)(t - last_input_ms) 无符号相减回绕成 2^64-Δ 的极大值,恒 >= 30s - * → 误判「空闲超时」→ 立即 meow_panel_off()。r3proii 日志铁证:auto screen-off after - * 30s idle (t=141578 last=145688),仅 122ms 即被误判为已空闲 30s。凡判定空闲一律经此 - * 函数取差:t < last(t 过期)时返回 0,绝不回绕。 */ -static unsigned long long idle_ms_since(unsigned long long t, unsigned long long last) -{ - return (t >= last) ? (t - last) : 0; -} - -/* remove oldest log files until total size <= limit (called at boot) */ -static void log_cleanup_old(void) -{ - struct dirent *de; - DIR *d = opendir(LOG_DIR_CARD); - if (!d) return; - /* collect meow_*.log with mtime + size */ - struct logfile { char name[64]; long long size; long long mtime; } files[64]; - int n = 0; - while ((de = readdir(d)) && n < 64) { - if (strncmp(de->d_name, "meow_", 5)) continue; - char path[320]; - snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, de->d_name); - struct stat st; - if (stat(path, &st) || !S_ISREG(st.st_mode)) continue; - files[n].mtime = (long long)st.st_mtime; - files[n].size = (long long)st.st_size; - snprintf(files[n].name, sizeof files[n].name, "%s", de->d_name); - n++; - } - closedir(d); - long long total = 0; - for (int i = 0; i < n; i++) total += files[i].size; - /* simple selection sort by mtime ascending (oldest first) */ - for (int i = 0; i < n; i++) - for (int j = i + 1; j < n; j++) - if (files[j].mtime < files[i].mtime) { - struct logfile t = files[i]; - files[i] = files[j]; files[j] = t; - } - int deleted = 0; - for (int i = 0; i < n && total > (long long)LOG_SIZE_LIMIT; i++) { - char path[320]; - snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, files[i].name); - if (unlink(path) == 0) { total -= files[i].size; deleted++; } - } - if (deleted) fprintf(stderr, "log_cleanup: deleted %d old log(s), total now %lld\n", - deleted, total); -} - -/* try to open the card log file; returns 0 on success */ -static int log_open_card(void) -{ - if (log_fd >= 0) return 0; - if (!card_available()) return -1; - char ts[64]; - boot_timestamp(ts, sizeof ts); - snprintf(log_dir, sizeof log_dir, "%s", LOG_DIR_CARD); - snprintf(log_file, sizeof log_file, "%s/%s.log", LOG_DIR_CARD, ts); - int fd = open(log_file, O_WRONLY | O_APPEND | O_CREAT, 0644); - if (fd < 0) return -1; - log_fd = fd; - return 0; -} - -static void logmsg(const char *fmt, ...); - -/* flush the /tmp boot buffer into the file log (once the card mounts) */ -static void log_poll_card(void) -{ - if (log_fd >= 0) return; - if (log_open_card() != 0) return; - /* move the /tmp boot buffer into the card log, then drop it */ - int fd = open(BOOT_BUF_TMP, O_RDONLY); - if (fd >= 0) { - char chunk[1024]; - ssize_t n; - while ((n = read(fd, chunk, sizeof chunk)) > 0) { - if (write(log_fd, chunk, n) < 0) { close(fd); log_fd = -1; return; } - } - close(fd); - unlink(BOOT_BUF_TMP); - logmsg("boot buffer moved into %s", log_file); - } else { - logmsg("card log open: %s", log_file); - } -} - -/* bounded append to the /tmp boot buffer (no card log open yet) */ -static void boot_buf_put(const char *line, int len) -{ - struct stat st; - if (stat(BOOT_BUF_TMP, &st) == 0 && st.st_size > BOOT_BUF_MAX) - unlink(BOOT_BUF_TMP); /* roll the buffer (boot-stage only) */ - int fd = open(BOOT_BUF_TMP, O_WRONLY | O_APPEND | O_CREAT, 0644); - if (fd < 0) return; - if (write(fd, line, len) < 0) { /* best effort */ } - close(fd); -} - -static void logmsg(const char *fmt, ...) -{ - char buf[512]; - va_list ap; - va_start(ap, fmt); - vsnprintf(buf, sizeof buf, fmt, ap); - va_end(ap); - char line[576]; - int l = snprintf(line, sizeof line, "%llu %s\n", now_ms(), buf); - if (log_fd >= 0) { - if (write(log_fd, line, l) < 0) log_fd = -1; /* card gone -> buffer */ - } - if (log_fd < 0) boot_buf_put(line, l); - fprintf(stderr, "%llu %s\n", now_ms(), buf); -} - -/* ================================================================== */ -/* fb0 helpers (RGB565 / RGBA8888 auto) */ -/* ================================================================== */ -/* NOTE: on this device FBIOGET_VSCREENINFO=0x4600, FSCREENINFO=0x4602 - * (device-verified legacy numbers; do NOT swap them). */ -#define FBIOGET_VSCREENINFO 0x4600 -#define FBIOGET_FSCREENINFO 0x4602 -#define FBIOPAN_DISPLAY 0x4606 -#define FBIOBLANK 0x4611 -#define FBIO_WAITFORVSYNC 0x4620 - -static unsigned char *fb; /* 绘制目标指针 = SDK 的 back_buf(A-1/A-2:launcher 与 app 统一渲染栈) */ -static unsigned char *fb_mem = NULL; /* 真实 mmap 帧缓冲(播放器帧备份/恢复源 + app 帧读回 + 双缓冲 yoffset 用) */ -static int fb_xres, fb_yres, fb_line, fb_bpp; -static 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 上屏」不同层,不动。 */ -static int back_buf_ok = 0; /* fb 是否指向 SDK back_buf(1=可经 SDK present 上屏) */ - -/* ---- M2.16 按键防抖 + 单击/长按界限 ---- */ -#define KEY_DEBOUNCE_MS 40 /* 键值须稳定此毫秒数才认可(抗 ADC 抖动) */ -#define CLICK_MS 400 /* 按压 <此毫秒 = 单击一次;≥ = 长按(连续) */ -struct kd_state { int raw, stable, down; int longpressed; unsigned long long since, down_at; }; - -/* ---- M2.17 超时自动息屏(display.screen_off_timeout / screen_off_mode)---- - * timeout 单位秒,0=常亮(不自动息屏);mode:0=panel(面板下电+背光0+FBIOBLANK(4),挂起触摸 - * 仅电源/任意键唤醒)、1=black(背光0全黑,触摸可唤醒)。见 SYSTEM_SETTINGS.md。 */ -#define SCREEN_OFF_TIMEOUT_DEFAULT 30 -#define SCREEN_OFF_MODE_DEFAULT 0 /* 0=panel, 1=black */ -static int screen_off_timeout = SCREEN_OFF_TIMEOUT_DEFAULT; -static int screen_off_mode = SCREEN_OFF_MODE_DEFAULT; - -/* ---- M2.37 电源长按语义缓存(power.longpress_menu_ms / power.forceoff_ms)---- - * 设置页可配;默认 3000/8000 与注释「3s 电源菜单 / 8s 强关」一致。缓存供 run_menu - * 电源按住判定使用(原为硬编码 2000/8000/3000,现接线读取以让设置生效)。 */ -#define PWR_MENU_MS_DEFAULT 3000 -#define PWR_FORCE_MS_DEFAULT 8000 -static int power_longpress_menu_ms = PWR_MENU_MS_DEFAULT; -static int power_forceoff_ms = PWR_FORCE_MS_DEFAULT; - -/* ---- M2.16 刷新率调度配置(display.refresh_rate / refresh_always_max)---- - * A-1/A-2:档位上限/恒满速开关由 launcher 配置(settings.conf),经 - * meow_ui_set_refresh_rate()/meow_ui_set_refresh_always_max() 交给 SDK 的 present - * 帧率门控/governor 执行(归口 PERF_SCHEDULER 显示刷新率子域)。当前有效档 eff_rate - * 与帧计数已在 SDK 侧,launcher 不再各自维护一套。 */ -#define REFRESH_RATE_CFG_DEFAULT 60 -static int refresh_rate_cfg = REFRESH_RATE_CFG_DEFAULT; /* 目标档 60/30/15(上限,default 60) */ -static int refresh_always_max = 0; /* 1=恒 max 不降档 */ - -/* M2.16 渲染/防抖前向声明(定义在下方;fb_open 等先引用) */ -static void meow_dirty_full(void); -static void meow_dirty_add(int x, int y, int w, int h); -static void meow_dirty_reset(void); - -static inline void put_px(int x, int y, uint32_t color) -{ - if (x < 0 || x >= fb_xres || y < 0 || y >= fb_yres) return; - unsigned char *p = fb + y * fb_line + x * (fb_bpp / 8); - if (fb_bpp == 16) { - uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) | - ((color >> 3) & 0x001F); - *(uint16_t *)p = c; - } else { - *(uint32_t *)p = (color << 8) | 0xFF; - } -} - -static void fill_rect(int x0, int y0, int w, int h, uint32_t color) -{ - for (int y = y0; y < y0 + h; y++) - for (int x = x0; x < x0 + w; x++) - put_px(x, y, color); -} - -/* 5x7 font (same table as hello.c) */ -static const unsigned char font[][8] = { - {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00}, - {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00}, - {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00}, - {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00}, - {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00}, - {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00}, - {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00}, - {0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00}, - {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00}, - {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00}, - {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, - {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00}, - {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00}, - {0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00}, - {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00}, - {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00}, - {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00}, - {0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, - {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00}, - {0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00}, - {0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00}, - {0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00}, - {0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00}, - {0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00}, - {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, - {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00}, - {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00}, - {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00}, - {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00}, - {0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00}, - {0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, - {0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00}, - {0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00}, - {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00}, - {0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00}, - {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00}, - {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, - {0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00}, - {0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00}, - {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, - {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, -}; - -static int glyph_index(char c) -{ - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'A' && c <= 'Z') return 10 + (c - 'A'); - if (c >= 'a' && c <= 'z') return 10 + (c - 'a'); - if (c == ' ') return 36; - if (c == '.') return 37; - if (c == '!') return 38; - if (c == '-' || c == '_') return 39; - return 36; -} - -static void draw_glyph(int x, int y, char c, int scale, uint32_t color) -{ - const unsigned char *g = font[glyph_index(c)]; - for (int row = 0; row < 7; row++) { - unsigned char bits = g[row]; - for (int col = 0; col < 5; col++) { - if (bits & (0x10 >> col)) { - for (int dy = 0; dy < scale; dy++) - for (int dx = 0; dx < scale; dx++) - put_px(x + col * scale + dx, y + row * scale + dy, color); - } - } - } -} - -static void draw_text(int x, int y, const char *s, int scale, uint32_t color) -{ - while (*s) { - draw_glyph(x, y, *s, scale, color); - x += 6 * scale; - s++; - } -} - -/* ================================================================== */ -/* M2.6 meow_font 集成(agent-28 字体子系统,font/meow_font.a + libz.a)*/ -/* M2.6 MeowOS 字重结构:字库放 TF 卡 MeowOS/fonts/default/regular/font.png+font.idx - *(或固件 /usr/share/meowos/font/);MeowOS 启动 ensure_card_mounted 后再 load,卡晚 */ -/* 挂载时轮询重试。没加载成功时 ui_draw 回退 5x7 点阵(ASCII)。 */ -/* ================================================================== */ -#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" -/* M2.5x 多字体库 + M2.6 字重:字体目录 = <卡根>/MeowOS/fonts///(font.png+font.idx+font.json)。 - * <卡根> 默认 /usr/data/mnt/sd_0(card_present() 判定;--fontscan 可指定宿主根以便单测)。 */ -#define MEOW_FONTS_CFG "MeowOS/fonts" -#define FONT_PRESETS_MAX 17 /* 1 默认 + ≤16 卡内字体(防爆) */ -/* M2.6 字重结构:卡内字体 = <卡根>/MeowOS/fonts///(三件套)。 - * 每个 preset = 一套 /(如 misans/regular)。值 = 卡根相对路径 - * MeowOS/fonts///font.png;family/weight 单列供设置页「family→字重」子菜单 - * 分组与 meow_font_effective_paths_tiered 按 family+weight 选库。 */ -static const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */ -static 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]; -static const char *font_preset_fams[FONT_PRESETS_MAX]; /* family slug(目录一级) */ -static const char *font_preset_wts[FONT_PRESETS_MAX]; /* weight slug(目录二级) */ -static 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 持久存储 */ -static int font_presets_n = 0; /* 运行时动态计数(默认=1,仅"默认字体") */ -static int mfont_ready = 0; -static int mfont_forced = 0; /* font.file 变更→强制真实(un)load,跳过 SDK 复用捷径 */ - -/* 24-bit RGB → RGB565(与 put_px/meow_font 一致) */ -static uint16_t rgb565(uint32_t c) -{ - return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)); -} - -/* fb_open 后调用:设置字库绘制目标为当前双缓冲(buffer0 起点 = fb 偏移 0) */ -static 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 参数无效。 */ -static 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; -} - -/* 前向声明:meow_font_ensure 在下方 font 区中调用(定义在 meow_font_effective_paths_tiered 处)。 */ -static int meow_font_effective_tier(int app_tier); -static int meow_font_effective_paths_tiered(int tier, char *out_png, int plen, - char *out_idx, int ilen, char *out_family, int flen); - -/* 解析 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 点阵(系统不死)。 */ -static 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///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"。 */ -static 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///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。 */ -static 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/// 里找 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 无可用。 */ -static 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。 */ -static 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); -} - -/* 统一文本绘制:字库就绪用 meow_font(真字体/中文),否则回退 5x7 点阵。 - * size 为字库字号(28=状态栏/大标题,20=正文/按钮);回退时按 scale 近似。 */ -static void ui_draw(int x, int y, const char *s, uint32_t color, int size) -{ - if (mfont_ready) { - meow_font_drawtext((uint16_t *)fb, x, y, s, rgb565(color), size); - } else { - draw_text(x, y, s, (size >= 16) ? 2 : 1, color); - } -} - -/* 文本宽度(字库 or 回退估算),用于右对齐/居中 */ -static int ui_width(const char *s, int size) -{ - if (mfont_ready) return meow_font_width_text(s, size); - return (int)strlen(s) * 6 * ((size >= 16) ? 2 : 1); -} - -/* 文本高度(字库行高 or 回退 7×scale 估算),用于垂直居中 / 行高预算。 */ -static int ui_text_height(int size) -{ - if (mfont_ready) return meow_font_line_height(size); - return 7 * ((size >= 16) ? 2 : 1); -} - -static 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. */ -static 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. */ -static 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,无频闪),供唤醒/重绘路径调用,确保重画的界面一定落在被显示的缓冲。 */ -static 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. */ -static 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; -} - -/* ================================================================== */ -/* A-1/A-2 渲染桥:launcher 手动脏模型 → SDK 脏追踪,呈现走 meow_ui_flush() */ -/* launcher 的绘制(put_px/fill_rect/meow_font/dim_screen)直接把像素画进 */ -/* SDK 的 back_buf(fb),在此仅把「哪些区域变了」桥进 SDK 的脏追踪; */ -/* present 不再由 launcher 做,统一 meow_ui_flush() = SDK present(内含 */ -/* vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自带 meow_present/ */ -/* refresh_governor —— 帧率调度已归口 SDK(PERF_SCHEDULER 显示子域)。 */ -/* ================================================================== */ - -/* 标记全屏脏:桥进 SDK 的整帧脏,下次 meow_ui_flush() 整帧 blit。 */ -static void meow_dirty_full(void) { meow_gfx_mark_full(); } - -/* 清空脏集:SDK present 上屏后自清;launcher 侧无需再清(保留接口兼容)。 */ -static void meow_dirty_reset(void) { } - -/* 标记一个矩形脏:桥进 SDK 脏追踪(相交合并/超限转全屏由 SDK dirty_add 处理)。 */ -static void meow_dirty_add(int x, int y, int w, int h) { meow_gfx_mark_dirty(x, y, w, h); } - -/* ---- 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 }; -static 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. */ -static 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)"); -} - -/* Backlight handling (defined below): the menu only brings the panel back - * when it is actually dark (brightness 0 / panel blanked) and otherwise - * leaves the player's brightness untouched — the old force_backlight() - * yanked the brightness to 101 on every menu open (Bug A). */ -static int read_brightness(void); -static void write_brightness(int val); -static void ensure_backlight(void); - -/* ---- MeowOS 面板自持(M2.6:播放器已死,MeowOS 拥有面板/背光,无拔河)---- - * 电源键按时长三分:单击(短按松开)= 息屏/亮屏;3s=电源菜单;8s=强制关机。 - * 息屏 = 亮度0 + FBIOBLANK(4)(面板下电,同时挂起 goodix 触摸,无误触); - * 亮屏 = FBIOBLANK(0) + 恢复亮度。 */ -static int meow_screen_on = 1; -static int meow_saved_brightness = 60; -static int meow_auto_slept = 0; /* M2.17:当前是否「超时自动息屏」置位(手动电源息屏不置位) */ -static void meow_panel_off(void); -static void meow_panel_on(void); -static 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"); -} -static 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); -} -static void meow_toggle_panel(void) { if (meow_screen_on) meow_panel_off(); else meow_panel_on(); } - -/* ================================================================== */ -/* key state polling (EVIOCGKEY) */ -/* ================================================================== */ -struct keypoll { - int fd; - unsigned long cur[KEYBUF_WORDS]; - unsigned long prev[KEYBUF_WORDS]; - int ok; -}; - -static 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 时不动) */ -static 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. */ -static 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; -} - -static int kp_is_down(const struct keypoll *k, int code) -{ - return (k->cur[code / 32] >> (code % 32)) & 1; -} - -/* ================================================================== */ -/* app scanning (MeowAPPs spec) */ -/* ================================================================== */ -/* 路由行:route.=。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.=1 / route..keep_awake=1) */ - int fullscreen; /* 活动级全屏声名(app.json route..fullscreen=1;M3 活动切换时消费) */ -}; - -/* 服务行:service.=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. 行数 */ - 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 运行时声明过 */ -}; - -static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */ -static int g_naccts = 0; - -/* 阶段 D · M0:主循环排空 IPC 队列(定义在下方 IPC 骨架区;run_menu/main 调用) */ -static void meow_ipc_process(void); - -/* ---- 阶段 D · M1:路由注册表辅助(主循环单写者,无锁)---- */ -static struct meow_acct *meow_acct_find(const char *app_id) -{ - if (!app_id) return NULL; - for (int i = 0; i < g_naccts; i++) - if (!strcmp(g_accts[i].app_id, app_id)) return &g_accts[i]; - return NULL; -} - -/* 取(或在末尾新建)app_id 对应的注册表项。返回 NULL 当满。 */ -static struct meow_acct *meow_acct_upsert(const char *app_id) -{ - if (!app_id || !app_id[0]) return NULL; - struct meow_acct *a = meow_acct_find(app_id); - if (a) return a; - if (g_naccts >= MEOW_MAX_ACTS) { logmsg("act: registry full, drop %s", app_id); return NULL; } - a = &g_accts[g_naccts++]; - memset(a, 0, sizeof *a); - strncpy(a->app_id, app_id, MEOW_APP_ID_MAX - 1); - return a; -} - -/* 由 scan_apps 调用:把 app.json 静态路由整体同步进注册表(扫描即权威,替换)。 */ -static void meow_acct_sync(const char *app_id, const struct app *ap) -{ - if (!ap) return; - struct meow_acct *a = meow_acct_upsert(app_id); - if (!a) return; - a->framework = ap->framework; - strncpy(a->icon, ap->icon, 128); - strncpy(a->name, ap->name, 16); - strncpy(a->exec, ap->exec, 128); - a->ok = ap->ok; - a->nroute = ap->nroute > MEOW_MAX_ROUTES ? MEOW_MAX_ROUTES : ap->nroute; - for (int i = 0; i < a->nroute; i++) a->routes[i] = ap->routes[i]; - a->nsvc = ap->nsvc > MEOW_MAX_SVCS ? MEOW_MAX_SVCS : ap->nsvc; - for (int i = 0; i < a->nsvc; i++) a->svcs[i] = ap->svcs[i]; -} - -/* 运行时 HELLO:声明 app 存在(补入注册表),记录声明源。 */ -static void meow_acct_hello(const char *app_id, uint32_t version, uint32_t flags) -{ - struct meow_acct *a = meow_acct_upsert(app_id); - if (a) { a->declared = 1; (void)version; (void)flags; } - logmsg("act: hello app=%s ver=%u flags=%u", app_id, version, flags); -} - -/* 运行时 ACT_DECLARE:并集覆盖静态路由。entries=count 个 NUL 结尾 "id=chain" 串。 */ -static void meow_acct_declare(const char *app_id, const char *entries, int count) -{ - struct meow_acct *a = meow_acct_upsert(app_id); - if (!a || !entries || count <= 0) return; - a->declared = 1; - const char *p = entries; - for (int k = 0; k < count && *p; k++) { - const char *eq = strchr(p, '='); - if (eq && eq > p) { - int idlen = (int)(eq - p); - if (idlen >= 16) idlen = 15; - char id[16]; - memcpy(id, p, idlen); id[idlen] = 0; - const char *chain = eq + 1; - int ri = -1; - for (int i = 0; i < a->nroute; i++) - if (!strcmp(a->routes[i].act, id)) { ri = i; break; } - if (ri < 0) { - if (a->nroute >= MEOW_MAX_ROUTES) { p += strlen(p) + 1; continue; } - ri = a->nroute++; - } - strncpy(a->routes[ri].act, id, 15); - strncpy(a->routes[ri].chain, chain, MEOW_CHAIN_MAX - 1); - } - p += strlen(p) + 1; - } -} - -/* 链路深度 = ';' 分隔段数(根屏→目标屏的逻辑链长)。 */ -static int chain_depth(const char *c) -{ - if (!c || !*c) return 0; - int d = 1; - for (; *c; c++) if (*c == ';') d++; - return d; -} - -/* 解析 activity 链路:填 out_chain(root..target)与 depth。 - * 返回 0=成功;MEOW_ERR_NOENT=app 未注册 / activity 未注册。 - * 规则(FRAMEWORK_DESIGN §2):route 缺失(app 未声明任何路由)→ chain=activity、深度 1 兜底; - * 声明了路由但 activity 非 route 键 → MEOW_ERR_NOENT(activity 未注册)。 */ -static int resolve_chain(const char *app_id, const char *activity, char *out, int *depth) -{ - if (!out || !depth) return MEOW_ERR_INVAL; - struct meow_acct *a = meow_acct_find(app_id); - if (!a) return MEOW_ERR_NOENT; - if (a->nroute == 0) { - strncpy(out, activity, MEOW_CHAIN_MAX - 1); - *depth = 1; - return MEOW_OK; - } - for (int i = 0; i < a->nroute; i++) { - if (!strcmp(a->routes[i].act, activity)) { - strncpy(out, a->routes[i].chain, MEOW_CHAIN_MAX - 1); - *depth = chain_depth(a->routes[i].chain); - return MEOW_OK; - } - } - return MEOW_ERR_NOENT; -} - -/* ---- M2.17 keep_awake:前台当前顶层 activity 追踪 ---- - * 前台应用经 IPC MSG_ACT_FOCUS/BACK 上报其当前顶层 activity;fg_keep_awake() 依注册表判断该 - * activity 是否声明 keep_awake(app.json awake.=1 / route..keep_awake=1;运行时同类)。 - * 主界面(无上报/空)= 非 keep_awake(主页空闲照常超时息屏)。单一写者=主循环(IPC 排空处)。 */ -static char g_fg_app[MEOW_APP_ID_MAX] = ""; -static char g_fg_act[MEOW_ACT_MAX] = ""; -static void meow_fg_act_set(const char *app_id, const char *act) -{ - if (!app_id) return; - strncpy(g_fg_app, app_id, MEOW_APP_ID_MAX - 1); - g_fg_app[MEOW_APP_ID_MAX - 1] = 0; - if (act) { strncpy(g_fg_act, act, MEOW_ACT_MAX - 1); g_fg_act[MEOW_ACT_MAX - 1] = 0; } - else g_fg_act[0] = 0; - logmsg("keep_awake: fg_act app=%s act=%s", g_fg_app, g_fg_act); -} -static void meow_fg_act_back(void) -{ - g_fg_app[0] = 0; - g_fg_act[0] = 0; - logmsg("keep_awake: fg_act back to home (cleared)"); -} -/* 前台当前 activity 是否 keep_awake(无上报/未注册/未声明=0,即照常超时息屏)。 */ -static int fg_keep_awake(void) -{ - if (!g_fg_app[0] || !g_fg_act[0]) return 0; - struct meow_acct *a = meow_acct_find(g_fg_app); - if (!a) return 0; - for (int i = 0; i < a->nroute; i++) - if (!strcmp(a->routes[i].act, g_fg_act)) return a->routes[i].keep_awake; - return 0; -} - -/* 标识符字符(含 '.',用于防止 json_str 误命中更长键的一部分) */ -static int ident_ch(char c) -{ - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || c == '_' || c == '.'; -} - -/* 极简 JSON/扁平值提取:在一行里找 key,读取其值(跳过 ':'/'=' 与可选引号)。 - * 支持 JSON `"name": "Hello"` 与扁平 `framework=true`。返回拷贝到 out 的字符数。 - * 返回 0=未命中/空。 */ -static int json_str(const char *buf, const char *key, char *out, int olen) -{ - out[0] = 0; - if (!buf || !key || olen <= 0) return 0; - const char *p = strstr(buf, key); - if (!p) return 0; - if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */ - p += strlen(key); - if (*p == '"') p++; /* JSON 键自身右引号:跳到 ':'/'=' */ - while (*p && (*p == ':' || *p == '=')) p++; - while (*p == ' ' || *p == '\t') p++; - if (*p == '"') p++; /* 值左引号(JSON 字符串值) */ - int i = 0; - while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1) - out[i++] = *p++; - out[i] = 0; - while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0; - return i; -} - -/* state: 0 ok / 1 no storage card / 2 no usable apps */ -static int card_present(void) -{ - FILE *f = fopen(MOUNTS, "r"); - if (!f) return 0; - char line[256]; - int found = 0; - while (fgets(line, sizeof line, f)) { - if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; } - } - fclose(f); - return found; -} - -/* ================================================================== */ -/* MeowOS 系统拔营(M2.2 批次):TF 卡挂载 + WiFi 联网 */ -/* 官方环境由 hiby_player / sys_server 负责这两件事;MeowOS 作为主系统 */ -/* 时无播放器,必须由 launcher 主动接管。失败不阻塞主界面,后台周期重试。 */ -/* ================================================================== */ -static int sd_mounted(void) -{ - return card_present(); -} - -/* TF 卡设备节点是否出现(任一常见节点即视为在卡) */ -static int sd_device_present(void) -{ - return access("/dev/mmcblk0", F_OK) == 0 || access("/dev/mmcblk0p1", F_OK) == 0; -} - -/* 主动挂载 TF 卡到 /usr/data/mnt/sd_0。返回 1=已挂载,0=尚未(调用方轮询重试)。 - * 依次尝试 /dev/mmcblk0、/dev/mmcblk0p1,先 -t exfat 再自动探测;卡未插入返回 0。 */ -static int ensure_card_mounted(void) -{ - if (sd_mounted()) return 1; - if (!sd_device_present()) return 0; - const char *nodes[2] = { "/dev/mmcblk0", "/dev/mmcblk0p1" }; - for (int i = 0; i < 2; i++) { - if (access(nodes[i], F_OK) != 0) continue; - char cmd[160]; - snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; " - "PATH=/bin:/sbin:/usr/bin:/usr/sbin mount -t exfat %s /usr/data/mnt/sd_0 2>/dev/null", - nodes[i]); - if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted: %s", nodes[i]); return 1; } - snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; " - "PATH=/bin:/sbin:/usr/bin:/usr/sbin mount %s /usr/data/mnt/sd_0 2>/dev/null", - nodes[i]); - if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted (auto): %s", nodes[i]); return 1; } - } - return 0; -} - -/* ================================================================== */ -/* M2.5x 多字体库扫描 + M2.6 字重结构 / preset 动态化 */ -/* 字体目录 = <卡根>/MeowOS/fonts///,每套含三件套: */ -/* 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// 套件是否合法:三件套齐全 → 解析 label+档位+字重。 - * 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 - * 输出:label=显示名 ` (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); - /* 显示名 = ` (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* 扫描结果落盘为 /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); -} - -/* 扫描 /MeowOS/fonts/ 下两级 //,重建字体 preset 列表。 - * 每个 (family,weight) 组合一套(如 misans/regular、misans/bold);family 级 'default' 由 - * 索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX - * (1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。 - * - * M2.58 释放语义(规格 §5.7 需求 4「扫→写 index→释放(或按需读 index)」结论): - * - 持久来源 = /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(较大重构,超出本批边界,留待后续)。 */ -static 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); /* 一级:/ */ - 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:扫描结果落盘为 /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 失败 → 仅默认字体)。 */ -static void meow_font_scan_presets(void) -{ - meow_font_scan_presets_root("/usr/data/mnt/sd_0"); -} - -/* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */ -static 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(其定义在本函数之后)。 */ -static 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 进程) */ -static 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; -} - -/* 确保联网:wlan0 未 up 且尚无 wpa_supplicant 在跑 → 调用 /usr/bin/wifi_up.sh 连保存网络。 - * 幂等:连上 / 已有 wpa_supplicant 则不动;每轮询重试,不阻塞主界面。 - * M2.39:尊重 network.wifi_enabled —— 用户在设置页关 WiFi(=0)则不自动拉起(否则与 - * 手动关冲突);取值失败(未 init)保默认 1=允许自动连网。 */ -static void ensure_wifi(void) -{ - int enabled = 1; - if (meow_settings_get_bool("network.wifi_enabled", &enabled) == 0 && !enabled) return; - if (wlan_connected()) return; - if (proc_by_comm("wpa_supplicant")) return; - logmsg("wifi not up -> /usr/bin/wifi_up.sh"); - system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh"); -} - -/* 解析一个 app.json 流 → 填充 struct app 的 name/exec/icon/framework/routes/svcs。 - * 极简:不做 JSON 校验,仅匹配 key 与路由/服务扁平行(JSON 或扁平皆可)。 - * 不设 name/exec(由调用方从返回的 app 再复制),成功判定由调用方做。 */ -static void meow_parse_app_json(FILE *f, struct app *a) -{ - char buf[256]; - char name[17] = "", exec[129] = "", icon[129] = ""; - char package[MEOW_APP_ID_MAX] = ""; - int fram = 0, immersive = 0; - int fullscreen = 0; /* app.json fullscreen=(注入 MEOW_FULLSCREEN env,SDK 预先设全屏) */ - int fonts = 0, fonts_set = 0; /* app.json "fonts":<档>(0=默认4K常用 1=大字库GBK 2=Full;缺省 0) */ - struct app tmp; - memset(&tmp, 0, sizeof tmp); - while (fgets(buf, sizeof buf, f)) { - if (!name[0]) json_str(buf, "name", name, sizeof name); - if (!exec[0]) json_str(buf, "exec", exec, sizeof exec); - if (!icon[0]) json_str(buf, "icon", icon, sizeof icon); - if (!package[0]) json_str(buf, "package", package, sizeof package); /* 安卓化权威 app_id */ - /* route.=(activity 路由,M1) */ - char *rp = strstr(buf, "route."); - if (rp && tmp.nroute < MEOW_MAX_ROUTES) { - rp += 6; /* 跳过 "route." */ - int idl = 0; char id[32] = ""; /* 读满整键(识别 ".fullscreen" 后缀,cap 31) */ - while (*rp && *rp != '"' && *rp != ':' && *rp != '=' && idl < 31) - id[idl++] = *rp++; - id[idl] = 0; - if (idl > 0) { - char *vp = rp; - while (*vp && *vp != ':' && *vp != '=') vp++; - if (*vp == ':' || *vp == '=') vp++; - while (*vp == ' ' || *vp == '\t') vp++; - if (*vp == '"') vp++; - /* route..fullscreen=:活动级全屏声明(M1 顺带;M3 活动切换时消费)。 - * 命中既有 route. 置 fullscreen;不新建 route(避免 id 被截断误建 "…fulls")。 */ - if (idl >= 11 && !strcmp(id + idl - 11, ".fullscreen")) { - id[idl - 11] = 0; /* 去掉 ".fullscreen" 还原 act */ - char v[8] = ""; int k = 0; - while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' && - *vp != '\r' && k < 7) v[k++] = *vp++; - v[k] = 0; - int on = (!strcmp(v, "1") || !strcmp(v, "true") || !strcmp(v, "yes")); - for (int i = 0; i < tmp.nroute; i++) - if (!strcmp(tmp.routes[i].act, id)) { tmp.routes[i].fullscreen = on; break; } - } else { - int vl = 0; char chain[MEOW_CHAIN_MAX] = ""; - while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' && - *vp != '\r' && vl < MEOW_CHAIN_MAX - 1) - chain[vl++] = *vp++; - chain[vl] = 0; - while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t')) - chain[--vl] = 0; - strncpy(tmp.routes[tmp.nroute].act, id, 15); - tmp.routes[tmp.nroute].act[15] = 0; /* 显式收尾(原依赖 id 读长 ≤15) */ - strncpy(tmp.routes[tmp.nroute].chain, chain, MEOW_CHAIN_MAX - 1); - tmp.nroute++; - } - } - } - /* awake.=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。 - * 命中的既有 route. 置 keep_awake;无 route 则新建单 activity 路由。 */ - char *ap = strstr(buf, "awake."); - if (ap && tmp.nroute < MEOW_MAX_ROUTES) { - ap += 6; /* 跳过 "awake." */ - int al = 0; char aid[16] = ""; - while (*ap && *ap != '"' && *ap != ':' && *ap != '=' && al < 15) - aid[al++] = *ap++; - aid[al] = 0; - if (al > 0) { - char *vp = ap; - while (*vp && *vp != ':' && *vp != '=') vp++; - if (*vp == ':' || *vp == '=') vp++; - while (*vp == ' ' || *vp == '\t') vp++; - if (*vp == '"') vp++; - int on = atoi(vp); - int ri = -1; - for (int i = 0; i < tmp.nroute; i++) - if (!strcmp(tmp.routes[i].act, aid)) { ri = i; break; } - if (ri < 0) { - ri = tmp.nroute++; - strncpy(tmp.routes[ri].act, aid, 15); - strncpy(tmp.routes[ri].chain, aid, MEOW_CHAIN_MAX - 1); /* 单 activity 兜底链路 */ - } - tmp.routes[ri].keep_awake = (on != 0); - } - } - /* framework=true(看门狗返回发 SIGUSR1,M1 解析存储) */ - if (!fram && strstr(buf, "framework")) { - char v[8] = ""; - if (json_str(buf, "framework", v, sizeof v) > 0) { - if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")) - fram = 1; - } - } - /* immersive=true(隐藏虚拟返回键、返回手势移交 SDK;写 /tmp/meowapp_state) */ - if (!immersive && strstr(buf, "immersive")) { - char v[8] = ""; - if (json_str(buf, "immersive", v, sizeof v) > 0) { - if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")) - immersive = 1; - } - } - /* fullscreen=true(app 级:注入 MEOW_FULLSCREEN=1,SDK 端 meow_ui_init 预先设全屏)。 - * 与 immersive 是两回事:fullscreen 隐藏状态栏/沉浸,immersive 关虚拟返回键。 */ - if (!fullscreen && strstr(buf, "fullscreen")) { - char v[8] = ""; - if (json_str(buf, "fullscreen", v, sizeof v) > 0) { - if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")) - fullscreen = 1; - } - } - /* "fonts":<档>(字库档:0=默认4K常用 1=大字库GBK 2=Full;可选,缺省 0)。 - * launcher 注入 MEOW_FONT_TIER;大字库装载由后续 font 代理实现。 */ - if (!fonts_set && strstr(buf, "fonts")) { - char v[8] = ""; - if (json_str(buf, "fonts", v, sizeof v) > 0) { - if (!strcmp(v, "common") || !strcmp(v, "0")) fonts = 0; - else if (!strcmp(v, "gbk") || !strcmp(v, "1")) fonts = 1; - else if (!strcmp(v, "full") || !strcmp(v, "2")) fonts = 2; - else fonts = atoi(v); /* 数字兜底(越界收敛到 [0,2]) */ - if (fonts < 0) fonts = 0; - if (fonts > 2) fonts = 2; - fonts_set = 1; - } - } - /* service.=mode:restart:max:window_ms(M1 解析存储,保活 M4) */ - char *sp = strstr(buf, "service."); - if (sp && tmp.nsvc < MEOW_MAX_SVCS) { - sp += 8; /* 跳过 "service." */ - int sl = 0; char nm[16] = ""; - while (*sp && *sp != '"' && *sp != ':' && *sp != '=' && sl < 15) - nm[sl++] = *sp++; - nm[sl] = 0; - if (sl > 0) { - char *vp = sp; - while (*vp && *vp != ':' && *vp != '=') vp++; - if (*vp == ':' || *vp == '=') vp++; - while (*vp == ' ' || *vp == '\t') vp++; - if (*vp == '"') vp++; - int m = 0, r = 0, mx = 0, w = 0; - sscanf(vp, "%d:%d:%d:%d", &m, &r, &mx, &w); - strncpy(tmp.svcs[tmp.nsvc].name, nm, 15); - tmp.svcs[tmp.nsvc].mode = m; - tmp.svcs[tmp.nsvc].restart = r; - tmp.svcs[tmp.nsvc].max = mx; - tmp.svcs[tmp.nsvc].window_ms = w; - tmp.nsvc++; - } - } - } - strncpy(a->name, name, 16); - strncpy(a->exec, exec, 128); - strncpy(a->package, package, sizeof a->package - 1); - a->framework = fram ? '1' : 0; - a->immersive = immersive ? '1' : 0; - a->fullscreen = fullscreen ? '1' : 0; /* 注入 MEOW_FULLSCREEN env */ - a->fonts = (char)(fonts < 0 ? 0 : (fonts > 2 ? 2 : fonts)); /* 注入 MEOW_FONT_TIER env */ - strncpy(a->icon, icon, 128); - a->nroute = tmp.nroute; - if (tmp.nroute) memcpy(a->routes, tmp.routes, sizeof(a->routes[0]) * (size_t)tmp.nroute); - a->nsvc = tmp.nsvc; - if (tmp.nsvc) memcpy(a->svcs, tmp.svcs, sizeof(a->svcs[0]) * (size_t)tmp.nsvc); -} - -static int scan_apps(struct app apps[MAX_APPS]) -{ - memset(apps, 0, sizeof(struct app) * MAX_APPS); - if (!card_present()) return -1; /* state 1 */ - - DIR *d = opendir(MEOWAPPS); - if (!d) return -2; /* state 2 */ - int n = 0; - struct dirent *de; - while ((de = readdir(d)) && n < MAX_APPS) { - if (de->d_name[0] == '.') continue; - char jpath[192]; - snprintf(jpath, sizeof jpath, "%s/%s/app.json", MEOWAPPS, de->d_name); - FILE *f = fopen(jpath, "r"); - if (!f) continue; - struct app a0; - memset(&a0, 0, sizeof a0); - meow_parse_app_json(f, &a0); - fclose(f); - if (!a0.name[0] || !a0.exec[0]) continue; /* corrupt -> skip */ - struct app *a = &apps[n]; - memset(a, 0, sizeof *a); - memcpy(a, &a0, sizeof a0); - a->ok = (access(a->exec, X_OK) == 0); - /* 安卓化:app_id=app.json 的 package(权威),缺省回退目录名 de->d_name; - * 两者都存在且不同则 WARN(正常情况二者相等)。launch_app 注入 MEOW_LOG_APP 用 */ - const char *aid = a0.package[0] ? a0.package : de->d_name; - strncpy(a->id, aid, sizeof a->id - 1); - if (a0.package[0] && strcmp(a0.package, de->d_name) != 0) - logmsg("WARN: app '%s' package '%s' != dir '%s'", a0.name, a0.package, de->d_name); - /* M1 路由注册表同步(app_id = a->id,即 package 优先) */ - meow_acct_sync(a->id, a); - n++; - } - closedir(d); - return n; /* 0 -> state 2 */ -} - -/* ================================================================== */ -/* hiby_player management */ -/* ================================================================== */ -static int find_hiby_pid(void) -{ - /* scan /proc/N/exe for /usr/bin/hiby_player */ - DIR *d = opendir("/proc"); - if (!d) return 0; - struct dirent *de; - int pid = 0; - while ((de = readdir(d))) { - if (de->d_name[0] < '0' || de->d_name[0] > '9') continue; - char path[64], exe[256]; - snprintf(path, sizeof path, "/proc/%s/exe", de->d_name); - ssize_t n = readlink(path, exe, sizeof exe - 1); - if (n <= 0) continue; - exe[n] = 0; - if (!strcmp(exe, "/usr/bin/hiby_player")) { pid = atoi(de->d_name); break; } - } - closedir(d); - return pid; -} - -/* ---- MeowOS 常驻主界面判定(M2.2 修复批次)---- - * Launcher 被 MeowOS 拉起(bootmenu MeowOS 分支 exec,或热键切 MeowOS 后 exec)时, - * 播放器已死(find_hiby_pid()==0)。此时 launcher == MeowOS 主界面常驻:必须自绘 - * fb0(画 buffer0 + FBIOPAN yoffset=0),绝不走旧「备份播放器帧/冻结/SIGCONT」路径, - * 绝不遗留/恢复播放器死帧。仅当存在存活播放器(会话覆盖场景)才保留旧菜单覆盖语义。 */ -static int meow_mode = 0; - -static int is_meow_mode(void) -{ - return find_hiby_pid() <= 0; -} - -static int player_stopped = 0; -static int player_pid = 0; - -static int proc_is_stopped(int pid) -{ - char path[64], line[512]; - snprintf(path, sizeof path, "/proc/%d/stat", pid); - int fd = open(path, O_RDONLY); - if (fd < 0) return 0; - ssize_t n = read(fd, line, sizeof line - 1); - close(fd); - if (n <= 0) return 0; - line[n] = 0; - /* comm may contain spaces; find last ')' */ - char *p = strrchr(line, ')'); - if (!p || p + 2 >= line + n) return 0; - return p[2] == 'T'; -} - -static int stop_player(void) -{ - player_pid = find_hiby_pid(); - if (!player_pid) { logmsg("hiby_player not found, menu without stop"); return 0; } - if (kill(player_pid, SIGSTOP) < 0) { - logmsg("SIGSTOP %d failed: %s", player_pid, strerror(errno)); - return -1; - } - /* wait for state T */ - for (int i = 0; i < 100; i++) { - if (proc_is_stopped(player_pid)) break; - usleep(10000); - } - player_stopped = proc_is_stopped(player_pid); - logmsg("hiby_player pid=%d stopped=%d", player_pid, player_stopped); - return player_stopped ? 0 : -1; -} - -static void cont_player(void) -{ - if (player_stopped && player_pid > 0) { - kill(player_pid, SIGCONT); - logmsg("SIGCONT %d", player_pid); - } - player_stopped = 0; -} - -/* ================================================================== */ -/* menu */ -/* ================================================================== */ -#define ROW_H 52 -#define ROW_TOP 96 -#define EXIT_ROW 640 -#define TOAST_Y 590 - -/* brightness save/restore (defined below, used by run_menu) */ -static int saved_brightness = -1; - -/* set by run_menu: the player's panel was blanked (screen timeout) when the - * menu opened. fb_open() unblanks it so the menu is visible; on session end - * restore_backlight() re-blanks (brightness 0 + FBIOBLANK(4)) to put the - * physical panel back into the state the player's display state machine - * assumes — otherwise the player resumes believing the panel is blanked - * while the panel is actually lit (frozen frame). Blanking is idempotent - * and matches what the player itself did, so it cannot wedge the player. */ -static int menu_opened_blank = 0; - -/* ---- config (MeowAPPs/meow.conf): pure black background by default ---- */ -static uint32_t menu_bg = 0x000000; - -/* Hotkey scheme (v6.5): - * hotkey=power_voldown (default) — HOLD power, then a single VOL_DOWN tap - * 60-2500ms after the power-down edge opens the menu. The player sees - * the power press (its screen-off flow starts; accepted side effect) - * and the VOL_DOWN tap (volume -1; accepted). Menu open runs the - * verified unblank + bring-backlight-only-if-dark logic (§18.15). - * hotkey=seq (legacy) — same key quick-tapped N times - * (default VOL_UP x3 within seq_gap_ms each). The PREVIOUS key combo - * was retired (its GPIO never fires on this unit, §18.12) — a multi-tap - * of ONE key is ADC-friendly and physical-safe. - * meow.conf keys: - * hotkey=power_voldown | seq - * hotkey_seq=volup,volup,volup (same key name repeated = N taps, seq mode) - * hotkey_seq_gap=600 (ms between taps, total window ~2x gap) - */ -#define HOTKEY_MODE_PWRVOL 0 -#define HOTKEY_MODE_SEQ 1 -#define HK_PWRWIN_MIN 60 /* min power-hold time before VOL_DOWN counts */ - /* (2026-08-28: 300->60; 仅留防抖/去抖动, */ - /* natural ~220ms 电源+音量减 实测被 300 拦下) */ -#define HK_PWRWIN_MAX 2500 /* max power-hold window; beyond this no trig */ -static int hotkey_mode = HOTKEY_MODE_PWRVOL; -#define MAX_SEQ_LEN 6 -static int seq_codes[MAX_SEQ_LEN]; -static int seq_len = 0; -static 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; -} - -static 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); -} - -/* ---- 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; }; - -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); -} - -static struct vol_snap vol_snapshot(void) -{ - struct vol_snap s = { amixer_vol(1), amixer_vol(2) }; - return s; -} - -static 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); -} - -static int read_file_int(const char *path) -{ - int fd = open(path, 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); -} - -/* ================================================================== */ -/* 系统设置(阶段 B2a 统一后端:settings_volume 等仅作缓存) */ -/* 统一后端(SYSTEM_SETTINGS.md §3/§7):扁平 key=value 存板载 */ -/* /usr/data/meow/settings.conf(KB 级小文件)。单点数据源,clamp 集 */ -/* 中在本模块。此处的全局变量(settings_volume/screen_off_timeout/ */ -/* refresh_rate_cfg 等)仅作缓存,由 meow_settings 的 set 回调/加载 */ -/* 后同步,供既有大量引用这些变量的代码(DAC/状态栏/息屏/刷新率调度)*/ -/* 使用,零侵入、不破坏编译与功能。 */ -/* ================================================================== */ -#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) */ -static int settings_volume = MEOW_VOL_DEFAULT; /* 当前音量 0-100(= 写入 DAC 的映射值) */ -static int settings_volume_max = MEOW_VOL_MAX_DEFAULT; /* 最大音量上限 clamp */ -static int settings_volume_step = MEOW_VOL_STEP_DEFAULT; /* 跳步 */ - -/* M2.17/M2.16:把 meow_settings 当前值同步到本模块的缓存全局。读取方 - * (DAC/状态栏/息屏/刷新率调度)统一从缓存取,避免散读 backend。 */ -static 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)。 */ -static 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; } -} - -/* ---- 通知 dump 回调(通知#6 转储 TF 卡;做成真)---- - * meow_notif 在 TTL 到点(meow_notif_tick,§8 10s)调用此回调归档一条通知,随后释放槽。 - * 本回调把 meow_notif 结构体转成一行 CSV 追加写入 <卡>/MeowOS/notifications/notifications.log - * (含 mkdir -p 与按总大小滚动截断、保留最新 10MB),落盘实现在 meow_notif.c 的 - * meow_notif_dump_tf()。无卡/不可写仅打点降级(NOTIFICATION_SYSTEM.md §5/§8 + MEWSPEC §5.3)。 */ -static void notif_dump_tf_placeholder(const meow_notif_t *n) -{ - meow_notif_dump_tf(n); -} - -/* ---- 通知图标回退回调(用户定案:icon 为空 → 用该 app 默认图标)---- - * post 的 app_icon 为空时调用(meow_notif.c 的 notif_resolve_icon),按 app_id 在 app - * 注册表(scan_apps 静态 app.json 的 icon)里找该 app 的图标(name/path 或空)。 */ -static int notif_app_icon_fallback(const char *app_id, char *out, size_t outsz) -{ - if (!app_id || !app_id[0] || !out || outsz == 0) return 0; - struct meow_acct *a = meow_acct_find(app_id); - if (a && a->icon[0]) { snprintf(out, outsz, "%s", a->icon); return 1; } - return 0; /* 无注册/无图标 → 保持空(通知中心 UI 再回退) */ -} - -/* 启动加载(M2.11/M2.24):统一由 meow_settings 后端读 settings.conf; - * 此处仅注册监听(一次性)+ 把 backend 当前值同步到缓存全局。重载(应用期 - * watchdog 可能改音量)等价于 meow_settings_init() + 手动同步。 */ -static int settings_cache_registered = 0; /* M2.24:监听只注册一次 */ -static void settings_load(void) -{ - meow_settings_init(); /* 幂等:注册默认 + 读 settings.conf */ - meow_notif_init(); /* M2a:通知存储初始化(幂等,读 notify.* 配额) */ - meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 通知#6:挂 dump 回调 = TF 转储(meow_notif_dump_tf:一行 CSV 追加 + 10MB 滚动截断) */ - meow_notif_set_icon_cb(notif_app_icon_fallback); /* icon 空 → 按 app_id 回退该 app 默认图标(app.json icon) */ - if (!settings_cache_registered) { - meow_settings_add_listener(settings_sync_cache_listener); - settings_cache_registered = 1; - } - settings_sync_cache_from_backend(); - logmsg("settings: vol=%d(max=%d,step=%d) refresh=%d(always_max=%d) off=%ds mode=%s", - settings_volume, settings_volume_max, settings_volume_step, - refresh_rate_cfg, refresh_always_max, screen_off_timeout, - screen_off_mode ? "black" : "panel"); -} - -/* ---- 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 映射值)。 */ -static 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); -} - -/* ---- M2.38 音量滑条拖动节流:meow_dac_vol_write 每次 system() fork+exec /bin/sh + 2 个 - * amixer,嵌入式 MIPS 上极慢;滑条拖动逐帧 set → s_commit → system() 导致卡死十几秒。 - * 修法:墙钟节流(两次真实 DAC 写入 ≥ VOL_DRAG_DAC_MS),拖动中只更新缓存 + 刷状态栏, - * 抬手/结束时经 meow_dac_vol_flush() 无条件直写一次,保证最终音量必然落 DAC。 ---- */ -#define VOL_DRAG_DAC_MS 60 /* 滑条拖动期间两次真实 DAC 写入的最短墙钟间隔 */ -static unsigned long long vol_dac_last_ms = 0; - -static 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/落盘)。 */ -static 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。 */ -static 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(); /* 单点变更即落盘 */ -} -static int settings_volume_get(void) { return settings_volume; } - -/* ---- M2.14 线性渐变:step>1 时按 1% 递增爬到目标,避免一次跳 N% 突兀 ---- - * 每主循环 tick 最多迈 1%(VOL_RAMP_MS 墙钟节流,不阻塞主循环);到目标若 - * 键仍按住则继续延段(长按=连续渐变到松开/到上限);step=1 时一步即达(天然平滑)。 - * 落盘只在渐变结束时一次(meow_settings_save),不逐 1% 写盘。 */ -#define VOL_RAMP_MS 15 /* 每 1% 步进的最短墙钟间隔 */ -static 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 语义:单击( buf && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ')) e--; - *e = 0; - if (buf[0]) snprintf(g_meowos_version, sizeof g_meowos_version, "%s", buf); - } - fclose(f); - logmsg("meowos_version: read %s = '%s'", MEOWOS_VERSION_FILE, g_meowos_version); -} - -/* 读一份 sysfs 字符串(剥尾随换行);失败回退 "" */ -static void read_sys_str(const char *path, char *buf, size_t n) -{ - buf[0] = 0; - int fd = open(path, O_RDONLY); - if (fd < 0) return; - ssize_t r = read(fd, buf, n - 1); - close(fd); - if (r <= 0) { buf[0] = 0; return; } - buf[r] = 0; - while (r > 0 && (buf[r-1] == '\n' || buf[r-1] == '\r')) buf[--r] = 0; -} - -/* 蓝牙实际上电状态(A/E:不只看 hci0 存在——节点常在但控制器可能 DOWN)。 - * 本设备 /sys/class/bluetooth/hci0/powered 不存在(旧内核/BlueZ 5.54), - * 权威来源为 hciconfig -a hci0 的 UP/DOWN 行。费用较高(spawn),故缓存, - * 由 meow_sys_refresh()(进系统一次)/meow_sys_light()(轻刷)节流更新; - * 状态栏只读 g_sys.bt_on。 */ -static int hci_powered(void) -{ - FILE *p = popen("hciconfig -a hci0 2>/dev/null", "r"); - if (!p) return 0; - int up = 0; - char buf[256]; - while (fgets(buf, sizeof buf, p)) { - /* 状态行含 "UP RUNNING" 或 "DOWN"。匹配单词 UP(避免误中 UART 等)。 */ - char *s = buf, *tok; - while ((tok = strsep(&s, " \t\r\n")) != NULL) { - if (!strcmp(tok, "UP")) { up = 1; break; } - } - } - pclose(p); - return up; -} - -#define WIFI_LVL_UNKNOWN (-1000) /* wifi_signal_level 读取失败哨兵(低于任何真实 dBm) */ -/* 读 /proc/net/wireless 的 wlan0 level(dBm,如 -46);失败(文件不存在/无 wlan0 行/解析失败)返回 - * WIFI_LVL_UNKNOWN。行格式(Linux wext)形如 " wlan0: 0000 64. -46. -256 0 ..."; - * 接口名后第 3 个空白分隔数值 = level(注意末尾带 '.',需剥离)。状态栏 WiFi 信号分档喂给 SDK。 */ -static int wifi_signal_level(void) -{ - FILE *f = fopen("/proc/net/wireless", "r"); - if (!f) return WIFI_LVL_UNKNOWN; - char line[256]; - int lvl = WIFI_LVL_UNKNOWN; - while (fgets(line, sizeof line, f)) { - char *p = strstr(line, "wlan0:"); - if (!p) continue; - p += 6; /* 跳过 "wlan0:" */ - int tok = 0; - while (*p) { - while (*p == ' ' || *p == '\t') p++; - if (!*p) break; - char *t = p; - while (*p && *p != ' ' && *p != '\t' && *p != '\n') p++; - char save = *p; *p = 0; /* 临时截断 token */ - if (tok == 2) { /* 第 3 个数值(0 起) = level */ - size_t n = strlen(t); - if (n && t[n - 1] == '.') t[n - 1] = 0; /* 去尾部 '.'(-46. → -46) */ - lvl = atoi(t); - *p = save; - break; - } - *p = save; - tok++; - } - break; /* 只需 wlan0 行 */ - } - fclose(f); - return lvl; -} - -/* 全量重抓(进 MeowOS 时调用一次;阻塞可接受)。绝不放进 10ms 主循环。 */ -static void meow_sys_refresh(void) -{ - load_meowos_version(); /* M2.47:读 /etc/meowos_version(单一数据源,每次刷新对齐) */ - int b = read_file_int("/sys/class/power_supply/battery/capacity"); - g_sys.batt = (b >= 0 && b <= 100) ? b : -1; - char st[16]; - read_sys_str("/sys/class/power_supply/battery/status", st, sizeof st); - g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0; - g_sys.batt_mv = read_file_int("/sys/class/power_supply/battery/voltage_now") / 1000; - - /* WiFi:wifi_on=网卡管理态 up(射频开);wifi_connected=operstate up(已关联 AP)。 - * M2.48:二者分工,状态栏才能区分「开未连接(WIFI_NO)」与「已连接(信号分档)」。 */ - char op[16]; - read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op); - g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0; - g_sys.wifi_on = wlan_radio_up(); - g_sys.wifi_level = g_sys.wifi_connected ? wifi_signal_level() : WIFI_LVL_UNKNOWN; - g_sys.wifi_ip[0] = 0; g_sys.wifi_ssid[0] = 0; - if (g_sys.wifi_connected) { - /* 行形如 "2: wlan0 inet /24 brd ...",取 inet 后第一个斜杠前的 token。 - * 用大缓冲读整行,再从局部 token 拷贝到 16B 小字段(避免 OOB)。 */ - char big[256]; - FILE *p = popen("ip -o addr show wlan0 2>/dev/null | grep 'inet '", "r"); - if (p) { - if (fgets(big, sizeof big, p)) { - char *s = strstr(big, "inet "); - if (s) { s += 5; while (*s == ' ') s++; char *e = strchr(s, '/'); if (e) *e = 0; - snprintf(g_sys.wifi_ip, sizeof g_sys.wifi_ip, "%s", s); } - } - pclose(p); - } - /* SSID 尽力读(wpa_cli 若在;失败留空不外溢)。注意:wpa_cli 可能因 wpa_supplicant - * 未跑而挂起——用后台 + 短等待避免阻塞 UI;超时即放弃(SSID 仅面板用,状态栏不读)。 */ - } - - /* 蓝牙:hci0 存在性 + 上电状态 + M2.48 角色/连接(进系统全量重探一次) */ - g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0; - g_sys.bt_on = g_sys.bt_seen ? hci_powered() : 0; - bt_role_refresh(1); /* 立即探测角色/连接态,供状态栏与设置页"蓝牙模式"行 */ - - /* 音量(settings;进 MeowOS 全量时把真实 DAC 同步到当前档)/ 亮度 */ - g_sys.vol = settings_volume_get(); - meow_dac_vol_write(settings_volume); /* M2.13:进 MeowOS 即把 CS43198 DAC 置为当前音量 */ - int br = read_brightness(); - g_sys.brightness = (br >= 0) ? br : 0; - - /* 存储:/data 剩余 + TF 卡挂载容量 */ - g_sys.data_free_kb = 0; - FILE *df = popen("df -k /data 2>/dev/null | awk 'NR==2{print $4}'", "r"); - if (df) { char x[24]; if (fgets(x, sizeof x, df)) g_sys.data_free_kb = atol(x); pclose(df); } - g_sys.sd_mounted = sd_mounted(); - g_sys.sd_free_kb = g_sys.sd_total_kb = 0; - if (g_sys.sd_mounted) { - FILE *df2 = popen("df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'", "r"); - if (df2) { char x[48]; if (fgets(x, sizeof x, df2)) { sscanf(x, "%ld %ld", &g_sys.sd_free_kb, &g_sys.sd_total_kb); } pclose(df2); } - } - - snprintf(g_sys.ver, sizeof g_sys.ver, "%s", g_meowos_version); - logmsg("sys snapshot: batt=%d%% chg=%d wifi=%d ssid=%s ip=%s bt=%d/%d vol=%d bri=%d " - "data_free=%ldKB sd=%d(%ld/%ldKB)", - g_sys.batt, g_sys.charging, g_sys.wifi_on, g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-", - g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", g_sys.bt_on, g_sys.bt_seen, - g_sys.vol, g_sys.brightness, g_sys.data_free_kb, g_sys.sd_mounted, - g_sys.sd_free_kb, g_sys.sd_total_kb); -} - -/* 轻刷(1s 定时,只更新时钟/电量/WiFi 信号 + 节流探测蓝牙角色——便宜,绝不碰负载/温度) */ -static void meow_sys_light(void) -{ - int b = read_file_int("/sys/class/power_supply/battery/capacity"); - if (b >= 0 && b <= 100) g_sys.batt = b; - char op[16]; - read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op); - g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0; - g_sys.wifi_on = wlan_radio_up(); - g_sys.wifi_level = g_sys.wifi_connected ? wifi_signal_level() : WIFI_LVL_UNKNOWN; - g_sys.vol = settings_volume_get(); /* 每进 MeowOS 重抓;此处同步内存值 */ - bt_role_refresh(0); /* M2.48:节流探测蓝牙角色(约 5s 一次,不阻塞主循环) */ -} - -/* 40px status bar (Bug 1, 2026-08-28): the old 20px bar is doubled so the - * app list can also show voltage + bluetooth alongside time / battery / wifi. - * line 1: time (left) battery% WIFI BT (right) - * line 2: VOL nn% (left) - * M2.11:值全部取自 meow_sys_state 快照(g_sys),不再每帧读 amixer/sysfs; - * 音量 = settings audio.volume(非 CS43198 死镜像);蓝牙 = 实际 powered; - * WiFi = operstate 实际 up。纯黑背景 = menu_bg;分隔线 y=28(栏高 40→28)。*/ -/* ---- 状态栏固定完整格重绘(M2.17:修「整个状态栏部件脏区残留」)---- - * 根因:M2.16 各部件/数字更新只把「当前内容包围盒」标为脏区并用它清底——旧内容超出新包围盒的 - * 部分(时钟数字 '0' 底弧落于新 '1' 包围盒外、电量%/WiFi/BT 出现或消失、音量数字)未被重绘 → - * 残留;按音量键全屏刷新才清。修法:状态栏(0,0,480×28)用一条固定完整格:字段更新时整条清底 + - * 重绘时钟/右侧 + 整条标脏(SDK back_buf 做最终态 → meow_ui_flush 原子 blit 整条,无中间态)。 - * 任何旧内容残留被整条覆盖。480×28≈27KB 极小,每秒一 blit 无碍;分隔线 y=28 单独绘制不在此列。 - * (逐字段固定格在 480px 宽 + 「蓝牙」CJK 全字段同现时与时钟格重叠,故取整条退化——§18.46。) */ -#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) */ -#if 0 /* [RETIRED Stage-One] 旧自绘状态栏的时钟/右侧包围盒变量,仅供 sb_redraw 用,现已退役 */ -static int g_sb_clock_x, g_sb_clock_y, g_sb_clock_w, g_sb_clock_h; -static int g_sb_right_x, g_sb_right_y, g_sb_right_w, g_sb_right_h; -#endif - -/* ---- 主题(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; - -/* 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 -}; - -/* B2b 主题三方式(manual 深色 / manual 浅色 / 时段自动)。 - * display.theme_mode(enum "manual,auto"):manual=读 display.theme(dark/light); - * auto=按深色时段。display.theme 保留为手动深浅的当前值(manual 下 s_ui_theme 直读它)。 */ -#define THEME_MODE_MANUAL 0 /* display.theme_mode 枚举索引 0 = 手动 */ -#define THEME_MODE_AUTO 1 /* display.theme_mode 枚举索引 1 = 时段自动 */ -/* 获取当前本地时刻的「当天第几分钟」(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(由调用方决定回退策略)。 - * - startend:跨午夜,如 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。 */ -static 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; -} - -/* ---- 状态栏右簇图标化(MD3)---- 布局常量 */ -#define SB_TGAP 6 /* 图标右缘 ↔ 文字间隙 */ -#define SB_XGAP 10 /* 相邻两项(右→左)间隙 */ -#define SB_ICON_INK 16 /* 主图标目标 ink 高(px):24→16(28px 栏等比缩放,留上下边距) */ -#define SB_CLK_MARGIN 14 /* 右簇左缘相对时钟右缘须留出的安全间距 */ -/* 图标 ink 垂直中心线 = 状态栏文字垂直中心(28px 栏文字按 (28-行高)/2 锚定后中心≈14,见 sb_text_y)。 - * 图标 ink 对齐此线 → 与数字/文字同中线(修「图标比文字低」);如需微调只改这一个常量。 */ -#define SB_ICON_CY 14 -static int g_sb_icon_cy = SB_ICON_CY; - -/* 状态栏文字垂直锚 y(行首升线顶):(栏高-行高)/2 把文字垂直居中于 28px 栏; - * 下限 1 防极端行高(>栏高)时算出负 y 溢出顶端(不越界优先)。 */ -#if 0 /* [RETIRED Stage-One] 旧自绘状态栏专用,现由 SDK 全局状态栏绘制,退役(保留回退) */ -static int sb_text_y(void) -{ - int y = (SB_BAR_H - ui_text_height(SB_FONT)) / 2; - return (y < 1) ? 1 : y; -} -#endif - -/* 状态栏 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 同构——暗/亮主题下图标随状态栏底色一起换色。 */ -static 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); - } - } -} - -#if 0 /* [RETIRED Stage-One] 以下为旧「自绘状态栏」的右簇布局/信号分档,仅 sb_redraw 用,现已退役 */ -/* 画一个右对齐状态栏项:右边界 = *xr,画完向左推进 *xr(含 SB_XGAP 项间隙)。 - * icon>=0 画图标(可配右侧文字 txt;txt==NULL 则仅图标)。文字/图标同用主题色 col。 - * ink_h 传给 sb_blit_icon(主图标用 SB_ICON_INK 等大;WiFi 强弱档传 0 保持自然 ink)。 - * 返回该项左边缘(供 g_sb_right_x 包围盒)。 */ -static int sb_r_place(int *xr, int icon, const char *txt, uint32_t col, int ink_h) -{ - int iconw = (icon >= 0) ? SB_ICON_SIZE : 0; - int gap = (icon >= 0 && txt) ? SB_TGAP : 0; - int tw = txt ? ui_width(txt, SB_FONT) : 0; - int x = *xr - (iconw + gap + tw); - if (icon >= 0) { sb_blit_icon(icon, x, col, ink_h); x += iconw + gap; } - if (txt) ui_draw(x, sb_text_y(), txt, col, SB_FONT); - *xr -= (iconw + gap + tw) + SB_XGAP; - return *xr + SB_XGAP; -} - -#define WIFI_LVL_UNKNOWN (-1000) /* wifi_signal_level 读取失败哨兵(低于任何真实 dBm) */ -/* 读 /proc/net/wireless 的 wlan0 level(dBm,如 -46);失败(文件不存在/无 wlan0 行/解析失败)返回 - * WIFI_LVL_UNKNOWN。行格式(Linux wext)形如 " wlan0: 0000 64. -46. -256 0 ..."; - * 接口名后第 3 个空白分隔数值 = level(注意末尾带 '.',需剥离)。 */ -static int wifi_signal_level(void) -{ - FILE *f = fopen("/proc/net/wireless", "r"); - if (!f) return WIFI_LVL_UNKNOWN; - char line[256]; - int lvl = WIFI_LVL_UNKNOWN; - while (fgets(line, sizeof line, f)) { - char *p = strstr(line, "wlan0:"); - if (!p) continue; - p += 6; /* 跳过 "wlan0:" */ - int tok = 0; - while (*p) { - while (*p == ' ' || *p == '\t') p++; - if (!*p) break; - char *t = p; - while (*p && *p != ' ' && *p != '\t' && *p != '\n') p++; - char save = *p; *p = 0; /* 临时截断 token */ - if (tok == 2) { /* 第 3 个数值(0 起) = level */ - size_t n = strlen(t); - if (n && t[n - 1] == '.') t[n - 1] = 0; /* 去尾部 '.'(-46. → -46) */ - lvl = atoi(t); - *p = save; - break; - } - *p = save; - tok++; - } - break; /* 只需 wlan0 行 */ - } - fclose(f); - return lvl; -} -#endif - -#if 0 /* [RETIRED Stage-One] 旧「自绘状态栏」整条重绘。现由 SDK 全局状态栏(唯一绘制者)取代, - * 经 sb_sdk_refresh() 喂权威状态 + 标脏顶条驱动。保留以便回退,勿删逻辑。 */ -/* 整条状态栏:清底→重绘时钟+右侧信息→整条标脏。调用点:时钟每秒 / 电量·WiFi·音量事件。 */ -static void sb_redraw(void) -{ - const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 状态栏接主题(浅色主题浅底深字,不再固定深色) */ - uint32_t sb_bg = th->surface, sb_fg = th->on_surface; - fill_rect(0, SB_BAR_Y, fb_xres, SB_BAR_H, sb_bg); /* 整条清底→无旧内容残留 */ - char timebuf[16]; - time_t now = time(NULL); - struct tm tm; - localtime_r(&now, &tm); - snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); - ui_draw(8, sb_text_y(), timebuf, sb_fg, SB_FONT); - /* 图标 ink 垂直中心 = 状态栏文字中心(SB_ICON_CY=14,28px 栏文字中心):同中线。 */ - g_sb_icon_cy = SB_ICON_CY; - /* 右簇 MD3 图标化(从右端 fb_xres-8 往左):[电量图标+%] [音量图标+%] [蓝牙图标] [WiFi图标] [亮度图标+%]。 - * M2.46-2:音量/电量顺序互换(最右由音量改为电量);亮度由 on_surface_variant 改为主色 on_surface; - * WiFi 按信号强度分档(/proc/net/wireless level:≥-60 强 / -60..-75 中 / <-75 弱)。 - * 主项色 on_surface;次项(蓝牙)用 on_surface_variant 稍淡。亮度「可选」:仅在右簇左缘 - * 与时钟右缘相距足够(SB_CLK_MARGIN)时才画,否则整体让位,避免与时钟叠字。 */ - uint32_t sb_fg2 = th->on_surface_variant; - char vol[16], batt[16], bright[16]; - int xr = fb_xres - 8; - if (g_sys.batt >= 0) { /* 电量:图标+%(未知时不画)——最右项 */ - snprintf(batt, sizeof batt, "%d%%", g_sys.batt); - sb_r_place(&xr, SB_I_BATT, batt, sb_fg, SB_ICON_INK); - } - snprintf(vol, sizeof vol, "%d%%", g_sys.vol); - sb_r_place(&xr, SB_I_VOL, vol, sb_fg, SB_ICON_INK); /* 音量:图标+% */ - /* M2.48 蓝牙多态:关(bt_on=0 不画,见上行门);开未连接→SB_I_BT_NO;主模式/从模式→SB_I_BT_CONN。 - * 主/从区分用颜色:主模式走次色 sb_fg2(与外设相连),从模式(被连当耳机)走主色 sb_fg 稍醒目。 */ - if (g_sys.bt_seen && g_sys.bt_on) { - int bco = SB_I_BT_NO; /* 默认:开未连接 */ - uint32_t bcol = sb_fg2; - if (g_sys.bt_role == BT_ROLE_MASTER) { bco = SB_I_BT_CONN; bcol = sb_fg2; } - else if (g_sys.bt_role == BT_ROLE_SLAVE) { bco = SB_I_BT_CONN; bcol = sb_fg; } - sb_r_place(&xr, bco, NULL, bcol, SB_ICON_INK); - } - /* M2.48 WiFi 多态:网卡关(wifi_on=0 不画);开未连接→SB_I_WIFI_NO;已连接→按信号分档。 */ - if (g_sys.wifi_on) { /* WiFi:仅图标(按连接态+信号分档) */ - int wco = SB_I_WIFI_NO; /* 默认:网卡开但未连 */ - if (g_sys.wifi_connected) { - wco = SB_I_WIFI_STRONG; /* 已连接;读取失败/未知 → 满格 */ - int wl = wifi_signal_level(); - if (wl != WIFI_LVL_UNKNOWN) { - if (wl >= -60) wco = SB_I_WIFI_STRONG; - else if (wl >= -75) wco = SB_I_WIFI_MEDIUM; - else wco = SB_I_WIFI_WEAK; - } - } - /* 强弱档(MEDIUM/WEAK)保留自然 ink(少一格本身即语义);SRONG/满格/开未连接走等大。 */ - sb_r_place(&xr, wco, NULL, sb_fg, - (wco == SB_I_WIFI_MEDIUM || wco == SB_I_WIFI_WEAK) ? 0 : SB_ICON_INK); - } - if (g_sys.brightness > 0) { - int bl = (g_sys.brightness > 100) ? 100 : g_sys.brightness; - snprintf(bright, sizeof bright, "%d%%", bl); - int bw = SB_ICON_SIZE + SB_TGAP + ui_width(bright, SB_FONT) + SB_XGAP; - if (xr - bw > 8 + ui_width(timebuf, SB_FONT) + SB_CLK_MARGIN) - sb_r_place(&xr, SB_I_BRIGHT, bright, sb_fg, SB_ICON_INK); /* 亮度:图标+%(主色,与时钟不重叠才画) */ - } - g_sb_clock_x = 4; g_sb_clock_y = sb_text_y(); - g_sb_clock_w = 12 + ui_width(timebuf, SB_FONT); g_sb_clock_h = SB_BAR_H; - g_sb_right_x = xr + SB_XGAP; - g_sb_right_y = sb_text_y(); g_sb_right_w = (fb_xres - 8) - g_sb_right_x + 4; - g_sb_right_h = SB_BAR_H; - meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 整条标脏 */ -} -#endif - -/* ---- 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 全局状态栏(唯一绘制者),不再自绘。 */ -static void paint_sb_clock(void) { sb_sdk_refresh(); } -static void paint_sb_right(void) { sb_sdk_refresh(); } - -static void draw_statusbar(void) { sb_sdk_refresh(); } /* 对齐原语义:整条状态栏 → SDK 绘制 */ - -/* ===== [RETIRED Stage-One] 以下为 launcher 旧「自绘状态栏」实现,已退役(不再被调用)。 - * 保留以供回退,勿删逻辑(见注释)。移用者:仅注释存档;sb_blit_icon/g_sb_icon_cy/SB_ICON_CY - * 仍被通知中心 chip(ncp_sb_icon)复用,故其定义保留在使用处。 ===== */ -#if 0 -/* 只重画状态栏整条到 SDK back_buf 并标整条脏 ---- 每秒(时钟)/ 事件(电量/WiFi/音量)触发,绝不动列表。 - * (取代 M2.16 的「只标当前内容包围盒」路径——它造成残留。) */ -static void paint_sb_clock_retired(void) { sb_redraw(); } -static void paint_sb_right_retired(void) { sb_redraw(); } - -static void draw_statusbar_retired(void) -{ - sb_redraw(); - const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 分隔线随主题 outline */ - fill_rect(0, SB_BAR_H, fb_xres, 1, th->outline); /* separator(整条之外,单独绘制;栏底=28) */ -} -#endif - -/* returns 0 on success (app ran and exited), -1 on launch failure */ -static int launch_app(struct app *a); - -/* M2.36 B2b:实体系统设置页(定义在下方 write_brightness 之后;run_menu 触摸命中 - * 「系统设置」行时调用,返回后由调用方重置按键防抖基线防面板误切换)。 */ -static void run_settings_page(void); - -/* 阶段B:通知中心面板(定义在下方 run_settings_page 之后;run_menu 顶部热区下滑调用)。 - * *want_launch 初始为 -1;若点击通知命中可启动 app,返回时置为 apps[] 下标(run_menu 据此 - * 走标准启动链路)。返回后由调用方重置按键防抖基线防面板误切换。 */ -static void run_notif_center(struct app *apps, int napps, int *want_launch, - int live_open, int lox, int loy); - -static int touch_fd = -1; -static int t_last_x = -1, t_last_y = -1; -static int t_down = 0; -static int t_down_x = 0, t_down_y = 0; -/* M2.23 手势原点锁定:BTN_TOUCH=1 只标「已按下」,坐标(goodix)不保证先于按下边沿, - * 故 t_down_x/y 延迟到「本次按下之后」的首个有效 ABS 才锁定,避免把上一手势残留 - * (-1/旧坐标) 当本次原点。t_down_valid=0 表示本次尚未锁到有效坐标。 */ -static int t_down_valid = 0; -/* ⑨ 顶部热区「跟手下拉」进入通知中心的 per-gesture latch:进入后置 1,本次手势不再二次进入; - * 由下次 touch-down 复位。防止「拖动中切入 run_notif_center 后返回」把同次手势再判成打开/滑动。 */ -static int menu_live_open_pending = 0; -#define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */ -/* ⑨ 面板跟手下拉、半程可回(progress=手指位移映射;复用 mnp_anim_off/快照/ncp_anim_rebuild)。 - * NCP_DRAG_ENTER_SLOP = 顶部热区下拉进入 run_notif_center(run_menu 切入时机)的位移阈值(px); - * NCP_DRAG_THR_PCT = 松手锚定阈值(progress >= fix*40/100≈410 → 补到展开;否则收回=半程取消)。 - * 拖满=面板高 PH(g_ncp_ph),实现「面板顶缘 1:1 跟手」:off = dy - PH。 */ -#define NCP_DRAG_ENTER_SLOP 16 -#define NCP_DRAG_THR_PCT 40 -/* Bug 2/4 (2026-08-28): after an app exits we return to the menu, but touch - * events that were generated DURING the app run (the goodix ghost touch the - * v6.4 watchdog guards against, plus the back-tap/press that actually exited - * the app) are still queued on the launcher's touch fd and get replayed as - * fresh menu taps. A replayed tap on an app row relaunches the app (symptom: - * BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO - * PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen - * flashes back to the player — Bug 4). We drain the queue immediately after - * the app exits and ignore touches for a short cooldown that covers the - * measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */ -static unsigned long long menu_touch_cooldown_until = 0; - -/* M2.6 幽灵触摸根除:会话边界(app退出/电源菜单关闭/启动/重启确认)统一 - * drain 触摸队列 + 冷却(TAP_ARM_DELAY),避免触发开合的那次触摸/残留被 - * 重放成「点击第 0 行 hello」。 - * M2.22(D):700ms 偏激进,把正常点击也当「启动残留触摸」丢弃(点一下没反应要点几下)。 - * 用户要求「点一下就能进」,同时仍防 ghost 重放 → 调窄到 300ms。 */ -#define TAP_ARM_DELAY_MS 300 -static void menu_drain_touch(void) -{ - if (touch_fd >= 0) { - unsigned char ev[16]; - while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } - } -} -static void menu_arm_touch(long ms) -{ - menu_drain_touch(); - menu_touch_cooldown_until = now_ms() + ms; -} - -/* Drain the touch queue + arm the post-app cooldown (call right after the - * app exits, before the menu resumes polling touch). The drain always runs so - * touches queued by the launch-attempt tail are cleared; the cooldown only - * arms when an app actually ran (app_ran) so a failed launch doesn't lock the - * menu out for 2.5s. */ -static void menu_after_app_exit(int app_ran) -{ - menu_drain_touch(); - /* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。 - * 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短, - * 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */ - if (app_ran) menu_touch_cooldown_until = now_ms() + 1200; -} - -/* Header block (status bar + MEOWAPPS title + thin divider) covering the - * region y=0..58, kept separate from the app-list body so the ~1s clock tick - * (draw_clock) never redraws or tears the list. draw_menu fills the whole - * background then calls draw_header_content. The menu is pre-drawn before - * fb_norm_buf0 so the initial pan shows a complete frame. */ -static void draw_header_content(void) -{ - draw_statusbar(); - /* M2.6 标题真字体(28px;回退点阵3x) */ - if (mfont_ready) { - meow_font_drawtext((uint16_t *)fb, 24, 48, "MeowOS 应用", rgb565(0xFFE066), 28); - fill_rect(0, 48 + meow_font_line_height(28) + 2, fb_xres, 2, 0x3A3A5C); - } else { - draw_text(24, 28, "MEOWAPPS", 3, 0xFFE066); - fill_rect(0, 56, fb_xres, 2, 0x3A3A5C); - } -} - -/* 每秒一次刷新状态栏时钟(顶部 HH:MM:SS,唯一每秒都变的值)。Stage-One 状态栏收敛后, - * launcher 不再自绘状态栏——draw_clock 只经 paint_sb_clock() → sb_sdk_refresh() 把权威 - * 状态喂进 SDK 全局状态栏 + 标脏顶条,由 SDK present() 顶条重绘(时钟/电量/音量等一并刷新)。 - * 绝不动应用列表(仅顶条 28px 脏),故列表不重绘/不撕裂;无 amixer/sysfs 每帧开销。 */ -static void draw_clock(void) -{ - paint_sb_clock(); /* → sb_sdk_refresh():喂 SDK + 标脏顶条,present() 时 SDK 重绘状态栏 */ -} - -/* M2.36 右向小箭头(进入/下拉提示)。点阵后备字库(5x7)缺 '>' 字形(glyph_index 落到空 - * 格),故用像素画一个 4x7 的 '>';顶点在 (x,y) 指向右。设置页选择器/设置项行复用。 */ -static void s_draw_chevron(int x, int y, uint32_t color) -{ - for (int i = 0; i < 4; i++) { - put_px(x - i, y - i, color); - put_px(x - i, y + i, color); - } -} - -/* M2.39 左向小箭头(返回提示)。点阵后备字库同样缺 '<' 字形('‹' 会渲染成方框), - * 故用像素画一个指向左的 4x7 '<';顶点在 (x,y)。设置页底部「返回」/任何左向提示复用。 */ -static void s_draw_chevron_left(int x, int y, uint32_t color) -{ - for (int i = 0; i < 4; i++) { - put_px(x + i, y - i, color); - put_px(x + i, y + i, color); - } -} - -static void draw_menu(struct app apps[MAX_APPS], int napps, int state, - int cursor, int toast_ms, const char *toast) -{ - fill_rect(0, 0, fb_xres, fb_yres, menu_bg); - draw_header_content(); - - char line[64]; - int y = ROW_TOP; - if (state == 1) { - if (mfont_ready) { ui_draw(30, 200, "没有存储卡", 0xFFFFFF, 20); ui_draw(30, 234, "请插入TF卡后重开菜单", 0x9FB8D9, 20); } - else { draw_text(30, 200, "NO STORAGE CARD", 2, 0xFFFFFF); draw_text(30, 240, "INSERT TF CARD", 2, 0x9FB8D9); draw_text(30, 272, "THEN RE-OPEN MENU", 2, 0x9FB8D9); } - } else if (state == 2 || napps == 0) { - if (mfont_ready) { ui_draw(30, 200, "没有找到应用", 0xFFFFFF, 20); ui_draw(30, 234, "请放入应用到", 0x9FB8D9, 20); ui_draw(30, 268, "MeowAPPs//app.json", 0x9FB8D9, 20); } - else { draw_text(30, 200, "NO APPS FOUND", 2, 0xFFFFFF); draw_text(30, 240, "PUT APP IN", 2, 0x9FB8D9); draw_text(30, 272, "MeowAPPs//app.json", 2, 0x9FB8D9); } - } else { - for (int i = 0; i < napps; i++) { - int sel = (i == cursor); - if (sel) { - fill_rect(8, y - 4, fb_xres - 16, ROW_H - 8, 0x2A3A6A); - snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name); - ui_draw(24, y, line, 0xFFFFFF, 20); - if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0xFF6B6B, 20); - } else { - snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name); - ui_draw(24, y, line, 0xC8D0E8, 20); - if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0x8A4A4A, 20); - } - y += ROW_H; - } - } - - /* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前; - * 不依赖 .mpk(图标/文案就地绘制)。音量导航可把光标移到此处(cursor==napps), - * 触摸点击该行进入设置页(run_menu 分发到 run_settings_page)。 */ - { - int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */ - int selset = (cursor == napps); /* 光标已移到设置行 */ - fill_rect(8, sy - 4, fb_xres - 16, ROW_H - 8, selset ? 0x2A3A6A : 0x12142A); - if (mfont_ready) ui_draw(24, sy, "系统设置", selset ? 0xFFFFFF : 0xC8D0E8, 20); - else draw_text(24, sy, "SETTINGS", 2, selset ? 0xFFFFFF : 0xC8D0E8); - s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, 0x9FB8D9); /* 右向进入提示 */ - } - - /* exit row */ - fill_rect(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 0x1A1A3A); - /* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */ - if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", 0x8FBFD9, 20); - else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, 0x8FBFD9); - - /* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。屏幕高 fb_yres=720,字号 20 真实行高 - * meow_font_line_height(20)=27(ascent 21 + descent 6);原 y=700 使字底 = 700+27 = 727 - * > 720,溢出 7px ≈ 1/5 字符高被裁。改为 y = fb_yres - 行高 - 底部留白。非 mfont 回退 - * 分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */ - #define FOOTY_PAD 8 - /* dev version, bottom-right: single source MEOWOS_VERSION. */ - { - char ver[32]; - snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); - int foot_y = fb_yres - meow_font_line_height(20) - FOOTY_PAD; - ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, 0x666688, 20); - } - - if (toast_ms > 0 && toast && toast[0]) { - fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, 0x2A0A0A); - ui_draw(30, TOAST_Y, toast, 0xFF8080, 20); - } -} - -/* ================================================================== */ -/* M2.3A 电源菜单(Android 11 样式):长按电源 3s 弹出;8s 强制关机 */ -/* 半透明深色遮罩 + 居中 5 个大按钮(2 列×3 行,ASCII 点阵,不耦合字体) */ -/* 与「单击=息屏」「电源+音量减=切MeowOS」按电源按住时长区分。 */ -/* ================================================================== */ -/* M2.3A sim 钩子(电源菜单验证,物理留用户): - * --sim-power-hold N —— 主循环 N ms 后强制 pwr=1(模拟按住电源),驱动 - * 3s 弹电源菜单 / 8s 强制关机两条路径。 - * --sim-power-pick N —— 电源菜单打开 2s 后自动选中按钮 N(0..4;M2.29 起 5 钮)。 - * --sim-no-exec —— 免执行:电源动作(关机/重启/回HibyOS)/8s强关仅打 log, - * 不真正 poweroff/reboot(安全 sim 用)。 */ -static long sim_power_hold_ms = 0; -static int sim_power_pick = -1; -static int sim_no_exec = 0; - -#define PM_NUM 5 -/* M2.31 电源菜单 Android12(MD3) 圆钮:正中间圆角卡片 + 每行 2 个大圆钮 + 换行居中。 - * 5 钮 → 2 列×3 行(行3=取消,单钮居中,默认选中安全项)。 - * PM_R 半径50(直径100),行距 PM_GAPY=140、圆心距 PM_GAPX=130 保证圆下 label 不被下一行圆压住; - * 卡片 40..440 / 115..620 居中放置。 */ -#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 /* 圆角卡片圆角 */ -static const int pm_cx[PM_NUM] = { PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2, - PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2, PM_CARD_CX }; -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" }; - -static void fill_circle(int cx, int cy, int r, uint32_t color) -{ - for (int dy = -r; dy <= r; dy++) - for (int dx = -r; dx <= r; dx++) - if (dx * dx + dy * dy <= r * r) - put_px(cx + dx, cy + dy, color); -} - -/* 圆角矩形 */ -static void fill_round_rect(int x0, int y0, int w, int h, int r, uint32_t color) -{ - fill_rect(x0 + r, y0, w - 2 * r, h, color); - fill_rect(x0, y0 + r, w, h - 2 * r, color); - fill_circle(x0 + r, y0 + r, r, color); - fill_circle(x0 + w - r, y0 + r, r, color); - fill_circle(x0 + r, y0 + h - r, r, color); - fill_circle(x0 + w - r, y0 + h - r, r, color); -} - -/* 电源菜单圆钮图标:从 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); - } -} - -/* 把当前显示 buffer(buffer0)逐像素压暗(半透明遮罩模拟) */ -static 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 - * 列表帧,读回即现有「压暗列表底」效果,无回归。 */ -static 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)"); -} - -/* Bug 2 修复:直连 reboot(2) syscall。M2.6 用 system("... reboot"/"poweroff") 经 BusyBox - * 不生效(设备实测:动作日志打了但不起机/不关机——BusyBox reboot 非 PID1 时依赖 init 上的 - * 信号/teinit 握手,本设备不完成;且 system() fork+sh 有竞态)。改为直接 syscall 绕过 - * /bin/sh 与 init:sync() 后调用 reboot(2),内核立即重启/关机。 */ -#define LINUX_REBOOT_MAGIC1 0xfee1dead -#define LINUX_REBOOT_MAGIC2 672274793 /* 0x28121969 */ -#define LINUX_REBOOT_CMD_RESTART 0x01234567 -#define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc -static 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=取消。 */ -static 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); - } -} - -static 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; -} - - -/* returns: 0 = stay in menu, 1 = exit menu (BACK TO PLAYER / sim-exit), - * 2 = launch request */ - -/* M2.8 Bug 3-residual「统一重绘」:把当前 MeowOS 界面内容整体重绘到 buffer0 并强制 pan 到 - * buffer0。息屏唤醒(电源单击亮屏)时调用 —— 唤醒不是 draw 事件:FBIOBLANK(4) 下电 -> - * FBIOBLANK(0) 上电后,LCDC 指向的内容可能已失效/为空,必须把当前真实界面(应用列表主界面, - * 含「没有存储卡/没有找到应用」态)重新画一遍再强制 re-pan,而不是仅翻页到陈旧/空缓冲。 - * signature 传入当前界面状态(由 run_menu 持有),不把应用列表局部状态全局化。 */ -static void meow_repaint_surface(struct app apps[MAX_APPS], int napps, int state, int cursor) -{ - /* M2.11 E:记录唤醒上下文(时刻/亮度/当前 screen_on/待 yoffset),便于定位偶发「唤醒后黑屏 - * 又自愈」的竞态。 */ - int pre_bri = read_brightness(); - int pre_screen = meow_screen_on; - logmsg("wake repaint: pre bri=%d screen_on=%d (state=%d napps=%d cursor=%d)", - pre_bri, pre_screen, state, napps, cursor); - draw_menu(apps, napps, state, cursor, 0, ""); /* 完整重绘当前视图到 back_buf */ - fb_pan_buf0_force(); /* 确保落在被显示的缓冲(buffer0) */ - meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit) */ - int post_bri = read_brightness(); - logmsg("wake repaint done: post bri=%d (after force-pan buffer0)", post_bri); -} - -/* set by on_sigterm; run_menu breaks on it so the menu closes cleanly and - * the main loop restores the player (must be declared before run_menu) */ -static volatile sig_atomic_t sigterm_flag = 0; - -/* M2.2 sim 钩子:--sim-exit-tap N —— 菜单打开 N ms 后模拟「点 BACK TO PLAYER 行」, - * 用于验证 MeowOS 模式下 BACK TO PLAYER 显示需重启提示(而非恢复死帧)。 */ -static long sim_exit_tap_ms = 0; -static int sim_exit_tap_done = 0; - -/* 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)不去抢亮度——尊重播放器背光(§红线)。 */ -static 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); -} - -/* real menu loop */ -static void run_menu(struct app apps[MAX_APPS], int napps, int state, - long sim_tap_ms, long sim_exit_ms) -{ - int cursor = 0; - unsigned long long t0 = now_ms(); - int toast_ms = 0; - char toast[64] = ""; - int want_exit = 0; - /* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。 - * meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */ - unsigned long long last_input_ms = now_ms(); - int any_input = 0; - int fbfd = -1; - int had_player = 0; /* M2.2:菜单打开时是否存在存活播放器 */ - long sim_tap_done = 0, sim_exit_done = 0; - long meow_last_poll = 0; /* M2.2:MeowOS 后台拔营(卡/网)轮询时间戳 */ - - load_config(); /* meow.conf on the card: bg_color etc. */ - - /* A-1/A-2:launcher 用 SDK 渲染栈 —— 初始化 SDK(自建 back_buf/读几何/载字库), - * 并把刷新率档位/恒满速开关交给 SDK present 帧率门控(去中心化:launcher 不再自带一套)。 - * set_statusbar(1)(Stage-One 状态栏收敛):launcher 系统界面(主界面/设置页/电源菜单/ - * 通知中心)顶部状态栏统一由 SDK 全局状态栏绘制(MUI_SBAR_H=28),launcher 不再自绘 - * (draw_statusbar/sb_redraw 退役,改由 sb_sdk_refresh() 喂权威状态 + 标脏顶条驱动)。 - * 与 App 同一实现/同一主题/同日/夜。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ - meow_ui_init(NULL); - meow_ui_set_statusbar(1); - /* 「同一个状态栏」闭环(FRAMEWORK_DESIGN §10.2 / MEOW_APP_SDK §3.1):SDK 全局状态栏 - * 仅系统进程(launcher)渲染。launcher 是系统 UI 渲染宿主 → 置 system_mode=1 让 SDK 渲染顶条 - * /提示线;App 进程默认 0(不渲染、只 inset,由 launcher 叠加)。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ - meow_ui_sys_set_system_mode(1); - meow_ui_set_refresh_rate(refresh_rate_cfg); - meow_ui_set_refresh_always_max(refresh_always_max); - - /* record the brightness BEFORE we touch it, so any menu exit can - * restore the player's pre-menu value (0 = player standby -> default 60) */ - int raw_brightness = read_brightness(); - saved_brightness = (raw_brightness > 0) ? raw_brightness : 60; - logmsg("menu saved brightness = %d", saved_brightness); - /* remember whether the player had blanked the panel: fb_open() below - * unblanks so the menu is visible, and restore_backlight() re-blanks - * on exit to keep the player's display state machine consistent */ - menu_opened_blank = (raw_brightness <= 0); - - fbfd = fb_open(); - meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */ - /* M2.6 加载字库(卡上 MeowOS/fonts/default/,卡片已挂载则该步成功;未挂载由下方轮询补载) */ - meow_font_ensure(); - meow_font_reload_multifont(); /* M2.5x 多字体(读 settings display.multi_font + 6 类;关=单字体) */ - /* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份 - * 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才 - * 冻结 + 备份,退出时恢复其帧。 */ - had_player = (find_hiby_pid() > 0); - meow_mode = had_player ? 0 : 1; /* 权威判定:每次按播放器是否存活重置 */ - /* M2.11 A:每次进 MeowOS(热键切换/开机选 MeowOS/覆盖菜单)都全量重抓系统状态快照, - * HibyOS 里改过(蓝牙/WiFi/音量/亮度等)也同步。主循环 10ms 绝不读系统态。 */ - meow_sys_refresh(); - g_sys.mode = meow_mode; - g_sys.napps = napps; - if (had_player) { - stop_player(); /* freeze the player so the menu owns fb0 */ - } else { - logmsg("meow mode: hiby_player dead, self-drawn home (no stop/backup)"); - } - ensure_backlight(); /* only if the panel is dark, never yank brightness */ - unblank_panel(); /* player frozen: it can no longer re-blank */ - /* M2.53:MeowOS 主屏接管(播放器已死,had_player==0)时强制把亮度压回用户 - * display.brightness 设定值。HibyOS 播放器运行/恢复画面会把它自己的背光下限 - * (16/25)写进 sysfs;若以它为「当前亮度」,用户设的低亮度(<25)会被覆盖。 - * 覆盖菜单(had_player==1,播放器会话中)不压回——播电影时亮度归播放器管。 */ - if (!had_player) apply_user_brightness(); - if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */ - draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */ - fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */ - meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit 到 fb0) */ - - /* touch: event reads (event1 is not grabbed) */ - touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); - if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno)); - /* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */ - menu_arm_touch(TAP_ARM_DELAY_MS); - - /* key polling */ - struct keypoll kp_power, kp_keys; - kp_open(&kp_power, DEV_POWER); - kp_open(&kp_keys, DEV_KEYS); - /* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */ - struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; - memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); - - logmsg("menu open: napps=%d state=%d cursor=%d", - napps, state, cursor); - - /* key edge baselines: sample the initial state so keys already held - * when the menu opens (e.g. the hotkey release tail) do not fire - * edges; kp.prev cannot be used for this because kp_poll() syncs it - * to cur before we compare */ - int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; - } - /* M2.3A 电源按住计时:≥8s 强制关机;≥3s 弹电源菜单;与单击息屏/热键按时长区分 */ - unsigned long long pwr_hold_at = 0; - int pwr_menu_state = 0; - /* M2.18 Bug 2: 一次性标志——本次电源按下已触发「自动息屏唤醒」时置位,其释放 - * 只唤醒不 toggle(防同一次按键被唤醒路径与单单击 toggle 各消费一次 → 闪黑)。 */ - int pwr_woke = 0; - /* M2.34: 一次性标志——电源菜单刚关闭(无论确认/取消/超时)。返回主循环后,本次 - * 电源按下-松开不得被 3130「松开→单击」路径误判为「电源单击」→ toggle 息屏,否则: - * ①菜单点关机/重启:关机/重启画面被息屏覆盖;②焦点在取消键单击电源:取消同时熄屏。 - * 清除时机:电源松开(3130 松开分支末尾)或下一次电源按下边沿。 */ - int pwr_suppress_click = 0; - - /* The menu was already drawn into buffer0 above (pre-drawn before the pan - * to buffer0), so a redraw is only needed when something actually changes: - * cursor movement (input), app exit/enter, toast start/end, or a future - * notification-badge change. NEVER on a periodic timer — see the big - * comment on the redraw block below. */ - int dirty = 0; /* 结构变化(app 进出/state/唤醒/热键…)→ 全帧重画 */ - int need_menu_paint = 0; /* M2.16:需 draw_menu 全画 back_buf(光标/toast/test 变化) */ - int last_cursor = 0; /* pre-drawn frame showed cursor 0 */ - int toast_active = 0; /* tracks toast_ms>0 for start/end redraw edges */ - long last_status_tick = 0; /* ~1s status-bar-only refresh (clock) */ - - for (;;) { - unsigned long long t = now_ms(); - long el = (long)(t - t0); - any_input = 0; /* M2.17:本圈是否有输入(重置 auto screen-off 计时) */ - - /* SIGTERM during the menu: close it cleanly (restore + resume happen - * in the hotkey block / main loop after we return) */ - if (sigterm_flag) { logmsg("menu: sigterm, closing"); want_exit = 1; break; } - - /* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */ - meow_ipc_process(); - meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ - - /* sim hooks */ - if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) { - logmsg("SIM TAP row 0"); - sim_tap_done = 1; - if (napps > 0 && state == 0) { - /* fall through: select row 0 */ - int ret = launch_app(&apps[0]); - /* app exited: RETURN TO THE MENU (v6.3) — closing the menu - * here would leave the app's last frame (e.g. BYE) on fb0, - * and the player only redraws on input events (none arrive, - * injection is not delivered), so the screen would stay - * dark until a key press. Staying in the menu keeps the - * menu visible; BACK TO PLAYER then resumes the player. - * Bug 2/3/4: a queued touch from the app run must NOT be - * replayed as a fresh menu tap (relaunch / flash-to-player), - * and the "CANNOT START" toast only fires on a real launch - * failure (ret != 0), never after a clean app exit. */ - menu_after_app_exit(ret == 0); - last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ - dirty = 1; /* app wrote buffer0 during its run -> redraw menu */ - if (ret != 0) { - snprintf(toast, sizeof toast, "CANNOT START: %s", apps[0].name); - toast_ms = TOAST_MS; - } - } else { - snprintf(toast, sizeof toast, "NO APPS"); - toast_ms = TOAST_MS; - } - } - if (sim_exit_ms > 0 && !sim_exit_done && el >= sim_exit_ms) { - logmsg("SIM EXIT"); - sim_exit_done = 1; - want_exit = 1; - break; - } - /* M2.6 sim:点 BACK TO PLAYER 行 → MeowOS 模式弹「需重启」确认框并留下; - * 会话覆盖模式则正常退出。 */ - if (sim_exit_tap_ms > 0 && !sim_exit_tap_done && el >= sim_exit_tap_ms) { - logmsg("SIM TAP exit row"); - sim_exit_tap_done = 1; - if (meow_mode) { - int rb = confirm_reboot_player(); - menu_arm_touch(TAP_ARM_DELAY_MS); - dirty = 1; - logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb); - } else { - want_exit = 1; - break; - } - } - - /* M2.2 MeowOS 系统拔营:无播放器时由 launcher 接管官方交给 - * hiby_player/sys_server 的两件事 —— TF 卡挂载 + WiFi 联网。 - * 后台每 2s 重试,不阻塞主界面;卡片就绪后重扫应用列表并就地刷新。 - * 仅 meow 常驻模式需要(HibyOS 覆盖模式播放器仍在,/或其管理)。 - * M2.11 C:TF 卡热插拔最佳努力 —— 拔出检测(节点消失→无卡)、 - * 插入检测(节点出现→自动挂载+重扫+重载字库)。不阻塞、不崩。 */ - if (meow_mode && (long)(t - t0) - meow_last_poll >= 2000) { - meow_last_poll = (long)(t - t0); - ensure_wifi(); - if (!sd_device_present()) { - /* 卡被拔出:设备节点消失 → 无卡态。停止扫描;留 state=1(NO STORAGE CARD)。 - * 若此前是正常态,切到无卡并重绘。不杀正在跑的应用(其由 D 的监测处理)。 */ - if (state == 0 || state == 2) { state = 1; dirty = 1; logmsg("hotplug: card removed -> state=1"); } - g_sys.sd_mounted = 0; - log_poll_card(); /* 卡日志不可用时回退 /tmp 缓冲 */ - } else if (ensure_card_mounted()) { - /* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */ - int was_nocard = (state == 1); - if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); } - if (mfont_ready) meow_font_reload_multifont(); /* M2.5x:卡挂载后重配多字体(此前无全局可配) */ - if (was_nocard || state != 0 || napps == 0) { - struct app fresh[MAX_APPS]; - int fn = scan_apps(fresh); - int fstate = (fn < 0) ? (fn == -1 ? 1 : 2) : (fn == 0 ? 2 : 0); - if (fstate == 0 && fn > 0) { - for (int i = 0; i < fn; i++) apps[i] = fresh[i]; - napps = fn; state = 0; - logmsg("hotplug: card ready -> %d apps loaded", napps); - dirty = 1; - } else if (fstate != state) { - state = fstate; - logmsg("hotplug: state -> %d", state); - dirty = 1; - } - } - g_sys.sd_mounted = 1; - log_poll_card(); - } else if (!sd_mounted()) { - /* 卡在但未挂载(正挂载/失败):保持当前显示不误判;不崩。 */ - g_sys.sd_mounted = 0; - } - } - - /* keys: poll and handle edges. Power is NOT handled here anymore - * (v6.5): inside the menu the power key is a pure screen-off/on - * toggle handled by sys_server/kernel (the player being SIGSTOPped - * does not affect it) — the launcher must NOT touch brightness/panel - * or exit the menu on it. Exit is only via the BACK TO PLAYER touch - * row (below) or sim-exit. */ - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - int vold = 0, volu = 0, play = 0, pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; - } - if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */ - /* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */ - int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */ - (void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */ - /* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。 - * 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */ - (void)kd_tick(&kd_vold, vold); - (void)kd_tick(&kd_volu, volu); - (void)kd_tick(&kd_play, play); - (void)kd_tick(&kd_pwr, pwr); - vold = kd_vold.stable; volu = kd_volu.stable; - play = kd_play.stable; pwr = kd_pwr.stable; - /* M2.6 电源语义(MeowOS 自持面板,无播放器拔河): - * 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */ - if (meow_mode) { - if (pwr && !m_prev_pwr) { - pwr_hold_at = t; pwr_menu_state = 0; any_input = 1; - pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */ - /* M2.18 Bug 2: 若这次按下正好落在「自动息屏」态(auto_slept 且屏已关), - * 本次电源键只做唤醒(下方通用醒屏分支 meow_panel_on()),其释放不再走 - * 单单击 toggle —— 否则同一次按下先被唤醒点亮、又在释放时被 toggle 关掉 - * (症状:自动息屏后点电源「闪一下黑、再点才亮」)。 */ - if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; - } - if (pwr && pwr_hold_at) { - /* M2.10/M2.11 B:8s 强关只在该「按住」从未打开过电源菜单时才触发 - * (!pwr_menu_state)。一旦打开过菜单(用户已互动/取消),run_menu 的 - * 8s 强关永远不在此 hold 上再触发 —— 由菜单自身的「按当前项确认」/取消 - * 决定动作。避免「按住电源开菜单->确认取消->仍按住->run_menu 8s 强关」残留。 - * M2.37:阈值接 power.forceoff_ms(默认 8000),强关保持 > 弹菜单上限。 */ - if (t - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { - if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } - else { logmsg("pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } - m_prev_pwr = pwr; continue; - } else if (t - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { - pwr_menu_state = 1; - logmsg("pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); - show_power_menu(); /* 动作在函数内 system(); 取消则续循环 */ - /* M2.31:菜单打开期间主循环阻塞,last_input_ms 不会刷新;若停留超过自动息屏 - * 超时,取消返回后会被误判「空闲」而立即熄屏(用户点取消瞬间黑屏)。 - * 菜单返回即视为「刚有输入」,重置自动息屏计时。 */ - last_input_ms = now_ms(); - /* M2.11 B:菜单已关闭(取消)。清空按住计时与菜单态:仍按住/随后松开的电源 - * 按「新按下」处理,绝不把已互动的 hold 再累计到 8s 强关。 */ - pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; - pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */ - dirty = 1; /* 取消后重绘应用列表 */ - menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */ - /* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。 - * 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」, - * 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at, - * 随后稳定值转 0 时又被误判「单击」→ meow_toggle_panel 把面板关掉(黑屏且 - * 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw - * 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw - * 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */ - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - continue; - } - } - if (!pwr && m_prev_pwr) { - /* 松开:短按(<3s 且菜单未开)→ 单击 = 息屏/亮屏切换 */ - if (pwr_suppress_click) { - /* M2.34:本次电源按下来自「电源菜单内确认/取消」,其松开不得 toggle 息屏 - * (否则关机/重启画面被息屏覆盖,或焦点在取消键单击电源导致熄屏)。 */ - logmsg("pm: power release after power-menu -> no toggle (suppress)"); - } else if (pwr_hold_at && (t - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { - logmsg("pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); - meow_toggle_panel(); - if (meow_screen_on) { - /* M2.8 Bug 3-residual:唤醒不是 draw 事件,LCDC 在 FBIOBLANK(4)->(0) - * 后指向的内容可能已失效/为空;必须把当前界面(应用列表)整体重绘到 - * buffer0 并强制 re-pan,而非仅翻页/仅 dirty 重绘(M2.7 仍黑屏根因)。 */ - meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ - last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟(防 t 过期回绕) */ - meow_repaint_surface(apps, napps, state, cursor); - } else { - dirty = 1; /* 息屏:面板已黑,留 dirty 供下次亮屏重绘 */ - last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟 */ - } - } else if (pwr_woke) { - /* M2.18 Bug 2:本次电源按下已由自动息屏唤醒分支消费,释放只记录、不 toggle。 */ - logmsg("pm: power release after auto screen-off wake -> no toggle (wake only)"); - } - pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; - pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */ - } - m_prev_pwr = pwr; - } - /* volume/play edges against the launcher-maintained baselines - * (previously dead code: prev was re-seeded from cur each pass, so - * no edge could ever fire and the menu keys could not navigate or - * launch). - * M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp), - * 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */ - if (meow_mode) { - if (vold && !m_prev_vold) { - any_input = 1; - settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */ - logmsg("volume DOWN <- ramp step=%d", settings_volume_step); - } else if (volu && !m_prev_volu) { - any_input = 1; - settings_volume_adj(+settings_volume_step); - logmsg("volume UP <- ramp step=%d", settings_volume_step); - } - } else { - /* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。 - * 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */ - if (vold && !m_prev_vold && napps > 0 && state == 0) { - any_input = 1; - cursor++; - if (cursor > napps) cursor = 0; /* napps=设置行 */ - logmsg("menu cursor -> %d", cursor); - } - if (volu && !m_prev_volu && napps > 0 && state == 0) { - any_input = 1; - cursor--; - if (cursor < 0) cursor = napps; /* 回绕到设置行 */ - logmsg("menu cursor -> %d", cursor); - } - } - /* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */ - if (meow_mode && vol_ramp_dir != 0) { - int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */ - int going = vol_ramp_tick(held); - g_sys.vol = settings_volume_get(); - paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */ - if (!going) logmsg("volume ramp end -> %d", g_sys.vol); - } - /* M2.19:播放键不再是「应用列表的确认/打开」——它让位给全局媒体控制 - * (播放/暂停,路由到当前活跃媒体会话)。菜单/列表的确认只走触摸点选 - * (见下方 touch 段),物理确认交给电源键(power 菜单/确认框),播放键 - * 在 MeowOS 主循环里不再触发 launch_app。媒体预留:后续媒体会话接管, - * 此处仅作 any_input(any_input 仍用于重置自动息屏计时)。(原播放键=打开 - * 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */ - if (play && !m_prev_play) { - any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */ - logmsg("play key: media-reserved (no menu confirm)"); - } - m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; - - /* touch (event reads) */ - if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */ - struct pollfd pfd = { touch_fd, POLLIN, 0 }; - if (poll(&pfd, 1, 0) > 0) { - unsigned char ev[16]; - while (read(touch_fd, ev, sizeof ev) == 16) { - uint16_t type = *(uint16_t *)(ev + 8); - uint16_t 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) t_last_x = val; - else if (code == ABS_Y || code == ABS_MT_POSITION_Y) t_last_y = val; - /* M2.23 原点锁定:goodix 事件时序不保证 ABS 与 BTN_TOUCH 先后, - * 按下边沿只标 t_down,原点延迟到「本次按下之后」的首个有效坐标才锁定, - * 杜绝把上一手势残留(-1/旧坐标)当本次手势起点。 */ - if (t_down && !t_down_valid && t_last_x >= 0 && t_last_y >= 0) { - t_down_x = t_last_x; t_down_y = t_last_y; t_down_valid = 1; - } - /* ⑨ 顶部热区「跟手下拉」打开通知中心(拖动中即进,真跟手): - * 原点锁定 + 起点在顶部热区(y= 0 && t_down_y < SB_BAR_H && - (t_last_y - t_down_y) >= NCP_DRAG_ENTER_SLOP) { - menu_live_open_pending = 1; /* 本次手势仅触发一次 */ - int loy = t_down_y; - logmsg("menu: top-edge live drag-down -> notification center (dy=%d)", - t_last_y - loy); - t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */ - menu_arm_touch(TAP_ARM_DELAY_MS); - int want_launch = -1; - run_notif_center(apps, napps, &want_launch, 1, t_down_x, loy); - /* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */ - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - last_input_ms = now_ms(); - menu_arm_touch(800); - dirty = 1; - if (want_launch >= 0 && want_launch < napps) { - logmsg("menu: notification launch app idx=%d", want_launch); - int ret = launch_app(&apps[want_launch]); - menu_after_app_exit(ret == 0); - last_input_ms = now_ms(); - dirty = 1; - if (ret != 0) { - snprintf(toast, sizeof toast, "CANNOT START: %s", - apps[want_launch].name); - toast_ms = TOAST_MS; - } - } - continue; /* 本圈不再按 tap/其余处理(剩余事件因 t_down=0 成 no-op) */ - } - } else if (type == EV_KEY && code == BTN_TOUCH) { - if (val && !t_down) { - /* Bug 2/4: ignore touches inside the post-app - * cooldown (goodix ghost + back-tap replay). */ - if (now_ms() < menu_touch_cooldown_until) { - /* M2.18 排查乱跳:记录被冷却丢弃的按下(坐标 + 剩余冷却), - * 便于定位「概率进 Hello」是否 ghost/重放越过冷却。 */ - logmsg("menu touch dropped by cooldown (at %d,%d, rem %llu ms)", - t_last_x, t_last_y, - menu_touch_cooldown_until - now_ms()); - } else { - any_input = 1; /* M2.17:触摸重置自动息屏计时 */ - t_down = 1; - t_down_valid = 0; /* 原点待首个有效坐标锁定(防残留 -1/旧值) */ - t_last_x = -1; t_last_y = -1; /* 本次以「无坐标」起步(防跨手势残留) */ - menu_live_open_pending = 0; /* ⑨ 新触摸:复位 live-open latch */ - } - } - else if (!val && t_down) { - t_down = 0; - if (!t_down_valid) { - /* 本次按下从未锁到有效坐标(坐标滞后/ghost 空按下):丢本次。 */ - logmsg("menu tap dropped: no valid origin (last %d,%d)", - t_last_x, t_last_y); - t_down_x = t_down_y = -1; - } else { - int tx = t_down_x, ty = t_down_y; - int dx = t_last_x - tx, dy = t_last_y - ty; - int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; - t_down_valid = 0; - logmsg("menu tap at %d,%d (up %d,%d, d=(%d,%d))", tx, ty, - t_last_x, t_last_y, dx, dy); - /* 阶段B:顶部热区下滑打开通知中心。判定在「滑动」分支内先于左缘返回—— - * 左缘(down_x notification center (release edge, fallback)"); - menu_arm_touch(TAP_ARM_DELAY_MS); - int want_launch = -1; - run_notif_center(apps, napps, &want_launch, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */ - /* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */ - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - last_input_ms = now_ms(); - menu_arm_touch(800); - dirty = 1; - /* 点击通知命中可启动 app → 走标准启动链路(与触摸行同路径)。 */ - if (want_launch >= 0 && want_launch < napps) { - logmsg("menu: notification launch app idx=%d", want_launch); - int ret = launch_app(&apps[want_launch]); - menu_after_app_exit(ret == 0); - last_input_ms = now_ms(); - dirty = 1; - if (ret != 0) { - snprintf(toast, sizeof toast, "CANNOT START: %s", - apps[want_launch].name); - toast_ms = TOAST_MS; - } - } - continue; /* 本圈不再按 tap/其余处理 */ - } - /* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、 - * 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */ - if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) { - logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy); - } else if (tx >= 0 && ty >= 0) { - if (ty >= EXIT_ROW - 4 && ty < EXIT_ROW + ROW_H - 8) { - logmsg("menu: touch exit row"); - if (meow_mode) { - /* M2.6 G:播放器已死,弹出「返回播放器需重启」确认框。 - * 是=写选单默认HibyOS + reboot;否=留在 MeowOS。 - * 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */ - logmsg("meow mode: BACK TO PLAYER -> confirm reboot"); - int rb = confirm_reboot_player(); - menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */ - dirty = 1; - if (rb) { logmsg("reboot-to-player: yes, rebooting"); } - /* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */ - } else { - want_exit = 1; - break; - } - } - /* M2.36 B2b:系统设置行(位于 Exit 行上方,y ∈ [588-4, 588+52-8))。 - * 命中 → 进设置页。阻塞式 run_settings_page 返回后,重播 kd_* 并以 - * 当前 raw 对齐 m_prev_*(M2.25 黑屏修复同款),杜绝「设置页内按电源 - * 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */ - else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) { - logmsg("menu: touch settings row -> settings page"); - menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */ - run_settings_page(); - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - last_input_ms = now_ms(); /* 回主界面即视为输入 */ - menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */ - dirty = 1; /* 全帧重画应用列表 */ - } - /* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与 - * 应用行动作非互斥。应用很多(napps*ROW_H 上探到 EXIT 带)时, - * 一个 tap 会同时命中退出行(<~684)与应用行带,触发确认重启又启动 - * 应用(或直接 break 走人)→ 概率乱跳。退出行优先,命中即不再 - * 落到应用行。普通小列表两区间不重叠,行为不变。 */ - else if (ty >= ROW_TOP && ty < ROW_TOP + napps * ROW_H && napps > 0 && state == 0) { - int idx = (ty - ROW_TOP) / ROW_H; - if (idx < napps) { - logmsg("menu: touch row %d", idx); - int ret = launch_app(&apps[idx]); - /* app exited: return to menu (v6.3). - * menu_after_app_exit() drains queued - * app-run touches so they are not - * replayed as row/tap gestures. */ - menu_after_app_exit(ret == 0); - last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ - dirty = 1; /* app wrote buffer0 -> redraw menu */ - if (ret != 0) { - snprintf(toast, sizeof toast, "CANNOT START: %s", apps[idx].name); - toast_ms = TOAST_MS; - } - } - } - } - t_down_x = t_down_y = -1; /* 复位,下一手势从零开始 */ - } - } - } - } - if (want_exit) break; - } - } - - /* ---- M2.17 超时自动息屏(主界面;主循环 10ms 低频检查,不阻塞)---- - * 任何输入重置 last_input;空闲 > screen_off_timeout(0=常亮)且未在 keep_awake 页 → 息屏。 - * 自动息屏后任意输入唤醒(电源单击路径已在 meow_mode 块处理;这里处理其他输入/触摸)。 - * black 模式:面板仍上电、触摸未挂起,故在屏下额外探测一次 BTN_TOUCH 按下 → 触发唤醒 - * (不处理点击;唤醒后下一圈正常触摸处理)。panel 模式触摸已被 FBIOBLANK(4) 挂起,仅按键唤醒。 */ - if (meow_auto_slept && !meow_screen_on && screen_off_mode == 1 && touch_fd >= 0) { - struct pollfd spfd = { touch_fd, POLLIN, 0 }; - if (poll(&spfd, 1, 0) > 0) { - unsigned char ev[16]; - while (read(touch_fd, ev, sizeof ev) == 16) { - uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); - int32_t val = *(int32_t *)(ev + 12); - if (type == EV_KEY && code == BTN_TOUCH && val) any_input = 1; - } - } - } - /* M2.18 Bug 1: 用 now_ms() 而非本圈顶部捕获的 t —— launch_app() 阻塞期间 t 已过期 - * (停在 app 启动前小数),若仍写 t 会把 last_input 倒拨到 app 启动时刻;写 now_ms() - * 才是「输入发生」的真实墙钟。配合下方 idle_ms_since 的回绕保护,杜绝误判空闲。 */ - if (any_input) last_input_ms = now_ms(); - if (meow_auto_slept && any_input && !meow_screen_on) { - logmsg("auto screen-off: input wake (meow_mode=%d keep_awake=%d)", meow_mode, fg_keep_awake()); - meow_panel_on(); - meow_auto_slept = 0; - last_input_ms = now_ms(); - meow_repaint_surface(apps, napps, state, cursor); /* M2.8 唤醒重绘 */ - } - if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) { - /* M2.18 Bug 1: 用 idle_ms_since 取差,杜绝 t= (unsigned long long)screen_off_timeout * 1000) { - meow_panel_off(); - meow_auto_slept = 1; - logmsg("auto screen-off after %ds idle (mode=%s keep_awake=%d, t=%llu last=%llu)", - screen_off_timeout, screen_off_mode ? "black" : "panel", - fg_keep_awake(), t, last_input_ms); - } - } - - /* Redraw only when something actually changed: cursor movement (an - * input event), app exit/enter (which rewrote buffer0), toast - * start/end, or a future notification-badge change. NEVER on a - * periodic timer. - * - * v6.8 BUG (the ~3fps list flicker): the v6.8 "500ms safety redraw" - * rewrote the ENTIRE menu into the live-displayed buffer0 every 500ms - * (the v6.8 fix made last_redraw=el so the timer finally fired), plus - * draw_statusbar()'s amixer popen on every redraw. An in-place full - * rewrite of the buffer the LCDC is scanning produces visible tearing/ - * pulsing on the app list, and the slow amixer popen stalls the loop. - * Full-redraw in place is only correct right after a REAL change; the - * steady-state list must not be touched. */ - /* M2.16:只在真变化时标脏(精确到格子),绝无"动一下就全屏重绘"。 - * 光标移动 → 旧/新两行;toast 起止 → toast 区;其余结构变化 → dirty=1(全帧)。 - * Stage-One 状态栏收敛:音量渐变/时钟只经 paint_sb_right/draw_clock → sb_sdk_refresh() - * 把权威状态喂进 SDK 全局状态栏 + 标脏顶条(0,0,xres,28),由 SDK present() 顶条重绘。 */ - if (cursor != last_cursor) { - /* M2.36 B2b:光标可能在应用行(0..napps-1)或「系统设置」行(napps,位于 Exit 行上方)。 - * 分别标脏对应行 rect(设置行固定 y=EXIT_ROW-ROW_H,与 napps 无关)。 */ - if (cursor == napps || last_cursor == napps) - meow_dirty_add(8, EXIT_ROW - ROW_H - 4, fb_xres - 16, ROW_H - 8); - if (state == 0 && napps > 0) { - if (last_cursor >= 0 && last_cursor < napps) - meow_dirty_add(8, ROW_TOP + last_cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8); - if (cursor >= 0 && cursor < napps) - meow_dirty_add(8, ROW_TOP + cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8); - } - last_cursor = cursor; - need_menu_paint = 1; - } - if (toast_active != (toast_ms > 0)) { /* toast start/end */ - toast_active = (toast_ms > 0); - meow_dirty_add(20, TOAST_Y - 6, fb_xres - 40, 40); - need_menu_paint = 1; - } - if (dirty) { /* 结构变化 → 全帧重画 + 全屏 blit */ - meow_dirty_full(); - need_menu_paint = 1; - dirty = 0; - } - if (need_menu_paint) { - draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */ - need_menu_paint = 0; - } - /* A-1/A-2:呈现统一走 SDK meow_ui_flush()(内含帧率门控;无脏返回,门控未达期保留脏 - * 待下一轮)。fb_norm_buf0 确保 LCDC 显示 buffer0(yoffset 非 0 才 pan,短路无开销)。 - * 不再用 launcher 的 g_ndirty/g_dirty_full 做呈现门——SDK present 自会 no-op 于空脏。 */ - fb_norm_buf0(); /* Bug 3:确保 LCDC 显示 buffer0 */ - meow_ui_flush(); /* M2.16 无闪烁 + M2.16 帧率门控归口 SDK */ - if (toast_ms > 0) { - toast_ms -= POLL_MS; - if (toast_ms < 0) toast_ms = 0; - } - /* Clock refresh ~1s: redraw ONLY the clock digits (and the right info - * cell if battery/wifi changed), never the list. */ - if (el - last_status_tick >= 1000) { - last_status_tick = el; - meow_sys_light(); /* 同步电量/WiFi/音量进 g_sys */ - draw_clock(); /* Stage-One:喂 SDK 全局状态栏 + 标脏顶条;SDK present() 重绘 */ - fb_norm_buf0(); - meow_ui_flush(); /* blit 状态栏整条,不动列表 */ - } - - usleep(POLL_MS * 1000); - } - - if (touch_fd >= 0) close(touch_fd); - /* v6.6: restore the player's frame + pan back BEFORE releasing fb (the - * player is event-driven and won't redraw on its own — see §18.15). - * M2.2:仅当存在存活播放器才恢复其帧;MeowOS 常驻主界面(无播放器)绝不 - * 恢复死帧——屏幕保持自绘的应用列表(buffer0),不遗留播放器残帧。 */ - if (had_player) fb_restore_player(); - if (fbfd >= 0) { if (fb_fd == fbfd) fb_fd = -1; close(fbfd); } - logmsg("menu close (exit=%d meow_mode=%d had_player=%d)", want_exit, meow_mode, had_player); -} - -/* ================================================================== */ -/* app launch */ -/* ================================================================== */ -static void on_sigterm(int s) { (void)s; sigterm_flag = 1; } - -/* Drain pending events from all input devices (non-blocking read to EAGAIN). - * Called right before launching an app so residual events (the tap/key that - * selected the row, the hotkey combo's release tail) are consumed before the - * app opens its own fds. Note: events generated AFTER the drain still reach - * the app (multicast); the app therefore also drains itself at startup and - * only exits on a fresh press edge (see hello.c). */ -static void drain_input(void) -{ - for (int i = 0; i < 4; i++) { - char dev[32]; - snprintf(dev, sizeof dev, "/dev/input/event%d", i); - int fd = open(dev, O_RDONLY | O_NONBLOCK); - if (fd < 0) continue; - unsigned char ev[16]; - while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } - close(fd); - } -} - -/* ---- backlight save/restore ---- */ -static 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); -} - -static 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); -} - -/* ================================================================== */ -/* 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; - -/* 网络信息类别(SC_LIST 打开下拉/详情;对应 SMENU「网络」组行) */ -#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 /* 网络下拉一次最多展示项(屏幕高度限制) */ - -/* SC_PRESET 预设表(路径/枚举字符串的"预设项,自定义文件选择留后期") */ -static const char *const bg_presets[] = { "black", "blue", "green" }; -static const char *const bg_preset_labels[] = { "黑色", "深蓝", "深绿" }; -#define BG_PRESETS_N 3 -static const char *const hk_presets[] = { "power_voldown", "seq" }; -static const char *const hk_preset_labels[] = { "电源+音量减", "连按序列" }; -#define HK_PRESETS_N 2 -static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" }; -static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" }; -#define SEQ_PRESETS_N 2 -/* font.file 行(字体文件)不使用编译期静态表:其 presets/preset_labels 指向字体区 - * (font_presets/font_preset_labels, 见本文件字体区) 的运行时动态数组,计数由 - * s_row_presets() 动态返回(默认 1,进设置页扫描 TF 卡后更新)。见 SMENU 行。 */ -static const char *const out_presets[] = { "hw:0,0" }; -static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" }; -#define OUT_PRESETS_N 1 - -/* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s, - * 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按 - * 45s/分/刻/时 跳档,最高 2 时。滑条值 = 档位索引(0..S_TIME_TIERS_N-1,步进 1), - * 落盘仍写「秒」(display.screen_off_timeout);索引<->秒双向映射见 s_time_tier_*。 */ -static const int s_time_tiers[] = { - 0, /* 常亮 */ - 5, 10, 15, 20, /* 细粒度段:可精确到 30s 附近 */ - 30, 45, /* 半分钟/三刻钟 */ - 60, 90, 120, 180, /* 1 分 / 1.5 分 / 2 分 / 3 分 */ - 300, 600, 900, /* 5 分 / 10 分 / 15 分 */ - 1800, /* 30 分 */ - 3600, 7200 /* 1 时 / 2 时 */ -}; -#define S_TIME_TIERS_N ((int)(sizeof s_time_tiers / sizeof s_time_tiers[0])) - -/* 设置页扁平化菜单(分组=组标题行,其余=设置项)。顺序即显示顺序。 */ -/* 分组/键/类型/默认与 SYSTEM_SETTINGS.md §1 一致(新建项四端同步)。 */ -static const srow_t SMENU[] = { - /* 显示 */ - { "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0 }, - { "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0 }, - { "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - /* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动), - * 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */ - { "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, - { "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, - { "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N }, - /* 网络 */ - { "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "WiFi", "network.wifi_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "WiFi连接", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SSID }, - { "已保存网络", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SAVED }, - { "附近WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SCAN }, - { "蓝牙", "network.bluetooth_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "蓝牙模式", NULL, SC_INFO, 0,0,0, NULL,NULL,0, NET_LIST_BT_MODE }, - { "蓝牙设备", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_NEARBY }, - /* 电源 */ - { "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0 }, - { "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0 }, - { "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N }, - /* 开机 */ - { "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0 }, - /* 输入 */ - { "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N }, - { "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0 }, - { "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0 }, - { "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - /* 字体主题 */ - { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - /* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。 - * 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file); - * 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */ - { "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - /* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/// 列表; - * 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */ - { "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, - { "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 }, - { "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 }, - { "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 }, - /* 通知 */ - { "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - { "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0 }, - /* 日志 */ - { "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0 }, - { "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0 }, - /* 音频 */ - { "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0 }, - { "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0 }, - { "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0 }, - { "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N }, - /* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */ - { "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, - { "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT }, -}; -#define SMENU_N ((int)(sizeof SMENU / sizeof SMENU[0])) - -/* 设置页布局(单卡片,行高统一便于整行式滚动对位与裁剪)。 - * SET_CONTENT_H 取 SROW_H 的整数倍(520=10 行),使 scroll 恒为整行步进, - * 视口内行首行/末行都完整落位,避免部分可见行被画到卡片外(泄漏)。 */ -#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) */ -/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由 - * 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */ - -/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量, - * 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */ -static int s_sel_track_y = -1; -/* 滑条 track 布局常量(所有滑条行一致;track 纵向 = 行顶 + 40,由 s_sel_track_y 记录) */ -#define S_SLIDER_X (SET_CARD_X + 16) -#define S_SLIDER_W (SET_CONTENT_W) - -/* ---- 行高/位置/导航辅助 ---- */ -/* M2.5x 多字体:6 个「字体文件」行仅在 display.multi_font=1 时可见(开时显示)。 - * 可见性与分组的标题行无关(标题恒显示)。 */ -static int s_row_visible(int i) -{ - const srow_t *r = &SMENU[i]; - if (r->key && !strncmp(r->key, "font.class_", 11)) { - int on = 0; meow_settings_get_switch("display.multi_font", &on); - return on; - } - return 1; -} -/* 可见行数(= 压缩后占用的行数;标题行计入,隐藏的 class 行不计) */ -static int s_vis_count(void) -{ - int n = 0; - for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) n++; - return n; -} -/* SMENU 下标 → 压缩位置(第几个可见行);不可见返回 -1 */ -static int s_vis_of(int i) -{ - if (i < 0 || i >= SMENU_N || !s_row_visible(i)) return -1; - int p = 0; - for (int k = 0; k < i; k++) if (s_row_visible(k)) p++; - return p; -} -/* 压缩位置 → SMENU 下标 */ -static int s_vis_at(int p) -{ - int c = 0; - for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) { if (c == p) return i; c++; } - return SMENU_N - 1; -} - -static int s_setting_idx(int i) { return i >= 0 && i < SMENU_N && SMENU[i].ctrl != SC_HEADER && s_row_visible(i); } -static int s_first_setting(void) -{ - for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i; - return 0; -} -static int s_next_setting(int i) -{ - for (int k = i + 1; k < SMENU_N; k++) if (s_setting_idx(k)) return k; - return s_first_setting(); /* 回绕 */ -} - -/* 字体行(font.file + font.class_*)走「family→字重」两级子菜单(M2.6 字重结构) */ -static int s_row_is_font(const srow_t *r) -{ - return r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11)); -} - -/* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组 - * (TF 卡 MeowOS/fonts/// 扫描结果,见字体区 font_presets/font_presets_n), - * 其余行用编译期静态表(r->presets / r->preset_n)。 */ -static const char *const *s_row_presets(const srow_t *r, int *n) -{ - if (r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11))) { - if (n) *n = font_presets_n; - return font_presets; - } - if (n) *n = r->preset_n; - return r->presets; -} - -/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */ -static int s_clamp_scroll_range(int scroll) -{ - int total = s_vis_count() * SROW_H; - if (total <= SET_CONTENT_H) return 0; - int max_scroll = total - SET_CONTENT_H; - if (scroll < 0) scroll = 0; - if (scroll > max_scroll) scroll = max_scroll; - return scroll; -} - -/* 滚动:保证当前选中行在 [SET_CONTENT_Y, SET_CONTENT_BOT] 内 */ -static int s_clamp_scroll(int sel, int scroll) -{ - scroll = s_clamp_scroll_range(scroll); - int pos = s_vis_of(sel); - if (pos < 0) pos = s_vis_of(s_first_setting()); - int top = pos * SROW_H; - int bot = top + SROW_H; - if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */ - if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */ - return s_clamp_scroll_range(scroll); -} - -/* 行 y 落在 content 坐标 cy 时对应的行号(压缩位置 → SMENU 下标);越界返回 -1 */ -static int s_row_at_content_y(int cy) -{ - if (cy < 0) return -1; - int pos = cy / SROW_H; - if (pos >= s_vis_count()) return -1; - return s_vis_at(pos); -} - -/* ---- 值格式化/展示 ---- */ -static void s_fmt_time(int sec, char *buf, int len) -{ - if (sec <= 0) { snprintf(buf, len, "常亮"); return; } - if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; } - if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; } - snprintf(buf, len, "%d 时", sec / 3600); -} -/* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */ -static void s_fmt_minutes(int mins, char *buf, int len) -{ - if (mins < 0) mins = 0; - if (mins > 1439) mins = 1439; - snprintf(buf, len, "%02d:%02d", mins / 60, mins % 60); -} -/* 息屏时间:秒 → 最近档位索引(显示滑动位置用;等值优先,跨档取较近档,平手取高档) */ -static int s_time_tier_idx(int sec) -{ - if (sec <= s_time_tiers[0]) return 0; - for (int i = 1; i < S_TIME_TIERS_N; i++) { - if (sec == s_time_tiers[i]) return i; - if (sec < s_time_tiers[i]) { - int lo = s_time_tiers[i - 1], hi = s_time_tiers[i]; - return (sec - lo < hi - sec) ? i - 1 : i; - } - } - return S_TIME_TIERS_N - 1; -} -/* 息屏时间:档位索引 → 秒(落盘写给 display.screen_off_timeout 的实值) */ -static int s_time_tier_val(int idx) -{ - if (idx < 0) idx = 0; - if (idx >= S_TIME_TIERS_N) idx = S_TIME_TIERS_N - 1; - return s_time_tiers[idx]; -} -static void s_fmt_value(const srow_t *r, char *buf, int len) -{ - int v = 0; - if (meow_settings_get_int(r->key, &v) != 0) { snprintf(buf, len, "-"); return; } - if (!strcmp(r->key, "display.screen_off_timeout")) { s_fmt_time(v, buf, len); return; } - /* B2b 深色时段(分钟 0-1439 → "HH:MM");start==end 视为未配置,显示「未设置」 */ - if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { - int s2 = 0, e2 = 0; - meow_settings_get_int("display.theme_dark_start", &s2); - meow_settings_get_int("display.theme_dark_end", &e2); - if (s2 == e2) { snprintf(buf, len, "未设置"); return; } - s_fmt_minutes(v, buf, len); - return; - } - /* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */ - if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) { - if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000); - else snprintf(buf, len, "%.1f 秒", v / 1000.0); - return; - } - snprintf(buf, len, "%d", v); -} -static void s_display_value(const srow_t *r, char *buf, int len) -{ - if (r->ctrl == SC_ENUM) { - int idx = 0; - if (meow_settings_get_enum(r->key, &idx) != 0) idx = 0; - if (meow_settings_enum_label(r->key, idx, buf, len) != 0) snprintf(buf, len, "-"); - return; - } - /* SC_PRESET */ - char cur[MEOW_SET_MAX_LEN]; - if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; } - /* M2.5x 多字体:class 行空值 = 跟随全局 font.file */ - if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) { - snprintf(buf, len, "跟随全局"); - return; - } - int pn = 0; - const char *const *pps = s_row_presets(r, &pn); - for (int i = 0; i < pn; i++) { - if (!strcmp(pps[i], cur)) { - snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur); - return; - } - } - snprintf(buf, len, "%s", cur); -} - -/* ===== 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。 */ -static 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; -} - -/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作) */ -typedef struct { int id; char name[48]; } meow_net_t; - -/* 十六进制字符值('0'-'9'/'a'-'f'/'A'-'F');非法返回 -1。避免依赖 ctype.h(musl/内存受限)。 - * M2.40:wpa_cli 对非 ASCII SSID 输出 "\xNN" 转义文字(`\`+x+两位hex 的纯文字,非真字节), - * 需先还原成真实 UTF-8 字节才能进界面文本。 */ -static int hex_val(char c) -{ - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; -} - -/* wpa_decode_ssid:把 wpa_cli 的 "\xNN" 转义文字还原成真实字节(\xe5\xa4\xa9… = "天…")。 - * 遇 `\x`+两位合法 hex → 拼一字节;普通字符原样拷贝;dlen 为 dst 容量(含 NUL)。 - * 无 \x 的纯 ASCII 名(如 "")原样通过。 */ -static void wpa_decode_ssid(const char *src, char *dst, int dlen) -{ - int o = 0; - while (*src && o < dlen - 1) { - if (src[0] == '\\' && src[1] == 'x' && - hex_val(src[2]) >= 0 && hex_val(src[3]) >= 0) { - dst[o++] = (char)((hex_val(src[2]) << 4) | hex_val(src[3])); - src += 4; - } else { - dst[o++] = *src++; - } - } - dst[o] = 0; -} - -/* 通用选择器下拉(定义在 s_net_open 之后);网络列表复用同款模态 */ -static int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur); - - -/* 取 WiFi 当前连接 SSID(wpa_cli -i wlan0 status 的 ssid= 行);未连接返回空串 */ -static void net_wifi_ssid(char *buf, int len) -{ - buf[0] = 0; - if (!wlan_connected()) return; - char *o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null"); - if (!o) return; - char *s = strstr(o, "ssid="); - if (s) { - s += 5; - char *e = s; while (*e && *e != '\n' && *e != '\r') e++; - int l = (int)(e - s); - /* M2.40:status 的 ssid= 同样可能是中文 \xNN 转义文字,还原成真实 UTF-8 字节 */ - char raw[128]; - if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; - memcpy(raw, s, (size_t)l); raw[l] = 0; - wpa_decode_ssid(raw, buf, len); - } - free(o); -} - -/* 当前 WiFi 连接详情(点开「WiFi连接」行展示):解析 wpa_cli status + 路由网关。 - * 填充 info[][48](调用方提供的二维缓冲),每行 "标签: 值",返回行数(≤max)。 - * 当前SSID / AP MAC(bssid) / 本机MAC(address) / AP IP(默认网关) / 本机IP(ip_address) / 频道freq。 - * 未连接返回 0(调用方显示"未连接")。绝不阻塞:popen 带超时。 */ -static int net_wifi_info(char (*info)[48], int max) -{ - if (!wlan_connected()) return 0; - char *o = popen_read_tmo(1500, - "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null; ip route 2>/dev/null | grep default"); - if (!o) return 0; - char ssid[64] = "", bssid[24] = "", addr[24] = "", ip[32] = "", freq[16] = "", gw[32] = ""; - char *p = o; - while (p && *p) { - char *e = strchr(p, '\n'); if (e) *e = 0; - if (!strncmp(p, "ssid=", 5)) { char raw[128]; snprintf(raw, sizeof raw, "%s", p + 5); wpa_decode_ssid(raw, ssid, sizeof ssid); } - else if (!strncmp(p, "bssid=", 6)) snprintf(bssid, sizeof bssid, "%s", p + 6); - else if (!strncmp(p, "address=", 8)) snprintf(addr, sizeof addr, "%s", p + 8); - else if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11); - else if (!strncmp(p, "freq=", 5)) snprintf(freq, sizeof freq, "%s", p + 5); - else if (!strncmp(p, "default via ", 11)) { char *s = p + 11; char *g = s; while (*g && *g != ' ') g++; snprintf(gw, sizeof gw, "%s", s); } - p = e ? e + 1 : NULL; - } - free(o); - int n = 0; - if (n < max && ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid); - if (n < max && bssid[0]) snprintf(info[n++], 48, "AP MAC: %s", bssid); - if (n < max && addr[0]) snprintf(info[n++], 48, "本机MAC: %s", addr); - if (n < max && gw[0]) snprintf(info[n++], 48, "AP IP: %s", gw); - if (n < max && ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip); - if (n < max && freq[0]) snprintf(info[n++], 48, "频道: %s MHz", freq); - return n; -} - -/* 已保存网络(wpa_cli -i wlan0 list_networks):首行为表头,其余每行 "\t\t\t"。 - * 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN),返回项数(≤max)。 */ -static int net_wifi_saved(meow_net_t *out, int max) -{ - int n = 0; - char *o = popen_read_tmo(1500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 list_networks 2>/dev/null"); - if (!o) return 0; - char *line = strchr(o, '\n'); - if (line) line++; /* 跳表头 */ - while (line && *line && n < max) { - char *eol = strchr(line, '\n'); - if (eol) *eol = 0; - char *p = line; while (*p == ' ' || *p == '\t') p++; - if (*p && *p != '#') { - int id = atoi(p); - char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++; - while (*sp == ' ' || *sp == '\t') sp++; - char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++; - if (sp != se) { - /* M2.40:list_networks 对中文 SSID 输出 \xNN 转义文字(如 \xe5\xa4\xa9… = "天…"), - * 先拷进临时缓冲再还原成真实 UTF-8 字节存入 name,否则界面显示超长乱码。 */ - char raw[160]; - int l = (int)(se - sp); - if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; - memcpy(raw, sp, (size_t)l); raw[l] = 0; - out[n].id = id; - wpa_decode_ssid(raw, out[n].name, sizeof out[n].name); - n++; - } - } - line = eol ? eol + 1 : NULL; - } - free(o); - return n; -} - -/* WiFi 扫描触发(异步:wpa_cli scan 只是通知 supplicant 发起扫描,立即返回)。 - * 结果稍后经 net_wifi_scan() 读。 */ -static void net_wifi_scan_trigger(void) -{ - if (!wlan_connected()) return; - system("PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan 2>/dev/null"); -} - -/* 读 WiFi 扫描结果(wpa_cli -i wlan0 scan_results):首行列头,数据行形如 - * [flags..] [ssid...] - * flags 恒以 '[' 起;SSID 为 flags 后的整段余量(可含空格;为空=隐藏网络)。 - * M2.40:同一 AP 常出现多条(不同频段/空 SSID/重名),按 BSSID 去重——每个 BSSID 只留一条, - * 优先采用含完整 SSID 的行(后到且有 SSID 则覆盖先到的空 SSID 行),隐藏网络显示 "(隐藏网络)"。 - * 填 out[i].id=-1;name 存 SSID(或隐藏标记)。返回项数(≤max,且 ≤S_NET_MAX)。 */ -static int net_wifi_scan(meow_net_t *out, int max) -{ - int n = 0; - if (max > S_NET_MAX) max = S_NET_MAX; - char *o = popen_read_tmo(2500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan_results 2>/dev/null"); - if (!o) return 0; - char seen_bssid[S_NET_MAX][24]; - int named[S_NET_MAX] = {0}; - char *line = strchr(o, '\n'); - if (line) line++; - while (line && *line && n < max) { - char *eol = strchr(line, '\n'); - if (eol) *eol = 0; - char *p = line; while (*p == ' ' || *p == '\t') p++; - if (*p) { - /* 首 token = bssid */ - char bssid[24] = ""; - { char *b = p; while (*p && *p != ' ' && *p != '\t') p++; - int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; } - /* 找 flags(首个以 '[' 起始的 token),之后(trim 掉前导空白)才是 SSID */ - const char *ssid = ""; - char *br = strchr(p, '['); - if (br) { - char *q = br; - while (*q) { - while (*q == ' ' || *q == '\t') q++; - if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; } - else break; - } - ssid = q; - while (*ssid == ' ' || *ssid == '\t') ssid++; - } - /* SSID 去尾部空白/换行 */ - char *se = (char *)ssid; - while (*se && *se != '\n' && *se != '\r') se++; - while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--; - int has = (se > ssid && *ssid); - /* 去重:同一 BSSID 只留一条,优先具名 SSID */ - int idx = -1; - for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; } - if (idx < 0) { - if (n >= max) { line = eol ? eol + 1 : NULL; continue; } - idx = n++; - strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1); - seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0; - named[idx] = has; - out[idx].id = -1; - if (has) { - /* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */ - char raw[96]; - int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; - memcpy(raw, ssid, (size_t)l); raw[l] = 0; - wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name); - } else { - snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)"); - } - } else if (has && !named[idx]) { - /* 同 BSSID 先出现无 SSID 行、后有具名行 → 用后者 SSID 覆盖(解码 \xNN) */ - char raw[96]; - int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; - memcpy(raw, ssid, (size_t)l); raw[l] = 0; - wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name); - named[idx] = 1; - } - } - line = eol ? eol + 1 : NULL; - } - free(o); - return n; -} - -/* 蓝牙已配对设备(bluetoothctl paired-devices):每行 "Device "。 - * hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */ -static int net_bt_paired(meow_net_t *out, int max) -{ - if (!hci_powered()) return 0; - int n = 0; - char *o = popen_read_tmo(1800, "bluetoothctl paired-devices 2>/dev/null"); - if (!o) return 0; - char *line = o; - while (line && *line && n < max) { - char *eol = strchr(line, '\n'); - if (eol) *eol = 0; - char *p = strstr(line, "Device "); - if (p) { - p += 7; - char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++; - char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; - /* 设备名每行即名称 token;名称可含空格,这里取整行剩余部分(含 MAC 后的名字) */ - if (*nm) - snprintf(out[n].name, sizeof out[n].name, "%s", nm); - else { - int ml = (int)(mac - p); if (ml > 47) ml = 47; - memcpy(out[n].name, p, (size_t)ml); out[n].name[ml] = 0; - } - out[n].id = -1; - n++; - } - line = eol ? eol + 1 : NULL; - } - free(o); - return n; -} - -/* ---- 蓝牙附近设备(未配对扫描发现;异步,不阻塞主循环)---- - * M2.40:用户要的「附近的设备」= 蓝牙扫描发现(含未配对),而不只是已配对。扫描较慢(2-6s), - * 故后台有界驱动 bluetoothctl:scan on → sleep 4 → scan off → devices → quit,输出写 /tmp 缓存; - * system() 因尾部 & 立即返回(约 5s 后自灭),绝不卡死主循环。读取端 net_bt_scan 只解析缓存 - * (bluetoothctl `devices` 行 "Device ",含已配对+未配对发现,去重)。 */ -static long g_bt_scan_ms = 0; /* 最近一次发起扫描的 now_ms();0=从未 */ - -static void net_bt_scan_trigger(void) -{ - if (!hci_powered()) return; - long t = (long)now_ms(); - if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; /* 7s 内不重复触发,防堆积 */ - g_bt_scan_ms = t; - system("(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | bluetoothctl > /tmp/meow_bt_scan.txt 2>&1 &"); -} - -/* 读蓝牙附近设备缓存:解析 bluetoothctl `devices` 输出("Device ",去重;含已配对 - * 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。 - * 返回项数(≤max,且 ≤S_NET_MAX)。 */ -static int net_bt_scan(meow_net_t *out, int max) -{ - if (!hci_powered()) return 0; - if (max > S_NET_MAX) max = S_NET_MAX; - int n = 0; - FILE *f = fopen("/tmp/meow_bt_scan.txt", "r"); - if (f) { - char line[256]; - char seen[S_NET_MAX][24]; - while (fgets(line, sizeof line, f)) { - char *p = line; while (*p == ' ' || *p == '\t') p++; - /* 认两种行:`devices` 输出的 "Device ";发现会话里的 "[NEW] Device "。 - * 跳过 [CHG]/[DEL] 等属性/移除行(无稳定名称)。 */ - char *dv = NULL; - if (!strncmp(p, "Device ", 7)) dv = p + 7; - else if (!strncmp(p, "[NEW] Device ", 13)) dv = p + 13; - if (!dv) continue; - char *mac = dv; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n' && *mac != '\r') mac++; - int ml = (int)(mac - dv); - if (ml < 6 || !memchr(dv, ':', (size_t)ml)) continue; /* 需像 MAC(含冒号);过滤属性行 */ - /* 去重(同 MAC 只留一条,取首个出现) */ - int dup = 0; - for (int j = 0; j < n; j++) if (!strncmp(seen[j], dv, (size_t)ml)) { dup = 1; break; } - if (dup) continue; - if (n >= max) break; - char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; - char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; - *e = 0; /* 去行尾换行 */ - strncpy(seen[n], dv, (size_t)ml); seen[n][ml] = 0; - if (*nm) - snprintf(out[n].name, sizeof out[n].name, "%s", nm); - else { - int l = ml; if (l > 47) l = 47; - memcpy(out[n].name, dv, (size_t)l); out[n].name[l] = 0; - } - out[n].id = -1; - n++; - } - fclose(f); - if (n > 0) return n; - } - return net_bt_paired(out, max); /* 无扫描缓存 → 退回已配对列表 */ -} - -/* ---- 蓝牙角色判定(M2.48:状态栏多态 + 设置页"蓝牙模式"行)---- - * 返回 BT_ROLE_*;desc 非空则写中文描述(供设置页只读行展示)。全部 popen 带超时,绝不阻塞。 - * 判定(best-effort,蓝牙栈=BlueZ 5.54): - * 关 :hci0 不存在或 DOWN(hci_powered)。 - * 开未连接:hciconfig UP 但 hcitool con 无 ACL 连接。 - * 主模式 :有 ACL 连接(连了外围设备如耳机);设备侧 A2DP sink 命中 → 主。 - * 从模式 :有 ACL 连接且设备侧 A2DP source 命中(远端接入,设备被当耳机用)。 - * 注:master/slave 依赖 bluealsa-cli a2dp list 端点角色,其输出随固件格式不同;解析不到时, - * 有 ACL 一律按「主模式」兜底(仍可靠区分 关/开未连接/已连接三态)。若真机 a2dp 角色 - * 与注释相反,只需交换 a2dp_side() 的 1/2 返回值即可。 */ -static void copy_utf8_trunc(char *dst, int dlen, const char *src) -{ - int o = 0; - while (*src && o < dlen - 1) { - unsigned char c = (unsigned char)*src; - int nb = 1; - if ((c & 0xE0) == 0xC0) nb = 2; - else if ((c & 0xF0) == 0xE0) nb = 3; - else if ((c & 0xF8) == 0xF0) nb = 4; - if (o + nb > dlen - 1) break; /* 放不下整个字符就停(不截半字符) */ - for (int k = 0; k < nb; k++) { if (!src[k]) break; dst[o++] = src[k]; } - src += nb; - } - dst[o] = 0; -} - -/* A2DP 段端点角色:1=设备侧 sink(主模式),2=设备侧 source(从模式),-1=无法判定。 - * 宽松匹配:同段内出现 connected/running 且出现对应角色词。优先认 BlueALSA 对象路径 - * a2dpsnk/a2dpsrc(跨版本稳定),再退到 source/sink/source 单词。 */ -static int a2dp_side(const char *sec) -{ - if (!sec) return -1; - if (!strstr(sec, "connected") && !strstr(sec, "running")) return -1; - int src = (strstr(sec, "a2dpsrc") || strstr(sec, "source") || strstr(sec, "Source")) ? 1 : 0; - int snk = (strstr(sec, "a2dpsnk") || strstr(sec, "sink") || - strstr(sec, "Sink") || strstr(sec, "target")) ? 1 : 0; - if (src && !snk) return 2; /* 设备侧 source → 被当耳机(从) */ - if (snk && !src) return 1; /* 设备侧 sink → 送流给外围(主) */ - return -1; -} - -/* 蓝牙角色:见上方注释。一次 popen 拿 ACL + A2DP + 已连设备名三类信息,避免反复 spawn。 */ -static int bt_role(char *desc, int len) -{ - if (desc && len > 0) desc[0] = 0; - if (!g_sys.bt_seen || !hci_powered()) { - if (desc && len > 0) snprintf(desc, len, "关闭"); - return BT_ROLE_OFF; - } - /* 快的放前(HCI 连接、A2DP 端点),bluetoothctl 最慢放最后;整体超时防挂起(超时 SIGKILL)。 */ - char *o = popen_read_tmo(1800, - "PATH=/bin:/sbin:/usr/bin:/usr/sbin hcitool con 2>/dev/null; " - "echo '===A2DP==='; bluealsa-cli a2dp list 2>/dev/null; " - "echo '===NAMES==='; printf 'devices Connected\\nquit\\n' | bluetoothctl 2>/dev/null"); - if (!o) { /* 探测失败:保守按「开未连接」 */ - if (desc && len > 0) snprintf(desc, len, "开启未连接"); - return BT_ROLE_ON_NO_CONN; - } - /* 按标记分节(把标记首字符置 0 切断) */ - char *s_acl = o; - char *s_a2dp = NULL, *s_names = NULL; - char *p = strstr(o, "===A2DP==="); - if (p) { *p = 0; s_a2dp = p + 10; } /* "===A2DP===" 长 10 */ - if (s_a2dp) { char *q = strstr(s_a2dp, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } } - else { char *q = strstr(o, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } } - /* ACL:hcitool con 的连接行含 "ACL "(如 " < ACL 00:1A:.. handle.. state.. lm MASTER") */ - int has_acl = 0; - { - char *line = s_acl; - while (line && *line) { - char *eol = strchr(line, '\n'); if (eol) *eol = 0; - if (strstr(line, "ACL") && strchr(line, ':')) { has_acl = 1; break; } - line = eol ? eol + 1 : NULL; - } - } - /* 已连设备名(bluetoothctl `devices Connected`:"Device ",取首个)。 - * 定长 22:配合「从模式·被连(耳机…」前缀(UTF-8 约 25B)仍落在展示缓冲 48B 内。 - * 同时也作「已连接」的第二证据(hcitool 缺失时 bluetoothctl 兜底)。 */ - char dev[22] = ""; - int has_conn = has_acl; - if (s_names) { - char *line = s_names; - while (line && *line) { - char *eol = strchr(line, '\n'); if (eol) *eol = 0; - char *q = line; while (*q == ' ' || *q == '\t') q++; - if (!strncmp(q, "Device ", 7)) { - has_conn = 1; /* bluetoothctl 也确认有设备已连 */ - if (!dev[0]) { - q += 7; - char *mac = q; while (*mac && *mac != ' ' && *mac != '\t' && - *mac != '\n' && *mac != '\r') mac++; - char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; - char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; - *e = 0; - if (*nm) copy_utf8_trunc(dev, sizeof dev, nm); - else copy_utf8_trunc(dev, sizeof dev, q); - } - } - line = eol ? eol + 1 : NULL; - } - } - int side = a2dp_side(s_a2dp); - int role; - if (!has_conn) role = BT_ROLE_ON_NO_CONN; - else if (side == 2) role = BT_ROLE_SLAVE; /* 设备侧 source → 被当耳机 */ - else role = BT_ROLE_MASTER; /* sink 或 a2dp 不明但有连接 → 主 */ - if (desc && len > 0) { - if (role == BT_ROLE_ON_NO_CONN) snprintf(desc, len, "开启未连接"); - else if (role == BT_ROLE_SLAVE) snprintf(desc, len, "从模式·被连(耳机%s%s)", - dev[0] ? " " : "", dev); - else snprintf(desc, len, "主模式·已连%s%s", - dev[0] ? " " : "", dev[0] ? dev : "设备"); - } - free(o); - return role; -} - -/* 节流重探蓝牙角色(约 5s 一次;force=1 立即)。结果写 g_sys.bt_role + g_bt_mode, - * 并同步 g_sys.bt_on/bt_seen,保证状态栏与本行一致。蓝牙关时免费(只 spawn hciconfig)。 */ -static long g_bt_role_ms = 0; /* 最近一次 bt_role 探测的 now_ms();0=从未 */ -static 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 扫描触发) */ -static 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) */ -static 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)。 */ -static 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"); -} -static 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/取值失败则不动(保默认)。 */ -static 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(); -} - -/* M2.5x 系统设置「关于本机」:复用 s_dd_modal 弹只读设备信息(定义见下,供 s_net_open 分发)。 - * 前置声明:s_net_open 在其定义之前调用它。 */ -static void s_about_open(void); - -/* 打开 SC_LIST 网络下拉(重新解析最新列表):已保存=可 select_network 连接; - * 附近WiFi(扫描)/蓝牙设备(附近扫描+已配对)=只读展示(选择即关闭;带密码/配对仍留后期)。 - * 返回后刷新缓存。 */ -static 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(); -} - -/* 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 版本表示(与固件打包同源);无独立固件版本文件。 */ -static void s_about_open(void) -{ - char info[12][96]; - const char *item[12]; - 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); - - /* 内核版本 */ - 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 < 12; i++) item[i] = info[i]; - s_dd_modal("关于本机", item, n, -1); /* cur=-1:只读展示,无预勾选 */ -} - - -/* ---- 控件绘制(MD3 形态;全部用主题角色色,无散落硬编码)---- */ -static void s_draw_switch(int x, int y, int on, const meow_ui_theme_t *th) -{ - int w = 48, h = 28; - meow_gfx_fill_round(x, y, w, h, h / 2, on ? th->primary : th->surface_container); - int knob_r = h / 2 - 3; - int kx = on ? x + w - knob_r * 2 - 3 : x + 3; - meow_gfx_fill_round(kx, y + 3, knob_r * 2, knob_r * 2, knob_r, - on ? 0xFFFFFF : th->on_surface_variant); -} -static void s_draw_slider(int x, int y, int w, int val, int mn, int mx, - const meow_ui_theme_t *th) -{ - int h = 6, ty = y; - fill_rect(x, ty, w, h, th->surface_container); /* 未达段 */ - int range = mx - mn; - int frac = range ? (val - mn) * w / range : 0; - if (frac > 0) fill_rect(x, ty, frac, h, th->primary); /* 已达段 */ - int tx = x + frac, cy = ty + h / 2; - meow_gfx_fill_round(tx - 12, cy - 12, 24, 24, 12, th->primary); /* thumb */ - meow_gfx_fill_round(tx - 5, cy - 5, 10, 10, 5, 0xFFFFFF); /* thumb 中点 */ -} - -/* ---- set 后统一侧效应(DAC/背光/刷新率)+ 可选落盘 ---- */ -static void s_commit(const srow_t *r, int save_now) -{ - if (!r || !r->key) return; - if (!strcmp(r->key, "audio.volume")) { - /* M2.38 Bug1/2:音量变更——拖动中节流写 DAC(防 system() 高频卡死),抬手才无条件直写; - * 同时把 g_sys.vol 同步为当前值并刷整条状态栏(顶层 VOL 数字随之更新,不整屏重绘)。 */ - g_sys.vol = settings_volume_get(); - paint_sb_right(); - if (save_now) meow_dac_vol_flush(); /* 抬手/点击落盘=无条件直写真实 DAC */ - else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */ - } - else if (!strcmp(r->key, "display.brightness")) { - /* M2.46-2 ①:亮度滑条拖动时状态栏百分比数字实时跟。根因:旧代码只 write_brightness(v) - * 写背光,未同步 g_sys.brightness(状态栏亮度数字取它)也未重绘 → 数字卡住。 - * 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。 - * 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */ - int v; if (meow_settings_get_int("display.brightness", &v) == 0) { - write_brightness(v); - g_sys.brightness = v; - paint_sb_right(); - } - } - else if (!strcmp(r->key, "display.refresh_rate")) - meow_ui_set_refresh_rate(refresh_rate_cfg); - else if (!strcmp(r->key, "display.refresh_always_max")) - meow_ui_set_refresh_always_max(refresh_always_max); - else if (!strcmp(r->key, "network.wifi_enabled")) { - int on = 0; meow_settings_get_bool(r->key, &on); - s_net_toggle_wifi(on); - s_net_refresh(); - } - else if (!strcmp(r->key, "network.bluetooth_enabled")) { - int on = 0; meow_settings_get_bool(r->key, &on); - s_net_toggle_bt(on); - s_net_refresh(); - } - /* M2.44 背景选项真正生效:display.background(black/blue/green) → menu_bg(主界面背景)。 - * 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */ - else if (!strcmp(r->key, "display.background")) { - char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg); - if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */ - else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */ - else menu_bg = 0x000000; /* black(默认) */ - /* 状态栏(menu_bg)与主界面背景都会用到 menu_bg;退出设置页回主界面时重绘即可。 */ - } - /* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */ - else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") || - !strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { - paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */ - } - /* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。 - * 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走 - * font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */ - else if (!strcmp(r->key, "font.file")) { - mfont_ready = 0; - mfont_forced = 1; - if (!meow_font_ensure()) - logmsg("font.file: reload failed -> dotmatrix fallback"); - else - logmsg("font.file: reloaded"); - meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */ - meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */ - } - /* M2.5x「加载大字库」开关(font.load_full)变更 → 强制重载 launcher 自身字体(小/大字库切换), - * 并重配多字体;大字库 atlas 大,meow_font_ensure 先 unload 旧再 load 新(防双驻留)。 */ - else if (!strcmp(r->key, "font.load_full")) { - mfont_ready = 0; - mfont_forced = 1; /* 跳过 SDK 复用捷径,强制按生效档(gbk/common)真实 reload */ - if (!meow_font_ensure()) - logmsg("font.load_full: reload failed -> dotmatrix fallback"); - else { - int lf = 0; meow_settings_get_switch("font.load_full", &lf); - logmsg("font.load_full: reloaded (load_full=%d)", lf); - } - meow_font_reload_multifont(); - meow_dirty_full(); - } - /* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */ - else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) { - meow_font_reload_multifont(); - meow_dirty_full(); - } - if (save_now) meow_settings_save(); -} -static void s_set_int_value(const srow_t *r, int v, int save_now) -{ - if (!strcmp(r->key, "audio.volume")) v = vol_clamp(v); /* 尊重 audio.volume_max */ - meow_settings_set_int(r->key, v); - s_commit(r, save_now); -} -/* 滑条拖动/点击落值:对「档位型」息屏时间做 索引→秒 换算,其余行原样写盘。 - * sl_v 为滑条轨迹值(s_slider_val_from_x 返回值;息屏时间=档位索引)。 */ -static void s_slider_set_from_track(const srow_t *r, int sl_v, int save_now) -{ - if (!strcmp(r->key, "display.screen_off_timeout")) sl_v = s_time_tier_val(sl_v); - /* B2b 深色时段滑条:分钟按 15 分钟步进吸附(0-1439,整 15 分钟),避免拖出任意分钟 */ - if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { - sl_v = ((sl_v + 7) / 15) * 15; - if (sl_v < 0) sl_v = 0; - if (sl_v > 1439) sl_v = 1439; - } - s_set_int_value(r, sl_v, save_now); -} - -/* 滑条拖动值域映射:x(屏幕坐标)→ value,基于布局常量 S_SLIDER_X/W。 - * 所有滑条行共用同一 track 几何,故任意滑条行都能正确映射。 */ -static int s_slider_val_from_x(const srow_t *r, int px) -{ - int range = r->max - r->min; - int v = r->min + (range ? (px - S_SLIDER_X) * range / S_SLIDER_W : 0); - if (v < r->min) v = r->min; - if (v > r->max) v = r->max; - return v; -} - -/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */ -static void draw_settings_page(int sel, int scroll_off) -{ - const meow_ui_theme_t *th = s_ui_theme(); - int W = fb_xres, H = fb_yres; - fill_rect(0, 0, W, H, th->surface); - draw_statusbar(); /* Stage-One:驱动 SDK 全局状态栏(喂数据+标脏顶条) */ - if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28); - else draw_text(24, 30, "SETTINGS", 3, th->on_surface); - fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline); - - /* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。 - * 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底; - * 状态栏(0-28)与标题区(28-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */ - fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant); - - int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT; - s_sel_track_y = -1; - for (int i = 0; i < SMENU_N; i++) { - int pos = s_vis_of(i); - if (pos < 0) continue; /* 隐藏行(多字体关闭时 6 个 class 行) */ - int sy = SET_CONTENT_Y + pos * SROW_H - scroll_off; - if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */ - const srow_t *r = &SMENU[i]; - int selected = (i == sel); - if (selected) - meow_ui_fill_rect_alpha(SET_CARD_X + 4, sy, SET_CARD_W - 8, SROW_H, th->primary, 34); - meow_ui_fill_rect(SET_CARD_X + 12, sy + SROW_H - 1, SET_CONTENT_W, 1, th->outline); - - if (r->ctrl == SC_HEADER) { - ui_draw(SET_CARD_X + 16, sy + 14, r->label, th->on_surface_variant, 18); - } else if (r->ctrl == SC_SWITCH) { - ui_draw(SET_CARD_X + 16, sy + (SROW_H - meow_font_line_height(22)) / 2, r->label, - th->on_surface, 22); - int on = 0; meow_settings_get_switch(r->key, &on); - s_draw_switch(SET_CARD_X + SET_CARD_W - 16 - 48, sy + (SROW_H - 28) / 2, on, th); - } else if (r->ctrl == SC_SLIDER) { - int ly = sy + (SROW_H - meow_font_line_height(22)) / 2 - 6; /* 标题略上移,给下方滑条留位 */ - ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); - char vb[32]; s_fmt_value(r, vb, sizeof vb); - ui_draw(SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22), ly, vb, th->on_surface_variant, 22); - int v = 0; meow_settings_get_int(r->key, &v); - /* 息屏时间为档位索引型:把秒换算成档位索引再画(各档等距,便于触摸对位); - * 其余滑条防御性 clamp 到 [min,max],防止后端暂存值越界(如 brightness 旧值 101 - * 在画时被 clamp 到 100,thumb 不冲出 track)。 */ - if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v); - else { - if (v < r->min) v = r->min; - if (v > r->max) v = r->max; - } - int tyy = sy + 40; - s_draw_slider(S_SLIDER_X, tyy, S_SLIDER_W, v, r->min, r->max, th); - if (selected) s_sel_track_y = tyy; - } else if (r->ctrl == SC_INFO) { /* M2.39 只读动态信息(如 WiFi 连接 SSID) */ - int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; - ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); - char vb[48]; s_net_get(r, vb, sizeof vb); - int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22); - ui_draw(vx, ly, vb, th->on_surface_variant, 22); - } else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:摘要 + 右向展开提示 */ - int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; - ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); - char vb[48]; s_net_get(r, vb, sizeof vb); - int vx = SET_CARD_X + SET_CARD_W - 16 - 12 - ui_width(vb, 22); /* 给右向 chevron 留位 */ - ui_draw(vx, ly, vb, th->on_surface_variant, 22); - s_draw_chevron(SET_CARD_X + SET_CARD_W - 16, sy + SROW_H / 2, th->on_surface_variant); - } else { /* SC_ENUM / SC_PRESET */ - int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; - ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); - char vb[40]; s_display_value(r, vb, sizeof vb); - int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22); - ui_draw(vx, ly, vb, th->on_surface_variant, 22); - s_draw_chevron(vx - 8, sy + SROW_H / 2, th->on_surface_variant); - } - } - - /* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */ - int vis_total = s_vis_count() * SROW_H; - if (vis_total > SET_CONTENT_H) { - int total = vis_total; - int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total; - if (thumb_h < 24) thumb_h = 24; - int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H); - meow_gfx_fill_round(fb_xres - 12, SET_CONTENT_Y, 5, SET_CONTENT_H, 3, th->surface_container); - meow_gfx_fill_round(fb_xres - 12, th_y, 5, thumb_h, 3, th->on_surface_variant); - } - - /* 底部:返回提示(左)+ 版本号(右),复用应用列表底部对齐逻辑。 - * M2.39:返回按钮改用像素左向箭头 + 「返回」——点阵回退字库缺少 '‹' 字形, - * 原 「‹ 返回」在 !mfont_ready 时 '‹' 渲染成方框。 */ - { - char ver[32]; - snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); - int foot_y = fb_yres - meow_font_line_height(20) - 8; - s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant); - ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20); - ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20); - } -} - -/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */ -static const char *s_preset_value(const srow_t *r, int pick) -{ - int is_cls = r->key && !strncmp(r->key, "font.class_", 11); - if (is_cls) { - if (pick == 0) return ""; /* 跟随全局 font.file */ - pick--; - } - int pn = 0; - const char *const *pps = s_row_presets(r, &pn); - if (pick < 0 || pick >= pn) return is_cls ? "" : (pps && pn > 0 ? pps[0] : ""); - return pps[pick]; -} - -/* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)---- - * ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。 - * 返回选中索引;取消返回 -1。阻塞(自开输入设备)。 - * M2.39:modal 核心抽到 s_dd_modal(网络列表复用),本函数只负责组装选项数组。 */ -static int s_dropdown(const srow_t *r) -{ - int n = 0, cur = 0; - const char *names[8]; - char labels[8][48]; - const char *item[S_NET_MAX]; - if (r->ctrl == SC_ENUM) { - int kn = 0; - meow_settings_enum_names(r->key, names, &kn); - n = kn; - if (n > S_NET_MAX) n = S_NET_MAX; - for (int i = 0; i < n; i++) { - meow_settings_enum_label(r->key, i, labels[i], sizeof labels[i]); - item[i] = labels[i]; - } - int ci = 0; meow_settings_get_enum(r->key, &ci); cur = ci; - } else { - char cstr[MEOW_SET_MAX_LEN]; - meow_settings_get_str(r->key, cstr, sizeof cstr); - int pn = 0; - const char *const *pps = s_row_presets(r, &pn); - int is_cls = r->key && !strncmp(r->key, "font.class_", 11); - int base = is_cls ? 1 : 0; /* class 行 0 号留给「跟随全局」 */ - int cnt = pn; if (cnt > S_NET_MAX - base) cnt = S_NET_MAX - base; - n = base + cnt; - if (is_cls) { - names[0] = ""; - snprintf(labels[0], sizeof labels[0], "跟随全局字体"); - item[0] = labels[0]; - if (!cstr[0]) cur = 0; - } - for (int i = 0; i < cnt; i++) { - names[base + i] = pps[i]; - snprintf(labels[base + i], sizeof labels[base + i], "%s", - (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]); - item[base + i] = labels[base + i]; - if (!strcmp(pps[i], cstr)) cur = base + i; - } - } - if (n <= 0) return -1; - if (cur < 0 || cur >= n) cur = 0; - return s_dd_modal(r->label, item, n, cur); -} - -/* ---- 通用选择器下拉模态(MD3 §3.3:居中圆角卡片,列选项)---- - * title=标题;item[i]=第 i 项显示文本;n=项数(≤S_NET_MAX);cur=当前值下标(画勾选)。 - * 返回选中下标;取消/点卡片外返回 -1。阻塞(自开输入设备)。 - * s_dropdown(设置项 enum/preset)与 s_net_open(网络列表)共用。 - * M2.40:触摸命中判定改在抬手用抬手时刻最新 abs 坐标(见循环内注释),修「点选项无反应」。 - * 说明:模态内部仍保留音量键=移动选项高亮(这是「选值」不是「设置页列表光标」,属另一语义, - * 与 M2.39 仅移除 run_settings_page 列表光标移动的约束不冲突)。 */ -static int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur) -{ - int sel = cur; - const meow_ui_theme_t *th = s_ui_theme(); - int W = fb_xres, H = fb_yres; - int opt_row = 48, pad = 20, title_h = 40; - int d_h = title_h + n * opt_row + pad; - int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2; - if (d_y < SB_BAR_H) d_y = SB_BAR_H; /* 下拉不压状态栏(栏高 40→28,随常量) */ - if (H - d_y - d_h < 20) d_y = H - d_h - 20; - - struct keypoll kp_power, kp_keys; - kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); - struct kd_state kd_vold, kd_volu, kd_pwr; - memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_pwr, 0, sizeof kd_pwr); - int m_prev_vold = 0, m_prev_volu = 0, m_prev_pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; - } - int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); - if (tfd >= 0) { - /* M2.37:彻底清空「打开下拉」这次触摸的残留边沿。goodix 的抬起序列(ABS 坐标/ - * BTN_TOUCH=0/MT_TRACKING=-1)在开 fd 后仍可能陆续送达,若只清空一次,第一轮 - * 下拉循环会读到这些残边 → 误判抬起/选项错位/点卡片外穿透。故 排空→短延时→再排空。 */ - unsigned char ev[16]; - while (read(tfd, ev, sizeof ev) == 16) {} - usleep(40000); /* M2.38 Bug3:排空→40ms→再排空,吸收 goodix 抬起序列长尾 */ - while (read(tfd, ev, sizeof ev) == 16) {} - } - int pwr_pending = 0; - int result = -1; - int dlx = -1, dly = -1, dtd = 0; /* 下拉触摸解析(dlx/dly=最新有效坐标;dtd=已按下) */ - /* M2.40:不再锁按下时刻的 ddx/ddy —— goodix 时序 BTN_TOUCH 先于 ABS,按下边沿时坐标可能 - * 还是 -1/上一手势残留,锁原点会把点击判成「点卡片外=取消」。命中改在抬手时用抬手时刻的 - * dlx/dly(抬起时必已更新到本次最新有效坐标),与 meow_touch mt_up 语义同源。 */ - - /* 初始绘制:全屏压暗覆盖层 + 居中圆角卡片(fill 后画空心圆角描边) */ - meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); - meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container); - meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline); - if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20); - else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface); - for (int i = 0; i < n; i++) { - int ry = d_y + title_h + i * opt_row; - int cur_sel = (i == sel); - if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36); - ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "", - cur_sel ? 0xFFFFFF : th->on_surface, 20); - if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary); - } - meow_dirty_full(); meow_ui_flush(); - - for (;;) { - if (sigterm_flag) { result = -1; break; } - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - int vold = 0, volu = 0, pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; - } - (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_pwr, pwr); - vold = kd_vold.stable; volu = kd_volu.stable; pwr = kd_pwr.stable; - int need = 0; - if (vold && !m_prev_vold && sel < n - 1) { sel++; need = 1; } - if (volu && !m_prev_volu && sel > 0) { sel--; need = 1; } - if (pwr && !m_prev_pwr) pwr_pending = 1; /* 按下:待抬起确认 */ - if (!pwr && m_prev_pwr && pwr_pending) { pwr_pending = 0; result = sel; break; } /* 抬起=确认 */ - m_prev_vold = vold; m_prev_volu = volu; m_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) == 16) { - 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) dlx = val; - else if (code == ABS_Y || code == ABS_MT_POSITION_Y) dly = val; - } else if (type == EV_KEY && code == BTN_TOUCH) { - if (val) { - /* 按下边沿:只标「已按下」,绝不锁原点(goodix 时序 BTN_TOUCH 先于 - * ABS,此刻 dlx/dly 可能还是 -1 或上一手势残留)。 */ - dtd = 1; - } else if (dtd) { - dtd = 0; - /* 抬手判定:此刻 dlx/dly 必为本次最新有效坐标(抬起时 ABS 已先到)。 - * 卡片内命中 → 返回选项下标;卡片外 → 取消(-1);从未见有效坐标 → 丢弃。 */ - if (dlx >= 0 && dly >= 0) { - if (dlx >= d_x && dlx < d_x + d_w && dly >= d_y && dly < d_y + d_h) { - int i = (dly - d_y - title_h) / opt_row; - if (i >= 0 && i < n) { sel = i; result = sel; goto ddone; } - /* 卡片内但落在标题/内边距:保持弹出态,等待下一次点选 */ - } else { - result = -1; goto ddone; /* 点卡片外=取消 */ - } - } - /* dlx/dly 为 -1:ghost/残留抬手,丢弃不动作 */ - } - } - } - } - } - if (need) { - /* 重绘覆盖层+选项(仅 sel 高亮变化) */ - meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); - meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container); - meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline); - if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20); - for (int i = 0; i < n; i++) { - int ry = d_y + title_h + i * opt_row; - int cur_sel = (i == sel); - if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36); - ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "", - cur_sel ? 0xFFFFFF : th->on_surface, 20); - if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary); - } - meow_dirty_full(); meow_ui_flush(); - } - usleep(POLL_MS * 1000); - } -ddone: - if (tfd >= 0) close(tfd); - kp_close(&kp_power); kp_close(&kp_keys); - return result; -} - -/* ---- 字体行「family→字重」两级子菜单(M2.6 字重结构)---- - * 一级:列出扫描到的去重 family(class 行首项「跟随全局」=空值);选 family 后自动进入 - * 二级:该 family 的可用字重(regular/bold/italic…)。选中写 out_val = 卡根相对路径 - * MeowOS/fonts///font.png(或 class 行「跟随全局」=空串)。 - * 复用 s_dd_modal(与 WiFi/关于 同款模态)。返回 1=已选中(out_val 已填),0=取消。 */ -static int s_font_dropdown(const srow_t *r, char *out_val, int vlen) -{ - if (!r) return 0; - int is_cls = r->key && !strncmp(r->key, "font.class_", 11); - char cur[MEOW_SET_MAX_LEN]; - meow_settings_get_str(r->key, cur, sizeof cur); - char curfam[64] = "", curwt[32] = ""; - if (cur[0]) - font_family_weight_from_png(cur, curfam, sizeof curfam, curwt, sizeof curwt); - - /* ---- 一级:去重 family 列表(class 行首项「跟随全局」)---- */ - char fam_slug[S_NET_MAX][64], fam_disp[S_NET_MAX][64]; - int famn = 0; - if (is_cls) { - if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], ""); - snprintf(fam_disp[famn], sizeof fam_disp[0], "跟随全局字体"); famn++; } - } - for (int i = 0; i < font_presets_n && famn < S_NET_MAX; i++) { - const char *f = font_preset_fams[i] ? font_preset_fams[i] : ""; - int dup = 0; - for (int k = 0; k < famn; k++) if (!strcmp(fam_slug[k], f)) { dup = 1; break; } - if (dup) continue; - snprintf(fam_slug[famn], sizeof fam_slug[0], "%s", f); - snprintf(fam_disp[famn], sizeof fam_disp[0], "%s", - (font_preset_names[i] && font_preset_names[i][0]) ? font_preset_names[i] : f); - famn++; - } - if (famn <= 0) return 0; - const char *fitem[S_NET_MAX]; - char ftext[S_NET_MAX][64]; - for (int i = 0; i < famn; i++) { snprintf(ftext[i], sizeof ftext[i], "%s", fam_disp[i]); fitem[i] = ftext[i]; } - int cur_fam = is_cls ? 0 : -1; - for (int i = is_cls ? 1 : 0; i < famn; i++) - if (curfam[0] && !strcmp(fam_slug[i], curfam)) cur_fam = i; - int pick = s_dd_modal(r->label, fitem, famn, cur_fam); - if (pick < 0) return 0; - if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */ - const char *fam = fam_slug[pick]; - - /* ---- 二级:该 family 的字重列表(值=完整卡根相对路径,显示=label)---- */ - char wt_val[S_NET_MAX][MEOW_SET_MAX_LEN]; - char wt_text[S_NET_MAX][96]; - int wn = 0; - for (int i = 0; i < font_presets_n && wn < S_NET_MAX; i++) { - if (!font_preset_fams[i] || strcmp(font_preset_fams[i], fam)) continue; - snprintf(wt_val[wn], sizeof wt_val[0], "%s", font_presets[i]); - snprintf(wt_text[wn], sizeof wt_text[0], "%s", font_preset_labels[i]); - wn++; - } - if (wn <= 0) return 0; /* 该 family 无字重(理论不出现:能进 family 必已探到字重) */ - const char *witem[S_NET_MAX]; - for (int i = 0; i < wn; i++) witem[i] = wt_text[i]; - int cur_wt = -1; - for (int i = 0; i < wn; i++) if (cur[0] && !strcmp(wt_val[i], cur)) cur_wt = i; - int wp = s_dd_modal(r->label, witem, wn, cur_wt); - if (wp < 0) return 0; - snprintf(out_val, vlen, "%s", wt_val[wp]); - return 1; -} - -/* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */ -static void run_settings_page(void) -{ - int sel = s_first_setting(); - int scroll_off = 0; - int H = fb_yres; - - /* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts///,刷新「字体文件」下拉。 - * 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */ - meow_font_scan_presets(); - - struct keypoll kp_power, kp_keys; - kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); - struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; - memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); - int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; - } - int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); - if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } - unsigned long long touch_cooldown_until = now_ms() + 300; - unsigned long long last_tick = now_ms(); - unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */ - int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */ - - /* 触摸拖动滑条状态 */ - int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; - int drag = -1; /* 被拖动的行索引;-1=无 */ - /* M2.37 列表拖动滚动状态 */ - int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */ - int scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */ - int scroll_y0 = 0; /* 按下原点纵向坐标 */ - int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */ - int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */ - int wake_suppress = 0; /* 自动息屏唤醒后:吸收唤醒键边沿,待全部抬起再接管 */ - /* M2.x(KEY_CONTROL_DESIGN §2)电源键状态:设置页电源键 = 系统级语义(非返回), - * 对照主界面 run_menu 3130-3137 的同名变量。单击=息屏/亮屏、长按=电源菜单、8s=强关。 */ - unsigned long long pwr_hold_at = 0; /* 本次电源按下计时起点 */ - int pwr_menu_state = 0; /* 是否已因此次 hold 弹过电源菜单 */ - int pwr_woke = 0; /* 本次按下=自动息屏唤醒(释放只唤醒不 toggle) */ - int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */ - - /* 首次全帧绘制 */ - s_net_sync_from_sys(); /* M2.39:WiFi/蓝牙开关行同步真实硬件态 + 刷新网络信息缓存 */ - scroll_off = s_clamp_scroll(sel, scroll_off); - draw_settings_page(sel, scroll_off); - meow_dirty_full(); meow_ui_flush(); - - for (;;) { - unsigned long long now = now_ms(); - if (sigterm_flag) break; - /* 重绘触发:值/选中/滚动变化 */ - int need_paint = 0; - - /* ---- M2.37 自动息屏态:留在设置页,等待任意输入唤醒,恢复后重绘(不丢上下文)---- - * 对照 run_menu 主循环 meow_auto_slept 的唤醒重绘(M2.17/2.18)。休眠期间仅探测 - * 输入(按键原始按下 / black 模式触摸按下);一旦唤醒:亮屏、清自动态并整帧重绘。 */ - if (meow_auto_slept && !meow_screen_on) { - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - int wake = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1; - } - if (!wake && screen_off_mode == 1 && tfd >= 0) { /* black 模式:面板仍上电,触摸可唤醒 */ - struct pollfd spfd = { tfd, POLLIN, 0 }; - if (poll(&spfd, 1, 0) > 0) { - unsigned char ev[16]; - while (read(tfd, ev, sizeof ev) == 16) { - uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); - int32_t val = *(int32_t *)(ev + 12); - if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1; - } - } - } - if (wake) { - meow_panel_on(); - meow_auto_slept = 0; - wake_suppress = 1; - last_act = now_ms(); - /* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */ - memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - touch_cooldown_until = now_ms() + 300; - td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1; - logmsg("settings: auto screen-off -> input wake, stayed in settings page"); - scroll_off = s_clamp_scroll_range(scroll_off); - draw_settings_page(sel, scroll_off); - meow_dirty_full(); meow_ui_flush(); - } - usleep(POLL_MS * 1000); - continue; - } - - /* ---- 按键 ---- */ - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - int vold = 0, volu = 0, play = 0, pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; - } - (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); - (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); - vold = kd_vold.stable; volu = kd_volu.stable; - play = kd_play.stable; pwr = kd_pwr.stable; - if (wake_suppress) { - /* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作, - * 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */ - if (!pwr && !vold && !volu && !play) wake_suppress = 0; - } else { - /* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束: - * 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。 - * 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */ - if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } - if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } - /* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」: - * 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。 - * 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */ - if (play && !m_prev_play) { - logmsg("settings: play key caught (media-reserved, no exit)"); - last_act = now; - } - /* M2.x(KEY_CONTROL_DESIGN §2)电源键 = 系统级语义,对齐主界面 run_menu 3284-3366: - * 单击(按<长按阈值后松开)= 息屏/亮屏;长按>=longpress_menu_ms = 电源菜单; - * 按住>=forceoff_ms(且未开过菜单)= 强制关机;自动息屏按下只唤醒。 - * ※ 电源键在设置页不再映射为「返回」——退出只能靠触摸(底部返回按钮 + 左缘左滑)。 */ - if (pwr && !m_prev_pwr) { - pwr_hold_at = now; pwr_menu_state = 0; - pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */ - last_act = now; /* 视为输入,防按住期间误触发自动息屏 */ - /* M2.18 Bug 2 同款:按下正落在自动息屏态(auto_slept 且屏已关)→ 本次只做唤醒 */ - if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; - } - if (pwr && pwr_hold_at) { - /* M2.10/M2.11 B/M2.37 同款:8s 强关只在该「按住」从未开过菜单(!pwr_menu_state) - * 时触发;开过菜单则交给菜单内确认/取消,杜绝「开菜单->取消->仍按住->强关」残留。 */ - if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { - if (sim_no_exec) { logmsg("settings pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } - else { logmsg("settings pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } - /* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */ - } else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { - pwr_menu_state = 1; - logmsg("settings pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); - show_power_menu(); /* 动作在函数内 system();取消则续循环 */ - last_act = now_ms(); /* 菜单返回即视为「刚有输入」,重置自动息屏计时 */ - /* M2.11 B 同款:菜单已关闭。清空按住计时与菜单态,绝不把已互动的 hold 再累计到 8s 强关。 */ - pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; - pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */ - /* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配, - * 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */ - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */ - need_paint = 1; - /* 已按 raw 对齐 m_prev_*,跳过下方基线复写,防把 raw 值覆盖回旧稳定值 → 假单边沿 */ - goto settings_keys_seeded; - } - } - if (!pwr && m_prev_pwr) { - /* 松开:短按(<长按阈值且菜单未开)→ 单击 = 息屏/亮屏切换 */ - if (pwr_suppress_click) { - logmsg("settings pm: power release after power-menu -> no toggle (suppress)"); - } else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { - logmsg("settings pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); - meow_toggle_panel(); - meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ - last_act = now_ms(); - need_paint = 1; /* 亮屏重绘 / 息屏留屏(面板已黑,下次亮屏重绘) */ - } else if (pwr_woke) { - logmsg("settings pm: power release after auto screen-off wake -> no toggle (wake only)"); - } - pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; - pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */ - } - } - /* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */ - if (vol_ramp_dir != 0) { - int held = (vol_ramp_dir > 0) ? volu : vold; - int going = vol_ramp_tick(held); - g_sys.vol = settings_volume_get(); - paint_sb_right(); - need_paint = 1; - if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol); - } - m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; - settings_keys_seeded: - ; - - /* ---- 触摸 ---- */ - if (tfd >= 0 && meow_screen_on) { /* M2.x:手动电源息屏后不处理触摸(与主界面一致,且防屏黑时误退出/误改值) */ - struct pollfd pfd = { tfd, POLLIN, 0 }; - if (poll(&pfd, 1, 0) > 0) { - unsigned char ev[16]; - while (read(tfd, ev, sizeof ev) == 16) { - 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) tlx = val; - else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val; - if (td && !td_ok && tlx >= 0 && tly >= 0) { - tdx = tlx; tdy = tly; td_ok = 1; - scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */ - /* 按下原点落在选中滑条 track 带内 → 开始拖动 */ - if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 && - tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 && - tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) { - drag = sel; - const srow_t *r = &SMENU[sel]; - int v = s_slider_val_from_x(r, tlx); - s_slider_set_from_track(r, v, 0); - need_paint = 1; - } - } - /* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。 - * scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。 - * M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项); - * 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */ - if (td_ok && drag < 0 && !scroll_drag) { - int ddy = tly - scroll_y0; - if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { - scroll_drag = 1; - meow_settings_get_bool("input.swipe_invert", &scroll_invert); - scroll_base = scroll_off + (scroll_invert ? -ddy : ddy); - } - } - if (scroll_drag && td_ok) { - int dydt = tly - scroll_y0; - scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt)); - need_paint = 1; - } - if (drag >= 0 && td_ok) { - const srow_t *r = &SMENU[drag]; - int v = s_slider_val_from_x(r, tlx); - s_slider_set_from_track(r, v, 0); /* 拖动实时 set_*,抬起才落盘 */ - need_paint = 1; - } - } else if (type == EV_KEY && code == BTN_TOUCH) { - if (val && !td) { - if (now_ms() < touch_cooldown_until) { - tlx = tly = -1; /* 冷却丢弃 */ - } else { - td = 1; td_ok = 0; tlx = -1; tly = -1; drag = -1; scroll_drag = 0; - } - } else if (!val && td) { - td = 0; - if (drag >= 0) { /* 滑条拖动结束 → 落盘 */ - const srow_t *r = &SMENU[drag]; - s_commit(r, 1); - drag = -1; - need_paint = 1; - } else if (scroll_drag) { - scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */ - scroll_base = 0; scroll_y0 = 0; - need_paint = 1; - } else if (td_ok) { - int dx = tlx - tdx, dy = tly - tdy; - /* M2.39 左缘右滑返回:复用全局左缘返回判定 meow_edge_back() - * (与 SDK 全局左缘返回 A-0 同语义:左缘起点 + 右滑≥MEOW_EDGE_SWIPE - * 且横向主导),避免设置页自造一套重复判定。 */ - if (meow_edge_back(tdx, dx, dy)) { - if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } - goto done; - } - int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; - if (adx < 60 && ady < 60) { /* 视为 tap */ - int tx = tdx, ty = tdy; - /* 底部返回按钮 */ - if (ty >= H - 52 && tx >= SET_CARD_X && tx <= SET_CARD_X + 120) { - if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } - goto done; - } - int cy = ty - SET_CONTENT_Y + scroll_off; - int idx = s_row_at_content_y(cy); - if (idx >= 0 && !s_setting_idx(idx)) { - /* 点分组标题行 → 仅选中下一设置行 */ - int ns = s_next_setting(idx); - sel = ns; need_paint = 1; sel_changed = 1; - } else if (idx >= 0) { - const srow_t *r = &SMENU[idx]; - sel = idx; need_paint = 1; sel_changed = 1; - if (r->ctrl == SC_SWITCH) { - int on = 0; meow_settings_get_switch(r->key, &on); - meow_settings_set_switch(r->key, !on); - s_commit(r, 1); - } else if (r->ctrl == SC_LIST) { - /* M2.39 网络列表行:开下拉(重新解析)+ 重绘 + 排空本页 tfd - * (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。 */ - after_modal = 1; - s_net_open(r); - if (tfd >= 0) { - unsigned char e[16]; - while (read(tfd, e, sizeof e) == 16) {} - usleep(40000); - while (read(tfd, e, sizeof e) == 16) {} - } - td = 0; td_ok = 0; drag = -1; scroll_drag = 0; - scroll_base = 0; scroll_y0 = 0; - tdx = tdy = tlx = tly = -1; - touch_cooldown_until = now_ms() + 300; - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - need_paint = 1; - } else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) { - /* 单次点击即开选择器子对话框 */ - after_modal = 1; /* 关闭后整帧重绘(还原被 dim 的状态栏) */ - if (s_row_is_font(r)) { - /* M2.6 字重:字体行走「family→字重」两级子菜单 */ - char fval[MEOW_SET_MAX_LEN]; - if (s_font_dropdown(r, fval, sizeof fval)) { - meow_settings_set_str(r->key, fval); - s_commit(r, 1); - } - } else { - int pick = s_dropdown(r); - if (pick >= 0) { - if (r->ctrl == SC_ENUM) meow_settings_set_enum(r->key, pick); - else meow_settings_set_str(r->key, s_preset_value(r, pick)); - s_commit(r, 1); - } - } - /* M2.38 Bug3:s_dropdown 返回后彻底排空本页 tfd + - * 重置触摸基线。根因:input 子系统把事件广播到所有 - * 打开的 fd —— s_dropdown 用独立 fd 消费选项 tap,但 - * 本页 tfd 同样收到了整套下拉会话事件;若不清空, - * 外层 while(read) 会把它们当新 tap → 穿透到下层设置项。 - * 故 排空→40ms→再排空 + 重置触摸状态 + 冷却窗口吸收长尾。 */ - if (tfd >= 0) { - unsigned char e[16]; - while (read(tfd, e, sizeof e) == 16) {} - usleep(40000); - while (read(tfd, e, sizeof e) == 16) {} - } - td = 0; td_ok = 0; drag = -1; scroll_drag = 0; - scroll_base = 0; scroll_y0 = 0; - tdx = tdy = tlx = tly = -1; - touch_cooldown_until = now_ms() + 300; - /* 重置设置页按键防抖基线,防残边 */ - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - need_paint = 1; - } else if (r->ctrl == SC_SLIDER && tlx >= 0) { - int v = s_slider_val_from_x(r, tlx); - s_slider_set_from_track(r, v, 1); - } - } - } - } - td_ok = 0; - } - } - } - } - } - - /* ---- 滚动 + 重绘 ---- */ - if (need_paint) { - last_act = now; - /* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */ - if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; } - draw_settings_page(sel, scroll_off); - /* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化), - * 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。 - * 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */ - if (after_modal) { meow_dirty_full(); after_modal = 0; } - else meow_dirty_add(0, 42, fb_xres, fb_yres - 42); - meow_ui_flush(); - } - /* M2.37 超时自动息屏(与主界面一致):空闲 > screen_off_timeout → 关屏,但留在设置页: - * 置 meow_auto_slept 进入休眠态,由顶部睡眠门等待任意输入唤醒并重绘设置页、不丢上下文 - * (对照 run_menu 外层 meow_auto_slept 的唤醒重绘,M2.17/2.18 同款语义)。 */ - if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && - (unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) { - meow_panel_off(); - meow_auto_slept = 1; - wake_suppress = 1; /* 唤醒输入边沿待完全抬起后再接管,避免误动作 */ - logmsg("settings: idle %ds -> auto screen-off (stay in settings)", screen_off_timeout); - } - /* ~1s 状态栏时钟刷新 */ - if (now - last_tick >= 1000) { - last_tick = now; - draw_clock(); - fb_norm_buf0(); - meow_ui_flush(); - } - usleep(POLL_MS * 1000); - } -done: - if (tfd >= 0) close(tfd); - kp_close(&kp_power); kp_close(&kp_keys); - logmsg("settings page closed"); -} - -/* ================================================================== */ -/* 阶段B:通知中心面板(状态栏下滑,系统级叠加层,§6.1 ③ 仅框架内部) */ -/* - 入口:run_menu 顶部热区下滑 → run_notif_center 阻塞子循环。 */ -/* - 结构:顶部把手/标题 + 快捷开关行(WiFi/蓝牙/主题/亮度/音量) + */ -/* 亮度/音量滑条 + 通知列表(app 图标+标题+内容+时间+进度)。 */ -/* - 只读写 meow_settings_* / meow_notif 存储,零旁路;MD3 接 s_ui_theme()。*/ -/* - 手势:面板内上滑/点空白/左缘左滑关闭(左缘复用 meow_edge_back)。 */ -/* - 物理键:系统语义不变(电源=息屏/亮屏/菜单/8s强关、音量=调音量、 */ -/* 播放=媒体预留),绝不映射为返回/确认/exit(APPS §3.4.1)。 */ -/* - 性能:打开/交互只标面板体区域脏(状态栏 0-27 由 draw_statusbar */ -/* 单独维护),绝不整屏重绘;滑条拖动实时 set_*(后端 clamp),抬手落盘。*/ -/* ================================================================== */ - -/* ---- 面板布局常量 ---- */ -#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 起作用) */ - -/* 勿扰过滤阈值:dnd=1 只显示 urgency>=2(HIGH=2/CRITICAL=3) */ -#define NCP_DND_MIN_URGENCY 2 - -/* ⑧ 一键清除(勿扰对称处):与勿扰带同排(NCP_DND_Y/H),x 对半(中线=fb_xres/2)。 - * 勿扰=左半段 [NCP_ROW_X, fb_xres/2);清除=右半段 [fb_xres/2, fb_xres-NCP_ROW_X)。 */ -#define NCP_DND_SPLIT_X (fb_xres / 2) /* 勿扰/清除带分割中线(左右对称) */ - -/* ---- 开关平移动画(§9:状态时间轴 + 快照 blit 平移,不整屏重绘)---- */ -#define NCP_ANIM_FIX 1024 /* 进度 p 的定点分母(1/1024 精度) */ -#define NCP_ANIM_MS 180 /* 单程打开/关闭时长(ms,真机可调) */ - -/* 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; - -static ncp_row_t g_rows[NCP_MAX_ROWS]; -static int g_nrows = 0; -static struct app *g_ncp_apps = NULL; /* run_notif_center 注入(通知点击找 app 用) */ -static int g_ncp_napps = 0; - -/* ⑥ 勿扰面板内开关:当前 dnd 态(开面板/切换时从 meow_settings notify.dnd 读/写,零旁路)。 - * ncp_collect_cb / ncp_active_count 按它过滤行(only urgency>=NCP_DND_MIN_URGENCY when on)。 */ -static int g_ncp_dnd = 0; - -/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。 - * g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。 - * run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint) - * 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */ -static int g_ncp_swipe_row = -1; -static int g_ncp_swipe_off = 0; - -/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */ -static void ncp_strncpy(char *dst, const char *src, size_t len) -{ - if (!dst || len == 0) return; - if (!src) { dst[0] = 0; return; } - strncpy(dst, src, len - 1); - dst[len - 1] = 0; -} - -/* app_id → 显示名(apps[] 匹配则用 name,否则原样 app_id;无则空串) */ -static const char *ncp_app_name(const char *app_id) -{ - if (app_id && g_ncp_apps) - for (int i = 0; i < g_ncp_napps; i++) - if (!strcmp(g_ncp_apps[i].id, app_id)) return g_ncp_apps[i].name; - return app_id ? app_id : ""; -} - -/* 通知图标加载回调:把 app_icon(相对卡根/绝对)解成绝对路径,加载 meow_img。 */ -static void ncp_load_icon(ncp_row_t *r) -{ - if (!r) return; - r->img = NULL; - if (!r->icon[0]) return; - char path[MEOW_NOTIF_ICON_MAX + 80]; - if (r->icon[0] == '/') snprintf(path, sizeof path, "%s", r->icon); - else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", r->icon); - r->img = meow_img_load_file(path); -} - -static int ncp_collect_cb(const meow_notif_t *n, void *ctx) -{ - (void)ctx; - if (g_nrows >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */ - /* 勿扰(⑥):dnd 开时只收集 urgency>=NCP_DND_MIN_URGENCY(HIGH/CRITICAL)。 */ - if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0; - ncp_row_t *r = &g_rows[g_nrows++]; - memset(r, 0, sizeof *r); - r->id = n->id; - ncp_strncpy(r->app_id, n->app_id, sizeof r->app_id); - ncp_strncpy(r->summary, n->summary, sizeof r->summary); - ncp_strncpy(r->body, n->body, sizeof r->body); - ncp_strncpy(r->icon, n->app_icon, sizeof r->icon); - ncp_strncpy(r->target, n->target_activity, sizeof r->target); - r->created_ms = n->created_ms; - r->progress = n->progress; - r->urgency = n->urgency; - ncp_load_icon(r); - return 0; -} - -/* 释放全部缓存图标 + 清行数(面板关闭/列表刷新前用)。 */ -static void ncp_rows_free(void) -{ - for (int i = 0; i < g_nrows; i++) - if (g_rows[i].img) { meow_img_free(g_rows[i].img); g_rows[i].img = NULL; } - g_nrows = 0; -} - -/* 重新采集通知列表(比 meow_notif_count 变化时调用)。 */ -static int ncp_refresh_rows(void) -{ - ncp_rows_free(); - meow_notif_list(NULL, ncp_collect_cb, NULL); - return g_nrows; -} - -/* 当前应显示的活跃通知数(按 g_ncp_dnd 过滤,与 g_rows 同口径)——供变化检测/重采集用。 */ -static int ncp_count_cb(const meow_notif_t *n, void *ctx) -{ - if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0; - (*(int *)ctx)++; - return 0; -} -static int ncp_active_count(void) -{ - int c = 0; - meow_notif_list(NULL, ncp_count_cb, &c); - if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS; - return c; -} - -/* 通知列表是否变化(按过滤后活跃数比;新条/撤下/TTL 释放都反映在 count,含 dnd 过滤)。 */ -static int ncp_rows_changed(void) -{ - return ncp_active_count() != g_nrows; -} - -/* ---- 开关平移动画辅助(§9:快照 blit + 平移;只在 run_notif_center 内分配/释放)---- - * g_ncp_ph = 面板高 PH = fb_yres - NCP_TOP;两块快照各 PH × fb_line 字节: - * bg_snap = 进入面板时(绘制面板前)从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拍的背景(应用列表); - * panel_snap= 最终位置整面板渲染(draw_notif_center 后拍),稳态重绘后刷新。 - * 快照不可用(分配失败/无 fb)时动画降级为瞬时显示/消失(fallback)。 */ -static unsigned char *g_ncp_bg_snap = NULL; -static unsigned char *g_ncp_panel_snap = NULL; -static int g_ncp_ph = 0; - -static void ncp_snap_init(void) -{ - int ph = fb_yres - NCP_TOP; - if (ph <= 0 || fb_line <= 0) { g_ncp_ph = 0; return; } - size_t bsz = (size_t)ph * (size_t)fb_line; - g_ncp_bg_snap = malloc(bsz); - g_ncp_panel_snap = malloc(bsz); - if (!g_ncp_bg_snap || !g_ncp_panel_snap) { - free(g_ncp_bg_snap); g_ncp_bg_snap = NULL; - free(g_ncp_panel_snap); g_ncp_panel_snap = NULL; - g_ncp_ph = 0; - return; - } - g_ncp_ph = ph; -} -static void ncp_snap_free(void) -{ - free(g_ncp_bg_snap); g_ncp_bg_snap = NULL; - free(g_ncp_panel_snap); g_ncp_panel_snap = NULL; - g_ncp_ph = 0; -} - -/* 从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拷贝到 dst(行宽 fb_line)。 */ -static void ncp_snap_copy_rows(unsigned char *dst, const unsigned char *base) -{ - if (!dst || g_ncp_ph <= 0) return; - size_t row = (size_t)fb_line; - for (int i = 0; i < g_ncp_ph; i++) - memcpy(dst + (size_t)i * row, base + (size_t)i * row, row); -} -/* 拍背景(应用列表):进入面板时、面板绘制前调用。 */ -static void ncp_snap_bg(void) -{ - ncp_snap_copy_rows(g_ncp_bg_snap, fb + (size_t)NCP_TOP * fb_line); -} -/* 拍最终整面板:draw_notif_center 画完后 / 每次稳态重绘后调用。 */ -static void ncp_snap_panel(void) -{ - ncp_snap_copy_rows(g_ncp_panel_snap, fb + (size_t)NCP_TOP * fb_line); -} - -/* 重建动画帧:把 back_buf 面板区 [NCP_TOP..) 还原为「背景 + 平移后的面板」。 - * off∈[-PH,0];prev_bot=上一帧覆盖底缘(exclusive),用于关(收窄)时恢复新露出背景条。 - * 返回本帧覆盖底缘(exclusive,供调用方算脏区与传给下次)。 */ -static int ncp_anim_rebuild(int off, int prev_bot) -{ - if (!g_ncp_bg_snap || !g_ncp_panel_snap || g_ncp_ph <= 0) return NCP_TOP; - int top = NCP_TOP; - int ph = g_ncp_ph; - int cur_bot = mnp_anim_covered_bot(top, ph, off); /* ∈[top, top+ph](§9.3 裁切) */ - if (cur_bot < top) cur_bot = top; - if (cur_bot > top + ph) cur_bot = top + ph; - size_t row = (size_t)fb_line; - /* 1) 面板收窄(收回中):[cur_bot, prev_bot) 新露出 → 恢复背景(应用列表)。 */ - if (prev_bot > cur_bot) { - int b0 = prev_bot < top + ph ? prev_bot : top + ph; - for (int sy = cur_bot; sy < b0; sy++) - memcpy(fb + (size_t)sy * row, - g_ncp_bg_snap + (size_t)(sy - top) * row, row); - } - /* 2) 贴平移面板:[top, cur_bot) 从 panel_snap 行 yy = sy-(top+off) 拷回。 */ - for (int sy = top; sy < cur_bot; sy++) { - int yy = sy - (top + off); /* ∈[0, ph)(见 §9.3 裁切论证) */ - if (yy < 0) yy = 0; - if (yy >= ph) yy = ph - 1; - memcpy(fb + (size_t)sy * row, - g_ncp_panel_snap + (size_t)yy * row, row); - } - return cur_bot; -} - -/* ⑨ 应用一帧面板动画(开关时间轴 §9 与跟手拖动 ⑨ 共用):把 back_buf 面板区重建为 - * 「背景 + 平移后的面板」并标脏/呈现。p=当前进度;prev_bot=上一帧覆盖底缘(exclusive, - * 回填本帧);ok=快照可用(否则 no-op,动画降级为瞬时)。只刷面板矩形 + 新露出条, - * 顶= NCP_TOP(不盖状态栏)、不含 [0,NCP_TOP) 带;不重排面板控件(panel_snap 固定)。 */ -static void ncp_anim_frame(int p, int *prev_bot, int ok) -{ - if (!ok || g_ncp_ph <= 0 || !prev_bot) return; - int off = mnp_anim_off(p, NCP_ANIM_FIX, g_ncp_ph); - int cur_bot = ncp_anim_rebuild(off, *prev_bot); - int maxb = (*prev_bot > cur_bot) ? *prev_bot : cur_bot; - *prev_bot = cur_bot; - meow_dirty_add(0, NCP_TOP, fb_xres, maxb - NCP_TOP); - meow_ui_flush(); -} - -/* 把文本按宽度截断(UTF-8 安全:不切开多字节码),超出加 "..."。 */ -static void ncp_text(int x, int y, const char *s, int size, uint32_t color, int maxw) -{ - if (!s) s = ""; - if (ui_width(s, size) <= maxw) { ui_draw(x, y, s, color, size); return; } - int ell = ui_width("...", size); - int limit = maxw - ell; - int len = 0; - char buf[128]; - while (s[len] && len < (int)sizeof buf - 8) { - /* 取当前字节对应的 UTF-8 码长(1-4),避免切断多字节字符 */ - unsigned char c0 = (unsigned char)s[len]; - int cp = 1; - if ((c0 & 0xE0) == 0xC0) cp = 2; - else if ((c0 & 0xF0) == 0xE0) cp = 3; - else if ((c0 & 0xF8) == 0xF0) cp = 4; - if (len + cp >= (int)sizeof buf - 1) break; - for (int k = 0; k < cp; k++) buf[len + k] = s[len + k]; - buf[len + cp] = 0; - if (ui_width(buf, size) > limit) { buf[len] = 0; break; } - len += cp; - } - if (len > 0) { - if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; } - ui_draw(x, y, buf, color, size); - } else { - ui_draw(x, y, "...", color, size); - } -} - -/* 画一个小圆点(近似圆 = 圆角方 r=radius;主题 chip 用)。 */ -static void ncp_dot(int cx, int cy, int r, uint32_t col) -{ - meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, col); -} - -/* 画 32×32 图标(复用 sb_blit_icon;ink 垂直中心对齐 cy)。 */ -static void ncp_sb_icon(int icon, int slot_x, int cy, uint32_t col, int ink_h) -{ - int saved = g_sb_icon_cy; - g_sb_icon_cy = cy; - sb_blit_icon(icon, slot_x, col, ink_h); - g_sb_icon_cy = saved; -} - -/* chip 位置(4 个,居中排布)。返回下标对应 chip 的 x 左缘。 */ -static int ncp_chip_x(int chip) -{ - int total = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP; - int x0 = (fb_xres - total) / 2; - return x0 + chip * (NCP_CHIP_W + NCP_CHIP_GAP); -} -static int ncp_chip_hit(int chip, int tx, int ty) -{ - int x = ncp_chip_x(chip); - return (tx >= x && tx < x + NCP_CHIP_W && ty >= NCP_CHIP_Y && ty < NCP_CHIP_Y + NCP_CHIP_H); -} - -/* 面板当前 4 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */ -static void ncp_chip_state(int chip, int *on, char *val, int vlen) -{ - *on = 0; - if (val && vlen > 0) val[0] = 0; - if (chip == NCP_CHIP_WIFI) { - int v = 0; meow_settings_get_switch("network.wifi_enabled", &v); - *on = v; - } else if (chip == NCP_CHIP_BT) { - int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v); - *on = v; - } else if (chip == NCP_CHIP_THEME) { - int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */ - *on = (v == 1); - if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色"); - } else if (chip == NCP_CHIP_BRIGHT) { - int v = 0; meow_settings_get_int("display.brightness", &v); - int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin); - int vc = mnp_brightness_clamp(v, vmin); - if (val && vlen > 0) snprintf(val, vlen, "%d%%", vc); - *on = (vc >= 50); /* 过半视为"亮"态 */ - } else if (chip == NCP_CHIP_POWER) { - /* 电源菜单 chip:非开关(无 on/off 态),恒示中性;点击由 ncp_chip_apply 唤起电源菜单。 */ - *on = 0; - } -} - -/* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。 - * 返回 1=主题变化(需重刷状态栏);2=电源菜单已弹出(调用方置 after_modal=1 整帧重绘还原); - * 0=其它。电源菜单 chip 复用既有系统级 show_power_menu(含 dim/圆钮/音量导航/电源确认/8s强关/ - * 空闲自动息屏),真弹对话框,动作在函数内 system();取消则返回续 panel —— 与电源键长按同款。 */ -static int ncp_chip_apply(int chip) -{ - if (chip == NCP_CHIP_WIFI) { - int v = 0; meow_settings_get_switch("network.wifi_enabled", &v); - meow_settings_set_switch("network.wifi_enabled", !v); - s_net_toggle_wifi(!v); - s_net_refresh(); - } else if (chip == NCP_CHIP_BT) { - int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v); - meow_settings_set_switch("network.bluetooth_enabled", !v); - s_net_toggle_bt(!v); - s_net_refresh(); - } else if (chip == NCP_CHIP_THEME) { - /* 快捷主题:切到 manual(确保生效),再翻转 dark/light(display.theme)。 */ - int mode = THEME_MODE_MANUAL; - meow_settings_get_enum("display.theme_mode", &mode); - if (mode != THEME_MODE_MANUAL) meow_settings_set_enum("display.theme_mode", THEME_MODE_MANUAL); - int v = 0; meow_settings_get_enum("display.theme", &v); - meow_settings_set_enum("display.theme", v ? 0 : 1); - paint_sb_right(); /* 状态栏文字/底色随主题刷新 */ - meow_settings_save(); - return 1; /* 主题变化 → 重刷状态栏 */ - } else if (chip == NCP_CHIP_BRIGHT) { - /* 快捷亮度:在「极暗(下限)」与「100」之间切换。 */ - int cur = 0; meow_settings_get_int("display.brightness", &cur); - int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin); - if (mnp_brightness_clamp(cur, vmin) < 100) { - meow_settings_set_int("display.brightness", 100); - write_brightness(100); - g_sys.brightness = 100; - } else { - meow_settings_set_int("display.brightness", vmin); - write_brightness(vmin); - g_sys.brightness = vmin; - } - paint_sb_right(); - } else if (chip == NCP_CHIP_POWER) { - /* 电源菜单 chip:唤起既有系统级电源菜单(真弹对话框,动作在函数内 system();取消则返回)。 - * 不涉及任何设置项(电源菜单不走 meow_settings,零旁路)。返回 2 → 调用方整帧重绘还原面板/状态栏。 */ - logmsg("notif-center: power chip -> show power menu"); - show_power_menu(); - return 2; - } - meow_settings_save(); - return 0; -} - -/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。 - * off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */ -static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th) -{ - if (off >= 0) return; - int bw = -off; - if (bw <= 0) return; - int bx = NCP_ROW_X + NCP_ROW_W + off; /* 被空出的右条左缘 */ - fill_rect(bx, ry, bw, NCP_ROW_H - 6, th->error); - if (bw >= 44) { - int cx = bx + bw / 2, cy = ry + (NCP_ROW_H - 6) / 2; - ui_draw(cx - ui_width("删除", 16) / 2, cy - meow_font_line_height(16) / 2, - "删除", 0xFFFFFF, 16); - } -} - -/* 画一行内容(行卡 + 图标/APP名/时间/标题/正文/进度),整体随 off 左移(⑦ 行左滑跟手)。 - * i=g_rows 下标;ry=行顶(已含 scroll);off=行左移量(px,<=0)。 */ -static void ncp_draw_row(int i, int ry, int off, const meow_ui_theme_t *th) -{ - const ncp_row_t *r = &g_rows[i]; - meow_gfx_fill_round(NCP_ROW_X + off, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container); - /* 图标 */ - int icx = NCP_ICON_X + off, icy = ry + (NCP_ROW_H - 32) / 2; - if (r->img) meow_ui_img_cover(icx, icy, 32, r->img); - else { - /* 回退:主题色圆点 + 首字符 */ - const char *nm = ncp_app_name(r->app_id); - ncp_dot(icx + 16, icy + 16, 16, th->primary); - char ch = nm[0] ? nm[0] : '?'; - char cbuf[4] = { ch, 0, 0, 0 }; - /* 首字符若是多字节(中文),取整码 */ - if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0; - if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } } - int cw = ui_width(cbuf, 20); - ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20); - } - /* app 名 + 时间 */ - ui_draw(NCP_TEXT_X + off, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16); - char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf); - ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16) + off, ry + 12, tbuf, th->on_surface_variant, 16); - /* 标题 + 内容 */ - ncp_text(NCP_TEXT_X + off, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W); - ncp_text(NCP_TEXT_X + off, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W); - /* 进度(progress>=0):细条 */ - if (r->progress >= 0) { - int px = NCP_TEXT_X + off, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14; - fill_rect(px, py, pw, 4, th->surface_variant); - int fw = r->progress * pw / 100; - if (fw > 0) fill_rect(px, py, fw, 4, th->primary); - } -} - -/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。 - * ⑦:左滑中的行(g_ncp_swipe_row)按 g_ncp_swipe_off 左移,其下先画删除底(ncp_draw_delete_bg)。 */ -static void ncp_draw_list(int scroll) -{ - const meow_ui_theme_t *th = s_ui_theme(); - if (g_nrows == 0) { - ui_draw(NCP_ROW_X + 8, NCP_LIST_TOP + 40, "暂无通知", th->on_surface_variant, 22); - return; - } - int total = g_nrows * NCP_ROW_H; - scroll = mnp_scroll_clamp(scroll, total, NCP_LIST_BOT - NCP_LIST_TOP); - for (int i = 0; i < g_nrows; i++) { - int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll; - if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */ - int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */ - if (off < 0) ncp_draw_delete_bg(ry, off, th); - ncp_draw_row(i, ry, off, th); - } - /* 滚动条 */ - if (total > NCP_LIST_BOT - NCP_LIST_TOP) { - int view = NCP_LIST_BOT - NCP_LIST_TOP; - int thumb_h = view * view / total; if (thumb_h < 24) thumb_h = 24; - int ty = NCP_LIST_TOP + (view - thumb_h) * scroll / (total - view); - meow_gfx_fill_round(fb_xres - 8, NCP_LIST_TOP, 5, view, 3, th->surface_container); - meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant); - } -} - -/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底(面板 surface_variant), - * 再画可见行 + 滚动条;只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT)),面板体其它控件 / 状态栏带 - * [0,NCP_TOP) 均不触碰 —— 满足「滚动只重绘列表可视区,绝不整屏」。 */ -static void ncp_repaint_list(int scroll) -{ - const meow_ui_theme_t *th = s_ui_theme(); - fill_rect(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP, th->surface_variant); - ncp_draw_list(scroll); - meow_dirty_add(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP); - meow_ui_flush(); -} - -/* ⑦ 仅重绘一行(行左滑跟手/回弹/滑出专用;只刷该行矩形,不整面板、不刷状态栏)。 - * 清该行带(含被滑出的右条)→ 画删除底(若 off<0)+ 该行内容位移 → 只标该行带脏。 */ -static void ncp_repaint_row(int scroll, int row, int off) -{ - const meow_ui_theme_t *th = s_ui_theme(); - if (row < 0 || row >= g_nrows) return; - int ry = NCP_LIST_TOP + row * NCP_ROW_H - scroll; - if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */ - /* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */ - fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant); - if (off < 0) ncp_draw_delete_bg(ry, off, th); - ncp_draw_row(row, ry, off, th); - meow_dirty_add(0, ry, fb_xres, NCP_ROW_H); - meow_ui_flush(); -} - -/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段): - * 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [0, NCP_DND_SPLIT_X) → 勿扰命中。 - * 右半段归「一键清除」(ncp_clear_hit,互斥);不与 5 chip(y≤130)、滑条带(y≤238)、 - * 通知列表(y≥NCP_LIST_TOP=308)、状态栏(y<28)重叠。 */ -static int ncp_dnd_hit(int tx, int ty) -{ - return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H && - tx >= 0 && tx < NCP_DND_SPLIT_X); -} - -/* ⑧ 一键清除带命中(勿扰对称处的右半段): - * 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [NCP_DND_SPLIT_X, fb_xres) → 清除命中。 */ -static int ncp_clear_hit(int tx, int ty) -{ - return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H && - tx >= NCP_DND_SPLIT_X && tx < fb_xres); -} - -/* ⑥ 绘勿扰带(⑧ 收窄为左半段):label「勿扰」+ 铃铛图标 + 状态「开/关」 + 左半段右侧 s_draw_switch(48×28)。 - * 右半段由 ncp_draw_clear 画「一键清除」按钮(对称)。 */ -static void ncp_draw_dnd(const meow_ui_theme_t *th) -{ - int y = NCP_DND_Y; - int dnd_on = g_ncp_dnd; - int cy = y + NCP_DND_H / 2; - /* 铃铛(简化:圆顶 + 顶蒂 + 底部小圆点)。 */ - int bx = NCP_ROW_X + 4, bcy = cy - 2; - meow_gfx_fill_round(bx, bcy - 7, 16, 13, 7, th->on_surface_variant); /* 铃身圆顶 */ - meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */ - ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */ - /* label + 状态 */ - ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18); - char st[16]; - snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关"); - int lw = ui_width("勿扰", 18); - ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st, - dnd_on ? th->primary : th->on_surface_variant, 18); - /* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */ - s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th); -} - -/* ⑧ 绘一键清除按钮(勿扰对称处的右半段):圆角按钮底 + 垃圾桶字形 + 「清除」文字。 */ -static void ncp_draw_clear(const meow_ui_theme_t *th) -{ - int y = NCP_DND_Y; - int cy = y + NCP_DND_H / 2; - int cx0 = NCP_DND_SPLIT_X + 8; /* 右半段按钮左缘 */ - int cw = (fb_xres - NCP_ROW_X) - cx0; /* 右半段按钮宽 */ - if (cw <= 0) return; - meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container); - /* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */ - const char *lbl = "清除"; - int lw = ui_width(lbl, 18); - int unit = 22; /* 图标占位宽 + 与文字的间距 */ - int total = unit + lw; - int gx = cx0 + (cw - total) / 2; /* 图标左缘(居中) */ - int icy = cy; - fill_rect(gx, icy - 7, 14, 2, th->on_surface_variant); /* 顶盖 */ - meow_gfx_fill_round(gx + 1, icy - 3, 12, 12, 2, th->on_surface_variant);/* 桶身 */ - meow_gfx_fill_round(gx - 2, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手左 */ - meow_gfx_fill_round(gx + 14, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手右 */ - ui_draw(gx + unit, cy - meow_font_line_height(18) / 2 + 2, lbl, th->on_surface, 18); -} - -/* ---- ⑧ 一键清除(批量):先转储留档 → 再清空显示(红线:保留 ongoing/media/progress)---- */ -typedef struct { uint32_t ids[NCP_MAX_ROWS]; int n; } ncp_clear_ctx_t; -static int ncp_clear_collect_cb(const meow_notif_t *n, void *ctx) -{ - ncp_clear_ctx_t *c = ctx; - if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0; /* 保留红线项 */ - if (c->n >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */ - c->ids[c->n++] = n->id; - return 0; -} -/* 收集可清除通知 id 后逐条 dismiss(dismiss = archive_one(dump→TF) + 释放槽 = 转储留档 + 清出显示)。 - * 返回清除条数。 */ -static int ncp_clear_all(void) -{ - ncp_clear_ctx_t c; - memset(&c, 0, sizeof c); - meow_notif_list(NULL, ncp_clear_collect_cb, &c); - for (int i = 0; i < c.n; i++) meow_notif_dismiss(c.ids[i]); - return c.n; -} - -/* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。 - * 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过, - * 使脏区仅限面板体,绝不整屏重绘。 */ -static void draw_notif_center(int scroll, int redraw_sb) -{ - const meow_ui_theme_t *th = s_ui_theme(); - int W = fb_xres, H = fb_yres; - /* 状态栏带 [0,NCP_TOP) 仅当 redraw_sb=1(打开/主题变化)时清底,随后由下方 draw_statusbar() - * 覆盖重绘(Stage-One:draw_statusbar 只喂 SDK + 标脏顶条,实际由 SDK present() 重绘); - * redraw_sb=0(交互重绘,如拖滑条)时**绝不触碰**——保留 back_buf 中 SDK 状态栏/上次 top 脏, - * 避免被清成 surface 底仍被 blit → 状态栏数值闪烁/消失(真机①修复)。 - * 面板体(状态栏之下 surface_variant)态恒整片铺。 */ - if (redraw_sb) fill_rect(0, 0, W, NCP_TOP, th->surface); - fill_rect(0, NCP_TOP, W, H - NCP_TOP, th->surface_variant); - - /* 把手 */ - int grab_w = 44, grab_x = (W - grab_w) / 2; - meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline); - /* 标题 + 「上滑/点空白关闭」 提示 */ - ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22); - ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16); - - /* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */ - static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" }; - int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP; - int chip_x0 = (W - total_chip) / 2; - for (int c = 0; c < NCP_CHIP_N; c++) { - int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP); - int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val); - uint32_t bg = on ? th->primary : th->surface_container; - uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant; - meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg); - int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2; - int icon_cy = NCP_CHIP_Y + 14; - if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22); - else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22); - else if (c == NCP_CHIP_THEME) { - if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg); - else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); } - } - else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22); - else if (c == NCP_CHIP_POWER) { - /* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */ - int pc = icon_cx + SB_ICON_SIZE / 2; - meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */ - meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */ - meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */ - } - char label[24]; - snprintf(label, sizeof label, "%s", chip_lbl[c]); - if (val[0] && c == NCP_CHIP_BRIGHT) - snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */ - ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18, - label, fg, 14); - } - - /* 亮度滑条 */ - int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin); - int bval = 0; meow_settings_get_int("display.brightness", &bval); - bval = mnp_brightness_clamp(bval, bmin); - ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18); - char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval); - ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb, - th->on_surface_variant, 18); - s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th); - - /* 音量滑条 */ - int vval = 0; meow_settings_get_int("audio.volume", &vval); - vval = vol_clamp(vval); - ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18); - char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval); - ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl, - th->on_surface_variant, 18); - s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th); - - /* 分隔线 */ - fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline); - - /* ⑥ 勿扰面板内开关带(list 上方,左半段) */ - ncp_draw_dnd(th); - /* ⑧ 一键清除按钮(勿扰对称处的右半段) */ - ncp_draw_clear(th); - - /* 通知列表 */ - ncp_draw_list(scroll); - - /* 底部提示 */ - ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭", - th->on_surface_variant, 16); - - /* 状态栏画在面板体之上(0-27 由 SDK 全局状态栏维护,经 draw_statusbar→sb_sdk_refresh - * 喂数据+标脏顶条;不落面板体脏区)。仅 redraw_sb=1(打开/主题变化)才喂/标脏,交互时跳过 - * 以保持脏区只限面板体(顶条像素留在 back_buf,由 SDK 已合成的状态栏保持)。 */ - if (redraw_sb) draw_statusbar(); -} - -/* 触摸滑条带命中:origin 落在 (slugX, slugY) 的滑条行带内。返回 1=命中。 */ -static int ncp_slider_band(int tx, int ty, int sy) -{ - return (tx >= NCP_SLIDER_X - 14 && tx <= NCP_SLIDER_X + NCP_SLIDER_W + 14 && - ty >= sy - 30 && ty <= sy + 24); -} - -/* 滑条值应用(拖动实时 set_*,save_now=抬手落盘)。 - * 复用设置页方案:亮度 clamp [bmin,100];音量 vol_clamp 上限 + 跳步吸附 + DAC 节流去抖。 - * 返回 1=值实际变更(需重绘面板/刷状态栏);0=吸附+钳制后与当前已提交值相同(懒跳过: - * 不写 DAC/背光、不刷状态栏、不标脏)→ 拖动去抖,消除逐事件刷状态栏/DAC 的卡顿(真机②修复)。 - * 亮度写背光仅 sysfs 不引额外节流;音量写 DAC 节流(meow_dac_vol_write_throttled/flush 同 s_commit)。 */ -static int ncp_slider_apply(int which, int tx, int save_now) -{ - if (which == 1) { - int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin); - int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, bmin, 100); - v = mnp_brightness_clamp(v, bmin); /* 下限 brightness_min(≥1)、上限 100 */ - int cur = 0; meow_settings_get_int("display.brightness", &cur); - if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */ - meow_settings_set_int("display.brightness", v); - write_brightness(v); - g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */ - paint_sb_right(); - if (save_now) meow_settings_save(); - return 1; - } else { - int vmax = vol_clamp(settings_volume_max); /* 音量上限(尊重 audio.volume_max) */ - int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0, vmax); - v = mnp_step_round(v, settings_volume_step); /* 跳步吸附(默认 step=1 → 连续同设置页) */ - v = vol_clamp(v); /* 0..MEOW_VOL_MAX + 上限 clamp */ - if (!save_now && v == settings_volume_get()) return 0; /* 去抖:值未变跳过写 DAC/刷状态栏 */ - meow_settings_set_int("audio.volume", v); - g_sys.vol = settings_volume_get(); /* 状态栏数值源=最近写入值(含 clamp 后) */ - paint_sb_right(); - if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } /* 抬手=无条件直写 DAC */ - else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */ - return 1; - } -} - -/* 通知点击:dismiss(标记阅读)+ 若 app 有 entry 记录则请求启动。返回 1=应关闭面板。 */ -static int ncp_row_tap(int tx, int ty, int scroll, int *want_launch) -{ - int eff = ty - NCP_LIST_TOP + scroll; - int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H); - if (row < 0 || row >= g_nrows) return 0; - const ncp_row_t *r = &g_rows[row]; - meow_notif_dismiss(r->id); /* 点击 = 阅读,撤下 */ - /* 跳转:app entry 记录(apps[] 内且 ok)→ 记 want_launch(run_menu 走标准启动)。 - * target_activity 为后续 M3 导航预留下的跳转目标(activity 级路由后置)。 */ - if (want_launch) *want_launch = -1; - if (g_ncp_apps) - for (int i = 0; i < g_ncp_napps; i++) - if (!strcmp(g_ncp_apps[i].id, r->app_id) && g_ncp_apps[i].ok) { - if (want_launch) *want_launch = i; - break; - } - return 1; /* 点击即关(启动由 run_menu 执行;无记录则仅关闭) */ -} - -/* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */ -static void run_notif_center(struct app *apps, int napps, int *want_launch, - int live_open, int lox, int loy) -{ - if (want_launch) *want_launch = -1; - int list_scroll = 0; - unsigned char evbuf16[16]; - g_ncp_apps = apps; g_ncp_napps = napps; - - struct keypoll kp_power, kp_keys; - kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); - struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; - memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); - int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; - } - int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); - if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } - unsigned long long touch_cooldown_until = now_ms() + 300; - unsigned long long last_tick = now_ms(); - unsigned long long last_act = now_ms(); - unsigned long long last_listchk = now_ms(); - - int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; - int drag_slider = 0; /* 0=无, 1=亮度, 2=音量 */ - /* 通知列表拖动滚动(方向遵循 input.swipe_invert,与设置页列表同式;关闭手势区分见 §11.3) */ - int scroll_drag = 0; /* 是否处于列表区拖动滚动手势 */ - int scroll_base = 0; /* 进入滚动时锁定的 list_scroll 基准(连续跟手,防触发瞬间跳变) */ - int scroll_y0 = 0; /* 按下原点纵向坐标 */ - int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */ - int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */ - int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */ - /* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off): - * row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手); - * row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。 - * row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */ - int row_swipe_row = -1; - int row_off = 0; - int row_settle = 0; - int row_off_target = 0; - unsigned long long row_anim_at = 0; - int row_paint = 0; /* 仅重绘该行带(行左滑专用) */ - /* ⑨ 面板跟手下拉、半程可回: - * panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停); - * drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变); - * drag_y0 = 进入拖动时锁定手指 y(此后 reveal = seed + (tly - drag_y0),连续跟手/半程可回); - * drag_paint = 仅用动画重建路径重绘一帧(复用 §9 快照 blit,不整屏/不重排控件)。 */ - int panel_drag = 0; - int drag_seed_reveal = 0; - int drag_y0 = 0; - int drag_paint = 0; - int closed = 0; /* 非动画路径的兜底关面板(快照不可用时瞬时退出) */ - int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */ - int wake_suppress = 0; - unsigned long long pwr_hold_at = 0; - int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0; - - int need_sb = 1; /* 打开/主题变化时重绘状态栏;其余交互跳过(脏区只限面板体) */ - - /* 开关平移动画状态(§9):anim_p∈[0,NCP_ANIM_FIX],anim_target 为意图(FIX=开、0=关)。 - * 仅当快照可用(anim_ok)才做平滑动画;否则降级为瞬时显示/消失(closed 兜底)。 */ - int anim_p = 0, anim_target = NCP_ANIM_FIX; - int anim_last_target = NCP_ANIM_FIX; /* 记录上一步 anim_target(用于检测「意图切换」重置计时) */ - unsigned long long anim_last = 0; - int anim_prev_bot = NCP_TOP; /* 上一帧面板覆盖底缘(exclusive;初始=NCP_TOP=未覆盖) */ - int anim_ok = 0; - - /* ⑨ live_open 入口(run_menu 热区拖动中切入):手指此刻已按下且正拖动。 - * - 触摸:直接以传入原点作「已按下已锁原点」(evdev 广播,本 fd 只接后续 move/release)。 - * - 进度:面板从隐藏(anim_p=0)起步,target 未定(=0),由拖动路径实时映射 anim_p; - * 松手按阈值锚定 target。默认(非 live_open)仍为 trigger 式自动展开(防御路径)。 */ - if (live_open) { - td = 1; td_ok = 1; tdx = lox; tdy = loy; scroll_y0 = tdy; - anim_target = 0; anim_last_target = 0; /* 待拖动/松手锚定 */ - } - - /* ⑥ 读勿扰面板内开关态(开面板一次;切换在下面 tap 分支写回)。 */ - { - int d = 0; meow_settings_get_bool("notify.dnd", &d); - g_ncp_dnd = d; - } - - /* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0 - * 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */ - ncp_refresh_rows(); - list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, NCP_LIST_BOT - NCP_LIST_TOP); - ncp_snap_init(); - if (g_ncp_ph > 0) { - anim_ok = 1; - ncp_snap_bg(); /* 背景(应用列表)在面板绘制前拍 */ - draw_notif_center(list_scroll, 1); /* 渲染最终位置整面板 → back_buf */ - ncp_snap_panel(); /* 拍面板快照 */ - /* 起点 p=0:按「上一帧为全覆盖(NCP_TOP+PH)」重建 → 整段还原为背景(面板隐藏) */ - anim_prev_bot = ncp_anim_rebuild(mnp_anim_off(0, NCP_ANIM_FIX, g_ncp_ph), - NCP_TOP + g_ncp_ph); - meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP); - meow_ui_flush(); - } else { - draw_notif_center(list_scroll, 1); - meow_dirty_full(); meow_ui_flush(); - } - need_sb = 0; - anim_last = now_ms(); - - int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */ - - for (;;) { - unsigned long long now = now_ms(); - if (sigterm_flag) break; - int animating = anim_ok && (anim_p != anim_target); - - /* 自动息屏(同设置页语义) */ - if (meow_auto_slept && !meow_screen_on) { - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - int wak = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wak = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wak = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wak = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wak = 1; - } - if (!wak && screen_off_mode == 1 && tfd >= 0) { - struct pollfd spfd = { tfd, POLLIN, 0 }; - if (poll(&spfd, 1, 0) > 0) { - unsigned char ev[16]; - while (read(tfd, ev, sizeof ev) == 16) { - uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); - int32_t val = *(int32_t *)(ev + 12); - if (type == EV_KEY && code == BTN_TOUCH && val) wak = 1; - } - } - } - if (wak) { - meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms(); - memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - touch_cooldown_until = now_ms() + 300; - td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1; - scroll_drag = 0; scroll_base = 0; scroll_y0 = 0; - scroll_invert = 0; scroll_rebound = 0; list_paint = 0; - row_swipe_row = -1; row_off = 0; row_settle = 0; - row_off_target = 0; row_anim_at = 0; row_paint = 0; - g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; - panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0; - /* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */ - anim_p = anim_target = anim_last_target = NCP_ANIM_FIX; - if (anim_ok) anim_prev_bot = NCP_TOP + g_ncp_ph; - draw_notif_center(list_scroll, 1); - if (anim_ok) ncp_snap_panel(); - meow_dirty_full(); meow_ui_flush(); - anim_last = now_ms(); - need_sb = 0; - } - usleep(POLL_MS * 1000); - continue; - } - - /* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */ - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - int vold = 0, volu = 0, play = 0, pwr = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; - } - (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); - (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); - vold = kd_vold.stable; volu = kd_volu.stable; - play = kd_play.stable; pwr = kd_pwr.stable; - if (wake_suppress) { - if (!pwr && !vold && !volu && !play) wake_suppress = 0; - } else { - if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } - if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } - if (play && !m_prev_play) { - logmsg("notif-center: play key caught (media-reserved, no exit)"); - last_act = now; - } - if (pwr && !m_prev_pwr) { - pwr_hold_at = now; pwr_menu_state = 0; pwr_suppress_click = 0; last_act = now; - if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; - } - if (pwr && pwr_hold_at) { - if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { - if (sim_no_exec) logmsg("notif-center pm: [sim-no-exec] hold %dms -> would forceoff", power_forceoff_ms); - else { logmsg("notif-center pm: hold %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } - } else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { - pwr_menu_state = 1; - logmsg("notif-center pm: hold %dms -> power menu", power_longpress_menu_ms); - show_power_menu(); - last_act = now_ms(); - pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1; - memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - after_modal = 1; need_paint = 1; - goto ncp_keys_seeded; - } - } - if (!pwr && m_prev_pwr) { - if (pwr_suppress_click) - logmsg("notif-center pm: release after menu -> no toggle"); - else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { - logmsg("notif-center pm: single-click -> toggle panel"); - meow_toggle_panel(); meow_auto_slept = 0; last_act = now_ms(); need_paint = 1; - } else if (pwr_woke) { - logmsg("notif-center pm: release after wake -> no toggle"); - } - pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0; - } - } - if (vol_ramp_dir != 0) { - int held = (vol_ramp_dir > 0) ? volu : vold; - int going = vol_ramp_tick(held); - g_sys.vol = settings_volume_get(); - paint_sb_right(); - need_paint = 1; - if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol); - } - m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; - ncp_keys_seeded: - ; - - /* 触摸 */ - if (tfd >= 0 && meow_screen_on) { - /* 面板是否就位(稳态开):就位才允许控件交互;动画中抑制 chip/滑条/通知行命中 - * (面板在滑,控件坐标已定但只部分可见,命中会误触——见 §9.5)。 */ - int open_settled = anim_ok ? (anim_target >= NCP_ANIM_FIX && anim_p >= NCP_ANIM_FIX) : 1; - struct pollfd pfd = { tfd, POLLIN, 0 }; - if (poll(&pfd, 1, 0) > 0) { - while (read(tfd, evbuf16, sizeof evbuf16) == 16) { - uint16_t type = *(uint16_t *)(evbuf16 + 8), code = *(uint16_t *)(evbuf16 + 10); - int32_t val = *(int32_t *)(evbuf16 + 12); - if (type == EV_ABS) { - if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val; - else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val; - if (td && !td_ok && tlx >= 0 && tly >= 0) { - tdx = tlx; tdy = tly; td_ok = 1; - scroll_y0 = tdy; /* 列表拖动基准:按下原点(进滚动时才锁 scroll_base) */ - /* 按下原点落在滑条带 → 开始拖动(仅面板就位) */ - if (open_settled && drag_slider == 0) { - if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; } - else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; } - } - } - if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; } - else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; } - /* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop, - * 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。 - * 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含 - * row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大: - * 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */ - if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) { - int ddx = tlx - tdx, ddy = tly - tdy; - int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy; - if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) { - int eff = tdy - NCP_LIST_TOP + list_scroll; - int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H); - /* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */ - if (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && - row >= 0 && row < g_nrows) { - row_swipe_row = row; - row_off = mnp_swipe_clamp(ddx, NCP_ROW_W); - row_settle = 0; row_off_target = 0; - row_anim_at = now_ms(); - g_ncp_swipe_row = row; g_ncp_swipe_off = row_off; - row_paint = 1; - } - } - } - /* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */ - if (row_swipe_row >= 0 && td_ok && !row_settle) { - row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W); - g_ncp_swipe_off = row_off; - row_paint = 1; - } - /* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、 - * 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y, - * 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */ - if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) { - int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy); - if (ady2 >= S_SCROLL_SLOP) { - int av = g_nrows * NCP_ROW_H; - int vw = NCP_LIST_BOT - NCP_LIST_TOP; - int in_over = (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && - mnp_scroll_max(av, vw) > 0); - if (!in_over) { - int cur_off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph); - drag_seed_reveal = (g_ncp_ph > 0) ? (g_ncp_ph + cur_off) : 0; - drag_y0 = tly; - panel_drag = 1; - } - } - } - /* ⑨ 跟手:panel_drag 时 reveal=seed+位移 → progress 更新 anim_p;拖动期时间轴暂停。 */ - if (panel_drag && td_ok) { - int reveal = mnp_drag_reveal(drag_seed_reveal, tly - drag_y0, g_ncp_ph); - int np = mnp_drag_progress(reveal, NCP_ANIM_FIX, g_ncp_ph); - if (np != anim_p) { anim_p = np; drag_paint = 1; } - } - /* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 + - * 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式); - * 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */ - if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) { - int ddy = tly - scroll_y0; - if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { - int total = g_nrows * NCP_ROW_H; - int view = NCP_LIST_BOT - NCP_LIST_TOP; - if (mnp_scroll_max(total, view) > 0 && - tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT) { - scroll_drag = 1; - meow_settings_get_bool("input.swipe_invert", &scroll_invert); - scroll_base = mnp_scroll_base(list_scroll, ddy, scroll_invert); - scroll_rebound = 0; - } - } - } - if (scroll_drag && td_ok) { - int total = g_nrows * NCP_ROW_H; - int view = NCP_LIST_BOT - NCP_LIST_TOP; - int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert); - list_scroll = mnp_scroll_clamp(target, total, view); - scroll_rebound = mnp_scroll_rebound(target, mnp_scroll_max(total, view), - NCP_SCROLL_REBOUND_TOL); - list_paint = 1; - } - } else if (type == EV_KEY && code == BTN_TOUCH) { - if (val && !td) { - if (now_ms() < touch_cooldown_until) { tlx = tly = -1; } - else { - td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0; - scroll_drag = 0; scroll_base = 0; scroll_y0 = 0; - scroll_invert = 0; scroll_rebound = 0; - /* ⑦ 新触摸:清行左滑(防上一手势残留) */ - row_swipe_row = -1; row_off = 0; row_settle = 0; - row_off_target = 0; row_anim_at = 0; row_paint = 0; - g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; - /* ⑨ 新触摸:清面板跟手(防上一拖动残留) */ - panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0; - } - } else if (!val && td) { - td = 0; - if (drag_slider) { /* 滑条拖动结束 → 落盘 */ - ncp_slider_apply(drag_slider, tlx >= 0 ? tlx : tdx, 1); - drag_slider = 0; need_paint = 1; - } else if (td_ok) { - int dx = tlx - tdx, dy = tly - tdy; - int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; - /* 左缘左滑 = 关闭(复用全局左缘返回语义;动画中同样可打断)—— - * 优先于列表滚动落定,保「左缘返回关」语义不受滚动影响 */ - if (meow_edge_back(tdx, dx, dy)) { - if (anim_ok) anim_target = 0; else closed = 1; - break; - } - if (row_swipe_row >= 0) { - /* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。 - * 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */ - /* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */ - row_off = mnp_swipe_clamp(dx, NCP_ROW_W); - g_ncp_swipe_off = row_off; - if (mnp_swipe_delete(row_off, NCP_ROW_W, - NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) { - row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */ - } else { - row_off_target = 0; /* 回弹复原 */ - } - row_settle = 1; row_anim_at = now_ms(); - } else if (scroll_drag) { - /* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值); - * 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。 - * 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过 - * 下方 mnp_vertical_up 关闭判定。 */ - int total = g_nrows * NCP_ROW_H; - int view = NCP_LIST_BOT - NCP_LIST_TOP; - int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert); - list_scroll = mnp_scroll_clamp(target, total, view); - if (scroll_rebound) { - if (anim_ok) anim_target = 0; else closed = 1; - break; - } - list_paint = 1; - scroll_base = 0; scroll_y0 = 0; scroll_rebound = 0; scroll_invert = 0; - } else if (panel_drag) { - /* ⑨ 面板跟手拖动松手:按阈值锚定 target(≥40% → 补到展开; - * <40% 或反向拖回 → 回 0 收回 = 半程取消/关闭过阈值)。时间轴接管 - * 把 anim_p 补到位;不误关面板、不走 tap/上滑/点空白(本手势锁定)。 */ - int go_open = mnp_drag_anchor(anim_p, NCP_ANIM_FIX, NCP_DRAG_THR_PCT); - if (anim_ok) anim_target = go_open ? NCP_ANIM_FIX : 0; - else if (!go_open) closed = 1; /* 无快照降级:收回 → 直接退出 */ - panel_drag = 0; drag_paint = 0; drag_seed_reveal = 0; drag_y0 = 0; - } else if (adx < 60 && ady < 60) { /* tap */ - if (animating) break; /* 动画进行中:tap 忽略(控件命中会误触,见 §9.5) */ - int tx = tdx, ty = tdy; - int hit = 0; - for (int c = 0; c < NCP_CHIP_COUNT; c++) - if (ncp_chip_hit(c, tx, ty)) { - int ar = ncp_chip_apply(c); - if (ar == 1) need_sb = 1; /* 主题变化 → 重刷状态栏 */ - else if (ar == 2) { - after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */ - /* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配, - * 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */ - memset(&kd_pwr, 0, sizeof kd_pwr); - memset(&kd_vold, 0, sizeof kd_vold); - memset(&kd_volu, 0, sizeof kd_volu); - memset(&kd_play, 0, sizeof kd_play); - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); - m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); - m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); - m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); - /* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空, - * 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */ - if (tfd >= 0) { - unsigned char ev2[16]; - while (read(tfd, ev2, sizeof ev2) == 16) {} - } - } - hit = 1; need_paint = 1; break; - } - if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { hit = 1; if (ncp_slider_apply(1, tx, 1)) need_paint = 1; } - else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { hit = 1; if (ncp_slider_apply(2, tx, 1)) need_paint = 1; } - /* ⑥ 勿扰面板内开关(列表上方小 toggle):tap 即切换(整带命中) */ - if (!hit && ncp_dnd_hit(tx, ty)) { - int dv = 0; meow_settings_get_bool("notify.dnd", &dv); - meow_settings_set_bool("notify.dnd", dv ? 0 : 1); - meow_settings_save(); - g_ncp_dnd = dv ? 0 : 1; - ncp_refresh_rows(); /* 按 dnd 过滤重采集 */ - list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, - NCP_LIST_BOT - NCP_LIST_TOP); - hit = 1; need_paint = 1; - } - /* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 ongoing/media/progress)。 */ - if (!hit && ncp_clear_hit(tx, ty)) { - int n = ncp_clear_all(); - logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n); - /* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */ - row_swipe_row = -1; row_off = 0; row_settle = 0; - row_off_target = 0; row_anim_at = 0; row_paint = 0; - g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; - ncp_refresh_rows(); - list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, - NCP_LIST_BOT - NCP_LIST_TOP); - hit = 1; need_paint = 1; - } - if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) { - if (ncp_row_tap(tx, ty, list_scroll, want_launch)) { - hit = 1; - if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */ - else closed = 1; - break; - } - } - if (!hit) { /* 点空白(标题/把手/提示/分隔线/列表外)→ 关闭 */ - if (anim_ok) anim_target = 0; else closed = 1; - break; - } - } else if (mnp_vertical_up(dx, dy, MENU_SWIPE_THRESH)) { - if (anim_ok) anim_target = 0; else closed = 1; /* 面板内上滑 → 关闭 */ - break; - } else if (anim_ok && anim_target == 0 && - mnp_top_swipe_down(tdx, tdy, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) { - anim_target = NCP_ANIM_FIX; /* 收回途中顶部热区下滑 → 重新打开(平滑转向) */ - } - /* 其余横滑/下滑忽略(不关闭不动作) */ - } - td_ok = 0; - } - } - } - } - } - if (closed) break; /* 非动画兜底:上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */ - - /* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */ - if (now - last_listchk >= 500) { - last_listchk = now; - meow_notif_tick(); - if (ncp_rows_changed()) { - /* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */ - row_swipe_row = -1; row_off = 0; row_settle = 0; - row_off_target = 0; row_anim_at = 0; row_paint = 0; - g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; - panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0; - ncp_refresh_rows(); - list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, - NCP_LIST_BOT - NCP_LIST_TOP); - need_paint = 1; - } - } - - /* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。 - * 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */ - if (anim_ok && anim_p != anim_target && !panel_drag) { - unsigned long long anow = now_ms(); - int dt; - if (anim_target != anim_last_target) { - /* 意图刚切换(打开↔关闭):重置计时起点,本 tick 不计时, - * 避免稳态闲置后 anim_last 陈旧导致首帧就跳到目标(不产生动画)。 */ - anim_last_target = anim_target; - anim_last = anow; - dt = 0; - } else { - dt = (int)((anow > anim_last) ? (anow - anim_last) : 0); - anim_last = anow; - } - int np = mnp_anim_step(anim_p, anim_target, NCP_ANIM_FIX, - (unsigned long long)dt, NCP_ANIM_MS); - if (np != anim_p) { - anim_p = np; - ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); /* §9 快照 blit 平移(只刷面板矩形) */ - } - } - if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */ - - /* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。 - * 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。 - * 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */ - if (row_swipe_row >= 0 && row_settle) { - unsigned long long rnow = now_ms(); - int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0); - row_anim_at = rnow; - int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE); - if (noff != row_off) { - row_off = noff; - g_ncp_swipe_off = noff; - row_paint = 1; - } - if (noff == row_off_target) { - if (row_off_target == 0) { - /* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */ - ncp_repaint_row(list_scroll, row_swipe_row, 0); - row_swipe_row = -1; row_settle = 0; row_off = 0; - row_off_target = 0; row_paint = 0; - g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; - if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */ - } else { - /* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */ - if (row_swipe_row >= 0 && row_swipe_row < g_nrows) - meow_notif_dismiss(g_rows[row_swipe_row].id); - row_swipe_row = -1; row_settle = 0; row_off = 0; - row_off_target = 0; row_paint = 0; - g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; - ncp_refresh_rows(); - list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, - NCP_LIST_BOT - NCP_LIST_TOP); - need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */ - } - } - } - - /* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。 - * need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍 - * 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。 - * 动画进行中:不画稳态面板(会覆盖正在滑动的帧),need_paint 保持到到位后再补画。 */ - if (need_paint) { - last_act = now; - if (!(anim_ok && anim_p != anim_target)) { - draw_notif_center(list_scroll, need_sb); - need_sb = 0; - if (anim_ok) ncp_snap_panel(); /* 面板内容已变:刷新快照(供滑出动画用) */ - if (after_modal) { meow_dirty_full(); after_modal = 0; } - else meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP); - meow_ui_flush(); - need_paint = 0; - list_paint = 0; /* 整面板已含列表:滚动重绘标记随之清除 */ - } - /* 否则动画进行中:保留 need_paint,到位后(下一稳态 tick)补画最终面板 */ - } - /* 仅列表区重绘(滚动专用:只刷列表带,不整面板/不刷状态栏;同受动画门控, - * 动画进行中不画稳态,list_paint 保持到到位后再补画一次列表终值)。 */ - if (list_paint) { - last_act = now; - if (!(anim_ok && anim_p != anim_target)) { - ncp_repaint_list(list_scroll); - if (anim_ok) ncp_snap_panel(); /* 列表已变:刷新面板快照(供滑出动画用) */ - list_paint = 0; - } - } - /* ⑨ 面板跟手拖动重绘:只刷面板矩形 + 新露出条(§9 快照 blit 平移;不整屏/不重排控件)。 - * 拖动期 anim_p 直接由手指映射,故不受下方「animating」门控(非时间轴推进),直接呈现。 */ - if (drag_paint) { - last_act = now; - ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); - drag_paint = 0; - } - /* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。 - * row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */ - if (row_paint) { - last_act = now; - if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target)) - ncp_repaint_row(list_scroll, row_swipe_row, row_off); - row_paint = 0; - } - /* ~1s 状态栏时钟 */ - if (now - last_tick >= 1000) { - last_tick = now; - draw_clock(); - fb_norm_buf0(); - meow_ui_flush(); - } - usleep(POLL_MS * 1000); - } - if (tfd >= 0) close(tfd); - kp_close(&kp_power); kp_close(&kp_keys); - ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */ - ncp_rows_free(); - logmsg("notification center closed"); -} - -/* 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). */ -static 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). */ -static 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"); - } -} - -/* ================================================================== */ -/* M2.12 MeowAPPs 包数据目录 + .mpk 接口 */ -/* exFAT 无符号链接 → 数据目录用环境变量(MEOW_DATA_DIR/CACHE_DIR) */ -/* 传址给应用,不做 symlink(APPS.md 附录 v2 修订)。 */ -/* ================================================================== */ - -/* 由 exec 绝对路径取包名:/usr/data/mnt/sd_0/MeowAPPs// → */ -static int meow_pkg_name(const char *exec, char *out, int n) -{ - if (!exec || strncmp(exec, MEOWAPPS, strlen(MEOWAPPS)) != 0) { if (n>0) out[0]=0; return -1; } - const char *p = exec + strlen(MEOWAPPS); - if (*p != '/') { if (n>0) out[0]=0; return -1; } - p++; - const char *end = strchr(p, '/'); - int len = end ? (int)(end - p) : (int)strlen(p); - if (len <= 0 || len >= n) { if (n>0) out[0]=0; return -1; } - memcpy(out, p, len); out[len] = 0; - return 0; -} - -/* 启动应用前准备数据目录:建 MeowData//{data,cache}(如缺)。幂等。 - * 失败只 log(MeowData 所在卡不存在/只读),绝不崩 —— 零卡依赖:应用需按 - * APPS.md 规范容忍无数据目录。返回 0=就绪,-1=pkg 不在卡上/不能判名。 */ -static int meow_app_prepare(const char *exec) -{ - char pkg[64]; - if (meow_pkg_name(exec, pkg, sizeof pkg) != 0) return -1; - if (!sd_device_present()) return -1; /* 卡不在,跳过(应用仍可跑非卡资源) */ - char cmd[320]; - snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' " - "'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg); - int rc = system(cmd); - logmsg("app_prepare: pkg=%s rc=%d", pkg, rc); - return rc == 0 ? 0 : -1; -} - -/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs//。 - * 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 .mpk。 - * 同名先备份;解压后校验 app.json;随后数据 init(MeowData//{data,cache})。 - * 返回 0 ok / 负=错误。 */ -static int meow_pkg_install(const char *mpk_path) -{ - if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; } - if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; } - if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; } - /* 包名 = 文件名茎(去掉 .mpk) */ - const char *bn = strrchr(mpk_path, '/'); - bn = bn ? bn + 1 : mpk_path; - const char *dot = strrchr(bn, '.'); - char pkg[64]; - if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need .mpk"); return -1; } - int len = (int)(dot - bn); - if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; } - memcpy(pkg, bn, len); pkg[len] = 0; - /* 目标 MeowAPPs//;同名先备份(.bak_),再解压,再校验 app.json */ - char dst[256], bak[256], cmd[700]; - snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg); - snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid()); - /* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */ - snprintf(cmd, sizeof cmd, - "rm -rf '%s' 2>/dev/null; " - "[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; " - "mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && " - "[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst); - if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; } - /* 数据 init:MeowData//{data,cache} */ - snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' " - "'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg); - system(cmd); - logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak); - return 0; -} - -/* .mpk 导出:打包 MeowAPPs//(数据目录本就由 env 指向 MeowData,MeowAPPs/ 下无 - * data/cache 实体,直接全打包)→ .mpk。返回 0 ok。 */ -static int meow_pkg_export(const char *pkg, const char *out_path) -{ - if (!pkg || !out_path || strpbrk(pkg, " ;`$'\"&|<>/" ) || strpbrk(out_path, " ;`$'\"&|<>")) { - logmsg("pkg_export: unsafe path"); return -2; - } - if (!sd_mounted()) { logmsg("pkg_export: card not mounted"); return -1; } - char src[256], dst[256]; - snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg); - if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; } - snprintf(dst, sizeof dst, "%s", out_path); - /* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */ - char cmd[512]; - snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && " - "mv '%s.tmp' '%s'", src, dst, dst, dst); - if (system(cmd) != 0) { logmsg("pkg_export: tar/gzip fail"); return -1; } - logmsg("pkg_export: %s -> %s", pkg, dst); - return 0; -} - -/* ================================================================== */ -/* 阶段 D · M0:AF_UNIX IPC 骨架 */ -/* IPC 线程只读帧+解析+入队(SPSC 无锁环 + eventfd 唤醒主循环); */ -/* 主循环 meow_ipc_process() 单写者改状态并回 ACK/ERR/RESULT。 */ -/* 遵守 FRAMEWORK_DESIGN §1/§6:主循环 10ms 热键轮询绝不被 socket IO 阻塞。 */ -/* ================================================================== */ -#define MEOW_IPC_RING 16 /* 无锁环深度(单生产者=IPC线程,单消费者=主循环) */ -#define MEOW_IPC_CONNS 10 /* 同时接入连接上限(backlog 4 + 余量) */ -#define MEOW_IPC_BACKLOG 4 - -/* 入队消息:类型 + 请求 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]; -}; - -static struct meow_ipc_msg ipc_ring[MEOW_IPC_RING]; -static volatile uint32_t ipc_head = 0; /* 消费端(主循环)推进 */ -static volatile uint32_t ipc_tail = 0; /* 生产端(IPC线程)推进 */ -static void ipc_dispatch(struct meow_ipc_msg *m); /* 定义在下方,主循环排空用 */ -static int ipc_event_fd = -1; -static volatile int ipc_shutdown = 0; -static volatile int ipc_thread_ok = 0; /* bind+listen 成功 */ -static char ipc_sock_path[MEOW_IPC_SOCK_LEN] = MEOW_IPC_PATH; -static pthread_t ipc_pthread; - -/* 每连接字节流帧组装状态(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; -}; -static struct meow_ipc_conn ipc_conns[MEOW_IPC_CONNS]; - -/* 写一帧(16B 头 + 负载)。返回写入字节;<=0 视为失败。 */ -static size_t ipc_send(int fd, uint16_t type, uint32_t req_id, - const void *payload, uint32_t len) -{ - if (fd < 0) return 0; - uint8_t hdr[MEOW_IPC_HDR_LEN]; - uint32_t magic = MEOW_IPC_MAGIC; - uint16_t ver = MEOW_IPC_VERSION; - memcpy(hdr + 0, &magic, 4); - memcpy(hdr + 4, &ver, 2); - memcpy(hdr + 6, &type, 2); - memcpy(hdr + 8, &req_id, 4); - memcpy(hdr + 12, &len, 4); - size_t off = 0; - while (off < sizeof hdr) { - ssize_t w = write(fd, hdr + off, sizeof hdr - off); - if (w <= 0) return off; - off += (size_t)w; - } - if (len && payload) { - size_t p = 0; - while (p < len) { - ssize_t w = write(fd, (const uint8_t *)payload + p, len - p); - if (w <= 0) break; - p += (size_t)w; - } - off += p; - } - return off; -} - -/* 组装完的帧入队(SPSC 无锁,先写槽再推进 tail,__sync 发布)。 */ -static void ipc_enqueue_frame(struct meow_ipc_conn *c) -{ - uint16_t type; - uint32_t req_id, len; - memcpy(&type, c->hdr + 6, 2); - memcpy(&req_id, c->hdr + 8, 4); - len = c->pay_tot; - if ((ipc_tail - ipc_head) >= MEOW_IPC_RING) { - logmsg("ipc: ring full, drop frame type=0x%04x", type); - return; - } - uint32_t slot = ipc_tail % MEOW_IPC_RING; - struct meow_ipc_msg *m = &ipc_ring[slot]; - m->type = type; - m->req_id = req_id; - m->fd = c->fd; - uint32_t copy = len < MEOW_IPC_CARGO ? len : MEOW_IPC_CARGO; - m->plen = (uint16_t)copy; - memcpy(m->payload, c->pay, copy); - __sync_synchronize(); - ipc_tail++; - if (ipc_event_fd >= 0) { uint64_t one = 1; (void)write(ipc_event_fd, &one, sizeof one); } -} - -/* 读一个连接的可用字节并组装成帧(>=1 完整帧即入队)。返回 0=ok(可能已入队), - * -1=连接关闭/坏帧(调用方应关闭并回收)。 */ -static int ipc_conn_read(struct meow_ipc_conn *c) -{ - for (;;) { - if (c->hdr_n < MEOW_IPC_HDR_LEN) { - ssize_t r = read(c->fd, c->hdr + c->hdr_n, MEOW_IPC_HDR_LEN - c->hdr_n); - if (r < 0) { if (errno == EAGAIN) return 0; return -1; } - if (r == 0) return -1; - c->hdr_n += (int)r; - if (c->hdr_n < MEOW_IPC_HDR_LEN) return 0; - uint32_t magic, len; - uint16_t ver; - memcpy(&magic, c->hdr, 4); - memcpy(&ver, c->hdr + 4, 2); - memcpy(&len, c->hdr + 12, 4); - if (magic != MEOW_IPC_MAGIC) { logmsg("ipc: bad magic %08x", magic); return -1; } - if (ver != MEOW_IPC_VERSION) { logmsg("ipc: bad version %u", ver); return -1; } - if (len > MEOW_IPC_MAX) { logmsg("ipc: frame len %u too long", len); return -1; } - c->pay_tot = len; - c->pay_n = 0; - if (len == 0) { - ipc_enqueue_frame(c); - c->hdr_n = 0; - continue; - } - } - if (c->pay_n < c->pay_tot) { - ssize_t r = read(c->fd, c->pay + c->pay_n, c->pay_tot - c->pay_n); - if (r < 0) { if (errno == EAGAIN) return 0; return -1; } - if (r == 0) return -1; - c->pay_n += (uint32_t)r; - if (c->pay_n < c->pay_tot) return 0; - } - ipc_enqueue_frame(c); - c->hdr_n = 0; - c->pay_n = 0; - } -} - -static void *ipc_thread(void *arg) -{ - (void)arg; - int lfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (lfd < 0) { logmsg("ipc: socket() fail %s", strerror(errno)); return NULL; } - unlink(ipc_sock_path); - struct sockaddr_un a; - memset(&a, 0, sizeof a); - a.sun_family = AF_UNIX; - strncpy(a.sun_path, ipc_sock_path, sizeof a.sun_path - 1); - if (bind(lfd, (struct sockaddr *)&a, sizeof a) < 0) { - logmsg("ipc: bind %s fail %s", ipc_sock_path, strerror(errno)); - close(lfd); - return NULL; - } - chmod(ipc_sock_path, 0666); - if (listen(lfd, MEOW_IPC_BACKLOG) < 0) { - logmsg("ipc: listen fail %s", strerror(errno)); - close(lfd); - return NULL; - } - fcntl(lfd, F_SETFL, O_NONBLOCK); - ipc_thread_ok = 1; - logmsg("ipc: listening on %s (backlog %d)", ipc_sock_path, MEOW_IPC_BACKLOG); - - struct pollfd fds[MEOW_IPC_CONNS + 1]; - int nfds = 1; - fds[0].fd = lfd; fds[0].events = POLLIN; fds[0].revents = 0; - while (!ipc_shutdown) { - int r = poll(fds, nfds, 400); - if (r < 0) { if (errno == EINTR) continue; break; } - if (r == 0) continue; - if (fds[0].revents & POLLIN) { - for (;;) { - int c = accept(lfd, NULL, NULL); - if (c < 0) break; - fcntl(c, F_SETFL, O_NONBLOCK); - if (nfds <= MEOW_IPC_CONNS) { - ipc_conns[nfds - 1].fd = c; - ipc_conns[nfds - 1].hdr_n = 0; - ipc_conns[nfds - 1].pay_n = 0; - ipc_conns[nfds - 1].pay_tot = 0; - fds[nfds].fd = c; - fds[nfds].events = POLLIN; - fds[nfds].revents = 0; - nfds++; - logmsg("ipc: conn %d accept (nfds=%d)", c, nfds - 1); - } else { - close(c); - logmsg("ipc: conn %d rejected (full)", c); - } - } - } - int i = 1; - while (i < nfds) { - int ev = fds[i].revents; - if (ev & (POLLIN | POLLHUP | POLLERR | POLLNVAL)) { - struct meow_ipc_conn *cc = &ipc_conns[i - 1]; - if (ipc_conn_read(cc) < 0) { - logmsg("ipc: conn %d closed", cc->fd); - close(cc->fd); - for (int j = i; j < nfds - 1; j++) { - fds[j] = fds[j + 1]; - ipc_conns[j - 1] = ipc_conns[j]; - } - nfds--; - continue; - } - } - i++; - } - for (int j = 0; j < nfds; j++) fds[j].revents = 0; - } - for (int i = 1; i < nfds; i++) close(fds[i].fd); - close(lfd); - unlink(ipc_sock_path); - logmsg("ipc: thread exit"); - return NULL; -} - -/* 启动 IPC 线程(主循环任意时刻调一次)。返回 0=就绪,-1=线程创建失败。 - * (bind/listen 失败记 ipc_thread_ok=0,随后 meow_ipc_process 仍安全轮询空队列。) */ -static int meow_ipc_init(void) -{ - ipc_event_fd = eventfd(0, EFD_NONBLOCK); - if (ipc_event_fd < 0) - logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno)); - if (pthread_create(&ipc_pthread, NULL, ipc_thread, NULL) != 0) { - logmsg("ipc: pthread_create fail"); - return -1; - } - usleep(60000); /* 给 bind+listen 一点时间,log 就绪状态 */ - if (ipc_thread_ok) logmsg("ipc: ready (eventfd=%d)", ipc_event_fd); - else logmsg("ipc: NOT ready (bind/listen failed, thread exiting)"); - return ipc_thread_ok ? 0 : -1; -} - -/* 主循环:非阻塞排空 eventfd + 处理入队帧(单写者改状态并回包)。 - * 事件fd 仅为低延迟优化;若为 -1,10ms 轮询仍保证每圈排空。 */ -static void meow_ipc_process(void) -{ - if (ipc_event_fd >= 0) { - uint64_t v; - while (read(ipc_event_fd, &v, sizeof v) == (ssize_t)sizeof v) {} - } - while (ipc_head != ipc_tail) { - struct meow_ipc_msg *m = &ipc_ring[ipc_head % MEOW_IPC_RING]; - ipc_dispatch(m); - ipc_head++; - } - __sync_synchronize(); -} - -/* 处理一条入队帧:HELLO→ACK、ACT_QUERY→ACT_RESULT/ERR、ACT_DECLARE→ACK、其他→ERR。 */ -static void ipc_dispatch(struct meow_ipc_msg *m) -{ - switch (m->type) { - case MSG_HELLO: { - m_hello_t h; - memset(&h, 0, sizeof h); - memcpy(&h, m->payload, m->plen < (uint16_t)sizeof h ? m->plen : (uint16_t)sizeof h); - h.app_id[MEOW_APP_ID_MAX - 1] = 0; - meow_acct_hello(h.app_id, h.version, h.flags); - m_ack_t a; - a.code = MEOW_OK; - a.status = (uint32_t)g_naccts; - ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); - logmsg("ipc: HELLO app=%s ver=%u -> ACK naccts=%d", h.app_id, h.version, g_naccts); - break; - } - case MSG_ACT_DECLARE: { - m_act_declare_t d; - memset(&d, 0, sizeof d); - memcpy(&d, m->payload, m->plen < (uint16_t)sizeof d ? m->plen : (uint16_t)sizeof d); - d.app_id[MEOW_APP_ID_MAX - 1] = 0; - meow_acct_declare(d.app_id, d.entries, d.count); - m_ack_t a; - a.code = MEOW_OK; - a.status = (uint32_t)g_naccts; - ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); - logmsg("ipc: ACT_DECLARE app=%s n=%u -> ACK", d.app_id, d.count); - break; - } - case MSG_ACT_QUERY: { - m_act_query_t q; - memset(&q, 0, sizeof q); - memcpy(&q, m->payload, m->plen < (uint16_t)sizeof q ? m->plen : (uint16_t)sizeof q); - q.app_id[MEOW_APP_ID_MAX - 1] = 0; - q.activity[MEOW_ACT_MAX - 1] = 0; - char chain[MEOW_CHAIN_MAX] = ""; - int depth = 0; - int code = resolve_chain(q.app_id, q.activity, chain, &depth); - if (code == MEOW_OK) { - m_act_result_t r; - memset(&r, 0, sizeof r); - r.depth = (uint32_t)depth; - r.code = MEOW_OK; - size_t cl = strlen(chain); - if (cl >= MEOW_CHAIN_MAX) cl = MEOW_CHAIN_MAX - 1; - memcpy(r.chain, chain, cl); - r.chain_len = (uint16_t)cl; - ipc_send(m->fd, MSG_ACT_RESULT, m->req_id, &r, sizeof r); - logmsg("ipc: ACT_QUERY app=%s act=%s -> depth=%d chain=%s", - q.app_id, q.activity, depth, chain); - } else { - m_err_t e; - memset(&e, 0, sizeof e); - e.code = (uint32_t)code; - const char *msg = (code == MEOW_ERR_NOENT) ? "unregistered app/activity" - : "resolve failed"; - e.msg_len = (uint16_t)strlen(msg); - memcpy(e.msg, msg, e.msg_len); - ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e); - logmsg("ipc: ACT_QUERY app=%s act=%s -> ERR(0x%x)", - q.app_id, q.activity, code); - } - break; - } - case MSG_ACT_FOCUS: { - m_act_focus_t f; - memset(&f, 0, sizeof f); - memcpy(&f, m->payload, m->plen < (uint16_t)sizeof f ? m->plen : (uint16_t)sizeof f); - f.app_id[MEOW_APP_ID_MAX - 1] = 0; - f.activity[MEOW_ACT_MAX - 1] = 0; - meow_fg_act_set(f.app_id, f.activity); /* M2.17:当前顶层 activity(keep_awake 判定) */ - m_ack_t a; - a.code = MEOW_OK; - a.status = (uint32_t)g_naccts; - ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); - logmsg("ipc: ACT_FOCUS app=%s act=%s -> ACK", f.app_id, f.activity); - break; - } - case MSG_ACT_BACK: { - meow_fg_act_back(); /* 回到根/无 activity:清 keep_awake */ - m_ack_t a; - a.code = MEOW_OK; - a.status = (uint32_t)g_naccts; - ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); - logmsg("ipc: ACT_BACK -> ACK"); - break; - } - /* M2a:通知存储(NOTIFICATION_SYSTEM.md §4/§7/§8)。IPC 线程只入队,此处在主循环 - * 单写者落存储 + 回 ACK(与 HELLO/QUERY 同一线程模型,绝不在 socket IO 阻塞)。 */ - case MSG_NOTIFY: { - m_notify_t mn; - memset(&mn, 0, sizeof mn); - memcpy(&mn, m->payload, m->plen < (uint16_t)sizeof mn ? m->plen : (uint16_t)sizeof mn); - mn.app_id[MEOW_APP_ID_MAX - 1] = 0; - mn.category[23] = 0; - mn.summary[63] = 0; - mn.body[95] = 0; - mn.app_icon[47] = 0; - mn.target_activity[MEOW_ACT_MAX - 1] = 0; - /* 映射到存储结构(freedesktop + MeowOS 扩展;actions/hints v1 空) */ - meow_notif_t n; - memset(&n, 0, sizeof n); - n.id = mn.replaces_id; /* replaces_id==0 => 新建;>0 => 替换(不存在则新建) */ - snprintf(n.app_id, sizeof n.app_id, "%s", mn.app_id); - snprintf(n.category, sizeof n.category, "%s", mn.category); - snprintf(n.summary, sizeof n.summary, "%s", mn.summary); - snprintf(n.body, sizeof n.body, "%s", mn.body); - snprintf(n.app_icon, sizeof n.app_icon, "%s", mn.app_icon); - snprintf(n.target_activity, sizeof n.target_activity, "%s", mn.target_activity); - n.expire_timeout = mn.expire_timeout; - n.progress = mn.progress; - n.urgency = mn.urgency; /* 0-3(低/普通/高/紧急);旧调用=0 → LOW(静默),新调用缺省 NORMAL */ - n.ongoing = mn.ongoing; - n.media = mn.media; - uint32_t nid = 0; - int rc = meow_notif_post(&n, &nid); - m_ack_t a; - a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc; - a.status = nid; /* 返回分配/更新的通知 id */ - ipc_send(m->fd, MSG_NOTIFY_ACK, m->req_id, &a, sizeof a); - logmsg("ipc: NOTIFY app=%s cat=%s id=%u rc=%d -> ACK", mn.app_id, mn.category, nid, rc); - break; - } - case MSG_NOTIFY_DISMISS: { - uint32_t id = 0; - if (m->plen >= 4) memcpy(&id, m->payload, 4); - int rc = meow_notif_dismiss(id); - m_ack_t a; - a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc; - a.status = 0; - ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); - logmsg("ipc: NOTIFY_DISMISS id=%u rc=%d -> ACK", id, rc); - break; - } - case MSG_NOTIFY_PROGRESS: { - m_notify_progress_t p; - memset(&p, 0, sizeof p); - memcpy(&p, m->payload, m->plen < (uint16_t)sizeof p ? m->plen : (uint16_t)sizeof p); - /* 实时进度:仅改 launcher 内存活跃槽(不落盘);progress>=100 视为结束 → 里程碑落盘 */ - int rc = meow_notif_update_progress(p.nid, p.progress); - m_ack_t a; - a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc; - a.status = 0; - ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); - logmsg("ipc: NOTIFY_PROGRESS nid=%u p=%d rc=%d -> ACK", p.nid, p.progress, rc); - break; - } - case MSG_NOTIFY_LIST: - /* ↓ 守护→App 下发通知清单(重同步)。本帧不来自客户端(无请求类型),故不在此应答; - * 需用时由守护主动经 meow_notif_serialize_list() + ipc_send 下发(M5 通知中心接入)。 */ - logmsg("ipc: NOTIFY_LIST (downlink) not dispatched here"); - break; - default: - logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id); - { - m_err_t e; - memset(&e, 0, sizeof e); - e.code = MEOW_ERR_INVAL; - char msg[40]; - snprintf(msg, sizeof msg, "unsupported type 0x%04x", m->type); - e.msg_len = (uint16_t)strlen(msg); - memcpy(e.msg, msg, e.msg_len); - ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e); - } - break; - } -} - -/* returns 0 on success (app ran and exited), -1 on launch failure */ -/* GESTURE_BACK §2.4:把 app.json 的 immersive 声明写进 /tmp/meowapp_state, - * watchdog 据此决定是否画虚拟返回键 / 走 immersive 手势(watchdog 每 100ms 读一次)。 - * immersive=1→"1";0→"0";<0→清空(应用退出,恢复默认非沉浸)。 */ -static void meow_write_app_state(int immersive) -{ - if (immersive < 0) { unlink("/tmp/meowapp_state"); return; } - int fd = open("/tmp/meowapp_state", O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd < 0) return; - if (write(fd, immersive ? "1" : "0", 1) < 0) { /* ignore write error */ } - close(fd); -} - -/* ---- App 前台状态栏叠加(「同一个状态栏」闭环,FRAMEWORK_DESIGN §10.2 / MEOW_APP_SDK §3.1)---- - * App 前台时状态栏由 launcher(系统进程)作为**常驻**系统叠加层绘制顶条 28px(App 进程不渲染、 - * 只 inset,§6.1 ③)。本函数由 launch_app 的 waitpid 循环定时调用: - * 1) 读 App 顶条模式(/tmp/meowapp_sbar,App 进程 SDK 写)→ 感知全屏/收合态(agent-146); - * 2) 刷新权威数据 meow_sys_light()(电池/WiFi/音量/蓝牙)+ 喂 SDK → 唯一数据源(主界面与 App 一致); - * 3) 标脏「当前可见条高」+ meow_ui_flush() → SDK present() 画顶条/收合提示线并原子 blit(仅可见条高); - * 4) 只 blit 可见条高(收合时其余行=App 内容,保留)→ 不覆盖 App、不 SIGSTOP App(App 继续播/响应)。 - * 与通知中心/电源菜单(SIGSTOP + 全屏叠画)的差异:状态栏常驻、非模态、只碰顶条。 - * 全屏/收合:见「全屏状态栏收合(agent-146)」——收合只叠提示线(无时钟/图标/值),热区事件与渲染分离。 */ - -/* App 前台顶条模式文件(App 进程 SDK 写,launcher 读;与 /tmp/meowapp_state 语义独立)。 - * 值:'0'=展开 28px 整条;'1'=收合(提示线);'2'=隐藏(沉浸 inset0)。 */ -#define MEOW_SBAR_MODE_FILE "/tmp/meowapp_sbar" - -/* 上次读到的 App 顶条模式(用于在 launch_app waitpid 循环里判「模式新变 → 立即叠一次」)。 - * 高整文件作用域;每次启动 App 前重置为 -1(强制首帧同步一次)。 */ -static int g_sb_mode_last = -1; - -/* 读 App 顶条模式:0=展开 1=收合 2=隐藏;-1=无文件/读取失败(按展开处理,默认安全)。 */ -static int app_sbar_mode_read(void) -{ - int fd = open(MEOW_SBAR_MODE_FILE, O_RDONLY); - if (fd < 0) return -1; - char ch[8]; - int n = read(fd, ch, sizeof ch - 1); - close(fd); - if (n <= 0) return -1; - if (ch[0] == '1') return 1; - if (ch[0] == '2') return 2; - return 0; -} - -static void app_overlay_statusbar(void) -{ - if (!back_buf_ok || !fb) return; /* 无 SDK back_buf(直写模式):无法叠加,跳过 */ - int mode = app_sbar_mode_read(); - if (mode == 2) { /* 全屏隐藏(immersive,inset=0):launcher 不叠顶条 */ - meow_ui_set_fullscreen(1); - meow_ui_statusbar_set_collapsed(0); - return; /* 无内容可叠:交 App 全屏(其 present inset0 已写满屏) */ - } - if (mode == 1) { /* 收合:SDK 切收合(上移过渡 + 提示线),无时钟/图标/值 */ - meow_ui_set_fullscreen(0); - meow_ui_statusbar_set_collapsed(1); - meow_ui_statusbar_animate(MEOW_SBAR_ANIM_MS); /* 平滑上移/恢复过渡时长 */ - } else { /* 展开(默认/无文件):整条 28px */ - meow_ui_set_fullscreen(0); - meow_ui_statusbar_set_collapsed(0); - } - meow_sys_light(); /* 刷新 g_sys(电池/WiFi/音量/蓝牙;唯一权威源) */ - 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(顶条必重画) */ - /* 撤销 set_collapsed 可能引发的「整帧脏」:launcher 在 App 上叠加只应 blit 顶条,绝不能整帧 blit - * (否则会把自身 back_buf 的 stale 菜单内容盖到 App 内容上,属灾难性回归——agent-146)。 - * 随后只标「心跳」脏(顶条左上角 1px)唤醒 present();实际 blit 区由 SDK 渲染在 present() 内标脏决定 - * (render_off 画 [0, 28-off) 可见条高 / 收合稳定画 [0,4) 提示线 / 展开画 [0,28) 整条),故上移时 - * 只 blit 可见条高、其余行保留 App 内容 —— 平滑上收无残留、不黑框。 */ - meow_gfx_dirty_reset(); - meow_dirty_add(0, SB_BAR_Y, 1, 1); - meow_ui_flush(); /* SDK present:vsync + 原子 blit(区随渲染标脏) */ -} - -static int launch_app(struct app *a); -/* (definition follows) */ - -static int launch_app(struct app *a) -{ - logmsg("launch: %s (%s)", a->name, a->exec); - if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; } - - /* M2.12:启动前准备 MeowData//{data,cache}(幂等;卡不在/失败只 log 不崩)。 - * 应用经环境变量 MEOW_DATA_DIR/MEOW_CACHE_DIR 定位数据(exFAT 无 symlink)。 */ - meow_app_prepare(a->exec); - - /* save the brightness the player had before the menu/app session, then - * drain input queues so the app starts clean */ - saved_brightness = read_brightness(); - if (saved_brightness <= 0) saved_brightness = 60; /* standby/unknown */ - logmsg("saved brightness = %d", saved_brightness); - drain_input(); - - /* Bug 3: distinguish a REAL exec failure from a clean app exit. The child - * holds the write end of a CLOEXEC pipe; if execve() fails it writes the - * errno and _exit(127), if it succeeds the fd closes on exec so the parent - * read() returns EOF. Parent blocks on read() until the child either execs - * (success) or reports the error (failure); only then do we spawn the - * watchdog and waitpid — so an exec failure never leaves an orphan watchdog - * and never masquerades as "app ran and exited". */ - int execpipe[2]; - if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return -1; } - fcntl(execpipe[1], F_SETFD, FD_CLOEXEC); - - pid_t pid = fork(); - if (pid < 0) { - close(execpipe[0]); close(execpipe[1]); - logmsg("fork failed: %s", strerror(errno)); return -1; - } - if (pid == 0) { - /* child: exec the app. M2.12 注入 MEOW_LOG_DIR + MEOW_DATA_DIR/MEOW_CACHE_DIR - * (exFAT 无 symlink,数据目录用 env 传给应用)。PATH 继承 launcher(含卡上 opt/bin - * 若在),卡不在则回退普通 PATH。M2.52 追加注入 MEOW_LOG_APP(app_id)+MEOW_LOG_LEVEL - * (读全局 log.level),使 SDK meow_log() 按 app 分离写 apps/.log 并做级别过滤。 */ - close(execpipe[0]); - char *argv[] = { a->exec, NULL }; - char env_path[320], env_log[320], env_data[320], env_cache[320]; - char env_app[MEOW_APP_ID_MAX + 16], env_lvl[48]; - char env_fpng[400], env_fidx[400], env_fapp[96]; - char env_mf[24]; - char env_tier[24]; - char env_fs[20]; - char env_cls[MEOW_FCLS_COUNT][2][400]; - char *envp[40]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),tier(1),class(≤12),fullscreen(1),data,cache */ - int ne = 0; - const char *cp = getenv("PATH"); - snprintf(env_path, sizeof env_path, "PATH=%s", cp ? cp : "/usr/bin:/bin"); - envp[ne++] = env_path; - envp[ne++] = "HOME=/root"; - if (log_dir[0]) { snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", log_dir); envp[ne++] = env_log; } - /* M2.52:MEOW_LOG_APP=app_id(=app.json.package,缺省回退目录名;与注册表一致;无 id 则跳过,SDK 兜底 "app") */ - if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; } - /* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3), - * SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */ - { - static const char *lvl_name[] = { "ERROR", "WARN", "INFO", "DEBUG" }; - int lv = 2; /* 默认 info */ - meow_settings_get_enum("log.level", &lv); - if (lv < 0 || lv > 3) lv = 2; - snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]); - envp[ne++] = env_lvl; - } - /* M2.x:注入「当前生效字体」= 系统 font.file(launcher 已 load 的那套), - * SDK meow_ui_gfx.load_font 优先读 MEOW_FONT_PNG/IDX,app 未自带字库(§5.7 load_handle - * 未实现)时跟随系统字体,不再硬编码 MeowAPPs/fonts。无卡/无可用字体则跳过注入, - * SDK 自行回退卡默认→固件→点阵。 */ - { - static const char *cls_env_png[MEOW_FCLS_COUNT] = { - "MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_NUM_PNG", - "MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_OTHER_PNG" }; - static const char *cls_env_idx[MEOW_FCLS_COUNT] = { - "MEOW_FONT_CLS_CJK_IDX", "MEOW_FONT_CLS_CJK_SYM_IDX", "MEOW_FONT_CLS_NUM_IDX", - "MEOW_FONT_CLS_LATIN_IDX", "MEOW_FONT_CLS_LATIN_SYM_IDX", "MEOW_FONT_CLS_OTHER_IDX" }; - 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" }; - char fpng[400], fidx[400], fam[96]; - int have_global; - int mf_on = 0; - meow_settings_get_switch("display.multi_font", &mf_on); - snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0); - envp[ne++] = env_mf; - /* M2.5x:字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入,并合并 - * 「加载大字库」开关(font.load_full → 强制 ≥GBK)得**生效档**;0=默认4K常用, - * 1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档并按档装载(见 - * meow_font_effective_paths_tiered;真正装载由 meow_font_reload_tier 实现)。 */ - int eff_tier = meow_font_effective_tier(a->fonts); - snprintf(env_tier, sizeof env_tier, "MEOW_FONT_TIER=%d", eff_tier); - envp[ne++] = env_tier; - have_global = (meow_font_effective_paths_tiered(eff_tier, fpng, sizeof fpng, - fidx, sizeof fidx, fam, sizeof fam) == 0); - if (have_global) { - snprintf(env_fpng, sizeof env_fpng, "MEOW_FONT_PNG=%s", fpng); - snprintf(env_fidx, sizeof env_fidx, "MEOW_FONT_IDX=%s", fidx); - snprintf(env_fapp, sizeof env_fapp, "MEOW_FONT_APP=%s", fam); - envp[ne++] = env_fpng; - envp[ne++] = env_fidx; - envp[ne++] = env_fapp; - logmsg("launch: font env %s/%s (family=%s)", fpng, fidx, fam); - } - /* M2.5x 多字体:每类注入 png/idx(命中则覆盖全局;类值为空=跟随全局,不注入)。 - * SDK 读这些 env 构建多字体映射;同 family 去重由 SDK 侧 meow_font_config_multifont 处理。 */ - if (mf_on) - for (int c = 0; c < MEOW_FCLS_COUNT; c++) { - char ref[MEOW_SET_MAX_LEN], abs[400], idxabs[400]; - if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) != 0 || !ref[0]) continue; - if (meow_font_resolve_path(ref, abs, sizeof abs) != 0 || access(abs, R_OK) != 0) continue; - snprintf(idxabs, sizeof idxabs, "%s", abs); - size_t n4 = strlen(idxabs); - if (n4 > 4 && !strcmp(idxabs + n4 - 4, ".png")) { - idxabs[n4-4]='.'; idxabs[n4-3]='i'; idxabs[n4-2]='d'; idxabs[n4-1]='x'; idxabs[n4]=0; - } - snprintf(env_cls[c][0], sizeof env_cls[c][0], "%s=%s", cls_env_png[c], abs); - snprintf(env_cls[c][1], sizeof env_cls[c][1], "%s=%s", cls_env_idx[c], idxabs); - envp[ne++] = env_cls[c][0]; - envp[ne++] = env_cls[c][1]; - } - } - /* 全屏声明:app.json "fullscreen"(app 级)→ 注入 MEOW_FULLSCREEN=1,SDK 端 - * meow_ui_init 据此预先设全屏。基座只接 app 级;活动级 route..fullscreen - * (meow_route.fullscreen)在 M3 活动切换时由后续代理消费(此处暂不下发)。 - * 未声明则不注入(SDK 默认非全屏)。 */ - if (a->fullscreen) { - snprintf(env_fs, sizeof env_fs, "MEOW_FULLSCREEN=1"); - envp[ne++] = env_fs; - } - char pkg[64]; - if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) { - snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg); - snprintf(env_cache, sizeof env_cache, "MEOW_CACHE_DIR=/usr/data/mnt/sd_0/MeowData/%s/cache", pkg); - envp[ne++] = env_data; - envp[ne++] = env_cache; - } - envp[ne] = NULL; - execve(a->exec, argv, envp); - int e = errno; - (void)write(execpipe[1], &e, sizeof e); /* reached only on exec failure */ - _exit(127); - } - close(execpipe[1]); - int exe_err = 0; - ssize_t er = read(execpipe[0], &exe_err, sizeof exe_err); - close(execpipe[0]); - if (er > 0) { - logmsg("launch FAILED execve(%s): %s (errno %d)", a->exec, - strerror(exe_err), exe_err); - int st = 0; - waitpid(pid, &st, 0); /* reap the failed child */ - return -1; - } - - /* GESTURE_BACK §2.4:应用启动即写 immersive 状态,watchdog 依此隐藏/保留虚拟返回键 */ - meow_write_app_state(a->immersive == '1' ? 1 : 0); - - /* watchdog for virtual back key / gestures */ - char wd1[192], wd2[192], pidstr[16]; - pid_t wpid = 0; - snprintf(pidstr, sizeof pidstr, "%d", pid); - if (access(WATCHDOG1, X_OK) == 0) - snprintf(wd1, sizeof wd1, "%s", WATCHDOG1); - else if (access(WATCHDOG2, X_OK) == 0) - snprintf(wd1, sizeof wd1, "%s", WATCHDOG2); - else - wd1[0] = 0; - if (wd1[0]) { - wpid = fork(); - if (wpid == 0) { - /* pass the same boot log file + dir so all MeowAPPs components - * log to one place */ - char *argv[] = { wd1, pidstr, NULL }; - char *envp[] = { "PATH=/usr/bin:/bin", NULL, NULL, NULL }; - char file_env[320], dir_env[256]; - if (log_file[0]) { - snprintf(file_env, sizeof file_env, "MEOW_LOG_FILE=%s", log_file); - envp[1] = file_env; - } - if (log_dir[0]) { - snprintf(dir_env, sizeof dir_env, "MEOW_LOG_DIR=%s", log_dir); - envp[2] = dir_env; - } - execve(wd1, argv, envp); - _exit(0); - } - if (wpid > 0) logmsg("watchdog %s pid=%d", wd1, wpid); - } - - /* wait for app; in-app kill = VOL_DOWN x3 quick taps (the old - * PREVIOUS+VOL_UP hold combo is retired — the PREVIOUS key's GPIO - * never fires on this unit, §18.12). Volume is snapped on the first - * tap and restored when the sequence completes. */ - int status = 0; - int app_prev_vold = 0; - int app_seq_hits = 0; - unsigned long long app_seq_last = 0; - struct vol_snap app_vol0 = { -1, -1 }; - /* M2.11 D:应用二进制/资源若在卡上,卡被拔可能 SIGBUS 崩。启动前记录,运行期监测卡移除。 - * (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */ - int app_on_card = (strncmp(a->exec, MEOWAPPS, strlen(MEOWAPPS)) == 0); - if (app_on_card) logmsg("app on card (%s): monitoring card removal during run", a->exec); - struct keypoll kp_power, kp_keys; - kp_open(&kp_power, DEV_POWER); - kp_open(&kp_keys, DEV_KEYS); - /* M2.17 前台应用会话:系统仍统一控制息屏(应用不自管)。launcher 无法看到应用内触摸, - * 故以「可物理键」为 idle 信号;前台当前 activity(经 IPC MSG_ACT_FOCUS 上报)若 keep_awake - * 则不解屏(fg_keep_awake)。唤醒时应用随其输入重绘,launcher 仅 fb_pan_buf0_force 兜底 - * (M2.8 唤醒重绘路径的首步)。见 §18.46。 */ - unsigned long long app_last_input = now_ms(); - unsigned long save_pow[KEYBUF_WORDS], save_keys[KEYBUF_WORDS]; - memcpy(save_pow, kp_power.cur, sizeof save_pow); - memcpy(save_keys, kp_keys.cur, sizeof save_keys); - /* 「同一个状态栏」闭环:App 前台状态栏由 launcher 常驻叠加(顶条 28px、非模态、不 SIGSTOP App)。 - * 立即叠一次(App 首帧前顶条就有),其后每 ~1s 重画(时钟每秒变 + 数据新鲜)。app 级 - * fullscreen 声明(沉浸,整条隐藏)时不叠。agent-146:读 App 顶条模式(meowapp_sbar),App 运行时 - * 全屏/收合 → launcher 顶条切收合提示线(无时钟/图标/值),热区事件与渲染分离。 */ - unsigned long long last_sb_overlay_ms = 0; - g_sb_mode_last = -1; /* 强制首帧同步(进入新 App 会话清零,避免沿用上一 App 的缓存态) */ - unlink(MEOW_SBAR_MODE_FILE); /* 清可能残留的上一 App 顶条模式(崩溃/被杀未清),防新 App 误收合 */ - if (!a->fullscreen) app_overlay_statusbar(); - /* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。 */ - unlink(POWMENU_MARKER); - unlink(POWMENU_ACTIVE); - for (;;) { - int r = waitpid(pid, &status, WNOHANG); - if (r == pid) break; - if (r < 0 && errno != EINTR) break; - if (sigterm_flag) { logmsg("sigterm during app"); kill(pid, SIGTERM); break; } - /* M2.11 D:卡被拔(设备节点消失)且前台应用在卡上 → 主动 SIGTERM 干净退出, - * 避免应用等 SIGBUS 崩。回菜单走正常应用退出路径。 */ - if (app_on_card && !sd_device_present()) { - logmsg("hotplug: card removed during app run -> SIGTERM app "); - kill(pid, SIGTERM); - usleep(300000); - if (waitpid(pid, &status, WNOHANG) != pid) { kill(pid, SIGKILL); waitpid(pid, &status, 0); } - break; - } - /* in-app kill: VOL_DOWN x3 quick taps */ - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - int vold = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; - } - /* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏; - * 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。 */ - { - int key_edge = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) - if (kp_power.cur[w] != save_pow[w] || kp_keys.cur[w] != save_keys[w]) { key_edge = 1; break; } - if (key_edge) { - memcpy(save_pow, kp_power.cur, sizeof save_pow); - memcpy(save_keys, kp_keys.cur, sizeof save_keys); - app_last_input = now_ms(); - if (meow_auto_slept) { - logmsg("auto screen-off (app): key wake"); - meow_panel_on(); - meow_auto_slept = 0; - fb_pan_buf0_force(); - } - } - if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) { - if (now_ms() - app_last_input >= (unsigned long long)screen_off_timeout * 1000) { - meow_panel_off(); - meow_auto_slept = 1; - logmsg("auto screen-off (app %s) after %ds idle (mode=%s keep_awake=%d)", - a->name, screen_off_timeout, screen_off_mode ? "black" : "panel", - fg_keep_awake()); - } - } - } - if (vold && !app_prev_vold) { - unsigned long long nw = now_ms(); - if (app_seq_hits == 0) app_vol0 = vol_snapshot(); - else if (nw - app_seq_last > (unsigned)seq_gap_ms) { - app_seq_hits = 0; - app_vol0 = vol_snapshot(); - } - app_seq_hits++; - app_seq_last = nw; - if (app_seq_hits >= 3) { - logmsg("kill seq: VOL_DOWN x3 -> SIGTERM app"); - vol_restore(app_vol0); - kill(pid, SIGTERM); - usleep(500000); - kill(pid, SIGKILL); - waitpid(pid, &status, 0); - break; - } - } - app_prev_vold = vold; - if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2) - app_seq_hits = 0; - /* M2.24 全面按键接管:watchdog 检测到电源长按≥3s 写 /tmp/meow_power_menu → 此处消费: - * SIGSTOP 应用冻结 → 写 powmenu_active 让 watchdog 让位 → 统一电源菜单(launcher 一处绘制, - * 复用 show_power_menu,含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)→ 菜单关 → - * 清 active → SIGCONT 应用续跑。 */ - if (access(POWMENU_MARKER, F_OK) == 0) { - unlink(POWMENU_MARKER); - logmsg("watchdog: power menu request (app %s) -> SIGSTOP + menu", a->name); - kill(pid, SIGSTOP); - int mfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (mfd >= 0) { write(mfd, "1", 1); close(mfd); } - int pm_ret = show_power_menu(); - unlink(POWMENU_ACTIVE); - kill(pid, SIGCONT); - logmsg("watchdog: power menu closed (ret=%d) -> SIGCONT app", pm_ret); - /* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge - * (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */ - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - memcpy(save_pow, kp_power.cur, sizeof save_pow); - memcpy(save_keys, kp_keys.cur, sizeof save_keys); - app_last_input = now_ms(); /* M2.17:菜单交互即输入,重置应用会话自动息屏计时 */ - if (pm_ret != 0) break; /* 关机/重启/回HibyOS:系统通常已 reboot,防御性退出 */ - } - /* 「同一个状态栏」闭环:常驻顶条叠加(agent-146 全屏收合接入)。 - * - 展开态每 ~1s 慢刷(时钟/数据新鲜;仅标脏顶条上屏,不碰 App 内容); - * - App 顶条模式新变(进入/退出全屏收合)→ 立即叠一次(即时切换,消除「半秒闪」); - * - 收合/恢复过渡期(meow_ui_statusbar_animating)每 ~30ms 快刷驱动平滑上移出屏。 */ - if (!a->fullscreen) { - unsigned long long now2 = now_ms(); - int m = app_sbar_mode_read(); - if (m != g_sb_mode_last) { - g_sb_mode_last = m; - last_sb_overlay_ms = now2; - app_overlay_statusbar(); - } else if (meow_ui_statusbar_animating()) { - if (now2 - last_sb_overlay_ms >= 30) { - last_sb_overlay_ms = now2; - app_overlay_statusbar(); - } - } else if (now2 - last_sb_overlay_ms >= 1000) { - last_sb_overlay_ms = now2; - app_overlay_statusbar(); - } - } - /* 顶条收合/恢复过渡期把循环睡得短些(~20ms),驱动更平滑的上移出屏;稳定后回 100ms。 */ - usleep(meow_ui_statusbar_animating() ? 20000 : 100000); - } - if (wpid > 0) { - /* reap the watchdog: SIGTERM then wait up to 2s (it exits when the - * app dies; bounded so we never hang if it is stuck) */ - kill(wpid, SIGTERM); - unsigned long long wstart = now_ms(); - while (now_ms() - wstart < 2000) { - if (waitpid(wpid, NULL, WNOHANG) == wpid) break; - usleep(50000); - } - } - meow_write_app_state(-1); /* GESTURE_BACK §2.4:应用退出,清 immersive 状态 */ - /* agent-146:App 退出 → 清顶条模式文件 + 复位 launcher SDK 收合/全屏态(菜单恢复整条 28px)。 - * 模式文件由 App 进程 SDK 写;此处 launcher 在退出时清理,防下一 App 读到残留 '1'(收合) 误收合。 */ - unlink(MEOW_SBAR_MODE_FILE); - g_sb_mode_last = -1; - meow_ui_statusbar_set_collapsed(0); - meow_ui_statusbar_animate(0); /* 回菜单:顶条恢复整条,瞬切(不做上移动画) */ - meow_ui_set_fullscreen(0); - meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 菜单状态栏需重画整条(恢复展开) */ - /* M2.24:应用运行期音量由 watchdog 写(settings.conf 同源)。返回菜单前 re-load, - * 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */ - settings_load(); - logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status)); - return 0; -} - -/* ================================================================== */ -/* main */ -/* ================================================================== */ -static void usage(void) -{ - fprintf(stderr, - "usage: meow_launcherd [--sim-hotkey N] [--sim-tap N] [--sim-exit N]\n" - " [--sim-exit-tap N] [--sim-power-hold N]\n" - " [--sim-power-pick N] [--sim-no-exec]\n" - " [--dumpkeys N] [--scan] [--sysdump]\n" - " [--pkg-install ] [--pkg-export ]\n" - " [--ipc-only N] [--ipc-sock ] [--appinfo ]\n" - " [--fontscan ]\n"); -} - -int main(int argc, char **argv) -{ - long sim_hotkey = 0, sim_tap = 0, sim_exit = 0, dumpkeys = 0; - int do_scan = 0, do_sysdump = 0, sim_vol = 0; - long ipc_only_ms = 0; - const char *appinfo_path = NULL; - const char *fontscan_root = NULL; /* --fontscan :宿主/qemu 测试字形扫描 */ - const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL; - for (int i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]); - else if (!strcmp(argv[i], "--sim-tap") && i + 1 < argc) sim_tap = atol(argv[++i]); - else if (!strcmp(argv[i], "--sim-exit") && i + 1 < argc) sim_exit = atol(argv[++i]); - else if (!strcmp(argv[i], "--sim-exit-tap") && i + 1 < argc) sim_exit_tap_ms = atol(argv[++i]); - else if (!strcmp(argv[i], "--sim-power-hold") && i + 1 < argc) sim_power_hold_ms = atol(argv[++i]); - else if (!strcmp(argv[i], "--sim-power-pick") && i + 1 < argc) sim_power_pick = atoi(argv[++i]); - else if (!strcmp(argv[i], "--sim-no-exec")) sim_no_exec = 1; - else if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]); - else if (!strcmp(argv[i], "--scan")) do_scan = 1; - else if (!strcmp(argv[i], "--sysdump")) do_sysdump = 1; - else if (!strcmp(argv[i], "--sim-vol") && i + 1 < argc) sim_vol = atoi(argv[++i]); - else if (!strcmp(argv[i], "--ipc-only") && i + 1 < argc) ipc_only_ms = atol(argv[++i]); - else if (!strcmp(argv[i], "--ipc-sock") && i + 1 < argc) { - snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]); - } - else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i]; - else if (!strcmp(argv[i], "--fontscan") && i + 1 < argc) fontscan_root = argv[++i]; - else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i]; - else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; } - else { usage(); return 1; } - } - - /* unified log: TF card (or /tmp boot buffer when the card is absent) */ - log_cleanup_old(); - if (log_open_card() == 0) - fprintf(stderr, "log file: %s\n", log_file); - else - fprintf(stderr, "log: no TF card, using /tmp boot buffer (%s)\n", - BOOT_BUF_TMP); - logmsg("=== meow_launcherd start (sim_hotkey=%ld sim_tap=%ld sim_exit=%ld dumpkeys=%ld scan=%d) ===", - sim_hotkey, sim_tap, sim_exit, dumpkeys, do_scan); - - load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per - * menu open so edits apply without a daemon restart) */ - settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */ - - /* M2.6:启动扫描一次 TF 卡 MeowOS/fonts///,填充「字体文件」下拉 preset。 - * 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */ - meow_font_scan_presets(); - - if (fontscan_root) { - /* M2.6 测试钩子:扫描指定卡根下的 MeowOS/fonts/// 并打印 preset(qemu/宿主可测)。 - * 用于验证 font.json 解析(含 weight)/三件套/缺件跳过/sha 校验 + preset 动态化逻辑。 */ - meow_font_scan_presets_root(fontscan_root); - printf("fonts=%d\n", font_presets_n); - for (int i = 0; i < font_presets_n; i++) - printf(" %d %s -> %s (fam=%s wt=%s)\n", i, font_preset_labels[i], font_presets[i], - font_preset_fams[i], font_preset_wts[i] ? font_preset_wts[i] : "regular"); - return 0; - } - - if (sim_vol > 0) { - /* M2.14 sim:验证线性渐变——step=1/2/5 下设定音量按 1% 逐级爬升/回落, - * 每 1% 经 meow_dac_vol_write 写一次真实 DAC,打点进 log(stderr)。 */ - settings_volume = 60; /* 固定起点,便于对比 */ - settings_volume_step = (sim_vol > 5) ? 5 : (sim_vol < 1 ? 1 : sim_vol); - int base = settings_volume; - /* UP */ - settings_volume_adj(+settings_volume_step); - for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } - int up = settings_volume; - /* DOWN */ - settings_volume_adj(-settings_volume_step); - for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } - int down = settings_volume; - printf("sim-vol: step=%d base=%d up=%d down=%d (each 1%% logged 'DAC vol -> L/R=')\n", - settings_volume_step, base, up, down); - /* 长按:held=1 持续延段爬升 → 连续渐变到上限 clamp */ - settings_volume = 60; - settings_volume_adj(+settings_volume_step); - for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(1); usleep(20000); } - printf("sim-vol-held: step=%d long-press top=%d (expect clamp 100)\n", - settings_volume_step, settings_volume); - return 0; - } - - if (pkg_install_mpk) { - /* M2.12 .mpk 安装接口(CLI 钩子;UI 后期)。安全、离开主循环。 */ - int r = meow_pkg_install(pkg_install_mpk); - printf("pkg_install %s -> %d\n", pkg_install_mpk, r); - return r; - } - if (pkg_export_name && pkg_export_out) { - /* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */ - int r = meow_pkg_export(pkg_export_name, pkg_export_out); - printf("pkg_export %s -> %s (%d)\n", pkg_export_name, pkg_export_out, r); - return r; - } - - if (do_sysdump) { - /* M2.11 测试钩子:安全打印系统状态快照(不碰 fb/输入设备,可在常驻 launcher - * 旁运行验证 A/E 的 BT/WiFi/电量/音量/亮度抓取),然后退出。 */ - meow_sys_refresh(); - printf("batt=%d%% chg=%d mv=%d\n", g_sys.batt, g_sys.charging, g_sys.batt_mv); - printf("wifi_on=%d ip=%s ssid=%s\n", g_sys.wifi_on, g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", - g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-"); - printf("bt_seen=%d bt_on=%d\n", g_sys.bt_seen, g_sys.bt_on); - printf("vol=%d brightness=%d\n", g_sys.vol, g_sys.brightness); - printf("data_free_kb=%ld sd_mounted=%d sd_free=%ld sd_total=%ld\n", - g_sys.data_free_kb, g_sys.sd_mounted, g_sys.sd_free_kb, g_sys.sd_total_kb); - printf("mode=%d napps=%d ver=%s\n", g_sys.mode, g_sys.napps, g_sys.ver); - printf("render: eff_rate=%d cfg=%d always_max=%d frames=%llu\n", - meow_ui_effective_rate(), refresh_rate_cfg, refresh_always_max, - meow_ui_frames_presented()); - return 0; - } - - if (do_scan) { - struct app apps[MAX_APPS]; - int n = scan_apps(apps); - printf("state=%d napps=%d\n", n < 0 ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0), n < 0 ? 0 : n); - for (int i = 0; i < n && n > 0; i++) - printf(" %d %s -> %s ok=%d\n", i, apps[i].name, apps[i].exec, apps[i].ok); - return 0; - } - - if (appinfo_path) { - /* 阶段 D · M1 debug:解析单个 app.json 并打印路由/框架/服务/图标(qemu 可测静态路由) */ - FILE *f = fopen(appinfo_path, "r"); - if (!f) { printf("appinfo: cannot open %s\n", appinfo_path); return 1; } - struct app a; - memset(&a, 0, sizeof a); - meow_parse_app_json(f, &a); - fclose(f); - printf("name=%s\n", a.name[0] ? a.name : "(missing)"); - printf("exec=%s\n", a.exec[0] ? a.exec : "(missing)"); - printf("ok=%d\n", (a.exec[0] && access(a.exec, X_OK) == 0) ? 1 : 0); - printf("framework=%d\n", a.framework ? 1 : 0); - printf("icon=%s\n", a.icon[0] ? a.icon : "(none)"); - printf("routes=%d\n", a.nroute); - for (int i = 0; i < a.nroute; i++) - printf(" route.%s -> %s (depth=%d keep_awake=%d)\n", a.routes[i].act, a.routes[i].chain, - chain_depth(a.routes[i].chain), a.routes[i].keep_awake); - printf("services=%d\n", a.nsvc); - for (int i = 0; i < a.nsvc; i++) - printf(" service.%s -> mode:%d restart:%d max:%d window_ms:%d\n", - a.svcs[i].name, a.svcs[i].mode, a.svcs[i].restart, - a.svcs[i].max, a.svcs[i].window_ms); - return 0; - } - - if (ipc_only_ms > 0) { - /* 阶段 D · M0/M1 sim:仅跑 IPC 骨架(免 fb/输入设备),供 qemu 下用 meow_ipc - * 验证 HELLO→ACK、ACT_QUERY→链路。无卡时 /tmp 缓冲承载日志。 */ - fprintf(stderr, "ipc-only: socket=%s\n", ipc_sock_path); - int rc = meow_ipc_init(); - if (rc != 0) { fprintf(stderr, "ipc-only: IPC init failed, exiting\n"); return 1; } - unsigned long long s0 = now_ms(); - while (!sigterm_flag && now_ms() - s0 < (unsigned long long)ipc_only_ms) { - meow_ipc_process(); - meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ - usleep(POLL_MS * 1000); - } - ipc_shutdown = 1; - unlink(ipc_sock_path); - logmsg("=== meow_launcherd ipc-only exit ==="); - return 0; - } - - /* pid file */ - int pfd = open(PID_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (pfd >= 0) { - char pbuf[16]; - int l = snprintf(pbuf, sizeof pbuf, "%d\n", getpid()); - write(pfd, pbuf, l); - close(pfd); - } - - signal(SIGTERM, on_sigterm); - signal(SIGINT, on_sigterm); - signal(SIGHUP, SIG_IGN); - - /* 阶段 D · M0:启动 IPC 线程(AF_UNIX /usr/data/meow_ipc.sock)。幂等;失败只 log, - * 主循环仍跑(10ms 热键轮询不受影响)。线程创建晚于进程但有独立生命周期。 */ - meow_ipc_init(); - - struct keypoll kp_power, kp_keys; - /* retry opening the input devices (they may not exist yet at boot) */ - for (int tries = 0; tries < 30; tries++) { - if (kp_open(&kp_power, DEV_POWER) == 0 && - kp_open(&kp_keys, DEV_KEYS) == 0) - break; - logmsg("input devices not ready, retry %d/30 in 1s", tries + 1); - log_poll_card(); /* flush boot buffer even if input devices lag */ - sleep(1); - } - if (!kp_power.ok || !kp_keys.ok) - logmsg("WARNING: input devices unavailable, launcher blind"); - - /* ===== MeowOS 常驻主界面(M2.2 修复批次)===== - * 播放器已死(find_hiby_pid()==0:bootmenu MeowOS 分支 exec、或热键切 MeowOS 后 exec, - * 二者皆此语境)时,launcher == MeowOS 主界面常驻: - * - 启动即扫描并自绘应用列目录(run_menu 画 buffer0 + FBIOPAN yoffset=0), - * 绝不等到首次热键才画 —— 修复「切到 MeowOS 后屏幕停留在播放器最后一帧」; - * - 无存活播放器 → run_menu 内不 SIGSTOP / 不备份 / 不 SIGCONT 死播放器、不恢复死帧; - * - BACK TO PLAYER → 显示「需重启」提示并留在主界面(不渲染死帧); - * - 仅 SIGTERM 退出(rcS/SSH 关停),退出时屏幕保持应用列表主界面(无残帧)。 - * 若此时竟有存活播放器(不应用于 MeowOS,防御性),回落到下面的旧覆盖路径。 */ - meow_mode = is_meow_mode(); - /* M2.12:卡在则把 /usr/data/mnt/sd_0/MeowOS/opt/bin 前插 PATH(外围 CLI 已迁卡, - * 按需可用;卡不在跳过,绝不崩)。SSH 会话另由 rootfs /etc/profile.d/meow_card_path.sh 处理。 */ - if (ensure_card_mounted() || sd_mounted()) { - setenv("PATH", "/usr/data/mnt/sd_0/MeowOS/opt/bin:/usr/bin:/bin:/usr/sbin:/sbin", 1); - logmsg("PATH: card MeowOS/opt/bin prepended"); - } else { - logmsg("PATH: no card, MeowOS/opt/bin skipped"); - } - if (meow_mode) { - /* M2.2 拔营:启动即尝试挂 TF 卡 + 联网(非阻塞);失败由 run_menu 后台周期重试。 */ - ensure_card_mounted(); - ensure_wifi(); - struct app apps[MAX_APPS]; - int n = scan_apps(apps); - int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); - int napps = n < 0 ? 0 : n; - logmsg("MeowOS mode: hiby_player NOT running -> resident app-list home " - "(state=%d napps=%d), drain-to-home immediately", state, napps); - run_menu(apps, napps, state, sim_tap, sim_exit); - logmsg("=== meow_launcherd exit (MeowOS mode) ==="); - if (log_fd >= 0) { close(log_fd); log_fd = -1; } - ipc_shutdown = 1; unlink(ipc_sock_path); - unlink(PID_PATH); - return 0; - } - - /* hotkey state: power_voldown (default) + legacy seq */ - int prev_pwr = 0, prev_prev = 0; - int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_nxt = 0; - int pwr_hold_armed = 0; /* power held, within trigger window */ - unsigned long long pwr_down_at = 0; - int seq_key_prev = 0; /* the configured seq key, edge-tracked */ - int seq_hits = 0; - unsigned long long seq_last_at = 0; - struct vol_snap seq_vol0 = { -1, -1 }; - - long sim_hk_done = 0; - unsigned long long t0 = now_ms(); - int log_tick = 0; /* periodic card-log retry (~1s) while no card log */ - - if (hotkey_mode == HOTKEY_MODE_PWRVOL) - logmsg("ready, hotkey = power_voldown (hold power, tap VOL_DOWN)"); - else - logmsg("ready, hotkey = %d taps of key code %d (gap %dms)", - seq_len, seq_codes[0], seq_gap_ms); - - while (!sigterm_flag) { - unsigned long long t = now_ms(); - long el = (long)(t - t0); - - /* 阶段 D · M0:排空 IPC 队列(非阻塞,零延迟;热键轮询 10ms 不受 socket 阻塞) */ - meow_ipc_process(); - meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ - - /* sample current key states */ - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - int pwr = 0, prev = 0, vold = 0, volu = 0, play = 0, nxt = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; - if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; - if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) nxt = 1; - } - - /* sim hotkey: emulate the configured hotkey. - * power_voldown (default): HOLD power from N, VOL_DOWN tap at N+800 - * (within the 60-2500ms window) — this drives pwr/vold so the - * real detection below sees the same DOWN edges a physical press - * would, and fires the menu at ~N+800. - * seq: 3 quick VOL_UP taps at N, N+300, N+600. */ - if (sim_hotkey > 0 && !sim_hk_done) { - if (hotkey_mode == HOTKEY_MODE_PWRVOL) { - if (el >= sim_hotkey && el < sim_hotkey + 3000) pwr = 1; - if (el >= sim_hotkey + 800 && el < sim_hotkey + 950) vold = 1; - } else { - if (el >= sim_hotkey && el < sim_hotkey + 150) volu = 1; - if (el >= sim_hotkey + 300 && el < sim_hotkey + 450) volu = 1; - if (el >= sim_hotkey + 600 && el < sim_hotkey + 750) volu = 1; - if (el >= sim_hotkey + 750) sim_hk_done = 1; - } - } - - if (vold != prev_vold) logmsg("key VOL_DOWN %s", vold ? "DOWN" : "UP"); - if (volu != prev_volu) logmsg("key VOL_UP %s", volu ? "DOWN" : "UP"); - if (play != prev_play) logmsg("key PLAY %s", play ? "DOWN" : "UP"); - if (nxt != prev_nxt) logmsg("key NEXT %s", nxt ? "DOWN" : "UP"); - if (pwr != prev_pwr) logmsg("key POWER %s", pwr ? "DOWN" : "UP"); - if (prev != prev_prev) logmsg("key PREVIOUS %s", prev ? "DOWN" : "UP"); - - /* hotkey trigger (v6.5). - * DEFAULT — power_voldown: the launcher arms on the power DOWN edge - * and fires when a VOL_DOWN DOWN edge lands while power is still - * held, 60-2500ms after the power press. The player sees the power - * press (its screen-off flow starts — accepted side effect) and the - * VOL_DOWN tap (volume -1 — accepted). The menu open runs the - * verified unblank + bring-backlight-only-if-dark logic, so a - * power-initiated screen-off is undone for the menu. Power release - * or window expiry disarms; a VOL_DOWN before 60ms or after 2500ms - * is ignored (a plain volume press while holding power). - * LEGACY — seq: N quick taps of one key within seq_gap_ms opens the - * menu (single/double presses stay ordinary volume keys); the taps - * raise volume, snapshotted and restored on trigger. */ - if (pwr && !prev_pwr && hotkey_mode == HOTKEY_MODE_PWRVOL) { - pwr_hold_armed = 1; - pwr_down_at = t; - logmsg("hotkey: power DOWN, arm hold-window"); - } - if (!pwr && prev_pwr) pwr_hold_armed = 0; /* release: disarm */ - - int hotkey_fired = 0; - if (hotkey_mode == HOTKEY_MODE_PWRVOL) { - /* disarm once the hold window has passed (power still held) */ - if (pwr_hold_armed && pwr && t - pwr_down_at > HK_PWRWIN_MAX) - pwr_hold_armed = 0; - if (pwr_hold_armed && pwr && vold && !prev_vold) { - long since = (long)(t - pwr_down_at); - if (since >= HK_PWRWIN_MIN && since <= HK_PWRWIN_MAX) { - logmsg("HOTKEY: power held + VOL_DOWN tap (%ldms) -> menu", - since); - hotkey_fired = 1; - } else { - logmsg("hotkey: POWER+VOL_DOWN ignored (%ldms outside window)", - since); - } - } - } else { - int seq_key = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_power.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; - if (kp_keys.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; - } - if (seq_key && !seq_key_prev) { /* DOWN edge */ - if (seq_hits == 0) seq_vol0 = vol_snapshot(); - else if (t - seq_last_at > (unsigned)seq_gap_ms) { - seq_hits = 0; /* gap too long: restart */ - seq_vol0 = vol_snapshot(); - } - seq_hits++; - seq_last_at = t; - logmsg("seq tap %d/%d", seq_hits, seq_len); - if (seq_hits >= seq_len) { - logmsg("SEQ HOTKEY: %d taps of key %d -> menu", - seq_len, seq_codes[0]); - vol_restore(seq_vol0); - seq_hits = 0; - hotkey_fired = 1; - } - } else if (!seq_key && seq_key_prev) { /* UP edge: nothing */ - } - seq_key_prev = seq_key; - if (seq_hits > 0 && t - seq_last_at > (unsigned)seq_gap_ms * 2) - seq_hits = 0; - } - - if (hotkey_fired) { - struct app apps[MAX_APPS]; - int n = scan_apps(apps); - int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); - int napps = n < 0 ? 0 : n; - run_menu(apps, napps, state, sim_tap, sim_exit); - /* restore the player's panel state FIRST (re-blank if the menu - * opened while blanked), THEN let it resume — so the player never - * sees a panel that contradicts its display state machine - * (v6.2 hotfix) */ - restore_backlight(); - cont_player(); - sim_hk_done = 1; - pwr_hold_armed = 0; - /* absorb current state as baseline for the menu keys */ - kp_poll(&kp_power, NULL, NULL); - kp_poll(&kp_keys, NULL, NULL); - prev_vold = prev_volu = prev_play = prev_nxt = 0; - prev_pwr = prev_prev = 0; - seq_key_prev = 0; - seq_hits = 0; - for (int w = 0; w < KEYBUF_WORDS; w++) { - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) prev_vold = 1; - if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) prev_volu = 1; - if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) prev_play = 1; - if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) prev_nxt = 1; - if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) prev_pwr = 1; - if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev_prev = 1; - } - /* the menu just proved the card is present: flush */ - log_poll_card(); - continue; - } - - prev_vold = vold; prev_volu = volu; prev_play = play; prev_nxt = nxt; - prev_pwr = pwr; prev_prev = prev; - - if (dumpkeys > 0 && el >= dumpkeys) { - logmsg("dumpkeys duration reached, exit"); - break; - } - - /* card may have mounted after S90: open the card log and move the - * /tmp boot buffer into it (no menu needed to see boot logs) */ - if (log_fd < 0 && (++log_tick % 100) == 0) log_poll_card(); - - /* NOTE: the Bug B backlight keeper was REMOVED (2026-08-28 hotfix). - * It restored brightness + FBIOBLANK(0) after the player's bkl_time - * screen timeout, which unblanked the panel behind the player's back - * and froze its display state machine (UI gone, keys dead — device - * log: keeper at 40.9s, player wedged). The player owns the panel - * blank state; the launcher never touches fb0/backlight outside the - * menu/app sessions anymore. The player's own "背光时间" timeout is - * expected stock behaviour (set it to 常亮 in the player settings UI - * if unwanted). */ - - usleep(POLL_MS * 1000); - } - - /* If the player is still stopped we never made it through the menu path's - * restore+resume (e.g. SIGKILL-free SIGTERM race): restore the panel state - * before resuming. When the player is already resumed the menu path did it, - * and re-running restore_backlight here would write brightness onto a panel - * the player still believes is blanked — leave it alone. */ - if (player_stopped) { fb_restore_player(); restore_backlight(); } - cont_player(); - logmsg("=== meow_launcherd exit ==="); - if (log_fd >= 0) { close(log_fd); log_fd = -1; } - ipc_shutdown = 1; unlink(ipc_sock_path); - unlink(PID_PATH); - return 0; -} diff --git a/patch_v2/meow_launcherd/meow_notif.c b/patch_v2/meow_launcherd/meow_notif.c index f4f7c42..85cae32 100644 --- a/patch_v2/meow_launcherd/meow_notif.c +++ b/patch_v2/meow_launcherd/meow_notif.c @@ -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 说明」。 */ diff --git a/patch_v2/meow_launcherd/meow_notif.h b/patch_v2/meow_launcherd/meow_notif.h index 12dfa57..a7c0dad 100644 --- a/patch_v2/meow_launcherd/meow_notif.h +++ b/patch_v2/meow_launcherd/meow_notif.h @@ -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 diff --git a/patch_v2/meow_launcherd/meow_notif_panel.h b/patch_v2/meow_launcherd/meow_notif_panel.h index 1dafab8..102083c 100644 --- a/patch_v2/meow_launcherd/meow_notif_panel.h +++ b/patch_v2/meow_launcherd/meow_notif_panel.h @@ -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 头,仅 —— 纯宿主可编。 */ @@ -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)。 */ diff --git a/patch_v2/meow_launcherd/meow_settings.c b/patch_v2/meow_launcherd/meow_settings.c index 118742a..8ad4934 100644 --- a/patch_v2/meow_launcherd/meow_settings.c +++ b/patch_v2/meow_launcherd/meow_settings.c @@ -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 diff --git a/patch_v2/meow_launcherd/meow_settings.h b/patch_v2/meow_launcherd/meow_settings.h index d7a2e89..3c29a59 100644 --- a/patch_v2/meow_launcherd/meow_settings.h +++ b/patch_v2/meow_launcherd/meow_settings.h @@ -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)后续复用。 */ diff --git a/patch_v2/meow_launcherd/net.c b/patch_v2/meow_launcherd/net.c new file mode 100644 index 0000000..dacca49 --- /dev/null +++ b/patch_v2/meow_launcherd/net.c @@ -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 名(如 "")原样通过。 */ +static void wpa_decode_ssid(const char *src, char *dst, int dlen) +{ + int o = 0; + while (*src && o < dlen - 1) { + if (src[0] == '\\' && src[1] == 'x' && + hex_val(src[2]) >= 0 && hex_val(src[3]) >= 0) { + dst[o++] = (char)((hex_val(src[2]) << 4) | hex_val(src[3])); + src += 4; + } else { + dst[o++] = *src++; + } + } + dst[o] = 0; +} + + + +/* 取 WiFi 当前连接 SSID(wpa_cli -i wlan0 status 的 ssid= 行);未连接返回空串 */ +void net_wifi_ssid(char *buf, int len) +{ + buf[0] = 0; + if (!wlan_connected()) return; + char *o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null"); + if (!o) return; + char *s = strstr(o, "ssid="); + if (s) { + s += 5; + char *e = s; while (*e && *e != '\n' && *e != '\r') e++; + int l = (int)(e - s); + /* M2.40:status 的 ssid= 同样可能是中文 \xNN 转义文字,还原成真实 UTF-8 字节 */ + char raw[128]; + if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; + memcpy(raw, s, (size_t)l); raw[l] = 0; + wpa_decode_ssid(raw, buf, len); + } + free(o); +} + + +/* 当前 WiFi 连接详情(点开「WiFi连接」行展示):解析 wpa_cli status + 路由网关。 + * 填充 info[][48](调用方提供的二维缓冲),每行 "标签: 值",返回行数(≤max)。 + * 当前SSID / AP MAC(bssid) / 本机MAC(address) / AP IP(默认网关) / 本机IP(ip_address) / 频道freq。 + * 未连接返回 0(调用方显示"未连接")。绝不阻塞:popen 带超时。 */ +static int net_wifi_info(char (*info)[48], int max) +{ + if (!wlan_connected()) return 0; + char *o = popen_read_tmo(1500, + "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null; ip route 2>/dev/null | grep default"); + if (!o) return 0; + char ssid[64] = "", bssid[24] = "", addr[24] = "", ip[32] = "", freq[16] = "", gw[32] = ""; + char *p = o; + while (p && *p) { + char *e = strchr(p, '\n'); if (e) *e = 0; + if (!strncmp(p, "ssid=", 5)) { char raw[128]; snprintf(raw, sizeof raw, "%s", p + 5); wpa_decode_ssid(raw, ssid, sizeof ssid); } + else if (!strncmp(p, "bssid=", 6)) snprintf(bssid, sizeof bssid, "%s", p + 6); + else if (!strncmp(p, "address=", 8)) snprintf(addr, sizeof addr, "%s", p + 8); + else if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11); + else if (!strncmp(p, "freq=", 5)) snprintf(freq, sizeof freq, "%s", p + 5); + else if (!strncmp(p, "default via ", 11)) { char *s = p + 11; char *g = s; while (*g && *g != ' ') g++; snprintf(gw, sizeof gw, "%s", s); } + p = e ? e + 1 : NULL; + } + free(o); + int n = 0; + if (n < max && ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid); + if (n < max && bssid[0]) snprintf(info[n++], 48, "AP MAC: %s", bssid); + if (n < max && addr[0]) snprintf(info[n++], 48, "本机MAC: %s", addr); + if (n < max && gw[0]) snprintf(info[n++], 48, "AP IP: %s", gw); + if (n < max && ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip); + if (n < max && freq[0]) snprintf(info[n++], 48, "频道: %s MHz", freq); + return n; +} + + +/* 已保存网络(wpa_cli -i wlan0 list_networks):首行为表头,其余每行 "\t\t\t"。 + * 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN),返回项数(≤max)。 */ +static int net_wifi_saved(meow_net_t *out, int max) +{ + int n = 0; + char *o = popen_read_tmo(1500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 list_networks 2>/dev/null"); + if (!o) return 0; + char *line = strchr(o, '\n'); + if (line) line++; /* 跳表头 */ + while (line && *line && n < max) { + char *eol = strchr(line, '\n'); + if (eol) *eol = 0; + char *p = line; while (*p == ' ' || *p == '\t') p++; + if (*p && *p != '#') { + int id = atoi(p); + char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++; + while (*sp == ' ' || *sp == '\t') sp++; + char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++; + if (sp != se) { + /* M2.40:list_networks 对中文 SSID 输出 \xNN 转义文字(如 \xe5\xa4\xa9… = "天…"), + * 先拷进临时缓冲再还原成真实 UTF-8 字节存入 name,否则界面显示超长乱码。 */ + char raw[160]; + int l = (int)(se - sp); + if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; + memcpy(raw, sp, (size_t)l); raw[l] = 0; + out[n].id = id; + wpa_decode_ssid(raw, out[n].name, sizeof out[n].name); + n++; + } + } + line = eol ? eol + 1 : NULL; + } + free(o); + return n; +} + + +/* WiFi 扫描触发(异步:wpa_cli scan 只是通知 supplicant 发起扫描,立即返回)。 + * 结果稍后经 net_wifi_scan() 读。 */ +static void net_wifi_scan_trigger(void) +{ + if (!wlan_connected()) return; + system("PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan 2>/dev/null"); +} + + +/* 读 WiFi 扫描结果(wpa_cli -i wlan0 scan_results):首行列头,数据行形如 + * [flags..] [ssid...] + * flags 恒以 '[' 起;SSID 为 flags 后的整段余量(可含空格;为空=隐藏网络)。 + * M2.40:同一 AP 常出现多条(不同频段/空 SSID/重名),按 BSSID 去重——每个 BSSID 只留一条, + * 优先采用含完整 SSID 的行(后到且有 SSID 则覆盖先到的空 SSID 行),隐藏网络显示 "(隐藏网络)"。 + * 填 out[i].id=-1;name 存 SSID(或隐藏标记)。返回项数(≤max,且 ≤S_NET_MAX)。 */ +static int net_wifi_scan(meow_net_t *out, int max) +{ + int n = 0; + if (max > S_NET_MAX) max = S_NET_MAX; + char *o = popen_read_tmo(2500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan_results 2>/dev/null"); + if (!o) return 0; + char seen_bssid[S_NET_MAX][24]; + int named[S_NET_MAX] = {0}; + char *line = strchr(o, '\n'); + if (line) line++; + while (line && *line && n < max) { + char *eol = strchr(line, '\n'); + if (eol) *eol = 0; + char *p = line; while (*p == ' ' || *p == '\t') p++; + if (*p) { + /* 首 token = bssid */ + char bssid[24] = ""; + { char *b = p; while (*p && *p != ' ' && *p != '\t') p++; + int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; } + /* 找 flags(首个以 '[' 起始的 token),之后(trim 掉前导空白)才是 SSID */ + const char *ssid = ""; + char *br = strchr(p, '['); + if (br) { + char *q = br; + while (*q) { + while (*q == ' ' || *q == '\t') q++; + if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; } + else break; + } + ssid = q; + while (*ssid == ' ' || *ssid == '\t') ssid++; + } + /* SSID 去尾部空白/换行 */ + char *se = (char *)ssid; + while (*se && *se != '\n' && *se != '\r') se++; + while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--; + int has = (se > ssid && *ssid); + /* 去重:同一 BSSID 只留一条,优先具名 SSID */ + int idx = -1; + for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; } + if (idx < 0) { + if (n >= max) { line = eol ? eol + 1 : NULL; continue; } + idx = n++; + strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1); + seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0; + named[idx] = has; + out[idx].id = -1; + if (has) { + /* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */ + char raw[96]; + int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; + memcpy(raw, ssid, (size_t)l); raw[l] = 0; + wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name); + } else { + snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)"); + } + } else if (has && !named[idx]) { + /* 同 BSSID 先出现无 SSID 行、后有具名行 → 用后者 SSID 覆盖(解码 \xNN) */ + char raw[96]; + int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1; + memcpy(raw, ssid, (size_t)l); raw[l] = 0; + wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name); + named[idx] = 1; + } + } + line = eol ? eol + 1 : NULL; + } + free(o); + return n; +} + + +/* 蓝牙已配对设备(bluetoothctl paired-devices):每行 "Device "。 + * hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */ +static int net_bt_paired(meow_net_t *out, int max) +{ + if (!hci_powered()) return 0; + int n = 0; + char *o = popen_read_tmo(1800, "bluetoothctl paired-devices 2>/dev/null"); + if (!o) return 0; + char *line = o; + while (line && *line && n < max) { + char *eol = strchr(line, '\n'); + if (eol) *eol = 0; + char *p = strstr(line, "Device "); + if (p) { + p += 7; + char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++; + char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; + /* 设备名每行即名称 token;名称可含空格,这里取整行剩余部分(含 MAC 后的名字) */ + if (*nm) + snprintf(out[n].name, sizeof out[n].name, "%s", nm); + else { + int ml = (int)(mac - p); if (ml > 47) ml = 47; + memcpy(out[n].name, p, (size_t)ml); out[n].name[ml] = 0; + } + out[n].id = -1; + n++; + } + line = eol ? eol + 1 : NULL; + } + free(o); + return n; +} + + +/* ---- 蓝牙附近设备(未配对扫描发现;异步,不阻塞主循环)---- + * M2.40:用户要的「附近的设备」= 蓝牙扫描发现(含未配对),而不只是已配对。扫描较慢(2-6s), + * 故后台有界驱动 bluetoothctl:scan on → sleep 4 → scan off → devices → quit,输出写 /tmp 缓存; + * system() 因尾部 & 立即返回(约 5s 后自灭),绝不卡死主循环。读取端 net_bt_scan 只解析缓存 + * (bluetoothctl `devices` 行 "Device ",含已配对+未配对发现,去重)。 */ +static long g_bt_scan_ms = 0; /* 最近一次发起扫描的 now_ms();0=从未 */ + + +static void net_bt_scan_trigger(void) +{ + if (!hci_powered()) return; + long t = (long)now_ms(); + if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; /* 7s 内不重复触发,防堆积 */ + g_bt_scan_ms = t; + system("(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | bluetoothctl > /tmp/meow_bt_scan.txt 2>&1 &"); +} + + +/* 读蓝牙附近设备缓存:解析 bluetoothctl `devices` 输出("Device ",去重;含已配对 + * 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。 + * 返回项数(≤max,且 ≤S_NET_MAX)。 */ +static int net_bt_scan(meow_net_t *out, int max) +{ + if (!hci_powered()) return 0; + if (max > S_NET_MAX) max = S_NET_MAX; + int n = 0; + FILE *f = fopen("/tmp/meow_bt_scan.txt", "r"); + if (f) { + char line[256]; + char seen[S_NET_MAX][24]; + while (fgets(line, sizeof line, f)) { + char *p = line; while (*p == ' ' || *p == '\t') p++; + /* 认两种行:`devices` 输出的 "Device ";发现会话里的 "[NEW] Device "。 + * 跳过 [CHG]/[DEL] 等属性/移除行(无稳定名称)。 */ + char *dv = NULL; + if (!strncmp(p, "Device ", 7)) dv = p + 7; + else if (!strncmp(p, "[NEW] Device ", 13)) dv = p + 13; + if (!dv) continue; + char *mac = dv; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n' && *mac != '\r') mac++; + int ml = (int)(mac - dv); + if (ml < 6 || !memchr(dv, ':', (size_t)ml)) continue; /* 需像 MAC(含冒号);过滤属性行 */ + /* 去重(同 MAC 只留一条,取首个出现) */ + int dup = 0; + for (int j = 0; j < n; j++) if (!strncmp(seen[j], dv, (size_t)ml)) { dup = 1; break; } + if (dup) continue; + if (n >= max) break; + char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; + char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; + *e = 0; /* 去行尾换行 */ + strncpy(seen[n], dv, (size_t)ml); seen[n][ml] = 0; + if (*nm) + snprintf(out[n].name, sizeof out[n].name, "%s", nm); + else { + int l = ml; if (l > 47) l = 47; + memcpy(out[n].name, dv, (size_t)l); out[n].name[l] = 0; + } + out[n].id = -1; + n++; + } + fclose(f); + if (n > 0) return n; + } + return net_bt_paired(out, max); /* 无扫描缓存 → 退回已配对列表 */ +} + + +/* ---- 蓝牙角色判定(M2.48:状态栏多态 + 设置页"蓝牙模式"行)---- + * 返回 BT_ROLE_*;desc 非空则写中文描述(供设置页只读行展示)。全部 popen 带超时,绝不阻塞。 + * 判定(best-effort,蓝牙栈=BlueZ 5.54): + * 关 :hci0 不存在或 DOWN(hci_powered)。 + * 开未连接:hciconfig UP 但 hcitool con 无 ACL 连接。 + * 主模式 :有 ACL 连接(连了外围设备如耳机);设备侧 A2DP sink 命中 → 主。 + * 从模式 :有 ACL 连接且设备侧 A2DP source 命中(远端接入,设备被当耳机用)。 + * 注:master/slave 依赖 bluealsa-cli a2dp list 端点角色,其输出随固件格式不同;解析不到时, + * 有 ACL 一律按「主模式」兜底(仍可靠区分 关/开未连接/已连接三态)。若真机 a2dp 角色 + * 与注释相反,只需交换 a2dp_side() 的 1/2 返回值即可。 */ +static void copy_utf8_trunc(char *dst, int dlen, const char *src) +{ + int o = 0; + while (*src && o < dlen - 1) { + unsigned char c = (unsigned char)*src; + int nb = 1; + if ((c & 0xE0) == 0xC0) nb = 2; + else if ((c & 0xF0) == 0xE0) nb = 3; + else if ((c & 0xF8) == 0xF0) nb = 4; + if (o + nb > dlen - 1) break; /* 放不下整个字符就停(不截半字符) */ + for (int k = 0; k < nb; k++) { if (!src[k]) break; dst[o++] = src[k]; } + src += nb; + } + dst[o] = 0; +} + + +/* A2DP 段端点角色:1=设备侧 sink(主模式),2=设备侧 source(从模式),-1=无法判定。 + * 宽松匹配:同段内出现 connected/running 且出现对应角色词。优先认 BlueALSA 对象路径 + * a2dpsnk/a2dpsrc(跨版本稳定),再退到 source/sink/source 单词。 */ +static int a2dp_side(const char *sec) +{ + if (!sec) return -1; + if (!strstr(sec, "connected") && !strstr(sec, "running")) return -1; + int src = (strstr(sec, "a2dpsrc") || strstr(sec, "source") || strstr(sec, "Source")) ? 1 : 0; + int snk = (strstr(sec, "a2dpsnk") || strstr(sec, "sink") || + strstr(sec, "Sink") || strstr(sec, "target")) ? 1 : 0; + if (src && !snk) return 2; /* 设备侧 source → 被当耳机(从) */ + if (snk && !src) return 1; /* 设备侧 sink → 送流给外围(主) */ + return -1; +} + + +/* 蓝牙角色:见上方注释。一次 popen 拿 ACL + A2DP + 已连设备名三类信息,避免反复 spawn。 */ +static int bt_role(char *desc, int len) +{ + if (desc && len > 0) desc[0] = 0; + if (!g_sys.bt_seen || !hci_powered()) { + if (desc && len > 0) snprintf(desc, len, "关闭"); + return BT_ROLE_OFF; + } + /* 快的放前(HCI 连接、A2DP 端点),bluetoothctl 最慢放最后;整体超时防挂起(超时 SIGKILL)。 */ + char *o = popen_read_tmo(1800, + "PATH=/bin:/sbin:/usr/bin:/usr/sbin hcitool con 2>/dev/null; " + "echo '===A2DP==='; bluealsa-cli a2dp list 2>/dev/null; " + "echo '===NAMES==='; printf 'devices Connected\\nquit\\n' | bluetoothctl 2>/dev/null"); + if (!o) { /* 探测失败:保守按「开未连接」 */ + if (desc && len > 0) snprintf(desc, len, "开启未连接"); + return BT_ROLE_ON_NO_CONN; + } + /* 按标记分节(把标记首字符置 0 切断) */ + char *s_acl = o; + char *s_a2dp = NULL, *s_names = NULL; + char *p = strstr(o, "===A2DP==="); + if (p) { *p = 0; s_a2dp = p + 10; } /* "===A2DP===" 长 10 */ + if (s_a2dp) { char *q = strstr(s_a2dp, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } } + else { char *q = strstr(o, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } } + /* ACL:hcitool con 的连接行含 "ACL "(如 " < ACL 00:1A:.. handle.. state.. lm MASTER") */ + int has_acl = 0; + { + char *line = s_acl; + while (line && *line) { + char *eol = strchr(line, '\n'); if (eol) *eol = 0; + if (strstr(line, "ACL") && strchr(line, ':')) { has_acl = 1; break; } + line = eol ? eol + 1 : NULL; + } + } + /* 已连设备名(bluetoothctl `devices Connected`:"Device ",取首个)。 + * 定长 22:配合「从模式·被连(耳机…」前缀(UTF-8 约 25B)仍落在展示缓冲 48B 内。 + * 同时也作「已连接」的第二证据(hcitool 缺失时 bluetoothctl 兜底)。 */ + char dev[22] = ""; + int has_conn = has_acl; + if (s_names) { + char *line = s_names; + while (line && *line) { + char *eol = strchr(line, '\n'); if (eol) *eol = 0; + char *q = line; while (*q == ' ' || *q == '\t') q++; + if (!strncmp(q, "Device ", 7)) { + has_conn = 1; /* bluetoothctl 也确认有设备已连 */ + if (!dev[0]) { + q += 7; + char *mac = q; while (*mac && *mac != ' ' && *mac != '\t' && + *mac != '\n' && *mac != '\r') mac++; + char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++; + char *e = nm; while (*e && *e != '\n' && *e != '\r') e++; + *e = 0; + if (*nm) copy_utf8_trunc(dev, sizeof dev, nm); + else copy_utf8_trunc(dev, sizeof dev, q); + } + } + line = eol ? eol + 1 : NULL; + } + } + int side = a2dp_side(s_a2dp); + int role; + if (!has_conn) role = BT_ROLE_ON_NO_CONN; + else if (side == 2) role = BT_ROLE_SLAVE; /* 设备侧 source → 被当耳机 */ + else role = BT_ROLE_MASTER; /* sink 或 a2dp 不明但有连接 → 主 */ + if (desc && len > 0) { + if (role == BT_ROLE_ON_NO_CONN) snprintf(desc, len, "开启未连接"); + else if (role == BT_ROLE_SLAVE) snprintf(desc, len, "从模式·被连(耳机%s%s)", + dev[0] ? " " : "", dev); + else snprintf(desc, len, "主模式·已连%s%s", + dev[0] ? " " : "", dev[0] ? dev : "设备"); + } + free(o); + return role; +} + + +/* 节流重探蓝牙角色(约 5s 一次;force=1 立即)。结果写 g_sys.bt_role + g_bt_mode, + * 并同步 g_sys.bt_on/bt_seen,保证状态栏与本行一致。蓝牙关时免费(只 spawn hciconfig)。 */ +static long g_bt_role_ms = 0; /* 最近一次 bt_role 探测的 now_ms();0=从未 */ + +void bt_role_refresh(int force) +{ + if (!force && g_bt_role_ms && (now_ms() - g_bt_role_ms) < 5000) return; + g_bt_role_ms = now_ms(); + char d[48]; + g_sys.bt_role = bt_role(d, sizeof d); + snprintf(g_bt_mode, sizeof g_bt_mode, "%s", d); + g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0; + g_sys.bt_on = (g_sys.bt_role != BT_ROLE_OFF) ? 1 : 0; +} + + +/* 网络信息缓存(绘制路径只读,s_net_refresh 刷新;下拉打开时才重新解析列表) */ +static char g_net_ssid[64] = "未连接"; + +static int g_net_saved_n = 0; + +static int g_net_scan_n = 0; + +static int g_net_bt_n = 0; + + +/* 刷新网络信息缓存(进入设置页 / 网络动作后调用;含一次 WiFi 扫描触发) */ +void s_net_refresh(void) +{ + if (wlan_connected()) { + char ss[64]; net_wifi_ssid(ss, sizeof ss); + if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss); + else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接"); + } else { + snprintf(g_net_ssid, sizeof g_net_ssid, "未连接"); + } + net_wifi_scan_trigger(); /* 异步发起扫描,稍后附近/计数更接近真实 */ + net_bt_scan_trigger(); /* M2.40:异步发起蓝牙附近扫描(写 /tmp 缓存,不阻塞) */ + bt_role_refresh(1); /* M2.48:进入设置/网络动作时立即重探蓝牙角色,本行/状态栏最新 */ + meow_net_t tmp[S_NET_MAX]; + g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX); + g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX); + g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); /* 附近(已配对+扫描发现) */ +} + + +/* SC_INFO/SC_LIST 行的即时展示文本(读缓存;draw 路径不 popen) */ +int s_net_get(const srow_t *r, char *buf, int len) +{ + switch (r->net_list) { + case NET_LIST_WIFI_SSID: snprintf(buf, len, "%s", g_net_ssid); return 0; + case NET_LIST_WIFI_SAVED: snprintf(buf, len, "%d 个网络", g_net_saved_n); return 0; + case NET_LIST_WIFI_SCAN: snprintf(buf, len, "%d 个附近WiFi", g_net_scan_n); return 0; + case NET_LIST_BT_NEARBY: snprintf(buf, len, "%d 台设备", g_net_bt_n); return 0; + case NET_LIST_BT_MODE: snprintf(buf, len, "%s", g_bt_mode); return 0; + case NET_LIST_ABOUT: snprintf(buf, len, "MeowOS v%s", g_meowos_version); return 0; + } + snprintf(buf, len, "-"); + return 0; +} + + +/* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。 + * 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。 */ +void s_net_toggle_wifi(int on) +{ + if (on) system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh"); + else system("PATH=/bin:/sbin:/usr/bin:/usr/sbin ip link set wlan0 down 2>/dev/null"); +} + +void s_net_toggle_bt(int on) +{ + if (on) system("hciconfig hci0 up 2>/dev/null"); + else system("hciconfig hci0 down 2>/dev/null"); +} + + +/* 进入设置页时把 network.wifi_enabled/bluetooth_enabled 同步为真实硬件态, + * 让开关行反映 reality(仅同步内存 + 刷新缓存;不落盘 —— s_commit 的 save 才真正写)。 + * settings 未 init/取值失败则不动(保默认)。 */ +void s_net_sync_from_sys(void) +{ + int cur = 0; + if (meow_settings_get_bool("network.wifi_enabled", &cur) == 0) { + int real = wlan_connected() ? 1 : 0; + if (real != cur) meow_settings_set_bool("network.wifi_enabled", real); + } + if (meow_settings_get_bool("network.bluetooth_enabled", &cur) == 0) { + int real = hci_powered() ? 1 : 0; + if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real); + } + s_net_refresh(); +} + + +/* 打开 SC_LIST 网络下拉(重新解析最新列表):已保存=可 select_network 连接; + * 附近WiFi(扫描)/蓝牙设备(附近扫描+已配对)=只读展示(选择即关闭;带密码/配对仍留后期)。 + * 返回后刷新缓存。 */ +void s_net_open(const srow_t *r) +{ + meow_net_t items[S_NET_MAX]; + const char *txt[S_NET_MAX]; + char back[S_NET_MAX][48]; + int n = 0; + switch (r->net_list) { + case NET_LIST_WIFI_SSID: /* 点开「WiFi连接」→ 显示当前连接详情(只读) */ + n = net_wifi_info(back, S_NET_MAX); + if (n == 0) { snprintf(back[0], sizeof back[0], "%s", wlan_connected() ? "已连接(无详情)" : "未连接"); n = 1; } + for (int i = 0; i < n; i++) txt[i] = back[i]; + s_dd_modal(r->label, txt, n, -1); /* 只读展示 */ + break; + case NET_LIST_WIFI_SAVED: + n = net_wifi_saved(items, S_NET_MAX); + if (n == 0) { snprintf(back[0], sizeof back[0], "无已保存网络"); txt[0] = back[0]; n = 1; items[0].id = -1; } + else for (int i = 0; i < n; i++) { + snprintf(back[i], sizeof back[i], "%s", items[i].name[0] ? items[i].name : "<未命名>"); + txt[i] = back[i]; + } + { + int pick = s_dd_modal(r->label, txt, n, -1); /* cur=-1:不预勾选 */ + if (pick >= 0 && pick < n && items[pick].id >= 0) { + char cmd[160]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 select_network %d 2>/dev/null", + items[pick].id); + system(cmd); + } + } + break; + case NET_LIST_WIFI_SCAN: + net_wifi_scan_trigger(); + n = net_wifi_scan(items, S_NET_MAX); + if (n == 0) { snprintf(back[0], sizeof back[0], "未扫描到附近网络"); txt[0] = back[0]; n = 1; } + else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; } + s_dd_modal(r->label, txt, n, -1); /* 只读展示 */ + break; + case NET_LIST_BT_NEARBY: + net_bt_scan_trigger(); /* M2.40:异步发起/刷新附近扫描(读缓存;绝不阻塞主循环) */ + n = net_bt_scan(items, S_NET_MAX); + if (n == 0) { + long t = (long)now_ms(); + if (g_bt_scan_ms && t - g_bt_scan_ms < 6000) + snprintf(back[0], sizeof back[0], "扫描中..."); + else + snprintf(back[0], sizeof back[0], "未发现附近设备"); + txt[0] = back[0]; n = 1; items[0].id = -1; + } + else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; } + s_dd_modal(r->label, txt, n, -1); /* 只读展示 */ + break; + case NET_LIST_ABOUT: + s_about_open(); /* M2.5x 系统设置「关于本机」:只读设备信息弹窗 */ + return; /* 不上网/蓝牙缓存刷新(信息已现场读取) */ + default: + return; + } + s_net_refresh(); +} + diff --git a/patch_v2/meow_launcherd/notif_center.c b/patch_v2/meow_launcherd/notif_center.c new file mode 100644 index 0000000..817fb5b --- /dev/null +++ b/patch_v2/meow_launcherd/notif_center.c @@ -0,0 +1,1363 @@ +/* notif_center.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ +/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ +#include "meow_launcher_internal.h" + + +static ncp_row_t g_rows[NCP_MAX_ROWS]; + +static int g_nrows = 0; + +static struct app *g_ncp_apps = NULL; /* run_notif_center 注入(通知点击找 app 用) */ + +static int g_ncp_napps = 0; + + +/* ⑥ 勿扰面板内开关:当前 dnd 态(开面板/切换时从 meow_settings notify.dnd 读/写,零旁路)。 + * ncp_collect_cb / ncp_active_count 按它过滤行(only urgency>=NCP_DND_MIN_URGENCY when on)。 */ +static int g_ncp_dnd = 0; + + +/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。 + * g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。 + * run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint) + * 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */ +static int g_ncp_swipe_row = -1; + +static int g_ncp_swipe_off = 0; + + +/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */ +static void ncp_strncpy(char *dst, const char *src, size_t len) +{ + if (!dst || len == 0) return; + if (!src) { dst[0] = 0; return; } + strncpy(dst, src, len - 1); + dst[len - 1] = 0; +} + + +/* app_id → 显示名(apps[] 匹配则用 name,否则原样 app_id;无则空串) */ +static const char *ncp_app_name(const char *app_id) +{ + if (app_id && g_ncp_apps) + for (int i = 0; i < g_ncp_napps; i++) + if (!strcmp(g_ncp_apps[i].id, app_id)) return g_ncp_apps[i].name; + return app_id ? app_id : ""; +} + + +/* 通知图标加载回调:把 app_icon(相对卡根/绝对)解成绝对路径,加载 meow_img。 */ +static void ncp_load_icon(ncp_row_t *r) +{ + if (!r) return; + r->img = NULL; + if (!r->icon[0]) return; + char path[MEOW_NOTIF_ICON_MAX + 80]; + if (r->icon[0] == '/') snprintf(path, sizeof path, "%s", r->icon); + else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", r->icon); + r->img = meow_img_load_file(path); +} + + +static int ncp_collect_cb(const meow_notif_t *n, void *ctx) +{ + (void)ctx; + if (g_nrows >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */ + /* 勿扰(⑥):dnd 开时只收集 urgency>=NCP_DND_MIN_URGENCY(HIGH/CRITICAL)。 */ + if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0; + ncp_row_t *r = &g_rows[g_nrows++]; + memset(r, 0, sizeof *r); + r->id = n->id; + ncp_strncpy(r->app_id, n->app_id, sizeof r->app_id); + ncp_strncpy(r->summary, n->summary, sizeof r->summary); + ncp_strncpy(r->body, n->body, sizeof r->body); + ncp_strncpy(r->icon, n->app_icon, sizeof r->icon); + ncp_strncpy(r->target, n->target_activity, sizeof r->target); + r->created_ms = n->created_ms; + r->progress = n->progress; + r->urgency = n->urgency; + ncp_load_icon(r); + return 0; +} + + +/* 释放全部缓存图标 + 清行数(面板关闭/列表刷新前用)。 */ +static void ncp_rows_free(void) +{ + for (int i = 0; i < g_nrows; i++) + if (g_rows[i].img) { meow_img_free(g_rows[i].img); g_rows[i].img = NULL; } + g_nrows = 0; +} + + +/* 重新采集通知列表(比 meow_notif_count 变化时调用)。 */ +static int ncp_refresh_rows(void) +{ + ncp_rows_free(); + meow_notif_list(NULL, ncp_collect_cb, NULL); + return g_nrows; +} + + +/* 当前应显示的活跃通知数(按 g_ncp_dnd 过滤,与 g_rows 同口径)——供变化检测/重采集用。 */ +static int ncp_count_cb(const meow_notif_t *n, void *ctx) +{ + if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0; + (*(int *)ctx)++; + return 0; +} + +static int ncp_active_count(void) +{ + int c = 0; + meow_notif_list(NULL, ncp_count_cb, &c); + if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS; + return c; +} + + +/* 通知列表是否变化(按过滤后活跃数比;新条/撤下/TTL 释放都反映在 count,含 dnd 过滤)。 */ +static int ncp_rows_changed(void) +{ + return ncp_active_count() != g_nrows; +} + + +/* ---- 开关平移动画辅助(§9:快照 blit + 平移;只在 run_notif_center 内分配/释放)---- + * g_ncp_ph = 面板高 PH = fb_yres - NCP_TOP;两块快照各 PH × fb_line 字节: + * bg_snap = 进入面板时(绘制面板前)从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拍的背景(应用列表); + * panel_snap= 最终位置整面板渲染(draw_notif_center 后拍),稳态重绘后刷新。 + * 快照不可用(分配失败/无 fb)时动画降级为瞬时显示/消失(fallback)。 */ +static unsigned char *g_ncp_bg_snap = NULL; + +static unsigned char *g_ncp_panel_snap = NULL; + +static int g_ncp_ph = 0; + + +static void ncp_snap_init(void) +{ + int ph = fb_yres - NCP_TOP; + if (ph <= 0 || fb_line <= 0) { g_ncp_ph = 0; return; } + size_t bsz = (size_t)ph * (size_t)fb_line; + g_ncp_bg_snap = malloc(bsz); + g_ncp_panel_snap = malloc(bsz); + if (!g_ncp_bg_snap || !g_ncp_panel_snap) { + free(g_ncp_bg_snap); g_ncp_bg_snap = NULL; + free(g_ncp_panel_snap); g_ncp_panel_snap = NULL; + g_ncp_ph = 0; + return; + } + g_ncp_ph = ph; +} + +static void ncp_snap_free(void) +{ + free(g_ncp_bg_snap); g_ncp_bg_snap = NULL; + free(g_ncp_panel_snap); g_ncp_panel_snap = NULL; + g_ncp_ph = 0; +} + + +/* 从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拷贝到 dst(行宽 fb_line)。 */ +static void ncp_snap_copy_rows(unsigned char *dst, const unsigned char *base) +{ + if (!dst || g_ncp_ph <= 0) return; + size_t row = (size_t)fb_line; + for (int i = 0; i < g_ncp_ph; i++) + memcpy(dst + (size_t)i * row, base + (size_t)i * row, row); +} + +/* 拍背景(应用列表):进入面板时、面板绘制前调用。 */ +static void ncp_snap_bg(void) +{ + ncp_snap_copy_rows(g_ncp_bg_snap, fb + (size_t)NCP_TOP * fb_line); +} + +/* 拍最终整面板:draw_notif_center 画完后 / 每次稳态重绘后调用。 */ +static void ncp_snap_panel(void) +{ + ncp_snap_copy_rows(g_ncp_panel_snap, fb + (size_t)NCP_TOP * fb_line); +} + + +/* 重建动画帧:把 back_buf 面板区 [NCP_TOP..) 还原为「背景 + 平移后的面板」。 + * off∈[-PH,0];prev_bot=上一帧覆盖底缘(exclusive),用于关(收窄)时恢复新露出背景条。 + * 返回本帧覆盖底缘(exclusive,供调用方算脏区与传给下次)。 */ +static int ncp_anim_rebuild(int off, int prev_bot) +{ + if (!g_ncp_bg_snap || !g_ncp_panel_snap || g_ncp_ph <= 0) return NCP_TOP; + int top = NCP_TOP; + int ph = g_ncp_ph; + int cur_bot = mnp_anim_covered_bot(top, ph, off); /* ∈[top, top+ph](§9.3 裁切) */ + if (cur_bot < top) cur_bot = top; + if (cur_bot > top + ph) cur_bot = top + ph; + size_t row = (size_t)fb_line; + /* 1) 面板收窄(收回中):[cur_bot, prev_bot) 新露出 → 恢复背景(应用列表)。 */ + if (prev_bot > cur_bot) { + int b0 = prev_bot < top + ph ? prev_bot : top + ph; + for (int sy = cur_bot; sy < b0; sy++) + memcpy(fb + (size_t)sy * row, + g_ncp_bg_snap + (size_t)(sy - top) * row, row); + } + /* 2) 贴平移面板:[top, cur_bot) 从 panel_snap 行 yy = sy-(top+off) 拷回。 */ + for (int sy = top; sy < cur_bot; sy++) { + int yy = sy - (top + off); /* ∈[0, ph)(见 §9.3 裁切论证) */ + if (yy < 0) yy = 0; + if (yy >= ph) yy = ph - 1; + memcpy(fb + (size_t)sy * row, + g_ncp_panel_snap + (size_t)yy * row, row); + } + return cur_bot; +} + + +/* ⑨ 应用一帧面板动画(开关时间轴 §9 与跟手拖动 ⑨ 共用):把 back_buf 面板区重建为 + * 「背景 + 平移后的面板」并标脏/呈现。p=当前进度;prev_bot=上一帧覆盖底缘(exclusive, + * 回填本帧);ok=快照可用(否则 no-op,动画降级为瞬时)。只刷面板矩形 + 新露出条, + * 顶= NCP_TOP(不盖状态栏)、不含 [0,NCP_TOP) 带;不重排面板控件(panel_snap 固定)。 */ +static void ncp_anim_frame(int p, int *prev_bot, int ok) +{ + if (!ok || g_ncp_ph <= 0 || !prev_bot) return; + int off = mnp_anim_off(p, NCP_ANIM_FIX, g_ncp_ph); + int cur_bot = ncp_anim_rebuild(off, *prev_bot); + int maxb = (*prev_bot > cur_bot) ? *prev_bot : cur_bot; + *prev_bot = cur_bot; + meow_dirty_add(0, NCP_TOP, fb_xres, maxb - NCP_TOP); + meow_ui_flush(); +} + + +/* 把文本按宽度截断(UTF-8 安全:不切开多字节码),超出加 "..."。 */ +static void ncp_text(int x, int y, const char *s, int size, uint32_t color, int maxw) +{ + if (!s) s = ""; + if (ui_width(s, size) <= maxw) { ui_draw(x, y, s, color, size); return; } + int ell = ui_width("...", size); + int limit = maxw - ell; + int len = 0; + char buf[128]; + while (s[len] && len < (int)sizeof buf - 8) { + /* 取当前字节对应的 UTF-8 码长(1-4),避免切断多字节字符 */ + unsigned char c0 = (unsigned char)s[len]; + int cp = 1; + if ((c0 & 0xE0) == 0xC0) cp = 2; + else if ((c0 & 0xF0) == 0xE0) cp = 3; + else if ((c0 & 0xF8) == 0xF0) cp = 4; + if (len + cp >= (int)sizeof buf - 1) break; + for (int k = 0; k < cp; k++) buf[len + k] = s[len + k]; + buf[len + cp] = 0; + if (ui_width(buf, size) > limit) { buf[len] = 0; break; } + len += cp; + } + if (len > 0) { + if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; } + ui_draw(x, y, buf, color, size); + } else { + ui_draw(x, y, "...", color, size); + } +} + + +/* 画一个小圆点(近似圆 = 圆角方 r=radius;主题 chip 用)。 */ +static void ncp_dot(int cx, int cy, int r, uint32_t col) +{ + meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, col); +} + + +/* 画 32×32 图标(复用 sb_blit_icon;ink 垂直中心对齐 cy)。 */ +static void ncp_sb_icon(int icon, int slot_x, int cy, uint32_t col, int ink_h) +{ + int saved = g_sb_icon_cy; + g_sb_icon_cy = cy; + sb_blit_icon(icon, slot_x, col, ink_h); + g_sb_icon_cy = saved; +} + + +/* chip 位置(4 个,居中排布)。返回下标对应 chip 的 x 左缘。 */ +static int ncp_chip_x(int chip) +{ + int total = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP; + int x0 = (fb_xres - total) / 2; + return x0 + chip * (NCP_CHIP_W + NCP_CHIP_GAP); +} + +static int ncp_chip_hit(int chip, int tx, int ty) +{ + int x = ncp_chip_x(chip); + return (tx >= x && tx < x + NCP_CHIP_W && ty >= NCP_CHIP_Y && ty < NCP_CHIP_Y + NCP_CHIP_H); +} + + +/* 面板当前 4 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */ +static void ncp_chip_state(int chip, int *on, char *val, int vlen) +{ + *on = 0; + if (val && vlen > 0) val[0] = 0; + if (chip == NCP_CHIP_WIFI) { + int v = 0; meow_settings_get_switch("network.wifi_enabled", &v); + *on = v; + } else if (chip == NCP_CHIP_BT) { + int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v); + *on = v; + } else if (chip == NCP_CHIP_THEME) { + int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */ + *on = (v == 1); + if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色"); + } else if (chip == NCP_CHIP_BRIGHT) { + int v = 0; meow_settings_get_int("display.brightness", &v); + int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin); + int vc = mnp_brightness_clamp(v, vmin); + if (val && vlen > 0) snprintf(val, vlen, "%d%%", vc); + *on = (vc >= 50); /* 过半视为"亮"态 */ + } else if (chip == NCP_CHIP_POWER) { + /* 电源菜单 chip:非开关(无 on/off 态),恒示中性;点击由 ncp_chip_apply 唤起电源菜单。 */ + *on = 0; + } +} + + +/* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。 + * 返回 1=主题变化(需重刷状态栏);2=电源菜单已弹出(调用方置 after_modal=1 整帧重绘还原); + * 0=其它。电源菜单 chip 复用既有系统级 show_power_menu(含 dim/圆钮/音量导航/电源确认/8s强关/ + * 空闲自动息屏),真弹对话框,动作在函数内 system();取消则返回续 panel —— 与电源键长按同款。 */ +static int ncp_chip_apply(int chip) +{ + if (chip == NCP_CHIP_WIFI) { + int v = 0; meow_settings_get_switch("network.wifi_enabled", &v); + meow_settings_set_switch("network.wifi_enabled", !v); + s_net_toggle_wifi(!v); + s_net_refresh(); + } else if (chip == NCP_CHIP_BT) { + int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v); + meow_settings_set_switch("network.bluetooth_enabled", !v); + s_net_toggle_bt(!v); + s_net_refresh(); + } else if (chip == NCP_CHIP_THEME) { + /* 快捷主题:切到 manual(确保生效),再翻转 dark/light(display.theme)。 */ + int mode = THEME_MODE_MANUAL; + meow_settings_get_enum("display.theme_mode", &mode); + if (mode != THEME_MODE_MANUAL) meow_settings_set_enum("display.theme_mode", THEME_MODE_MANUAL); + int v = 0; meow_settings_get_enum("display.theme", &v); + meow_settings_set_enum("display.theme", v ? 0 : 1); + paint_sb_right(); /* 状态栏文字/底色随主题刷新 */ + meow_settings_save(); + return 1; /* 主题变化 → 重刷状态栏 */ + } else if (chip == NCP_CHIP_BRIGHT) { + /* 快捷亮度:在「极暗(下限)」与「100」之间切换。 */ + int cur = 0; meow_settings_get_int("display.brightness", &cur); + int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin); + if (mnp_brightness_clamp(cur, vmin) < 100) { + meow_settings_set_int("display.brightness", 100); + write_brightness(100); + g_sys.brightness = 100; + } else { + meow_settings_set_int("display.brightness", vmin); + write_brightness(vmin); + g_sys.brightness = vmin; + } + paint_sb_right(); + } else if (chip == NCP_CHIP_POWER) { + /* 电源菜单 chip:唤起既有系统级电源菜单(真弹对话框,动作在函数内 system();取消则返回)。 + * 不涉及任何设置项(电源菜单不走 meow_settings,零旁路)。返回 2 → 调用方整帧重绘还原面板/状态栏。 */ + logmsg("notif-center: power chip -> show power menu"); + show_power_menu(); + return 2; + } + meow_settings_save(); + return 0; +} + + +/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。 + * off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */ +static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th) +{ + if (off >= 0) return; + int bw = -off; + if (bw <= 0) return; + int bx = NCP_ROW_X + NCP_ROW_W + off; /* 被空出的右条左缘 */ + fill_rect(bx, ry, bw, NCP_ROW_H - 6, th->error); + if (bw >= 44) { + int cx = bx + bw / 2, cy = ry + (NCP_ROW_H - 6) / 2; + ui_draw(cx - ui_width("删除", 16) / 2, cy - meow_font_line_height(16) / 2, + "删除", 0xFFFFFF, 16); + } +} + + +/* 画一行内容(行卡 + 图标/APP名/时间/标题/正文/进度),整体随 off 左移(⑦ 行左滑跟手)。 + * i=g_rows 下标;ry=行顶(已含 scroll);off=行左移量(px,<=0)。 */ +static void ncp_draw_row(int i, int ry, int off, const meow_ui_theme_t *th) +{ + const ncp_row_t *r = &g_rows[i]; + meow_gfx_fill_round(NCP_ROW_X + off, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container); + /* 图标 */ + int icx = NCP_ICON_X + off, icy = ry + (NCP_ROW_H - 32) / 2; + if (r->img) meow_ui_img_cover(icx, icy, 32, r->img); + else { + /* 回退:主题色圆点 + 首字符 */ + const char *nm = ncp_app_name(r->app_id); + ncp_dot(icx + 16, icy + 16, 16, th->primary); + char ch = nm[0] ? nm[0] : '?'; + char cbuf[4] = { ch, 0, 0, 0 }; + /* 首字符若是多字节(中文),取整码 */ + if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0; + if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } } + int cw = ui_width(cbuf, 20); + ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20); + } + /* app 名 + 时间 */ + ui_draw(NCP_TEXT_X + off, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16); + char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf); + ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16) + off, ry + 12, tbuf, th->on_surface_variant, 16); + /* 标题 + 内容 */ + ncp_text(NCP_TEXT_X + off, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W); + ncp_text(NCP_TEXT_X + off, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W); + /* 进度(progress>=0):细条 */ + if (r->progress >= 0) { + int px = NCP_TEXT_X + off, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14; + fill_rect(px, py, pw, 4, th->surface_variant); + int fw = r->progress * pw / 100; + if (fw > 0) fill_rect(px, py, fw, 4, th->primary); + } +} + + +/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。 + * ⑦:左滑中的行(g_ncp_swipe_row)按 g_ncp_swipe_off 左移,其下先画删除底(ncp_draw_delete_bg)。 */ +static void ncp_draw_list(int scroll) +{ + const meow_ui_theme_t *th = s_ui_theme(); + if (g_nrows == 0) { + ui_draw(NCP_ROW_X + 8, NCP_LIST_TOP + 40, "暂无通知", th->on_surface_variant, 22); + return; + } + int total = g_nrows * NCP_ROW_H; + scroll = mnp_scroll_clamp(scroll, total, NCP_LIST_BOT - NCP_LIST_TOP); + for (int i = 0; i < g_nrows; i++) { + int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll; + if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */ + int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */ + if (off < 0) ncp_draw_delete_bg(ry, off, th); + ncp_draw_row(i, ry, off, th); + } + /* 滚动条 */ + if (total > NCP_LIST_BOT - NCP_LIST_TOP) { + int view = NCP_LIST_BOT - NCP_LIST_TOP; + int thumb_h = view * view / total; if (thumb_h < 24) thumb_h = 24; + int ty = NCP_LIST_TOP + (view - thumb_h) * scroll / (total - view); + meow_gfx_fill_round(fb_xres - 8, NCP_LIST_TOP, 5, view, 3, th->surface_container); + meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant); + } +} + + +/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底(面板 surface_variant), + * 再画可见行 + 滚动条;只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT)),面板体其它控件 / 状态栏带 + * [0,NCP_TOP) 均不触碰 —— 满足「滚动只重绘列表可视区,绝不整屏」。 */ +static void ncp_repaint_list(int scroll) +{ + const meow_ui_theme_t *th = s_ui_theme(); + fill_rect(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP, th->surface_variant); + ncp_draw_list(scroll); + meow_dirty_add(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP); + meow_ui_flush(); +} + + +/* ⑦ 仅重绘一行(行左滑跟手/回弹/滑出专用;只刷该行矩形,不整面板、不刷状态栏)。 + * 清该行带(含被滑出的右条)→ 画删除底(若 off<0)+ 该行内容位移 → 只标该行带脏。 */ +static void ncp_repaint_row(int scroll, int row, int off) +{ + const meow_ui_theme_t *th = s_ui_theme(); + if (row < 0 || row >= g_nrows) return; + int ry = NCP_LIST_TOP + row * NCP_ROW_H - scroll; + if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */ + /* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */ + fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant); + if (off < 0) ncp_draw_delete_bg(ry, off, th); + ncp_draw_row(row, ry, off, th); + meow_dirty_add(0, ry, fb_xres, NCP_ROW_H); + meow_ui_flush(); +} + + +/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段): + * 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [0, NCP_DND_SPLIT_X) → 勿扰命中。 + * 右半段归「一键清除」(ncp_clear_hit,互斥);不与 5 chip(y≤130)、滑条带(y≤238)、 + * 通知列表(y≥NCP_LIST_TOP=308)、状态栏(y<28)重叠。 */ +static int ncp_dnd_hit(int tx, int ty) +{ + return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H && + tx >= 0 && tx < NCP_DND_SPLIT_X); +} + + +/* ⑧ 一键清除带命中(勿扰对称处的右半段): + * 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [NCP_DND_SPLIT_X, fb_xres) → 清除命中。 */ +static int ncp_clear_hit(int tx, int ty) +{ + return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H && + tx >= NCP_DND_SPLIT_X && tx < fb_xres); +} + + +/* ⑥ 绘勿扰带(⑧ 收窄为左半段):label「勿扰」+ 铃铛图标 + 状态「开/关」 + 左半段右侧 s_draw_switch(48×28)。 + * 右半段由 ncp_draw_clear 画「一键清除」按钮(对称)。 */ +static void ncp_draw_dnd(const meow_ui_theme_t *th) +{ + int y = NCP_DND_Y; + int dnd_on = g_ncp_dnd; + int cy = y + NCP_DND_H / 2; + /* 铃铛(简化:圆顶 + 顶蒂 + 底部小圆点)。 */ + int bx = NCP_ROW_X + 4, bcy = cy - 2; + meow_gfx_fill_round(bx, bcy - 7, 16, 13, 7, th->on_surface_variant); /* 铃身圆顶 */ + meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */ + ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */ + /* label + 状态 */ + ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18); + char st[16]; + snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关"); + int lw = ui_width("勿扰", 18); + ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st, + dnd_on ? th->primary : th->on_surface_variant, 18); + /* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */ + s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th); +} + + +/* ⑧ 绘一键清除按钮(勿扰对称处的右半段):圆角按钮底 + 垃圾桶字形 + 「清除」文字。 */ +static void ncp_draw_clear(const meow_ui_theme_t *th) +{ + int y = NCP_DND_Y; + int cy = y + NCP_DND_H / 2; + int cx0 = NCP_DND_SPLIT_X + 8; /* 右半段按钮左缘 */ + int cw = (fb_xres - NCP_ROW_X) - cx0; /* 右半段按钮宽 */ + if (cw <= 0) return; + meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container); + /* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */ + const char *lbl = "清除"; + int lw = ui_width(lbl, 18); + int unit = 22; /* 图标占位宽 + 与文字的间距 */ + int total = unit + lw; + int gx = cx0 + (cw - total) / 2; /* 图标左缘(居中) */ + int icy = cy; + fill_rect(gx, icy - 7, 14, 2, th->on_surface_variant); /* 顶盖 */ + meow_gfx_fill_round(gx + 1, icy - 3, 12, 12, 2, th->on_surface_variant);/* 桶身 */ + meow_gfx_fill_round(gx - 2, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手左 */ + meow_gfx_fill_round(gx + 14, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手右 */ + ui_draw(gx + unit, cy - meow_font_line_height(18) / 2 + 2, lbl, th->on_surface, 18); +} + +static int ncp_clear_collect_cb(const meow_notif_t *n, void *ctx) +{ + ncp_clear_ctx_t *c = ctx; + if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0; /* 保留红线项 */ + if (c->n >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */ + c->ids[c->n++] = n->id; + return 0; +} + +/* 收集可清除通知 id 后逐条 dismiss(dismiss = archive_one(dump→TF) + 释放槽 = 转储留档 + 清出显示)。 + * 返回清除条数。 */ +static int ncp_clear_all(void) +{ + ncp_clear_ctx_t c; + memset(&c, 0, sizeof c); + meow_notif_list(NULL, ncp_clear_collect_cb, &c); + for (int i = 0; i < c.n; i++) meow_notif_dismiss(c.ids[i]); + return c.n; +} + + +/* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。 + * 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过, + * 使脏区仅限面板体,绝不整屏重绘。 */ +static void draw_notif_center(int scroll, int redraw_sb) +{ + const meow_ui_theme_t *th = s_ui_theme(); + int W = fb_xres, H = fb_yres; + /* 状态栏带 [0,NCP_TOP) 仅当 redraw_sb=1(打开/主题变化)时清底,随后由下方 draw_statusbar() + * 覆盖重绘(Stage-One:draw_statusbar 只喂 SDK + 标脏顶条,实际由 SDK present() 重绘); + * redraw_sb=0(交互重绘,如拖滑条)时**绝不触碰**——保留 back_buf 中 SDK 状态栏/上次 top 脏, + * 避免被清成 surface 底仍被 blit → 状态栏数值闪烁/消失(真机①修复)。 + * 面板体(状态栏之下 surface_variant)态恒整片铺。 */ + if (redraw_sb) fill_rect(0, 0, W, NCP_TOP, th->surface); + fill_rect(0, NCP_TOP, W, H - NCP_TOP, th->surface_variant); + + /* 把手 */ + int grab_w = 44, grab_x = (W - grab_w) / 2; + meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline); + /* 标题 + 「上滑/点空白关闭」 提示 */ + ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22); + ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16); + + /* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */ + static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" }; + int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP; + int chip_x0 = (W - total_chip) / 2; + for (int c = 0; c < NCP_CHIP_N; c++) { + int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP); + int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val); + uint32_t bg = on ? th->primary : th->surface_container; + uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant; + meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg); + int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2; + int icon_cy = NCP_CHIP_Y + 14; + if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22); + else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22); + else if (c == NCP_CHIP_THEME) { + if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg); + else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); } + } + else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22); + else if (c == NCP_CHIP_POWER) { + /* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */ + int pc = icon_cx + SB_ICON_SIZE / 2; + meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */ + meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */ + meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */ + } + char label[24]; + snprintf(label, sizeof label, "%s", chip_lbl[c]); + if (val[0] && c == NCP_CHIP_BRIGHT) + snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */ + ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18, + label, fg, 14); + } + + /* 亮度滑条 */ + int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin); + int bval = 0; meow_settings_get_int("display.brightness", &bval); + bval = mnp_brightness_clamp(bval, bmin); + ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18); + char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval); + ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb, + th->on_surface_variant, 18); + s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th); + + /* 音量滑条 */ + int vval = 0; meow_settings_get_int("audio.volume", &vval); + vval = vol_clamp(vval); + ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18); + char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval); + ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl, + th->on_surface_variant, 18); + s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th); + + /* 分隔线 */ + fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline); + + /* ⑥ 勿扰面板内开关带(list 上方,左半段) */ + ncp_draw_dnd(th); + /* ⑧ 一键清除按钮(勿扰对称处的右半段) */ + ncp_draw_clear(th); + + /* 通知列表 */ + ncp_draw_list(scroll); + + /* 底部提示 */ + ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭", + th->on_surface_variant, 16); + + /* 状态栏画在面板体之上(0-27 由 SDK 全局状态栏维护,经 draw_statusbar→sb_sdk_refresh + * 喂数据+标脏顶条;不落面板体脏区)。仅 redraw_sb=1(打开/主题变化)才喂/标脏,交互时跳过 + * 以保持脏区只限面板体(顶条像素留在 back_buf,由 SDK 已合成的状态栏保持)。 */ + if (redraw_sb) draw_statusbar(); +} + + +/* 触摸滑条带命中:origin 落在 (slugX, slugY) 的滑条行带内。返回 1=命中。 */ +static int ncp_slider_band(int tx, int ty, int sy) +{ + return (tx >= NCP_SLIDER_X - 14 && tx <= NCP_SLIDER_X + NCP_SLIDER_W + 14 && + ty >= sy - 30 && ty <= sy + 24); +} + + +/* 滑条值应用(拖动实时 set_*,save_now=抬手落盘)。 + * 复用设置页方案:亮度 clamp [bmin,100];音量 vol_clamp 上限 + 跳步吸附 + DAC 节流去抖。 + * 返回 1=值实际变更(需重绘面板/刷状态栏);0=吸附+钳制后与当前已提交值相同(懒跳过: + * 不写 DAC/背光、不刷状态栏、不标脏)→ 拖动去抖,消除逐事件刷状态栏/DAC 的卡顿(真机②修复)。 + * 亮度写背光仅 sysfs 不引额外节流;音量写 DAC 节流(meow_dac_vol_write_throttled/flush 同 s_commit)。 */ +static int ncp_slider_apply(int which, int tx, int save_now) +{ + if (which == 1) { + int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin); + int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, bmin, 100); + v = mnp_brightness_clamp(v, bmin); /* 下限 brightness_min(≥1)、上限 100 */ + int cur = 0; meow_settings_get_int("display.brightness", &cur); + if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */ + meow_settings_set_int("display.brightness", v); + write_brightness(v); + g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */ + paint_sb_right(); + if (save_now) meow_settings_save(); + return 1; + } else { + int vmax = vol_clamp(settings_volume_max); /* 音量上限(尊重 audio.volume_max) */ + int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0, vmax); + v = mnp_step_round(v, settings_volume_step); /* 跳步吸附(默认 step=1 → 连续同设置页) */ + v = vol_clamp(v); /* 0..MEOW_VOL_MAX + 上限 clamp */ + if (!save_now && v == settings_volume_get()) return 0; /* 去抖:值未变跳过写 DAC/刷状态栏 */ + meow_settings_set_int("audio.volume", v); + g_sys.vol = settings_volume_get(); /* 状态栏数值源=最近写入值(含 clamp 后) */ + paint_sb_right(); + if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } /* 抬手=无条件直写 DAC */ + else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */ + return 1; + } +} + + +/* 通知点击:dismiss(标记阅读)+ 若 app 有 entry 记录则请求启动。返回 1=应关闭面板。 */ +static int ncp_row_tap(int tx, int ty, int scroll, int *want_launch) +{ + int eff = ty - NCP_LIST_TOP + scroll; + int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H); + if (row < 0 || row >= g_nrows) return 0; + const ncp_row_t *r = &g_rows[row]; + meow_notif_dismiss(r->id); /* 点击 = 阅读,撤下 */ + /* 跳转:app entry 记录(apps[] 内且 ok)→ 记 want_launch(run_menu 走标准启动)。 + * target_activity 为后续 M3 导航预留下的跳转目标(activity 级路由后置)。 */ + if (want_launch) *want_launch = -1; + if (g_ncp_apps) + for (int i = 0; i < g_ncp_napps; i++) + if (!strcmp(g_ncp_apps[i].id, r->app_id) && g_ncp_apps[i].ok) { + if (want_launch) *want_launch = i; + break; + } + return 1; /* 点击即关(启动由 run_menu 执行;无记录则仅关闭) */ +} + + +/* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */ +void run_notif_center(struct app *apps, int napps, int *want_launch, + int live_open, int lox, int loy) +{ + if (want_launch) *want_launch = -1; + int list_scroll = 0; + unsigned char evbuf16[16]; + g_ncp_apps = apps; g_ncp_napps = napps; + + struct keypoll kp_power, kp_keys; + kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); + struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; + memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); + int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; + } + int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } + unsigned long long touch_cooldown_until = now_ms() + 300; + unsigned long long last_tick = now_ms(); + unsigned long long last_act = now_ms(); + unsigned long long last_listchk = now_ms(); + + int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; + int drag_slider = 0; /* 0=无, 1=亮度, 2=音量 */ + /* 通知列表拖动滚动(方向遵循 input.swipe_invert,与设置页列表同式;关闭手势区分见 §11.3) */ + int scroll_drag = 0; /* 是否处于列表区拖动滚动手势 */ + int scroll_base = 0; /* 进入滚动时锁定的 list_scroll 基准(连续跟手,防触发瞬间跳变) */ + int scroll_y0 = 0; /* 按下原点纵向坐标 */ + int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */ + int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */ + int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */ + /* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off): + * row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手); + * row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。 + * row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */ + int row_swipe_row = -1; + int row_off = 0; + int row_settle = 0; + int row_off_target = 0; + unsigned long long row_anim_at = 0; + int row_paint = 0; /* 仅重绘该行带(行左滑专用) */ + /* ⑨ 面板跟手下拉、半程可回: + * panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停); + * drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变); + * drag_y0 = 进入拖动时锁定手指 y(此后 reveal = seed + (tly - drag_y0),连续跟手/半程可回); + * drag_paint = 仅用动画重建路径重绘一帧(复用 §9 快照 blit,不整屏/不重排控件)。 */ + int panel_drag = 0; + int drag_seed_reveal = 0; + int drag_y0 = 0; + int drag_paint = 0; + int closed = 0; /* 非动画路径的兜底关面板(快照不可用时瞬时退出) */ + int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */ + int wake_suppress = 0; + unsigned long long pwr_hold_at = 0; + int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0; + + int need_sb = 1; /* 打开/主题变化时重绘状态栏;其余交互跳过(脏区只限面板体) */ + + /* 开关平移动画状态(§9):anim_p∈[0,NCP_ANIM_FIX],anim_target 为意图(FIX=开、0=关)。 + * 仅当快照可用(anim_ok)才做平滑动画;否则降级为瞬时显示/消失(closed 兜底)。 */ + int anim_p = 0, anim_target = NCP_ANIM_FIX; + int anim_last_target = NCP_ANIM_FIX; /* 记录上一步 anim_target(用于检测「意图切换」重置计时) */ + unsigned long long anim_last = 0; + int anim_prev_bot = NCP_TOP; /* 上一帧面板覆盖底缘(exclusive;初始=NCP_TOP=未覆盖) */ + int anim_ok = 0; + + /* ⑨ live_open 入口(run_menu 热区拖动中切入):手指此刻已按下且正拖动。 + * - 触摸:直接以传入原点作「已按下已锁原点」(evdev 广播,本 fd 只接后续 move/release)。 + * - 进度:面板从隐藏(anim_p=0)起步,target 未定(=0),由拖动路径实时映射 anim_p; + * 松手按阈值锚定 target。默认(非 live_open)仍为 trigger 式自动展开(防御路径)。 */ + if (live_open) { + td = 1; td_ok = 1; tdx = lox; tdy = loy; scroll_y0 = tdy; + anim_target = 0; anim_last_target = 0; /* 待拖动/松手锚定 */ + } + + /* ⑥ 读勿扰面板内开关态(开面板一次;切换在下面 tap 分支写回)。 */ + { + int d = 0; meow_settings_get_bool("notify.dnd", &d); + g_ncp_dnd = d; + } + + /* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0 + * 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */ + ncp_refresh_rows(); + list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, NCP_LIST_BOT - NCP_LIST_TOP); + ncp_snap_init(); + if (g_ncp_ph > 0) { + anim_ok = 1; + ncp_snap_bg(); /* 背景(应用列表)在面板绘制前拍 */ + draw_notif_center(list_scroll, 1); /* 渲染最终位置整面板 → back_buf */ + ncp_snap_panel(); /* 拍面板快照 */ + /* 起点 p=0:按「上一帧为全覆盖(NCP_TOP+PH)」重建 → 整段还原为背景(面板隐藏) */ + anim_prev_bot = ncp_anim_rebuild(mnp_anim_off(0, NCP_ANIM_FIX, g_ncp_ph), + NCP_TOP + g_ncp_ph); + meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP); + meow_ui_flush(); + } else { + draw_notif_center(list_scroll, 1); + meow_dirty_full(); meow_ui_flush(); + } + need_sb = 0; + anim_last = now_ms(); + + int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */ + + for (;;) { + unsigned long long now = now_ms(); + if (sigterm_flag) break; + int animating = anim_ok && (anim_p != anim_target); + + /* 自动息屏(同设置页语义) */ + if (meow_auto_slept && !meow_screen_on) { + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + int wak = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wak = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wak = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wak = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wak = 1; + } + if (!wak && screen_off_mode == 1 && tfd >= 0) { + struct pollfd spfd = { tfd, POLLIN, 0 }; + if (poll(&spfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == 16) { + uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_KEY && code == BTN_TOUCH && val) wak = 1; + } + } + } + if (wak) { + meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms(); + memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + touch_cooldown_until = now_ms() + 300; + td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1; + scroll_drag = 0; scroll_base = 0; scroll_y0 = 0; + scroll_invert = 0; scroll_rebound = 0; list_paint = 0; + row_swipe_row = -1; row_off = 0; row_settle = 0; + row_off_target = 0; row_anim_at = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0; + /* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */ + anim_p = anim_target = anim_last_target = NCP_ANIM_FIX; + if (anim_ok) anim_prev_bot = NCP_TOP + g_ncp_ph; + draw_notif_center(list_scroll, 1); + if (anim_ok) ncp_snap_panel(); + meow_dirty_full(); meow_ui_flush(); + anim_last = now_ms(); + need_sb = 0; + } + usleep(POLL_MS * 1000); + continue; + } + + /* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */ + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + int vold = 0, volu = 0, play = 0, pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; + } + (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); + (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); + vold = kd_vold.stable; volu = kd_volu.stable; + play = kd_play.stable; pwr = kd_pwr.stable; + if (wake_suppress) { + if (!pwr && !vold && !volu && !play) wake_suppress = 0; + } else { + if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } + if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } + if (play && !m_prev_play) { + logmsg("notif-center: play key caught (media-reserved, no exit)"); + last_act = now; + } + if (pwr && !m_prev_pwr) { + pwr_hold_at = now; pwr_menu_state = 0; pwr_suppress_click = 0; last_act = now; + if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; + } + if (pwr && pwr_hold_at) { + if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { + if (sim_no_exec) logmsg("notif-center pm: [sim-no-exec] hold %dms -> would forceoff", power_forceoff_ms); + else { logmsg("notif-center pm: hold %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } + } else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { + pwr_menu_state = 1; + logmsg("notif-center pm: hold %dms -> power menu", power_longpress_menu_ms); + show_power_menu(); + last_act = now_ms(); + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1; + memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + after_modal = 1; need_paint = 1; + goto ncp_keys_seeded; + } + } + if (!pwr && m_prev_pwr) { + if (pwr_suppress_click) + logmsg("notif-center pm: release after menu -> no toggle"); + else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { + logmsg("notif-center pm: single-click -> toggle panel"); + meow_toggle_panel(); meow_auto_slept = 0; last_act = now_ms(); need_paint = 1; + } else if (pwr_woke) { + logmsg("notif-center pm: release after wake -> no toggle"); + } + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0; + } + } + if (vol_ramp_dir != 0) { + int held = (vol_ramp_dir > 0) ? volu : vold; + int going = vol_ramp_tick(held); + g_sys.vol = settings_volume_get(); + paint_sb_right(); + need_paint = 1; + if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol); + } + m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; + ncp_keys_seeded: + ; + + /* 触摸 */ + if (tfd >= 0 && meow_screen_on) { + /* 面板是否就位(稳态开):就位才允许控件交互;动画中抑制 chip/滑条/通知行命中 + * (面板在滑,控件坐标已定但只部分可见,命中会误触——见 §9.5)。 */ + int open_settled = anim_ok ? (anim_target >= NCP_ANIM_FIX && anim_p >= NCP_ANIM_FIX) : 1; + struct pollfd pfd = { tfd, POLLIN, 0 }; + if (poll(&pfd, 1, 0) > 0) { + while (read(tfd, evbuf16, sizeof evbuf16) == 16) { + uint16_t type = *(uint16_t *)(evbuf16 + 8), code = *(uint16_t *)(evbuf16 + 10); + int32_t val = *(int32_t *)(evbuf16 + 12); + if (type == EV_ABS) { + if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val; + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val; + if (td && !td_ok && tlx >= 0 && tly >= 0) { + tdx = tlx; tdy = tly; td_ok = 1; + scroll_y0 = tdy; /* 列表拖动基准:按下原点(进滚动时才锁 scroll_base) */ + /* 按下原点落在滑条带 → 开始拖动(仅面板就位) */ + if (open_settled && drag_slider == 0) { + if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; } + else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; } + } + } + if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; } + else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; } + /* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop, + * 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。 + * 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含 + * row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大: + * 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */ + if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) { + int ddx = tlx - tdx, ddy = tly - tdy; + int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy; + if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) { + int eff = tdy - NCP_LIST_TOP + list_scroll; + int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H); + /* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */ + if (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && + row >= 0 && row < g_nrows) { + row_swipe_row = row; + row_off = mnp_swipe_clamp(ddx, NCP_ROW_W); + row_settle = 0; row_off_target = 0; + row_anim_at = now_ms(); + g_ncp_swipe_row = row; g_ncp_swipe_off = row_off; + row_paint = 1; + } + } + } + /* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */ + if (row_swipe_row >= 0 && td_ok && !row_settle) { + row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W); + g_ncp_swipe_off = row_off; + row_paint = 1; + } + /* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、 + * 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y, + * 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */ + if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) { + int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy); + if (ady2 >= S_SCROLL_SLOP) { + int av = g_nrows * NCP_ROW_H; + int vw = NCP_LIST_BOT - NCP_LIST_TOP; + int in_over = (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && + mnp_scroll_max(av, vw) > 0); + if (!in_over) { + int cur_off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph); + drag_seed_reveal = (g_ncp_ph > 0) ? (g_ncp_ph + cur_off) : 0; + drag_y0 = tly; + panel_drag = 1; + } + } + } + /* ⑨ 跟手:panel_drag 时 reveal=seed+位移 → progress 更新 anim_p;拖动期时间轴暂停。 */ + if (panel_drag && td_ok) { + int reveal = mnp_drag_reveal(drag_seed_reveal, tly - drag_y0, g_ncp_ph); + int np = mnp_drag_progress(reveal, NCP_ANIM_FIX, g_ncp_ph); + if (np != anim_p) { anim_p = np; drag_paint = 1; } + } + /* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 + + * 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式); + * 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */ + if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) { + int ddy = tly - scroll_y0; + if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { + int total = g_nrows * NCP_ROW_H; + int view = NCP_LIST_BOT - NCP_LIST_TOP; + if (mnp_scroll_max(total, view) > 0 && + tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT) { + scroll_drag = 1; + meow_settings_get_bool("input.swipe_invert", &scroll_invert); + scroll_base = mnp_scroll_base(list_scroll, ddy, scroll_invert); + scroll_rebound = 0; + } + } + } + if (scroll_drag && td_ok) { + int total = g_nrows * NCP_ROW_H; + int view = NCP_LIST_BOT - NCP_LIST_TOP; + int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert); + list_scroll = mnp_scroll_clamp(target, total, view); + scroll_rebound = mnp_scroll_rebound(target, mnp_scroll_max(total, view), + NCP_SCROLL_REBOUND_TOL); + list_paint = 1; + } + } else if (type == EV_KEY && code == BTN_TOUCH) { + if (val && !td) { + if (now_ms() < touch_cooldown_until) { tlx = tly = -1; } + else { + td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0; + scroll_drag = 0; scroll_base = 0; scroll_y0 = 0; + scroll_invert = 0; scroll_rebound = 0; + /* ⑦ 新触摸:清行左滑(防上一手势残留) */ + row_swipe_row = -1; row_off = 0; row_settle = 0; + row_off_target = 0; row_anim_at = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + /* ⑨ 新触摸:清面板跟手(防上一拖动残留) */ + panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0; + } + } else if (!val && td) { + td = 0; + if (drag_slider) { /* 滑条拖动结束 → 落盘 */ + ncp_slider_apply(drag_slider, tlx >= 0 ? tlx : tdx, 1); + drag_slider = 0; need_paint = 1; + } else if (td_ok) { + int dx = tlx - tdx, dy = tly - tdy; + int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; + /* 左缘左滑 = 关闭(复用全局左缘返回语义;动画中同样可打断)—— + * 优先于列表滚动落定,保「左缘返回关」语义不受滚动影响 */ + if (meow_edge_back(tdx, dx, dy)) { + if (anim_ok) anim_target = 0; else closed = 1; + break; + } + if (row_swipe_row >= 0) { + /* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。 + * 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */ + /* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */ + row_off = mnp_swipe_clamp(dx, NCP_ROW_W); + g_ncp_swipe_off = row_off; + if (mnp_swipe_delete(row_off, NCP_ROW_W, + NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) { + row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */ + } else { + row_off_target = 0; /* 回弹复原 */ + } + row_settle = 1; row_anim_at = now_ms(); + } else if (scroll_drag) { + /* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值); + * 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。 + * 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过 + * 下方 mnp_vertical_up 关闭判定。 */ + int total = g_nrows * NCP_ROW_H; + int view = NCP_LIST_BOT - NCP_LIST_TOP; + int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert); + list_scroll = mnp_scroll_clamp(target, total, view); + if (scroll_rebound) { + if (anim_ok) anim_target = 0; else closed = 1; + break; + } + list_paint = 1; + scroll_base = 0; scroll_y0 = 0; scroll_rebound = 0; scroll_invert = 0; + } else if (panel_drag) { + /* ⑨ 面板跟手拖动松手:按阈值锚定 target(≥40% → 补到展开; + * <40% 或反向拖回 → 回 0 收回 = 半程取消/关闭过阈值)。时间轴接管 + * 把 anim_p 补到位;不误关面板、不走 tap/上滑/点空白(本手势锁定)。 */ + int go_open = mnp_drag_anchor(anim_p, NCP_ANIM_FIX, NCP_DRAG_THR_PCT); + if (anim_ok) anim_target = go_open ? NCP_ANIM_FIX : 0; + else if (!go_open) closed = 1; /* 无快照降级:收回 → 直接退出 */ + panel_drag = 0; drag_paint = 0; drag_seed_reveal = 0; drag_y0 = 0; + } else if (adx < 60 && ady < 60) { /* tap */ + if (animating) break; /* 动画进行中:tap 忽略(控件命中会误触,见 §9.5) */ + int tx = tdx, ty = tdy; + int hit = 0; + for (int c = 0; c < NCP_CHIP_COUNT; c++) + if (ncp_chip_hit(c, tx, ty)) { + int ar = ncp_chip_apply(c); + if (ar == 1) need_sb = 1; /* 主题变化 → 重刷状态栏 */ + else if (ar == 2) { + after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */ + /* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配, + * 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */ + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + /* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空, + * 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */ + if (tfd >= 0) { + unsigned char ev2[16]; + while (read(tfd, ev2, sizeof ev2) == 16) {} + } + } + hit = 1; need_paint = 1; break; + } + if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { hit = 1; if (ncp_slider_apply(1, tx, 1)) need_paint = 1; } + else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { hit = 1; if (ncp_slider_apply(2, tx, 1)) need_paint = 1; } + /* ⑥ 勿扰面板内开关(列表上方小 toggle):tap 即切换(整带命中) */ + if (!hit && ncp_dnd_hit(tx, ty)) { + int dv = 0; meow_settings_get_bool("notify.dnd", &dv); + meow_settings_set_bool("notify.dnd", dv ? 0 : 1); + meow_settings_save(); + g_ncp_dnd = dv ? 0 : 1; + ncp_refresh_rows(); /* 按 dnd 过滤重采集 */ + list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, + NCP_LIST_BOT - NCP_LIST_TOP); + hit = 1; need_paint = 1; + } + /* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 ongoing/media/progress)。 */ + if (!hit && ncp_clear_hit(tx, ty)) { + int n = ncp_clear_all(); + logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n); + /* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */ + row_swipe_row = -1; row_off = 0; row_settle = 0; + row_off_target = 0; row_anim_at = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + ncp_refresh_rows(); + list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, + NCP_LIST_BOT - NCP_LIST_TOP); + hit = 1; need_paint = 1; + } + if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) { + if (ncp_row_tap(tx, ty, list_scroll, want_launch)) { + hit = 1; + if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */ + else closed = 1; + break; + } + } + if (!hit) { /* 点空白(标题/把手/提示/分隔线/列表外)→ 关闭 */ + if (anim_ok) anim_target = 0; else closed = 1; + break; + } + } else if (mnp_vertical_up(dx, dy, MENU_SWIPE_THRESH)) { + if (anim_ok) anim_target = 0; else closed = 1; /* 面板内上滑 → 关闭 */ + break; + } else if (anim_ok && anim_target == 0 && + mnp_top_swipe_down(tdx, tdy, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) { + anim_target = NCP_ANIM_FIX; /* 收回途中顶部热区下滑 → 重新打开(平滑转向) */ + } + /* 其余横滑/下滑忽略(不关闭不动作) */ + } + td_ok = 0; + } + } + } + } + } + if (closed) break; /* 非动画兜底:上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */ + + /* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */ + if (now - last_listchk >= 500) { + last_listchk = now; + meow_notif_tick(); + if (ncp_rows_changed()) { + /* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */ + row_swipe_row = -1; row_off = 0; row_settle = 0; + row_off_target = 0; row_anim_at = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0; + ncp_refresh_rows(); + list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, + NCP_LIST_BOT - NCP_LIST_TOP); + need_paint = 1; + } + } + + /* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。 + * 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */ + if (anim_ok && anim_p != anim_target && !panel_drag) { + unsigned long long anow = now_ms(); + int dt; + if (anim_target != anim_last_target) { + /* 意图刚切换(打开↔关闭):重置计时起点,本 tick 不计时, + * 避免稳态闲置后 anim_last 陈旧导致首帧就跳到目标(不产生动画)。 */ + anim_last_target = anim_target; + anim_last = anow; + dt = 0; + } else { + dt = (int)((anow > anim_last) ? (anow - anim_last) : 0); + anim_last = anow; + } + int np = mnp_anim_step(anim_p, anim_target, NCP_ANIM_FIX, + (unsigned long long)dt, NCP_ANIM_MS); + if (np != anim_p) { + anim_p = np; + ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); /* §9 快照 blit 平移(只刷面板矩形) */ + } + } + if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */ + + /* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。 + * 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。 + * 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */ + if (row_swipe_row >= 0 && row_settle) { + unsigned long long rnow = now_ms(); + int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0); + row_anim_at = rnow; + int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE); + if (noff != row_off) { + row_off = noff; + g_ncp_swipe_off = noff; + row_paint = 1; + } + if (noff == row_off_target) { + if (row_off_target == 0) { + /* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */ + ncp_repaint_row(list_scroll, row_swipe_row, 0); + row_swipe_row = -1; row_settle = 0; row_off = 0; + row_off_target = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */ + } else { + /* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */ + if (row_swipe_row >= 0 && row_swipe_row < g_nrows) + meow_notif_dismiss(g_rows[row_swipe_row].id); + row_swipe_row = -1; row_settle = 0; row_off = 0; + row_off_target = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + ncp_refresh_rows(); + list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, + NCP_LIST_BOT - NCP_LIST_TOP); + need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */ + } + } + } + + /* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。 + * need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍 + * 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。 + * 动画进行中:不画稳态面板(会覆盖正在滑动的帧),need_paint 保持到到位后再补画。 */ + if (need_paint) { + last_act = now; + if (!(anim_ok && anim_p != anim_target)) { + draw_notif_center(list_scroll, need_sb); + need_sb = 0; + if (anim_ok) ncp_snap_panel(); /* 面板内容已变:刷新快照(供滑出动画用) */ + if (after_modal) { meow_dirty_full(); after_modal = 0; } + else meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP); + meow_ui_flush(); + need_paint = 0; + list_paint = 0; /* 整面板已含列表:滚动重绘标记随之清除 */ + } + /* 否则动画进行中:保留 need_paint,到位后(下一稳态 tick)补画最终面板 */ + } + /* 仅列表区重绘(滚动专用:只刷列表带,不整面板/不刷状态栏;同受动画门控, + * 动画进行中不画稳态,list_paint 保持到到位后再补画一次列表终值)。 */ + if (list_paint) { + last_act = now; + if (!(anim_ok && anim_p != anim_target)) { + ncp_repaint_list(list_scroll); + if (anim_ok) ncp_snap_panel(); /* 列表已变:刷新面板快照(供滑出动画用) */ + list_paint = 0; + } + } + /* ⑨ 面板跟手拖动重绘:只刷面板矩形 + 新露出条(§9 快照 blit 平移;不整屏/不重排控件)。 + * 拖动期 anim_p 直接由手指映射,故不受下方「animating」门控(非时间轴推进),直接呈现。 */ + if (drag_paint) { + last_act = now; + ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); + drag_paint = 0; + } + /* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。 + * row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */ + if (row_paint) { + last_act = now; + if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target)) + ncp_repaint_row(list_scroll, row_swipe_row, row_off); + row_paint = 0; + } + /* ~1s 状态栏时钟 */ + if (now - last_tick >= 1000) { + last_tick = now; + draw_clock(); + fb_norm_buf0(); + meow_ui_flush(); + } + usleep(POLL_MS * 1000); + } + if (tfd >= 0) close(tfd); + kp_close(&kp_power); kp_close(&kp_keys); + ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */ + ncp_rows_free(); + logmsg("notification center closed"); +} + diff --git a/patch_v2/meow_launcherd/power_menu.c b/patch_v2/meow_launcherd/power_menu.c new file mode 100644 index 0000000..bbe21ce --- /dev/null +++ b/patch_v2/meow_launcherd/power_menu.c @@ -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; +} + diff --git a/patch_v2/meow_launcherd/settings_page.c b/patch_v2/meow_launcherd/settings_page.c new file mode 100644 index 0000000..a1e99f7 --- /dev/null +++ b/patch_v2/meow_launcherd/settings_page.c @@ -0,0 +1,1322 @@ +/* settings_page.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ +/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ +#include "meow_launcher_internal.h" + + +/* SC_PRESET 预设表(路径/枚举字符串的"预设项,自定义文件选择留后期") */ +static const char *const bg_presets[] = { "black", "blue", "green" }; + +static const char *const bg_preset_labels[] = { "黑色", "深蓝", "深绿" }; + +static const char *const hk_presets[] = { "power_voldown", "seq" }; + +static const char *const hk_preset_labels[] = { "电源+音量减", "连按序列" }; + +static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" }; + +static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" }; + +/* font.file 行(字体文件)不使用编译期静态表:其 presets/preset_labels 指向字体区 + * (font_presets/font_preset_labels, 见本文件字体区) 的运行时动态数组,计数由 + * s_row_presets() 动态返回(默认 1,进设置页扫描 TF 卡后更新)。见 SMENU 行。 */ +static const char *const out_presets[] = { "hw:0,0" }; + +static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" }; + + +/* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s, + * 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按 + * 45s/分/刻/时 跳档,最高 2 时。滑条值 = 档位索引(0..S_TIME_TIERS_N-1,步进 1), + * 落盘仍写「秒」(display.screen_off_timeout);索引<->秒双向映射见 s_time_tier_*。 */ +static const int s_time_tiers[] = { + 0, /* 常亮 */ + 5, 10, 15, 20, /* 细粒度段:可精确到 30s 附近 */ + 30, 45, /* 半分钟/三刻钟 */ + 60, 90, 120, 180, /* 1 分 / 1.5 分 / 2 分 / 3 分 */ + 300, 600, 900, /* 5 分 / 10 分 / 15 分 */ + 1800, /* 30 分 */ + 3600, 7200 /* 1 时 / 2 时 */ +}; + + +/* 设置页扁平化菜单(分组=组标题行,其余=设置项)。顺序即显示顺序。 */ +/* 分组/键/类型/默认与 SYSTEM_SETTINGS.md §1 一致(新建项四端同步)。 */ +static const srow_t SMENU[] = { + /* 显示 */ + { "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0 }, + { "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, + { "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0 }, + { "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0 }, + { "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + /* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动), + * 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */ + { "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0 }, + { "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0 }, + { "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, + { "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0 }, + { "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N }, + /* 网络 */ + { "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "WiFi", "network.wifi_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "WiFi连接", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SSID }, + { "已保存网络", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SAVED }, + { "附近WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SCAN }, + { "蓝牙", "network.bluetooth_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "蓝牙模式", NULL, SC_INFO, 0,0,0, NULL,NULL,0, NET_LIST_BT_MODE }, + { "蓝牙设备", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_NEARBY }, + /* 电源 */ + { "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0 }, + { "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0 }, + { "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N }, + /* 开机 */ + { "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0 }, + { "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0 }, + /* 输入 */ + { "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N }, + { "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0 }, + { "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0 }, + { "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + /* 字体主题 */ + { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + /* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。 + * 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file); + * 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */ + { "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + /* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/// 列表; + * 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */ + { "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + { "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + { "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + { "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + { "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + { "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + { "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 }, + { "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 }, + { "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 }, + /* 通知 */ + { "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + { "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0 }, + /* 日志 */ + { "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0 }, + { "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0 }, + /* 音频 */ + { "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0 }, + { "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0 }, + { "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0 }, + { "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N }, + /* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */ + { "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, + { "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT }, +}; + +/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由 + * 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */ + +/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量, + * 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */ +static int s_sel_track_y = -1; + + +/* ---- 行高/位置/导航辅助 ---- */ +/* M2.5x 多字体:6 个「字体文件」行仅在 display.multi_font=1 时可见(开时显示)。 + * 可见性与分组的标题行无关(标题恒显示)。 */ +static int s_row_visible(int i) +{ + const srow_t *r = &SMENU[i]; + if (r->key && !strncmp(r->key, "font.class_", 11)) { + int on = 0; meow_settings_get_switch("display.multi_font", &on); + return on; + } + return 1; +} + +/* 可见行数(= 压缩后占用的行数;标题行计入,隐藏的 class 行不计) */ +static int s_vis_count(void) +{ + int n = 0; + for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) n++; + return n; +} + +/* SMENU 下标 → 压缩位置(第几个可见行);不可见返回 -1 */ +static int s_vis_of(int i) +{ + if (i < 0 || i >= SMENU_N || !s_row_visible(i)) return -1; + int p = 0; + for (int k = 0; k < i; k++) if (s_row_visible(k)) p++; + return p; +} + +/* 压缩位置 → SMENU 下标 */ +static int s_vis_at(int p) +{ + int c = 0; + for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) { if (c == p) return i; c++; } + return SMENU_N - 1; +} + + +static int s_setting_idx(int i) { return i >= 0 && i < SMENU_N && SMENU[i].ctrl != SC_HEADER && s_row_visible(i); } + +static int s_first_setting(void) +{ + for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i; + return 0; +} + +static int s_next_setting(int i) +{ + for (int k = i + 1; k < SMENU_N; k++) if (s_setting_idx(k)) return k; + return s_first_setting(); /* 回绕 */ +} + + +/* 字体行(font.file + font.class_*)走「family→字重」两级子菜单(M2.6 字重结构) */ +static int s_row_is_font(const srow_t *r) +{ + return r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11)); +} + + +/* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组 + * (TF 卡 MeowOS/fonts/// 扫描结果,见字体区 font_presets/font_presets_n), + * 其余行用编译期静态表(r->presets / r->preset_n)。 */ +static const char *const *s_row_presets(const srow_t *r, int *n) +{ + if (r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11))) { + if (n) *n = font_presets_n; + return font_presets; + } + if (n) *n = r->preset_n; + return r->presets; +} + + +/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */ +static int s_clamp_scroll_range(int scroll) +{ + int total = s_vis_count() * SROW_H; + if (total <= SET_CONTENT_H) return 0; + int max_scroll = total - SET_CONTENT_H; + if (scroll < 0) scroll = 0; + if (scroll > max_scroll) scroll = max_scroll; + return scroll; +} + + +/* 滚动:保证当前选中行在 [SET_CONTENT_Y, SET_CONTENT_BOT] 内 */ +static int s_clamp_scroll(int sel, int scroll) +{ + scroll = s_clamp_scroll_range(scroll); + int pos = s_vis_of(sel); + if (pos < 0) pos = s_vis_of(s_first_setting()); + int top = pos * SROW_H; + int bot = top + SROW_H; + if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */ + if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */ + return s_clamp_scroll_range(scroll); +} + + +/* 行 y 落在 content 坐标 cy 时对应的行号(压缩位置 → SMENU 下标);越界返回 -1 */ +static int s_row_at_content_y(int cy) +{ + if (cy < 0) return -1; + int pos = cy / SROW_H; + if (pos >= s_vis_count()) return -1; + return s_vis_at(pos); +} + + +/* ---- 值格式化/展示 ---- */ +static void s_fmt_time(int sec, char *buf, int len) +{ + if (sec <= 0) { snprintf(buf, len, "常亮"); return; } + if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; } + if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; } + snprintf(buf, len, "%d 时", sec / 3600); +} + +/* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */ +static void s_fmt_minutes(int mins, char *buf, int len) +{ + if (mins < 0) mins = 0; + if (mins > 1439) mins = 1439; + snprintf(buf, len, "%02d:%02d", mins / 60, mins % 60); +} + +/* 息屏时间:秒 → 最近档位索引(显示滑动位置用;等值优先,跨档取较近档,平手取高档) */ +static int s_time_tier_idx(int sec) +{ + if (sec <= s_time_tiers[0]) return 0; + for (int i = 1; i < S_TIME_TIERS_N; i++) { + if (sec == s_time_tiers[i]) return i; + if (sec < s_time_tiers[i]) { + int lo = s_time_tiers[i - 1], hi = s_time_tiers[i]; + return (sec - lo < hi - sec) ? i - 1 : i; + } + } + return S_TIME_TIERS_N - 1; +} + +/* 息屏时间:档位索引 → 秒(落盘写给 display.screen_off_timeout 的实值) */ +static int s_time_tier_val(int idx) +{ + if (idx < 0) idx = 0; + if (idx >= S_TIME_TIERS_N) idx = S_TIME_TIERS_N - 1; + return s_time_tiers[idx]; +} + +static void s_fmt_value(const srow_t *r, char *buf, int len) +{ + int v = 0; + if (meow_settings_get_int(r->key, &v) != 0) { snprintf(buf, len, "-"); return; } + if (!strcmp(r->key, "display.screen_off_timeout")) { s_fmt_time(v, buf, len); return; } + /* B2b 深色时段(分钟 0-1439 → "HH:MM");start==end 视为未配置,显示「未设置」 */ + if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { + int s2 = 0, e2 = 0; + meow_settings_get_int("display.theme_dark_start", &s2); + meow_settings_get_int("display.theme_dark_end", &e2); + if (s2 == e2) { snprintf(buf, len, "未设置"); return; } + s_fmt_minutes(v, buf, len); + return; + } + /* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */ + if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) { + if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000); + else snprintf(buf, len, "%.1f 秒", v / 1000.0); + return; + } + snprintf(buf, len, "%d", v); +} + +static void s_display_value(const srow_t *r, char *buf, int len) +{ + if (r->ctrl == SC_ENUM) { + int idx = 0; + if (meow_settings_get_enum(r->key, &idx) != 0) idx = 0; + if (meow_settings_enum_label(r->key, idx, buf, len) != 0) snprintf(buf, len, "-"); + return; + } + /* SC_PRESET */ + char cur[MEOW_SET_MAX_LEN]; + if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; } + /* M2.5x 多字体:class 行空值 = 跟随全局 font.file */ + if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) { + snprintf(buf, len, "跟随全局"); + return; + } + int pn = 0; + const char *const *pps = s_row_presets(r, &pn); + for (int i = 0; i < pn; i++) { + if (!strcmp(pps[i], cur)) { + snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur); + return; + } + } + snprintf(buf, len, "%s", cur); +} + + + +/* ---- 控件绘制(MD3 形态;全部用主题角色色,无散落硬编码)---- */ +void s_draw_switch(int x, int y, int on, const meow_ui_theme_t *th) +{ + int w = 48, h = 28; + meow_gfx_fill_round(x, y, w, h, h / 2, on ? th->primary : th->surface_container); + int knob_r = h / 2 - 3; + int kx = on ? x + w - knob_r * 2 - 3 : x + 3; + meow_gfx_fill_round(kx, y + 3, knob_r * 2, knob_r * 2, knob_r, + on ? 0xFFFFFF : th->on_surface_variant); +} + +void s_draw_slider(int x, int y, int w, int val, int mn, int mx, + const meow_ui_theme_t *th) +{ + int h = 6, ty = y; + fill_rect(x, ty, w, h, th->surface_container); /* 未达段 */ + int range = mx - mn; + int frac = range ? (val - mn) * w / range : 0; + if (frac > 0) fill_rect(x, ty, frac, h, th->primary); /* 已达段 */ + int tx = x + frac, cy = ty + h / 2; + meow_gfx_fill_round(tx - 12, cy - 12, 24, 24, 12, th->primary); /* thumb */ + meow_gfx_fill_round(tx - 5, cy - 5, 10, 10, 5, 0xFFFFFF); /* thumb 中点 */ +} + + +/* ---- set 后统一侧效应(DAC/背光/刷新率)+ 可选落盘 ---- */ +static void s_commit(const srow_t *r, int save_now) +{ + if (!r || !r->key) return; + if (!strcmp(r->key, "audio.volume")) { + /* M2.38 Bug1/2:音量变更——拖动中节流写 DAC(防 system() 高频卡死),抬手才无条件直写; + * 同时把 g_sys.vol 同步为当前值并刷整条状态栏(顶层 VOL 数字随之更新,不整屏重绘)。 */ + g_sys.vol = settings_volume_get(); + paint_sb_right(); + if (save_now) meow_dac_vol_flush(); /* 抬手/点击落盘=无条件直写真实 DAC */ + else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */ + } + else if (!strcmp(r->key, "display.brightness")) { + /* M2.46-2 ①:亮度滑条拖动时状态栏百分比数字实时跟。根因:旧代码只 write_brightness(v) + * 写背光,未同步 g_sys.brightness(状态栏亮度数字取它)也未重绘 → 数字卡住。 + * 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。 + * 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */ + int v; if (meow_settings_get_int("display.brightness", &v) == 0) { + write_brightness(v); + g_sys.brightness = v; + paint_sb_right(); + } + } + else if (!strcmp(r->key, "display.refresh_rate")) + meow_ui_set_refresh_rate(refresh_rate_cfg); + else if (!strcmp(r->key, "display.refresh_always_max")) + meow_ui_set_refresh_always_max(refresh_always_max); + else if (!strcmp(r->key, "network.wifi_enabled")) { + int on = 0; meow_settings_get_bool(r->key, &on); + s_net_toggle_wifi(on); + s_net_refresh(); + } + else if (!strcmp(r->key, "network.bluetooth_enabled")) { + int on = 0; meow_settings_get_bool(r->key, &on); + s_net_toggle_bt(on); + s_net_refresh(); + } + /* M2.44 背景选项真正生效:display.background(black/blue/green) → menu_bg(主界面背景)。 + * 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */ + else if (!strcmp(r->key, "display.background")) { + char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg); + if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */ + else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */ + else menu_bg = 0x000000; /* black(默认) */ + /* 状态栏(menu_bg)与主界面背景都会用到 menu_bg;退出设置页回主界面时重绘即可。 */ + } + /* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */ + else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") || + !strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { + paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */ + } + /* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。 + * 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走 + * font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */ + else if (!strcmp(r->key, "font.file")) { + mfont_ready = 0; + mfont_forced = 1; + if (!meow_font_ensure()) + logmsg("font.file: reload failed -> dotmatrix fallback"); + else + logmsg("font.file: reloaded"); + meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */ + meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */ + } + /* M2.5x「加载大字库」开关(font.load_full)变更 → 强制重载 launcher 自身字体(小/大字库切换), + * 并重配多字体;大字库 atlas 大,meow_font_ensure 先 unload 旧再 load 新(防双驻留)。 */ + else if (!strcmp(r->key, "font.load_full")) { + mfont_ready = 0; + mfont_forced = 1; /* 跳过 SDK 复用捷径,强制按生效档(gbk/common)真实 reload */ + if (!meow_font_ensure()) + logmsg("font.load_full: reload failed -> dotmatrix fallback"); + else { + int lf = 0; meow_settings_get_switch("font.load_full", &lf); + logmsg("font.load_full: reloaded (load_full=%d)", lf); + } + meow_font_reload_multifont(); + meow_dirty_full(); + } + /* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */ + else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) { + meow_font_reload_multifont(); + meow_dirty_full(); + } + if (save_now) meow_settings_save(); +} + +static void s_set_int_value(const srow_t *r, int v, int save_now) +{ + if (!strcmp(r->key, "audio.volume")) v = vol_clamp(v); /* 尊重 audio.volume_max */ + meow_settings_set_int(r->key, v); + s_commit(r, save_now); +} + +/* 滑条拖动/点击落值:对「档位型」息屏时间做 索引→秒 换算,其余行原样写盘。 + * sl_v 为滑条轨迹值(s_slider_val_from_x 返回值;息屏时间=档位索引)。 */ +static void s_slider_set_from_track(const srow_t *r, int sl_v, int save_now) +{ + if (!strcmp(r->key, "display.screen_off_timeout")) sl_v = s_time_tier_val(sl_v); + /* B2b 深色时段滑条:分钟按 15 分钟步进吸附(0-1439,整 15 分钟),避免拖出任意分钟 */ + if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { + sl_v = ((sl_v + 7) / 15) * 15; + if (sl_v < 0) sl_v = 0; + if (sl_v > 1439) sl_v = 1439; + } + s_set_int_value(r, sl_v, save_now); +} + + +/* 滑条拖动值域映射:x(屏幕坐标)→ value,基于布局常量 S_SLIDER_X/W。 + * 所有滑条行共用同一 track 几何,故任意滑条行都能正确映射。 */ +static int s_slider_val_from_x(const srow_t *r, int px) +{ + int range = r->max - r->min; + int v = r->min + (range ? (px - S_SLIDER_X) * range / S_SLIDER_W : 0); + if (v < r->min) v = r->min; + if (v > r->max) v = r->max; + return v; +} + + +/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */ +static void draw_settings_page(int sel, int scroll_off) +{ + const meow_ui_theme_t *th = s_ui_theme(); + int W = fb_xres, H = fb_yres; + fill_rect(0, 0, W, H, th->surface); + draw_statusbar(); /* Stage-One:驱动 SDK 全局状态栏(喂数据+标脏顶条) */ + if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28); + else draw_text(24, 30, "SETTINGS", 3, th->on_surface); + fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline); + + /* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。 + * 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底; + * 状态栏(0-28)与标题区(28-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */ + fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant); + + int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT; + s_sel_track_y = -1; + for (int i = 0; i < SMENU_N; i++) { + int pos = s_vis_of(i); + if (pos < 0) continue; /* 隐藏行(多字体关闭时 6 个 class 行) */ + int sy = SET_CONTENT_Y + pos * SROW_H - scroll_off; + if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */ + const srow_t *r = &SMENU[i]; + int selected = (i == sel); + if (selected) + meow_ui_fill_rect_alpha(SET_CARD_X + 4, sy, SET_CARD_W - 8, SROW_H, th->primary, 34); + meow_ui_fill_rect(SET_CARD_X + 12, sy + SROW_H - 1, SET_CONTENT_W, 1, th->outline); + + if (r->ctrl == SC_HEADER) { + ui_draw(SET_CARD_X + 16, sy + 14, r->label, th->on_surface_variant, 18); + } else if (r->ctrl == SC_SWITCH) { + ui_draw(SET_CARD_X + 16, sy + (SROW_H - meow_font_line_height(22)) / 2, r->label, + th->on_surface, 22); + int on = 0; meow_settings_get_switch(r->key, &on); + s_draw_switch(SET_CARD_X + SET_CARD_W - 16 - 48, sy + (SROW_H - 28) / 2, on, th); + } else if (r->ctrl == SC_SLIDER) { + int ly = sy + (SROW_H - meow_font_line_height(22)) / 2 - 6; /* 标题略上移,给下方滑条留位 */ + ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); + char vb[32]; s_fmt_value(r, vb, sizeof vb); + ui_draw(SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22), ly, vb, th->on_surface_variant, 22); + int v = 0; meow_settings_get_int(r->key, &v); + /* 息屏时间为档位索引型:把秒换算成档位索引再画(各档等距,便于触摸对位); + * 其余滑条防御性 clamp 到 [min,max],防止后端暂存值越界(如 brightness 旧值 101 + * 在画时被 clamp 到 100,thumb 不冲出 track)。 */ + if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v); + else { + if (v < r->min) v = r->min; + if (v > r->max) v = r->max; + } + int tyy = sy + 40; + s_draw_slider(S_SLIDER_X, tyy, S_SLIDER_W, v, r->min, r->max, th); + if (selected) s_sel_track_y = tyy; + } else if (r->ctrl == SC_INFO) { /* M2.39 只读动态信息(如 WiFi 连接 SSID) */ + int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; + ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); + char vb[48]; s_net_get(r, vb, sizeof vb); + int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22); + ui_draw(vx, ly, vb, th->on_surface_variant, 22); + } else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:摘要 + 右向展开提示 */ + int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; + ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); + char vb[48]; s_net_get(r, vb, sizeof vb); + int vx = SET_CARD_X + SET_CARD_W - 16 - 12 - ui_width(vb, 22); /* 给右向 chevron 留位 */ + ui_draw(vx, ly, vb, th->on_surface_variant, 22); + s_draw_chevron(SET_CARD_X + SET_CARD_W - 16, sy + SROW_H / 2, th->on_surface_variant); + } else { /* SC_ENUM / SC_PRESET */ + int ly = sy + (SROW_H - meow_font_line_height(22)) / 2; + ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22); + char vb[40]; s_display_value(r, vb, sizeof vb); + int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22); + ui_draw(vx, ly, vb, th->on_surface_variant, 22); + s_draw_chevron(vx - 8, sy + SROW_H / 2, th->on_surface_variant); + } + } + + /* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */ + int vis_total = s_vis_count() * SROW_H; + if (vis_total > SET_CONTENT_H) { + int total = vis_total; + int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total; + if (thumb_h < 24) thumb_h = 24; + int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H); + meow_gfx_fill_round(fb_xres - 12, SET_CONTENT_Y, 5, SET_CONTENT_H, 3, th->surface_container); + meow_gfx_fill_round(fb_xres - 12, th_y, 5, thumb_h, 3, th->on_surface_variant); + } + + /* 底部:返回提示(左)+ 版本号(右),复用应用列表底部对齐逻辑。 + * M2.39:返回按钮改用像素左向箭头 + 「返回」——点阵回退字库缺少 '‹' 字形, + * 原 「‹ 返回」在 !mfont_ready 时 '‹' 渲染成方框。 */ + { + char ver[32]; + snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); + int foot_y = fb_yres - meow_font_line_height(20) - 8; + s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant); + ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20); + ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20); + } +} + + +/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */ +static const char *s_preset_value(const srow_t *r, int pick) +{ + int is_cls = r->key && !strncmp(r->key, "font.class_", 11); + if (is_cls) { + if (pick == 0) return ""; /* 跟随全局 font.file */ + pick--; + } + int pn = 0; + const char *const *pps = s_row_presets(r, &pn); + if (pick < 0 || pick >= pn) return is_cls ? "" : (pps && pn > 0 ? pps[0] : ""); + return pps[pick]; +} + + +/* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)---- + * ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。 + * 返回选中索引;取消返回 -1。阻塞(自开输入设备)。 + * M2.39:modal 核心抽到 s_dd_modal(网络列表复用),本函数只负责组装选项数组。 */ +static int s_dropdown(const srow_t *r) +{ + int n = 0, cur = 0; + const char *names[8]; + char labels[8][48]; + const char *item[S_NET_MAX]; + if (r->ctrl == SC_ENUM) { + int kn = 0; + meow_settings_enum_names(r->key, names, &kn); + n = kn; + if (n > S_NET_MAX) n = S_NET_MAX; + for (int i = 0; i < n; i++) { + meow_settings_enum_label(r->key, i, labels[i], sizeof labels[i]); + item[i] = labels[i]; + } + int ci = 0; meow_settings_get_enum(r->key, &ci); cur = ci; + } else { + char cstr[MEOW_SET_MAX_LEN]; + meow_settings_get_str(r->key, cstr, sizeof cstr); + int pn = 0; + const char *const *pps = s_row_presets(r, &pn); + int is_cls = r->key && !strncmp(r->key, "font.class_", 11); + int base = is_cls ? 1 : 0; /* class 行 0 号留给「跟随全局」 */ + int cnt = pn; if (cnt > S_NET_MAX - base) cnt = S_NET_MAX - base; + n = base + cnt; + if (is_cls) { + names[0] = ""; + snprintf(labels[0], sizeof labels[0], "跟随全局字体"); + item[0] = labels[0]; + if (!cstr[0]) cur = 0; + } + for (int i = 0; i < cnt; i++) { + names[base + i] = pps[i]; + snprintf(labels[base + i], sizeof labels[base + i], "%s", + (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]); + item[base + i] = labels[base + i]; + if (!strcmp(pps[i], cstr)) cur = base + i; + } + } + if (n <= 0) return -1; + if (cur < 0 || cur >= n) cur = 0; + return s_dd_modal(r->label, item, n, cur); +} + + +/* ---- 通用选择器下拉模态(MD3 §3.3:居中圆角卡片,列选项)---- + * title=标题;item[i]=第 i 项显示文本;n=项数(≤S_NET_MAX);cur=当前值下标(画勾选)。 + * 返回选中下标;取消/点卡片外返回 -1。阻塞(自开输入设备)。 + * s_dropdown(设置项 enum/preset)与 s_net_open(网络列表)共用。 + * M2.40:触摸命中判定改在抬手用抬手时刻最新 abs 坐标(见循环内注释),修「点选项无反应」。 + * 说明:模态内部仍保留音量键=移动选项高亮(这是「选值」不是「设置页列表光标」,属另一语义, + * 与 M2.39 仅移除 run_settings_page 列表光标移动的约束不冲突)。 */ +/* 绘制下拉/关于本机模态:全屏压暗 + 居中圆角卡片 + 标题 + 可见行(按 dd_scroll 裁剪,视口外整行跳过)。 */ +static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int d_y, + int d_w, int d_h, int title_h, int opt_row, const char *title, + const char **item, int n, int sel, int cur, + int content_top, int content_bot, int dd_scroll) +{ + meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150); + meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container); + meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline); + if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20); + else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface); + for (int i = 0; i < n; i++) { + int ry = d_y + title_h + i * opt_row - dd_scroll; + if (ry + opt_row < content_top || ry >= content_bot) continue; /* 视口外整行跳过(配滚动) */ + int cur_sel = (i == sel); + if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36); + ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "", + cur_sel ? 0xFFFFFF : th->on_surface, 20); + if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary); + } +} + +int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur) +{ + int sel = cur; + const meow_ui_theme_t *th = s_ui_theme(); + int W = fb_xres, H = fb_yres; + int opt_row = 48, pad = 20, title_h = 40; + /* M2.5x 关于本机:内容超高(13 行)时把卡片高钳到屏内,内容区允许滚动(复用设置页列滚动语义)。 */ + int raw_d_h = title_h + n * opt_row + pad; + int max_card_h = H - SB_BAR_H - 20; /* 卡片可视高上限(不压状态栏、留底边) */ + int scrollable = (raw_d_h > max_card_h); + int d_h = scrollable ? max_card_h : raw_d_h; + int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2; + if (d_y < SB_BAR_H) d_y = SB_BAR_H; /* 下拉不压状态栏(栏高 40→28,随常量) */ + if (H - d_y - d_h < 20) d_y = H - d_h - 20; + /* 内容可视区(卡片体内、标题下、底部内边距上);dd_scroll 仅在 scrollable 时非 0。 */ + int content_top = d_y + title_h; + int content_bot = d_y + d_h - pad; + int dd_view_h = content_bot - content_top; + int dd_scroll_max = scrollable ? (n * opt_row - dd_view_h) : 0; + if (dd_scroll_max < 0) dd_scroll_max = 0; + int dd_scroll = 0; + int swipe_inv = 0; + if (scrollable) meow_settings_get_switch("input.swipe_invert", &swipe_inv); + + struct keypoll kp_power, kp_keys; + kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); + struct kd_state kd_vold, kd_volu, kd_pwr; + memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_pwr, 0, sizeof kd_pwr); + int m_prev_vold = 0, m_prev_volu = 0, m_prev_pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; + } + int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (tfd >= 0) { + /* M2.37:彻底清空「打开下拉」这次触摸的残留边沿。goodix 的抬起序列(ABS 坐标/ + * BTN_TOUCH=0/MT_TRACKING=-1)在开 fd 后仍可能陆续送达,若只清空一次,第一轮 + * 下拉循环会读到这些残边 → 误判抬起/选项错位/点卡片外穿透。故 排空→短延时→再排空。 */ + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == 16) {} + usleep(40000); /* M2.38 Bug3:排空→40ms→再排空,吸收 goodix 抬起序列长尾 */ + while (read(tfd, ev, sizeof ev) == 16) {} + } + int pwr_pending = 0; + int result = -1; + int dlx = -1, dly = -1, dtd = 0; /* 下拉触摸解析(dlx/dly=最新有效坐标;dtd=已按下) */ + /* M2.5x 滚动状态(仅 scrollable 生效;非滚动路径与原文一致) */ + int dd_oy = -1; /* 按下原点纵坐标(首个有效 ABS 后锁) */ + int dd_scrolling = 0; /* 本次手势是否已判为纵向滚动 */ + int dd_scroll_base = 0; /* 进入滚动时锁定的 dd_scroll 基准(连续跟手) */ + int sel_moved = 0; /* 音量键改变了 sel(滚动需跟随选中行) */ + /* M2.40:不再锁按下时刻的 ddx/ddy —— goodix 时序 BTN_TOUCH 先于 ABS,按下边沿时坐标可能 + * 还是 -1/上一手势残留,锁原点会把点击判成「点卡片外=取消」。命中改在抬手时用抬手时刻的 + * dlx/dly(抬起时必已更新到本次最新有效坐标),与 meow_touch mt_up 语义同源。 */ + + /* 初始绘制:全屏压暗覆盖层 + 居中圆角卡片 + 可见行(按 dd_scroll 裁剪) */ + dd_modal_paint(th, W, H, d_x, d_y, d_w, d_h, title_h, opt_row, title, item, n, + sel, cur, content_top, content_bot, dd_scroll); + meow_dirty_full(); meow_ui_flush(); + + for (;;) { + if (sigterm_flag) { result = -1; break; } + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + int vold = 0, volu = 0, pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; + } + (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_pwr, pwr); + vold = kd_vold.stable; volu = kd_volu.stable; pwr = kd_pwr.stable; + int need = 0; + if (vold && !m_prev_vold && sel < n - 1) { sel++; need = 1; sel_moved = 1; } + if (volu && !m_prev_volu && sel > 0) { sel--; need = 1; sel_moved = 1; } + if (pwr && !m_prev_pwr) pwr_pending = 1; /* 按下:待抬起确认 */ + if (!pwr && m_prev_pwr && pwr_pending) { pwr_pending = 0; result = sel; break; } /* 抬起=确认 */ + m_prev_vold = vold; m_prev_volu = volu; m_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) == 16) { + 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) dlx = val; + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) { + dly = val; + if (dtd) { + if (dd_oy < 0) dd_oy = val; /* 首个有效纵坐标 = 按下原点 */ + if (scrollable) { + int ddy = val - dd_oy; /* 手指纵向位移(正=下移) */ + if (!dd_scrolling && (ddy > S_SCROLL_SLOP || ddy < -S_SCROLL_SLOP)) { + dd_scrolling = 1; + dd_scroll_base = dd_scroll; /* 触发瞬间锁基准,不跳变 */ + } + if (dd_scrolling) { + /* 标准方向:手指上移(ddy<0)→内容上移/看下方→scroll 增;swipe_invert 反向 */ + int t = dd_scroll_base + (swipe_inv ? ddy : -ddy); + if (t < 0) t = 0; + if (t > dd_scroll_max) t = dd_scroll_max; + if (t != dd_scroll) { dd_scroll = t; need = 1; } + } + } + } + } + } else if (type == EV_KEY && code == BTN_TOUCH) { + if (val) { + /* 按下边沿:只标「已按下」,绝不锁原点(goodix 时序 BTN_TOUCH 先于 + * ABS,此刻 dlx/dly 可能还是 -1 或上一手势残留)。 */ + dtd = 1; dd_oy = -1; dd_scrolling = 0; + } else if (dtd) { + dtd = 0; + /* 抬手判定:此刻 dlx/dly 必为本次最新有效坐标(抬起时 ABS 已先到)。 + * 卡片内命中 → 返回选项下标;卡片外 → 取消(-1);从未见有效坐标 → 丢弃。 + * M2.5x:滚动手势抬手不选(不 launch);命中行换算叠加 dd_scroll。 */ + if (dd_scrolling) { + dd_scrolling = 0; + } else if (dlx >= 0 && dly >= 0) { + if (dlx >= d_x && dlx < d_x + d_w && dly >= d_y && dly < d_y + d_h) { + int i = (dly - d_y - title_h + dd_scroll) / opt_row; + if (i >= 0 && i < n) { sel = i; result = sel; goto ddone; } + /* 卡片内但落在标题/内边距:保持弹出态,等待下一次点选 */ + } else { + result = -1; goto ddone; /* 点卡片外=取消 */ + } + } + /* dlx/dly 为 -1:ghost/残留抬手,丢弃不动作 */ + } + } + } + } + } + if (need) { + /* 重绘覆盖层+选项(仅 sel 高亮/滚动变化);sel 经音量键移动时滚动跟随选中行。 */ + if (sel_moved && scrollable) { + int ry0 = content_top + sel * opt_row - dd_scroll; + if (ry0 < content_top) dd_scroll = sel * opt_row; + else if (ry0 + opt_row > content_bot) dd_scroll = sel * opt_row - (dd_view_h - opt_row); + if (dd_scroll < 0) dd_scroll = 0; + if (dd_scroll > dd_scroll_max) dd_scroll = dd_scroll_max; + } + sel_moved = 0; + dd_modal_paint(th, W, H, d_x, d_y, d_w, d_h, title_h, opt_row, title, item, n, + sel, cur, content_top, content_bot, dd_scroll); + meow_dirty_full(); meow_ui_flush(); + } + usleep(POLL_MS * 1000); + } +ddone: + if (tfd >= 0) close(tfd); + kp_close(&kp_power); kp_close(&kp_keys); + return result; +} + + +/* ---- 字体行「family→字重」两级子菜单(M2.6 字重结构)---- + * 一级:列出扫描到的去重 family(class 行首项「跟随全局」=空值);选 family 后自动进入 + * 二级:该 family 的可用字重(regular/bold/italic…)。选中写 out_val = 卡根相对路径 + * MeowOS/fonts///font.png(或 class 行「跟随全局」=空串)。 + * 复用 s_dd_modal(与 WiFi/关于 同款模态)。返回 1=已选中(out_val 已填),0=取消。 */ +static int s_font_dropdown(const srow_t *r, char *out_val, int vlen) +{ + if (!r) return 0; + int is_cls = r->key && !strncmp(r->key, "font.class_", 11); + char cur[MEOW_SET_MAX_LEN]; + meow_settings_get_str(r->key, cur, sizeof cur); + char curfam[64] = "", curwt[32] = ""; + if (cur[0]) + font_family_weight_from_png(cur, curfam, sizeof curfam, curwt, sizeof curwt); + + /* ---- 一级:去重 family 列表(class 行首项「跟随全局」)---- */ + char fam_slug[S_NET_MAX][64], fam_disp[S_NET_MAX][64]; + int famn = 0; + if (is_cls) { + if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], ""); + snprintf(fam_disp[famn], sizeof fam_disp[0], "跟随全局字体"); famn++; } + } + for (int i = 0; i < font_presets_n && famn < S_NET_MAX; i++) { + const char *f = font_preset_fams[i] ? font_preset_fams[i] : ""; + int dup = 0; + for (int k = 0; k < famn; k++) if (!strcmp(fam_slug[k], f)) { dup = 1; break; } + if (dup) continue; + snprintf(fam_slug[famn], sizeof fam_slug[0], "%s", f); + snprintf(fam_disp[famn], sizeof fam_disp[0], "%s", + (font_preset_names[i] && font_preset_names[i][0]) ? font_preset_names[i] : f); + famn++; + } + if (famn <= 0) return 0; + const char *fitem[S_NET_MAX]; + char ftext[S_NET_MAX][64]; + for (int i = 0; i < famn; i++) { snprintf(ftext[i], sizeof ftext[i], "%s", fam_disp[i]); fitem[i] = ftext[i]; } + int cur_fam = is_cls ? 0 : -1; + for (int i = is_cls ? 1 : 0; i < famn; i++) + if (curfam[0] && !strcmp(fam_slug[i], curfam)) cur_fam = i; + int pick = s_dd_modal(r->label, fitem, famn, cur_fam); + if (pick < 0) return 0; + if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */ + const char *fam = fam_slug[pick]; + + /* ---- 二级:该 family 的字重列表(值=完整卡根相对路径,显示=label)---- */ + char wt_val[S_NET_MAX][MEOW_SET_MAX_LEN]; + char wt_text[S_NET_MAX][96]; + int wn = 0; + for (int i = 0; i < font_presets_n && wn < S_NET_MAX; i++) { + if (!font_preset_fams[i] || strcmp(font_preset_fams[i], fam)) continue; + snprintf(wt_val[wn], sizeof wt_val[0], "%s", font_presets[i]); + snprintf(wt_text[wn], sizeof wt_text[0], "%s", font_preset_labels[i]); + wn++; + } + if (wn <= 0) return 0; /* 该 family 无字重(理论不出现:能进 family 必已探到字重) */ + const char *witem[S_NET_MAX]; + for (int i = 0; i < wn; i++) witem[i] = wt_text[i]; + int cur_wt = -1; + for (int i = 0; i < wn; i++) if (cur[0] && !strcmp(wt_val[i], cur)) cur_wt = i; + int wp = s_dd_modal(r->label, witem, wn, cur_wt); + if (wp < 0) return 0; + snprintf(out_val, vlen, "%s", wt_val[wp]); + return 1; +} + + +/* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */ +void run_settings_page(void) +{ + int sel = s_first_setting(); + int scroll_off = 0; + int H = fb_yres; + + /* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts///,刷新「字体文件」下拉。 + * 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */ + meow_font_scan_presets(); + + struct keypoll kp_power, kp_keys; + kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); + struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; + memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); + int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; + } + int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } + unsigned long long touch_cooldown_until = now_ms() + 300; + unsigned long long last_tick = now_ms(); + unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */ + int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */ + + /* 触摸拖动滑条状态 */ + int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; + int drag = -1; /* 被拖动的行索引;-1=无 */ + /* M2.37 列表拖动滚动状态 */ + int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */ + int scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */ + int scroll_y0 = 0; /* 按下原点纵向坐标 */ + int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */ + int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */ + int wake_suppress = 0; /* 自动息屏唤醒后:吸收唤醒键边沿,待全部抬起再接管 */ + /* M2.x(KEY_CONTROL_DESIGN §2)电源键状态:设置页电源键 = 系统级语义(非返回), + * 对照主界面 run_menu 3130-3137 的同名变量。单击=息屏/亮屏、长按=电源菜单、8s=强关。 */ + unsigned long long pwr_hold_at = 0; /* 本次电源按下计时起点 */ + int pwr_menu_state = 0; /* 是否已因此次 hold 弹过电源菜单 */ + int pwr_woke = 0; /* 本次按下=自动息屏唤醒(释放只唤醒不 toggle) */ + int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */ + + /* 首次全帧绘制 */ + s_net_sync_from_sys(); /* M2.39:WiFi/蓝牙开关行同步真实硬件态 + 刷新网络信息缓存 */ + scroll_off = s_clamp_scroll(sel, scroll_off); + draw_settings_page(sel, scroll_off); + meow_dirty_full(); meow_ui_flush(); + + for (;;) { + unsigned long long now = now_ms(); + if (sigterm_flag) break; + /* 重绘触发:值/选中/滚动变化 */ + int need_paint = 0; + + /* ---- M2.37 自动息屏态:留在设置页,等待任意输入唤醒,恢复后重绘(不丢上下文)---- + * 对照 run_menu 主循环 meow_auto_slept 的唤醒重绘(M2.17/2.18)。休眠期间仅探测 + * 输入(按键原始按下 / black 模式触摸按下);一旦唤醒:亮屏、清自动态并整帧重绘。 */ + if (meow_auto_slept && !meow_screen_on) { + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + int wake = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1; + } + if (!wake && screen_off_mode == 1 && tfd >= 0) { /* black 模式:面板仍上电,触摸可唤醒 */ + struct pollfd spfd = { tfd, POLLIN, 0 }; + if (poll(&spfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == 16) { + uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1; + } + } + } + if (wake) { + meow_panel_on(); + meow_auto_slept = 0; + wake_suppress = 1; + last_act = now_ms(); + /* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */ + memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + touch_cooldown_until = now_ms() + 300; + td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1; + logmsg("settings: auto screen-off -> input wake, stayed in settings page"); + scroll_off = s_clamp_scroll_range(scroll_off); + draw_settings_page(sel, scroll_off); + meow_dirty_full(); meow_ui_flush(); + } + usleep(POLL_MS * 1000); + continue; + } + + /* ---- 按键 ---- */ + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + int vold = 0, volu = 0, play = 0, pwr = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; + if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; + if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; + if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; + } + (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); + (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); + vold = kd_vold.stable; volu = kd_volu.stable; + play = kd_play.stable; pwr = kd_pwr.stable; + if (wake_suppress) { + /* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作, + * 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */ + if (!pwr && !vold && !volu && !play) wake_suppress = 0; + } else { + /* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束: + * 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。 + * 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */ + if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } + if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } + /* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」: + * 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。 + * 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */ + if (play && !m_prev_play) { + logmsg("settings: play key caught (media-reserved, no exit)"); + last_act = now; + } + /* M2.x(KEY_CONTROL_DESIGN §2)电源键 = 系统级语义,对齐主界面 run_menu 3284-3366: + * 单击(按<长按阈值后松开)= 息屏/亮屏;长按>=longpress_menu_ms = 电源菜单; + * 按住>=forceoff_ms(且未开过菜单)= 强制关机;自动息屏按下只唤醒。 + * ※ 电源键在设置页不再映射为「返回」——退出只能靠触摸(底部返回按钮 + 左缘左滑)。 */ + if (pwr && !m_prev_pwr) { + pwr_hold_at = now; pwr_menu_state = 0; + pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */ + last_act = now; /* 视为输入,防按住期间误触发自动息屏 */ + /* M2.18 Bug 2 同款:按下正落在自动息屏态(auto_slept 且屏已关)→ 本次只做唤醒 */ + if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; + } + if (pwr && pwr_hold_at) { + /* M2.10/M2.11 B/M2.37 同款:8s 强关只在该「按住」从未开过菜单(!pwr_menu_state) + * 时触发;开过菜单则交给菜单内确认/取消,杜绝「开菜单->取消->仍按住->强关」残留。 */ + if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { + if (sim_no_exec) { logmsg("settings pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } + else { logmsg("settings pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } + /* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */ + } else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { + pwr_menu_state = 1; + logmsg("settings pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); + show_power_menu(); /* 动作在函数内 system();取消则续循环 */ + last_act = now_ms(); /* 菜单返回即视为「刚有输入」,重置自动息屏计时 */ + /* M2.11 B 同款:菜单已关闭。清空按住计时与菜单态,绝不把已互动的 hold 再累计到 8s 强关。 */ + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; + pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */ + /* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配, + * 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */ + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */ + need_paint = 1; + /* 已按 raw 对齐 m_prev_*,跳过下方基线复写,防把 raw 值覆盖回旧稳定值 → 假单边沿 */ + goto settings_keys_seeded; + } + } + if (!pwr && m_prev_pwr) { + /* 松开:短按(<长按阈值且菜单未开)→ 单击 = 息屏/亮屏切换 */ + if (pwr_suppress_click) { + logmsg("settings pm: power release after power-menu -> no toggle (suppress)"); + } else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { + logmsg("settings pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); + meow_toggle_panel(); + meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ + last_act = now_ms(); + need_paint = 1; /* 亮屏重绘 / 息屏留屏(面板已黑,下次亮屏重绘) */ + } else if (pwr_woke) { + logmsg("settings pm: power release after auto screen-off wake -> no toggle (wake only)"); + } + pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; + pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */ + } + } + /* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */ + if (vol_ramp_dir != 0) { + int held = (vol_ramp_dir > 0) ? volu : vold; + int going = vol_ramp_tick(held); + g_sys.vol = settings_volume_get(); + paint_sb_right(); + need_paint = 1; + if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol); + } + m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; + settings_keys_seeded: + ; + + /* ---- 触摸 ---- */ + if (tfd >= 0 && meow_screen_on) { /* M2.x:手动电源息屏后不处理触摸(与主界面一致,且防屏黑时误退出/误改值) */ + struct pollfd pfd = { tfd, POLLIN, 0 }; + if (poll(&pfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == 16) { + 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) tlx = val; + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val; + if (td && !td_ok && tlx >= 0 && tly >= 0) { + tdx = tlx; tdy = tly; td_ok = 1; + scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */ + /* 按下原点落在选中滑条 track 带内 → 开始拖动 */ + if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 && + tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 && + tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) { + drag = sel; + const srow_t *r = &SMENU[sel]; + int v = s_slider_val_from_x(r, tlx); + s_slider_set_from_track(r, v, 0); + need_paint = 1; + } + } + /* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。 + * scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。 + * M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项); + * 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */ + if (td_ok && drag < 0 && !scroll_drag) { + int ddy = tly - scroll_y0; + if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { + scroll_drag = 1; + meow_settings_get_bool("input.swipe_invert", &scroll_invert); + scroll_base = scroll_off + (scroll_invert ? -ddy : ddy); + } + } + if (scroll_drag && td_ok) { + int dydt = tly - scroll_y0; + scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt)); + need_paint = 1; + } + if (drag >= 0 && td_ok) { + const srow_t *r = &SMENU[drag]; + int v = s_slider_val_from_x(r, tlx); + s_slider_set_from_track(r, v, 0); /* 拖动实时 set_*,抬起才落盘 */ + need_paint = 1; + } + } else if (type == EV_KEY && code == BTN_TOUCH) { + if (val && !td) { + if (now_ms() < touch_cooldown_until) { + tlx = tly = -1; /* 冷却丢弃 */ + } else { + td = 1; td_ok = 0; tlx = -1; tly = -1; drag = -1; scroll_drag = 0; + } + } else if (!val && td) { + td = 0; + if (drag >= 0) { /* 滑条拖动结束 → 落盘 */ + const srow_t *r = &SMENU[drag]; + s_commit(r, 1); + drag = -1; + need_paint = 1; + } else if (scroll_drag) { + scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */ + scroll_base = 0; scroll_y0 = 0; + need_paint = 1; + } else if (td_ok) { + int dx = tlx - tdx, dy = tly - tdy; + /* M2.39 左缘右滑返回:复用全局左缘返回判定 meow_edge_back() + * (与 SDK 全局左缘返回 A-0 同语义:左缘起点 + 右滑≥MEOW_EDGE_SWIPE + * 且横向主导),避免设置页自造一套重复判定。 */ + if (meow_edge_back(tdx, dx, dy)) { + if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } + goto done; + } + int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; + if (adx < 60 && ady < 60) { /* 视为 tap */ + int tx = tdx, ty = tdy; + /* 底部返回按钮 */ + if (ty >= H - 52 && tx >= SET_CARD_X && tx <= SET_CARD_X + 120) { + if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} } + goto done; + } + int cy = ty - SET_CONTENT_Y + scroll_off; + int idx = s_row_at_content_y(cy); + if (idx >= 0 && !s_setting_idx(idx)) { + /* 点分组标题行 → 仅选中下一设置行 */ + int ns = s_next_setting(idx); + sel = ns; need_paint = 1; sel_changed = 1; + } else if (idx >= 0) { + const srow_t *r = &SMENU[idx]; + sel = idx; need_paint = 1; sel_changed = 1; + if (r->ctrl == SC_SWITCH) { + int on = 0; meow_settings_get_switch(r->key, &on); + meow_settings_set_switch(r->key, !on); + s_commit(r, 1); + } else if (r->ctrl == SC_LIST) { + /* M2.39 网络列表行:开下拉(重新解析)+ 重绘 + 排空本页 tfd + * (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。 */ + after_modal = 1; + s_net_open(r); + if (tfd >= 0) { + unsigned char e[16]; + while (read(tfd, e, sizeof e) == 16) {} + usleep(40000); + while (read(tfd, e, sizeof e) == 16) {} + } + td = 0; td_ok = 0; drag = -1; scroll_drag = 0; + scroll_base = 0; scroll_y0 = 0; + tdx = tdy = tlx = tly = -1; + touch_cooldown_until = now_ms() + 300; + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + need_paint = 1; + } else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) { + /* 单次点击即开选择器子对话框 */ + after_modal = 1; /* 关闭后整帧重绘(还原被 dim 的状态栏) */ + if (s_row_is_font(r)) { + /* M2.6 字重:字体行走「family→字重」两级子菜单 */ + char fval[MEOW_SET_MAX_LEN]; + if (s_font_dropdown(r, fval, sizeof fval)) { + meow_settings_set_str(r->key, fval); + s_commit(r, 1); + } + } else { + int pick = s_dropdown(r); + if (pick >= 0) { + if (r->ctrl == SC_ENUM) meow_settings_set_enum(r->key, pick); + else meow_settings_set_str(r->key, s_preset_value(r, pick)); + s_commit(r, 1); + } + } + /* M2.38 Bug3:s_dropdown 返回后彻底排空本页 tfd + + * 重置触摸基线。根因:input 子系统把事件广播到所有 + * 打开的 fd —— s_dropdown 用独立 fd 消费选项 tap,但 + * 本页 tfd 同样收到了整套下拉会话事件;若不清空, + * 外层 while(read) 会把它们当新 tap → 穿透到下层设置项。 + * 故 排空→40ms→再排空 + 重置触摸状态 + 冷却窗口吸收长尾。 */ + if (tfd >= 0) { + unsigned char e[16]; + while (read(tfd, e, sizeof e) == 16) {} + usleep(40000); + while (read(tfd, e, sizeof e) == 16) {} + } + td = 0; td_ok = 0; drag = -1; scroll_drag = 0; + scroll_base = 0; scroll_y0 = 0; + tdx = tdy = tlx = tly = -1; + touch_cooldown_until = now_ms() + 300; + /* 重置设置页按键防抖基线,防残边 */ + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + need_paint = 1; + } else if (r->ctrl == SC_SLIDER && tlx >= 0) { + int v = s_slider_val_from_x(r, tlx); + s_slider_set_from_track(r, v, 1); + } + } + } + } + td_ok = 0; + } + } + } + } + } + + /* ---- 滚动 + 重绘 ---- */ + if (need_paint) { + last_act = now; + /* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */ + if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; } + draw_settings_page(sel, scroll_off); + /* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化), + * 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。 + * 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */ + if (after_modal) { meow_dirty_full(); after_modal = 0; } + else meow_dirty_add(0, 42, fb_xres, fb_yres - 42); + meow_ui_flush(); + } + /* M2.37 超时自动息屏(与主界面一致):空闲 > screen_off_timeout → 关屏,但留在设置页: + * 置 meow_auto_slept 进入休眠态,由顶部睡眠门等待任意输入唤醒并重绘设置页、不丢上下文 + * (对照 run_menu 外层 meow_auto_slept 的唤醒重绘,M2.17/2.18 同款语义)。 */ + if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && + (unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) { + meow_panel_off(); + meow_auto_slept = 1; + wake_suppress = 1; /* 唤醒输入边沿待完全抬起后再接管,避免误动作 */ + logmsg("settings: idle %ds -> auto screen-off (stay in settings)", screen_off_timeout); + } + /* ~1s 状态栏时钟刷新 */ + if (now - last_tick >= 1000) { + last_tick = now; + draw_clock(); + fb_norm_buf0(); + meow_ui_flush(); + } + usleep(POLL_MS * 1000); + } +done: + if (tfd >= 0) close(tfd); + kp_close(&kp_power); kp_close(&kp_keys); + logmsg("settings page closed"); +} + diff --git a/patch_v2/meow_launcherd/statusbar.c b/patch_v2/meow_launcherd/statusbar.c new file mode 100644 index 0000000..977dafd --- /dev/null +++ b/patch_v2/meow_launcherd/statusbar.c @@ -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; + diff --git a/patch_v2/meow_launcherd/test_notif_panel b/patch_v2/meow_launcherd/test_notif_panel new file mode 100755 index 0000000..984a0f0 Binary files /dev/null and b/patch_v2/meow_launcherd/test_notif_panel differ diff --git a/patch_v2/meow_launcherd/util.c b/patch_v2/meow_launcherd/util.c new file mode 100644 index 0000000..5898bd9 --- /dev/null +++ b/patch_v2/meow_launcherd/util.c @@ -0,0 +1,1099 @@ +/* util.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ +/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ +#include "meow_launcher_internal.h" + +int log_fd = -1; /* file log fd on the TF card */ + +char log_dir[256] = ""; /* "MeowOS/logs" on card, or "" */ + +char log_file[256] = ""; /* full path of the boot log file */ + + +static int card_available(void) +{ + /* TF card mounted? */ + FILE *f = fopen(MOUNTS, "r"); + if (!f) return 0; + char line[256]; + int found = 0; + while (fgets(line, sizeof line, f)) { + if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; } + } + fclose(f); + if (!found) return 0; + /* writable logs dir? */ + mkdir(LOG_DIR_CARD, 0755); + return access(LOG_DIR_CARD, W_OK) == 0; +} + + +static void boot_timestamp(char *out, size_t n) +{ + /* reuse the timestamp cached for this boot session (/tmp is cleared on + * reboot) so repeated launcher starts in one boot share one log file */ + int fd = open("/tmp/meow_bootts", O_RDONLY); + if (fd >= 0) { + int r = read(fd, out, (int)n - 1); + close(fd); + if (r > 0) { out[r] = 0; return; } + } + time_t now = time(NULL); + time_t up = 0; + fd = open("/proc/uptime", O_RDONLY); + if (fd >= 0) { + char buf[64]; + int r = read(fd, buf, sizeof buf - 1); + close(fd); + if (r > 0) { + buf[r] = 0; + char *p = strchr(buf, ' '); + if (p) *p = 0; + up = (time_t)atof(buf); + } + } + time_t boot = (time_t)((double)now - (double)up + 0.5); + struct tm tm; + localtime_r(&boot, &tm); + strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm); + /* cache for the rest of this boot session */ + fd = open("/tmp/meow_bootts", O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) { + write(fd, out, strlen(out)); + close(fd); + } +} + + +unsigned long long now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + + +/* M2.18 Bug 1: 无符号回绕保护的「空闲时长」。 + * launch_app()/confirm_reboot_player() 是阻塞调用,主循环 for(;;) 顶部的 t=now_ms() + * 在 app 运行期间被卡住而停在启动前旧值;app 退出后 last_input_ms 被置为 app 退出 + * 时刻(较大值)。若该圈 any_input==0(如触摸释放单独落在下一圈、音量/播放边沿发生在 + * 更早圈),line「if (any_input) last_input_ms = t」不执行 → 检查处 t < last_input_ms, + * (unsigned long long)(t - last_input_ms) 无符号相减回绕成 2^64-Δ 的极大值,恒 >= 30s + * → 误判「空闲超时」→ 立即 meow_panel_off()。r3proii 日志铁证:auto screen-off after + * 30s idle (t=141578 last=145688),仅 122ms 即被误判为已空闲 30s。凡判定空闲一律经此 + * 函数取差:t < last(t 过期)时返回 0,绝不回绕。 */ +unsigned long long idle_ms_since(unsigned long long t, unsigned long long last) +{ + return (t >= last) ? (t - last) : 0; +} + + +/* remove oldest log files until total size <= limit (called at boot) */ +void log_cleanup_old(void) +{ + struct dirent *de; + DIR *d = opendir(LOG_DIR_CARD); + if (!d) return; + /* collect meow_*.log with mtime + size */ + struct logfile { char name[64]; long long size; long long mtime; } files[64]; + int n = 0; + while ((de = readdir(d)) && n < 64) { + if (strncmp(de->d_name, "meow_", 5)) continue; + char path[320]; + snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, de->d_name); + struct stat st; + if (stat(path, &st) || !S_ISREG(st.st_mode)) continue; + files[n].mtime = (long long)st.st_mtime; + files[n].size = (long long)st.st_size; + snprintf(files[n].name, sizeof files[n].name, "%s", de->d_name); + n++; + } + closedir(d); + long long total = 0; + for (int i = 0; i < n; i++) total += files[i].size; + /* simple selection sort by mtime ascending (oldest first) */ + for (int i = 0; i < n; i++) + for (int j = i + 1; j < n; j++) + if (files[j].mtime < files[i].mtime) { + struct logfile t = files[i]; + files[i] = files[j]; files[j] = t; + } + int deleted = 0; + for (int i = 0; i < n && total > (long long)LOG_SIZE_LIMIT; i++) { + char path[320]; + snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, files[i].name); + if (unlink(path) == 0) { total -= files[i].size; deleted++; } + } + if (deleted) fprintf(stderr, "log_cleanup: deleted %d old log(s), total now %lld\n", + deleted, total); +} + + +/* try to open the card log file; returns 0 on success */ +int log_open_card(void) +{ + if (log_fd >= 0) return 0; + if (!card_available()) return -1; + char ts[64]; + boot_timestamp(ts, sizeof ts); + snprintf(log_dir, sizeof log_dir, "%s", LOG_DIR_CARD); + snprintf(log_file, sizeof log_file, "%s/%s.log", LOG_DIR_CARD, ts); + int fd = open(log_file, O_WRONLY | O_APPEND | O_CREAT, 0644); + if (fd < 0) return -1; + log_fd = fd; + return 0; +} + + +/* flush the /tmp boot buffer into the file log (once the card mounts) */ +void log_poll_card(void) +{ + if (log_fd >= 0) return; + if (log_open_card() != 0) return; + /* move the /tmp boot buffer into the card log, then drop it */ + int fd = open(BOOT_BUF_TMP, O_RDONLY); + if (fd >= 0) { + char chunk[1024]; + ssize_t n; + while ((n = read(fd, chunk, sizeof chunk)) > 0) { + if (write(log_fd, chunk, n) < 0) { close(fd); log_fd = -1; return; } + } + close(fd); + unlink(BOOT_BUF_TMP); + logmsg("boot buffer moved into %s", log_file); + } else { + logmsg("card log open: %s", log_file); + } +} + + +/* bounded append to the /tmp boot buffer (no card log open yet) */ +static void boot_buf_put(const char *line, int len) +{ + struct stat st; + if (stat(BOOT_BUF_TMP, &st) == 0 && st.st_size > BOOT_BUF_MAX) + unlink(BOOT_BUF_TMP); /* roll the buffer (boot-stage only) */ + int fd = open(BOOT_BUF_TMP, O_WRONLY | O_APPEND | O_CREAT, 0644); + if (fd < 0) return; + if (write(fd, line, len) < 0) { /* best effort */ } + close(fd); +} + + +void logmsg(const char *fmt, ...) +{ + char buf[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof buf, fmt, ap); + va_end(ap); + char line[576]; + int l = snprintf(line, sizeof line, "%llu %s\n", now_ms(), buf); + if (log_fd >= 0) { + if (write(log_fd, line, l) < 0) log_fd = -1; /* card gone -> buffer */ + } + if (log_fd < 0) boot_buf_put(line, l); + fprintf(stderr, "%llu %s\n", now_ms(), buf); +} + +int screen_off_timeout = SCREEN_OFF_TIMEOUT_DEFAULT; + +int screen_off_mode = SCREEN_OFF_MODE_DEFAULT; + +int power_longpress_menu_ms = PWR_MENU_MS_DEFAULT; + +int power_forceoff_ms = PWR_FORCE_MS_DEFAULT; + +int refresh_rate_cfg = REFRESH_RATE_CFG_DEFAULT; /* 目标档 60/30/15(上限,default 60) */ + +int refresh_always_max = 0; /* 1=恒 max 不降档 */ + +static void meow_dirty_reset(void); + + +void put_px(int x, int y, uint32_t color) +{ + if (x < 0 || x >= fb_xres || y < 0 || y >= fb_yres) return; + unsigned char *p = fb + y * fb_line + x * (fb_bpp / 8); + if (fb_bpp == 16) { + uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) | + ((color >> 3) & 0x001F); + *(uint16_t *)p = c; + } else { + *(uint32_t *)p = (color << 8) | 0xFF; + } +} + + +void fill_rect(int x0, int y0, int w, int h, uint32_t color) +{ + for (int y = y0; y < y0 + h; y++) + for (int x = x0; x < x0 + w; x++) + put_px(x, y, color); +} + + +/* 5x7 font (same table as hello.c) */ +static const unsigned char font[][8] = { + {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00}, + {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00}, + {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00}, + {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00}, + {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00}, + {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00}, + {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00}, + {0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00}, + {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00}, + {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00}, + {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, + {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00}, + {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00}, + {0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00}, + {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00}, + {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00}, + {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00}, + {0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, + {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00}, + {0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00}, + {0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00}, + {0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00}, + {0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00}, + {0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00}, + {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, + {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00}, + {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00}, + {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00}, + {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00}, + {0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00}, + {0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, + {0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00}, + {0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00}, + {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00}, + {0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00}, + {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00}, + {0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00}, + {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, +}; + + +static int glyph_index(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'Z') return 10 + (c - 'A'); + if (c >= 'a' && c <= 'z') return 10 + (c - 'a'); + if (c == ' ') return 36; + if (c == '.') return 37; + if (c == '!') return 38; + if (c == '-' || c == '_') return 39; + return 36; +} + + +static void draw_glyph(int x, int y, char c, int scale, uint32_t color) +{ + const unsigned char *g = font[glyph_index(c)]; + for (int row = 0; row < 7; row++) { + unsigned char bits = g[row]; + for (int col = 0; col < 5; col++) { + if (bits & (0x10 >> col)) { + for (int dy = 0; dy < scale; dy++) + for (int dx = 0; dx < scale; dx++) + put_px(x + col * scale + dx, y + row * scale + dy, color); + } + } + } +} + + +void draw_text(int x, int y, const char *s, int scale, uint32_t color) +{ + while (*s) { + draw_glyph(x, y, *s, scale, color); + x += 6 * scale; + s++; + } +} + + +/* 24-bit RGB → RGB565(与 put_px/meow_font 一致) */ +uint16_t rgb565(uint32_t c) +{ + return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)); +} + + +/* 统一文本绘制:字库就绪用 meow_font(真字体/中文),否则回退 5x7 点阵。 + * size 为字库字号(28=状态栏/大标题,20=正文/按钮);回退时按 scale 近似。 */ +void ui_draw(int x, int y, const char *s, uint32_t color, int size) +{ + if (mfont_ready) { + meow_font_drawtext((uint16_t *)fb, x, y, s, rgb565(color), size); + } else { + draw_text(x, y, s, (size >= 16) ? 2 : 1, color); + } +} + + +/* 文本宽度(字库 or 回退估算),用于右对齐/居中 */ +int ui_width(const char *s, int size) +{ + if (mfont_ready) return meow_font_width_text(s, size); + return (int)strlen(s) * 6 * ((size >= 16) ? 2 : 1); +} + + +/* 文本高度(字库行高 or 回退 7×scale 估算),用于垂直居中 / 行高预算。 */ +static int ui_text_height(int size) +{ + if (mfont_ready) return meow_font_line_height(size); + return 7 * ((size >= 16) ? 2 : 1); +} + + +/* ================================================================== */ +/* A-1/A-2 渲染桥:launcher 手动脏模型 → SDK 脏追踪,呈现走 meow_ui_flush() */ +/* launcher 的绘制(put_px/fill_rect/meow_font/dim_screen)直接把像素画进 */ +/* SDK 的 back_buf(fb),在此仅把「哪些区域变了」桥进 SDK 的脏追踪; */ +/* present 不再由 launcher 做,统一 meow_ui_flush() = SDK present(内含 */ +/* vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自带 meow_present/ */ +/* refresh_governor —— 帧率调度已归口 SDK(PERF_SCHEDULER 显示子域)。 */ +/* ================================================================== */ + +/* 标记全屏脏:桥进 SDK 的整帧脏,下次 meow_ui_flush() 整帧 blit。 */ +void meow_dirty_full(void) { meow_gfx_mark_full(); } + + +/* 清空脏集:SDK present 上屏后自清;launcher 侧无需再清(保留接口兼容)。 */ +static void meow_dirty_reset(void) { } + + +/* 标记一个矩形脏:桥进 SDK 脏追踪(相交合并/超限转全屏由 SDK dirty_add 处理)。 */ +void meow_dirty_add(int x, int y, int w, int h) { meow_gfx_mark_dirty(x, y, w, h); } + + +/* 标识符字符(含 '.',用于防止 json_str 误命中更长键的一部分) */ +static int ident_ch(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '.'; +} + + +/* 极简 JSON/扁平值提取:在一行里找 key,读取其值(跳过 ':'/'=' 与可选引号)。 + * 支持 JSON `"name": "Hello"` 与扁平 `framework=true`。返回拷贝到 out 的字符数。 + * 返回 0=未命中/空。 */ +int json_str(const char *buf, const char *key, char *out, int olen) +{ + out[0] = 0; + if (!buf || !key || olen <= 0) return 0; + const char *p = strstr(buf, key); + if (!p) return 0; + if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */ + p += strlen(key); + if (*p == '"') p++; /* JSON 键自身右引号:跳到 ':'/'=' */ + while (*p && (*p == ':' || *p == '=')) p++; + while (*p == ' ' || *p == '\t') p++; + if (*p == '"') p++; /* 值左引号(JSON 字符串值) */ + int i = 0; + while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1) + out[i++] = *p++; + out[i] = 0; + while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0; + return i; +} + + +/* state: 0 ok / 1 no storage card / 2 no usable apps */ +int card_present(void) +{ + FILE *f = fopen(MOUNTS, "r"); + if (!f) return 0; + char line[256]; + int found = 0; + while (fgets(line, sizeof line, f)) { + if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; } + } + fclose(f); + return found; +} + + +/* ================================================================== */ +/* MeowOS 系统拔营(M2.2 批次):TF 卡挂载 + WiFi 联网 */ +/* 官方环境由 hiby_player / sys_server 负责这两件事;MeowOS 作为主系统 */ +/* 时无播放器,必须由 launcher 主动接管。失败不阻塞主界面,后台周期重试。 */ +/* ================================================================== */ +int sd_mounted(void) +{ + return card_present(); +} + + +/* TF 卡设备节点是否出现(任一常见节点即视为在卡) */ +int sd_device_present(void) +{ + return access("/dev/mmcblk0", F_OK) == 0 || access("/dev/mmcblk0p1", F_OK) == 0; +} + + +/* 主动挂载 TF 卡到 /usr/data/mnt/sd_0。返回 1=已挂载,0=尚未(调用方轮询重试)。 + * 依次尝试 /dev/mmcblk0、/dev/mmcblk0p1,先 -t exfat 再自动探测;卡未插入返回 0。 */ +int ensure_card_mounted(void) +{ + if (sd_mounted()) return 1; + if (!sd_device_present()) return 0; + const char *nodes[2] = { "/dev/mmcblk0", "/dev/mmcblk0p1" }; + for (int i = 0; i < 2; i++) { + if (access(nodes[i], F_OK) != 0) continue; + char cmd[160]; + snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; " + "PATH=/bin:/sbin:/usr/bin:/usr/sbin mount -t exfat %s /usr/data/mnt/sd_0 2>/dev/null", + nodes[i]); + if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted: %s", nodes[i]); return 1; } + snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; " + "PATH=/bin:/sbin:/usr/bin:/usr/sbin mount %s /usr/data/mnt/sd_0 2>/dev/null", + nodes[i]); + if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted (auto): %s", nodes[i]); return 1; } + } + return 0; +} + + +int read_file_int(const char *path) +{ + int fd = open(path, 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); +} + + +/* ---- 通知 dump 回调(通知#6 转储 TF 卡;做成真)---- + * meow_notif 在 TTL 到点(meow_notif_tick,§8 10s)调用此回调归档一条通知,随后释放槽。 + * 本回调把 meow_notif 结构体转成一行 CSV 追加写入 <卡>/MeowOS/notifications/notifications.log + * (含 mkdir -p 与按总大小滚动截断、保留最新 10MB),落盘实现在 meow_notif.c 的 + * meow_notif_dump_tf()。无卡/不可写仅打点降级(NOTIFICATION_SYSTEM.md §5/§8 + MEWSPEC §5.3)。 */ +static void notif_dump_tf_placeholder(const meow_notif_t *n) +{ + meow_notif_dump_tf(n); +} + + +/* ---- 通知图标回退回调(用户定案:icon 为空 → 用该 app 默认图标)---- + * post 的 app_icon 为空时调用(meow_notif.c 的 notif_resolve_icon),按 app_id 在 app + * 注册表(scan_apps 静态 app.json 的 icon)里找该 app 的图标(name/path 或空)。 */ +static int notif_app_icon_fallback(const char *app_id, char *out, size_t outsz) +{ + if (!app_id || !app_id[0] || !out || outsz == 0) return 0; + struct meow_acct *a = meow_acct_find(app_id); + if (a && a->icon[0]) { snprintf(out, outsz, "%s", a->icon); return 1; } + return 0; /* 无注册/无图标 → 保持空(通知中心 UI 再回退) */ +} + +void settings_load(void) +{ + meow_settings_init(); /* 幂等:注册默认 + 读 settings.conf */ + meow_notif_init(); /* M2a:通知存储初始化(幂等,读 notify.* 配额) */ + meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 通知#6:挂 dump 回调 = TF 转储(meow_notif_dump_tf:一行 CSV 追加 + 10MB 滚动截断) */ + meow_notif_set_icon_cb(notif_app_icon_fallback); /* icon 空 → 按 app_id 回退该 app 默认图标(app.json icon) */ + if (!settings_cache_registered) { + meow_settings_add_listener(settings_sync_cache_listener); + settings_cache_registered = 1; + } + settings_sync_cache_from_backend(); + logmsg("settings: vol=%d(max=%d,step=%d) refresh=%d(always_max=%d) off=%ds mode=%s", + settings_volume, settings_volume_max, settings_volume_step, + refresh_rate_cfg, refresh_always_max, screen_off_timeout, + screen_off_mode ? "black" : "panel"); +} + +meow_sys_state g_sys; + +char g_meowos_version[16] = MEOWOS_VERSION_DEF; + +static void load_meowos_version(void) +{ + FILE *f = fopen(MEOWOS_VERSION_FILE, "r"); + if (!f) { logmsg("meowos_version: %s missing, use default %s", MEOWOS_VERSION_FILE, g_meowos_version); return; } + char buf[32]; + if (fgets(buf, sizeof buf, f)) { + char *e = buf + strlen(buf); + while (e > buf && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ')) e--; + *e = 0; + if (buf[0]) snprintf(g_meowos_version, sizeof g_meowos_version, "%s", buf); + } + fclose(f); + logmsg("meowos_version: read %s = '%s'", MEOWOS_VERSION_FILE, g_meowos_version); +} + + +/* 读一份 sysfs 字符串(剥尾随换行);失败回退 "" */ +static void read_sys_str(const char *path, char *buf, size_t n) +{ + buf[0] = 0; + int fd = open(path, O_RDONLY); + if (fd < 0) return; + ssize_t r = read(fd, buf, n - 1); + close(fd); + if (r <= 0) { buf[0] = 0; return; } + buf[r] = 0; + while (r > 0 && (buf[r-1] == '\n' || buf[r-1] == '\r')) buf[--r] = 0; +} + + +/* 蓝牙实际上电状态(A/E:不只看 hci0 存在——节点常在但控制器可能 DOWN)。 + * 本设备 /sys/class/bluetooth/hci0/powered 不存在(旧内核/BlueZ 5.54), + * 权威来源为 hciconfig -a hci0 的 UP/DOWN 行。费用较高(spawn),故缓存, + * 由 meow_sys_refresh()(进系统一次)/meow_sys_light()(轻刷)节流更新; + * 状态栏只读 g_sys.bt_on。 */ +int hci_powered(void) +{ + FILE *p = popen("hciconfig -a hci0 2>/dev/null", "r"); + if (!p) return 0; + int up = 0; + char buf[256]; + while (fgets(buf, sizeof buf, p)) { + /* 状态行含 "UP RUNNING" 或 "DOWN"。匹配单词 UP(避免误中 UART 等)。 */ + char *s = buf, *tok; + while ((tok = strsep(&s, " \t\r\n")) != NULL) { + if (!strcmp(tok, "UP")) { up = 1; break; } + } + } + pclose(p); + return up; +} + +/* 读 /proc/net/wireless 的 wlan0 level(dBm,如 -46);失败(文件不存在/无 wlan0 行/解析失败)返回 + * WIFI_LVL_UNKNOWN。行格式(Linux wext)形如 " wlan0: 0000 64. -46. -256 0 ..."; + * 接口名后第 3 个空白分隔数值 = level(注意末尾带 '.',需剥离)。状态栏 WiFi 信号分档喂给 SDK。 */ +static int wifi_signal_level(void) +{ + FILE *f = fopen("/proc/net/wireless", "r"); + if (!f) return WIFI_LVL_UNKNOWN; + char line[256]; + int lvl = WIFI_LVL_UNKNOWN; + while (fgets(line, sizeof line, f)) { + char *p = strstr(line, "wlan0:"); + if (!p) continue; + p += 6; /* 跳过 "wlan0:" */ + int tok = 0; + while (*p) { + while (*p == ' ' || *p == '\t') p++; + if (!*p) break; + char *t = p; + while (*p && *p != ' ' && *p != '\t' && *p != '\n') p++; + char save = *p; *p = 0; /* 临时截断 token */ + if (tok == 2) { /* 第 3 个数值(0 起) = level */ + size_t n = strlen(t); + if (n && t[n - 1] == '.') t[n - 1] = 0; /* 去尾部 '.'(-46. → -46) */ + lvl = atoi(t); + *p = save; + break; + } + *p = save; + tok++; + } + break; /* 只需 wlan0 行 */ + } + fclose(f); + return lvl; +} + + +/* 全量重抓(进 MeowOS 时调用一次;阻塞可接受)。绝不放进 10ms 主循环。 */ +void meow_sys_refresh(void) +{ + load_meowos_version(); /* M2.47:读 /etc/meowos_version(单一数据源,每次刷新对齐) */ + int b = read_file_int("/sys/class/power_supply/battery/capacity"); + g_sys.batt = (b >= 0 && b <= 100) ? b : -1; + char st[16]; + read_sys_str("/sys/class/power_supply/battery/status", st, sizeof st); + g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0; + g_sys.batt_mv = read_file_int("/sys/class/power_supply/battery/voltage_now") / 1000; + + /* WiFi:wifi_on=网卡管理态 up(射频开);wifi_connected=operstate up(已关联 AP)。 + * M2.48:二者分工,状态栏才能区分「开未连接(WIFI_NO)」与「已连接(信号分档)」。 */ + char op[16]; + read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op); + g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0; + g_sys.wifi_on = wlan_radio_up(); + g_sys.wifi_level = g_sys.wifi_connected ? wifi_signal_level() : WIFI_LVL_UNKNOWN; + g_sys.wifi_ip[0] = 0; g_sys.wifi_ssid[0] = 0; + if (g_sys.wifi_connected) { + /* 行形如 "2: wlan0 inet /24 brd ...",取 inet 后第一个斜杠前的 token。 + * 用大缓冲读整行,再从局部 token 拷贝到 16B 小字段(避免 OOB)。 */ + char big[256]; + FILE *p = popen("ip -o addr show wlan0 2>/dev/null | grep 'inet '", "r"); + if (p) { + if (fgets(big, sizeof big, p)) { + char *s = strstr(big, "inet "); + if (s) { s += 5; while (*s == ' ') s++; char *e = strchr(s, '/'); if (e) *e = 0; + snprintf(g_sys.wifi_ip, sizeof g_sys.wifi_ip, "%s", s); } + } + pclose(p); + } + /* SSID 尽力读(wpa_cli 若在;失败留空不外溢)。注意:wpa_cli 可能因 wpa_supplicant + * 未跑而挂起——用后台 + 短等待避免阻塞 UI;超时即放弃(SSID 仅面板用,状态栏不读)。 */ + } + + /* 蓝牙:hci0 存在性 + 上电状态 + M2.48 角色/连接(进系统全量重探一次) */ + g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0; + g_sys.bt_on = g_sys.bt_seen ? hci_powered() : 0; + bt_role_refresh(1); /* 立即探测角色/连接态,供状态栏与设置页"蓝牙模式"行 */ + + /* 音量(settings;进 MeowOS 全量时把真实 DAC 同步到当前档)/ 亮度 */ + g_sys.vol = settings_volume_get(); + meow_dac_vol_write(settings_volume); /* M2.13:进 MeowOS 即把 CS43198 DAC 置为当前音量 */ + int br = read_brightness(); + g_sys.brightness = (br >= 0) ? br : 0; + + /* 存储:/data 剩余 + TF 卡挂载容量 */ + g_sys.data_free_kb = 0; + FILE *df = popen("df -k /data 2>/dev/null | awk 'NR==2{print $4}'", "r"); + if (df) { char x[24]; if (fgets(x, sizeof x, df)) g_sys.data_free_kb = atol(x); pclose(df); } + g_sys.sd_mounted = sd_mounted(); + g_sys.sd_free_kb = g_sys.sd_total_kb = 0; + if (g_sys.sd_mounted) { + FILE *df2 = popen("df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'", "r"); + if (df2) { char x[48]; if (fgets(x, sizeof x, df2)) { sscanf(x, "%ld %ld", &g_sys.sd_free_kb, &g_sys.sd_total_kb); } pclose(df2); } + } + + snprintf(g_sys.ver, sizeof g_sys.ver, "%s", g_meowos_version); + logmsg("sys snapshot: batt=%d%% chg=%d wifi=%d ssid=%s ip=%s bt=%d/%d vol=%d bri=%d " + "data_free=%ldKB sd=%d(%ld/%ldKB)", + g_sys.batt, g_sys.charging, g_sys.wifi_on, g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-", + g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", g_sys.bt_on, g_sys.bt_seen, + g_sys.vol, g_sys.brightness, g_sys.data_free_kb, g_sys.sd_mounted, + g_sys.sd_free_kb, g_sys.sd_total_kb); +} + + +/* 轻刷(1s 定时,只更新时钟/电量/WiFi 信号 + 节流探测蓝牙角色——便宜,绝不碰负载/温度) */ +void meow_sys_light(void) +{ + int b = read_file_int("/sys/class/power_supply/battery/capacity"); + if (b >= 0 && b <= 100) g_sys.batt = b; + char op[16]; + read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op); + g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0; + g_sys.wifi_on = wlan_radio_up(); + g_sys.wifi_level = g_sys.wifi_connected ? wifi_signal_level() : WIFI_LVL_UNKNOWN; + g_sys.vol = settings_volume_get(); /* 每进 MeowOS 重抓;此处同步内存值 */ + bt_role_refresh(0); /* M2.48:节流探测蓝牙角色(约 5s 一次,不阻塞主循环) */ +} + +const int pm_cx[PM_NUM] = { PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2, + PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2, PM_CARD_CX }; + + +static struct meow_ipc_msg ipc_ring[MEOW_IPC_RING]; + +static volatile uint32_t ipc_head = 0; /* 消费端(主循环)推进 */ + +static volatile uint32_t ipc_tail = 0; /* 生产端(IPC线程)推进 */ + +static void ipc_dispatch(struct meow_ipc_msg *m); /* 定义在下方,主循环排空用 */ + +static int ipc_event_fd = -1; + +volatile int ipc_shutdown = 0; + +static volatile int ipc_thread_ok = 0; /* bind+listen 成功 */ + +char ipc_sock_path[MEOW_IPC_SOCK_LEN] = MEOW_IPC_PATH; + +static pthread_t ipc_pthread; + +static struct meow_ipc_conn ipc_conns[MEOW_IPC_CONNS]; + + +/* 写一帧(16B 头 + 负载)。返回写入字节;<=0 视为失败。 */ +static size_t ipc_send(int fd, uint16_t type, uint32_t req_id, + const void *payload, uint32_t len) +{ + if (fd < 0) return 0; + uint8_t hdr[MEOW_IPC_HDR_LEN]; + uint32_t magic = MEOW_IPC_MAGIC; + uint16_t ver = MEOW_IPC_VERSION; + memcpy(hdr + 0, &magic, 4); + memcpy(hdr + 4, &ver, 2); + memcpy(hdr + 6, &type, 2); + memcpy(hdr + 8, &req_id, 4); + memcpy(hdr + 12, &len, 4); + size_t off = 0; + while (off < sizeof hdr) { + ssize_t w = write(fd, hdr + off, sizeof hdr - off); + if (w <= 0) return off; + off += (size_t)w; + } + if (len && payload) { + size_t p = 0; + while (p < len) { + ssize_t w = write(fd, (const uint8_t *)payload + p, len - p); + if (w <= 0) break; + p += (size_t)w; + } + off += p; + } + return off; +} + + +/* 组装完的帧入队(SPSC 无锁,先写槽再推进 tail,__sync 发布)。 */ +static void ipc_enqueue_frame(struct meow_ipc_conn *c) +{ + uint16_t type; + uint32_t req_id, len; + memcpy(&type, c->hdr + 6, 2); + memcpy(&req_id, c->hdr + 8, 4); + len = c->pay_tot; + if ((ipc_tail - ipc_head) >= MEOW_IPC_RING) { + logmsg("ipc: ring full, drop frame type=0x%04x", type); + return; + } + uint32_t slot = ipc_tail % MEOW_IPC_RING; + struct meow_ipc_msg *m = &ipc_ring[slot]; + m->type = type; + m->req_id = req_id; + m->fd = c->fd; + uint32_t copy = len < MEOW_IPC_CARGO ? len : MEOW_IPC_CARGO; + m->plen = (uint16_t)copy; + memcpy(m->payload, c->pay, copy); + __sync_synchronize(); + ipc_tail++; + if (ipc_event_fd >= 0) { uint64_t one = 1; (void)write(ipc_event_fd, &one, sizeof one); } +} + + +/* 读一个连接的可用字节并组装成帧(>=1 完整帧即入队)。返回 0=ok(可能已入队), + * -1=连接关闭/坏帧(调用方应关闭并回收)。 */ +static int ipc_conn_read(struct meow_ipc_conn *c) +{ + for (;;) { + if (c->hdr_n < MEOW_IPC_HDR_LEN) { + ssize_t r = read(c->fd, c->hdr + c->hdr_n, MEOW_IPC_HDR_LEN - c->hdr_n); + if (r < 0) { if (errno == EAGAIN) return 0; return -1; } + if (r == 0) return -1; + c->hdr_n += (int)r; + if (c->hdr_n < MEOW_IPC_HDR_LEN) return 0; + uint32_t magic, len; + uint16_t ver; + memcpy(&magic, c->hdr, 4); + memcpy(&ver, c->hdr + 4, 2); + memcpy(&len, c->hdr + 12, 4); + if (magic != MEOW_IPC_MAGIC) { logmsg("ipc: bad magic %08x", magic); return -1; } + if (ver != MEOW_IPC_VERSION) { logmsg("ipc: bad version %u", ver); return -1; } + if (len > MEOW_IPC_MAX) { logmsg("ipc: frame len %u too long", len); return -1; } + c->pay_tot = len; + c->pay_n = 0; + if (len == 0) { + ipc_enqueue_frame(c); + c->hdr_n = 0; + continue; + } + } + if (c->pay_n < c->pay_tot) { + ssize_t r = read(c->fd, c->pay + c->pay_n, c->pay_tot - c->pay_n); + if (r < 0) { if (errno == EAGAIN) return 0; return -1; } + if (r == 0) return -1; + c->pay_n += (uint32_t)r; + if (c->pay_n < c->pay_tot) return 0; + } + ipc_enqueue_frame(c); + c->hdr_n = 0; + c->pay_n = 0; + } +} + + +static void *ipc_thread(void *arg) +{ + (void)arg; + int lfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (lfd < 0) { logmsg("ipc: socket() fail %s", strerror(errno)); return NULL; } + unlink(ipc_sock_path); + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + strncpy(a.sun_path, ipc_sock_path, sizeof a.sun_path - 1); + if (bind(lfd, (struct sockaddr *)&a, sizeof a) < 0) { + logmsg("ipc: bind %s fail %s", ipc_sock_path, strerror(errno)); + close(lfd); + return NULL; + } + chmod(ipc_sock_path, 0666); + if (listen(lfd, MEOW_IPC_BACKLOG) < 0) { + logmsg("ipc: listen fail %s", strerror(errno)); + close(lfd); + return NULL; + } + fcntl(lfd, F_SETFL, O_NONBLOCK); + ipc_thread_ok = 1; + logmsg("ipc: listening on %s (backlog %d)", ipc_sock_path, MEOW_IPC_BACKLOG); + + struct pollfd fds[MEOW_IPC_CONNS + 1]; + int nfds = 1; + fds[0].fd = lfd; fds[0].events = POLLIN; fds[0].revents = 0; + while (!ipc_shutdown) { + int r = poll(fds, nfds, 400); + if (r < 0) { if (errno == EINTR) continue; break; } + if (r == 0) continue; + if (fds[0].revents & POLLIN) { + for (;;) { + int c = accept(lfd, NULL, NULL); + if (c < 0) break; + fcntl(c, F_SETFL, O_NONBLOCK); + if (nfds <= MEOW_IPC_CONNS) { + ipc_conns[nfds - 1].fd = c; + ipc_conns[nfds - 1].hdr_n = 0; + ipc_conns[nfds - 1].pay_n = 0; + ipc_conns[nfds - 1].pay_tot = 0; + fds[nfds].fd = c; + fds[nfds].events = POLLIN; + fds[nfds].revents = 0; + nfds++; + logmsg("ipc: conn %d accept (nfds=%d)", c, nfds - 1); + } else { + close(c); + logmsg("ipc: conn %d rejected (full)", c); + } + } + } + int i = 1; + while (i < nfds) { + int ev = fds[i].revents; + if (ev & (POLLIN | POLLHUP | POLLERR | POLLNVAL)) { + struct meow_ipc_conn *cc = &ipc_conns[i - 1]; + if (ipc_conn_read(cc) < 0) { + logmsg("ipc: conn %d closed", cc->fd); + close(cc->fd); + for (int j = i; j < nfds - 1; j++) { + fds[j] = fds[j + 1]; + ipc_conns[j - 1] = ipc_conns[j]; + } + nfds--; + continue; + } + } + i++; + } + for (int j = 0; j < nfds; j++) fds[j].revents = 0; + } + for (int i = 1; i < nfds; i++) close(fds[i].fd); + close(lfd); + unlink(ipc_sock_path); + logmsg("ipc: thread exit"); + return NULL; +} + + +/* 启动 IPC 线程(主循环任意时刻调一次)。返回 0=就绪,-1=线程创建失败。 + * (bind/listen 失败记 ipc_thread_ok=0,随后 meow_ipc_process 仍安全轮询空队列。) */ +int meow_ipc_init(void) +{ + ipc_event_fd = eventfd(0, EFD_NONBLOCK); + if (ipc_event_fd < 0) + logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno)); + if (pthread_create(&ipc_pthread, NULL, ipc_thread, NULL) != 0) { + logmsg("ipc: pthread_create fail"); + return -1; + } + usleep(60000); /* 给 bind+listen 一点时间,log 就绪状态 */ + if (ipc_thread_ok) logmsg("ipc: ready (eventfd=%d)", ipc_event_fd); + else logmsg("ipc: NOT ready (bind/listen failed, thread exiting)"); + return ipc_thread_ok ? 0 : -1; +} + + +/* 主循环:非阻塞排空 eventfd + 处理入队帧(单写者改状态并回包)。 + * 事件fd 仅为低延迟优化;若为 -1,10ms 轮询仍保证每圈排空。 */ +void meow_ipc_process(void) +{ + if (ipc_event_fd >= 0) { + uint64_t v; + while (read(ipc_event_fd, &v, sizeof v) == (ssize_t)sizeof v) {} + } + while (ipc_head != ipc_tail) { + struct meow_ipc_msg *m = &ipc_ring[ipc_head % MEOW_IPC_RING]; + ipc_dispatch(m); + ipc_head++; + } + __sync_synchronize(); +} + + +/* 处理一条入队帧:HELLO→ACK、ACT_QUERY→ACT_RESULT/ERR、ACT_DECLARE→ACK、其他→ERR。 */ +static void ipc_dispatch(struct meow_ipc_msg *m) +{ + switch (m->type) { + case MSG_HELLO: { + m_hello_t h; + memset(&h, 0, sizeof h); + memcpy(&h, m->payload, m->plen < (uint16_t)sizeof h ? m->plen : (uint16_t)sizeof h); + h.app_id[MEOW_APP_ID_MAX - 1] = 0; + meow_acct_hello(h.app_id, h.version, h.flags); + m_ack_t a; + a.code = MEOW_OK; + a.status = (uint32_t)g_naccts; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: HELLO app=%s ver=%u -> ACK naccts=%d", h.app_id, h.version, g_naccts); + break; + } + case MSG_ACT_DECLARE: { + m_act_declare_t d; + memset(&d, 0, sizeof d); + memcpy(&d, m->payload, m->plen < (uint16_t)sizeof d ? m->plen : (uint16_t)sizeof d); + d.app_id[MEOW_APP_ID_MAX - 1] = 0; + meow_acct_declare(d.app_id, d.entries, d.count); + m_ack_t a; + a.code = MEOW_OK; + a.status = (uint32_t)g_naccts; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: ACT_DECLARE app=%s n=%u -> ACK", d.app_id, d.count); + break; + } + case MSG_ACT_QUERY: { + m_act_query_t q; + memset(&q, 0, sizeof q); + memcpy(&q, m->payload, m->plen < (uint16_t)sizeof q ? m->plen : (uint16_t)sizeof q); + q.app_id[MEOW_APP_ID_MAX - 1] = 0; + q.activity[MEOW_ACT_MAX - 1] = 0; + char chain[MEOW_CHAIN_MAX] = ""; + int depth = 0; + int code = resolve_chain(q.app_id, q.activity, chain, &depth); + if (code == MEOW_OK) { + m_act_result_t r; + memset(&r, 0, sizeof r); + r.depth = (uint32_t)depth; + r.code = MEOW_OK; + size_t cl = strlen(chain); + if (cl >= MEOW_CHAIN_MAX) cl = MEOW_CHAIN_MAX - 1; + memcpy(r.chain, chain, cl); + r.chain_len = (uint16_t)cl; + ipc_send(m->fd, MSG_ACT_RESULT, m->req_id, &r, sizeof r); + logmsg("ipc: ACT_QUERY app=%s act=%s -> depth=%d chain=%s", + q.app_id, q.activity, depth, chain); + } else { + m_err_t e; + memset(&e, 0, sizeof e); + e.code = (uint32_t)code; + const char *msg = (code == MEOW_ERR_NOENT) ? "unregistered app/activity" + : "resolve failed"; + e.msg_len = (uint16_t)strlen(msg); + memcpy(e.msg, msg, e.msg_len); + ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e); + logmsg("ipc: ACT_QUERY app=%s act=%s -> ERR(0x%x)", + q.app_id, q.activity, code); + } + break; + } + case MSG_ACT_FOCUS: { + m_act_focus_t f; + memset(&f, 0, sizeof f); + memcpy(&f, m->payload, m->plen < (uint16_t)sizeof f ? m->plen : (uint16_t)sizeof f); + f.app_id[MEOW_APP_ID_MAX - 1] = 0; + f.activity[MEOW_ACT_MAX - 1] = 0; + meow_fg_act_set(f.app_id, f.activity); /* M2.17:当前顶层 activity(keep_awake 判定) */ + m_ack_t a; + a.code = MEOW_OK; + a.status = (uint32_t)g_naccts; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: ACT_FOCUS app=%s act=%s -> ACK", f.app_id, f.activity); + break; + } + case MSG_ACT_BACK: { + meow_fg_act_back(); /* 回到根/无 activity:清 keep_awake */ + m_ack_t a; + a.code = MEOW_OK; + a.status = (uint32_t)g_naccts; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: ACT_BACK -> ACK"); + break; + } + /* M2a:通知存储(NOTIFICATION_SYSTEM.md §4/§7/§8)。IPC 线程只入队,此处在主循环 + * 单写者落存储 + 回 ACK(与 HELLO/QUERY 同一线程模型,绝不在 socket IO 阻塞)。 */ + case MSG_NOTIFY: { + m_notify_t mn; + memset(&mn, 0, sizeof mn); + memcpy(&mn, m->payload, m->plen < (uint16_t)sizeof mn ? m->plen : (uint16_t)sizeof mn); + mn.app_id[MEOW_APP_ID_MAX - 1] = 0; + mn.category[23] = 0; + mn.summary[63] = 0; + mn.body[95] = 0; + mn.app_icon[47] = 0; + mn.target_activity[MEOW_ACT_MAX - 1] = 0; + /* 映射到存储结构(freedesktop + MeowOS 扩展;actions/hints v1 空) */ + meow_notif_t n; + memset(&n, 0, sizeof n); + n.id = mn.replaces_id; /* replaces_id==0 => 新建;>0 => 替换(不存在则新建) */ + snprintf(n.app_id, sizeof n.app_id, "%s", mn.app_id); + snprintf(n.category, sizeof n.category, "%s", mn.category); + snprintf(n.summary, sizeof n.summary, "%s", mn.summary); + snprintf(n.body, sizeof n.body, "%s", mn.body); + snprintf(n.app_icon, sizeof n.app_icon, "%s", mn.app_icon); + snprintf(n.target_activity, sizeof n.target_activity, "%s", mn.target_activity); + n.expire_timeout = mn.expire_timeout; + n.progress = mn.progress; + n.urgency = mn.urgency; /* 0-3(低/普通/高/紧急);旧调用=0 → LOW(静默),新调用缺省 NORMAL */ + n.ongoing = mn.ongoing; + n.media = mn.media; + uint32_t nid = 0; + int rc = meow_notif_post(&n, &nid); + m_ack_t a; + a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc; + a.status = nid; /* 返回分配/更新的通知 id */ + ipc_send(m->fd, MSG_NOTIFY_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: NOTIFY app=%s cat=%s id=%u rc=%d -> ACK", mn.app_id, mn.category, nid, rc); + break; + } + case MSG_NOTIFY_DISMISS: { + uint32_t id = 0; + if (m->plen >= 4) memcpy(&id, m->payload, 4); + int rc = meow_notif_dismiss(id); + m_ack_t a; + a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc; + a.status = 0; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: NOTIFY_DISMISS id=%u rc=%d -> ACK", id, rc); + break; + } + case MSG_NOTIFY_PROGRESS: { + m_notify_progress_t p; + memset(&p, 0, sizeof p); + memcpy(&p, m->payload, m->plen < (uint16_t)sizeof p ? m->plen : (uint16_t)sizeof p); + /* 实时进度:仅改 launcher 内存活跃槽(不落盘);progress>=100 视为结束 → 里程碑落盘 */ + int rc = meow_notif_update_progress(p.nid, p.progress); + m_ack_t a; + a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc; + a.status = 0; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: NOTIFY_PROGRESS nid=%u p=%d rc=%d -> ACK", p.nid, p.progress, rc); + break; + } + case MSG_NOTIFY_LIST: + /* ↓ 守护→App 下发通知清单(重同步)。本帧不来自客户端(无请求类型),故不在此应答; + * 需用时由守护主动经 meow_notif_serialize_list() + ipc_send 下发(M5 通知中心接入)。 */ + logmsg("ipc: NOTIFY_LIST (downlink) not dispatched here"); + break; + default: + logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id); + { + m_err_t e; + memset(&e, 0, sizeof e); + e.code = MEOW_ERR_INVAL; + char msg[40]; + snprintf(msg, sizeof msg, "unsupported type 0x%04x", m->type); + e.msg_len = (uint16_t)strlen(msg); + memcpy(e.msg, msg, e.msg_len); + ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e); + } + break; + } +} + diff --git a/patch_v2/sdk/meow_ui_gfx.c b/patch_v2/sdk/meow_ui_gfx.c index 4eb9c3c..fc359cc 100644 --- a/patch_v2/sdk/meow_ui_gfx.c +++ b/patch_v2/sdk/meow_ui_gfx.c @@ -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 = 或 "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__PNG/IDX(命中则覆盖全局;未注入 = 该类跟随全局)。 * 仅当全局字库已装载(g_mfont_ready)才配置;关/无 env → 单字体,零额外开销。 */ static void meow_gfx_load_multifont(void) diff --git a/patch_v2/sdk/meow_ui_internal.h b/patch_v2/sdk/meow_ui_internal.h index 361e769..a38a059 100644 --- a/patch_v2/sdk/meow_ui_internal.h +++ b/patch_v2/sdk/meow_ui_internal.h @@ -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 未知 */ diff --git a/patch_v2/sdk/meow_ui_vol.c b/patch_v2/sdk/meow_ui_vol.c index b428653..2f3cb4d 100644 --- a/patch_v2/sdk/meow_ui_vol.c +++ b/patch_v2/sdk/meow_ui_vol.c @@ -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 读循环 diff --git a/patch_v2/sdk/meow_ui_vol.h b/patch_v2/sdk/meow_ui_vol.h index 9a94bed..fa28c42 100644 --- a/patch_v2/sdk/meow_ui_vol.h +++ b/patch_v2/sdk/meow_ui_vol.h @@ -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(不进负值)。