From e6f0b665e3905c3bba701c05438d176d3646199c Mon Sep 17 00:00:00 2001 From: MeowOS Date: Sun, 30 Aug 2026 21:38:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor(launcher):=20=E6=8B=86=E5=88=86=20meow?= =?UTF-8?q?=5Flauncherd.c(9778=E8=A1=8C)=E2=86=92meow=5Flauncherd/=20?= =?UTF-8?q?=E5=A4=9A=E6=96=87=E4=BB=B6(agent-149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 14 模块 .c + meow_launcher_internal.h(642): util/font_theme/fb_compositor/settings_page/power_menu/notif_center/app_launcher/main_menu/audio/backlight/net/keys/about/statusbar; meow_launcherd.c 已删 - 按行逐字搬移(0-diff), 118函数提升非static+47组extern(内部头), #if0退役区不编入(源码留.pre_split.bak) - 关于本机小改: 作者 AskaEth/17 + info/item[13]防越界 + s_dd_modal 超高滚动(clamp/触摸滚动/音量键跟随/裁剪, 仅超高启用) - build_launcher.sh 编译列表→模块; 文档/注释引用 meow_launcherd.c→模块/目录 - 编译0warning/-Wall -Wextra告警集与原文一致(77条无新增)/qemu-mipsel冒烟一致(--scan/--sysdump); launcher=c0799357(505KB; md5因多文件重编译与原文异,非回归依据) - 备份 meow_launcherd.c.pre_split.bak/meow_launcherd.pre_split.bak; M2.69(0.7.64)已推卡并用户验证OK --- FRAMEWORK_DESIGN.md | 2 +- GESTURE_BACK_DESIGN.md | 2 +- KEY_CONTROL_DESIGN.md | 4 +- MEOW_APP_SDK.md | 8 +- patch_v2/meow_launcherd/about.c | 102 + patch_v2/meow_launcherd/app_launcher.c | 1006 ++ patch_v2/meow_launcherd/audio.c | 224 + patch_v2/meow_launcherd/backlight.c | 130 + patch_v2/meow_launcherd/build_launcher.sh | 12 +- patch_v2/meow_launcherd/fb_compositor.c | 222 + patch_v2/meow_launcherd/font_theme.c | 732 ++ patch_v2/meow_launcherd/keys.c | 204 + patch_v2/meow_launcherd/main_menu.c | 1480 +++ patch_v2/meow_launcherd/meow_card_path.sh | 2 +- .../meow_launcherd/meow_launcher_internal.h | 642 ++ patch_v2/meow_launcherd/meow_launcherd | Bin 508316 -> 505452 bytes patch_v2/meow_launcherd/meow_launcherd.c | 9778 ----------------- patch_v2/meow_launcherd/meow_notif.c | 2 +- patch_v2/meow_launcherd/meow_notif.h | 5 +- patch_v2/meow_launcherd/meow_notif_panel.h | 8 +- patch_v2/meow_launcherd/meow_settings.c | 2 +- patch_v2/meow_launcherd/meow_settings.h | 2 +- patch_v2/meow_launcherd/net.c | 720 ++ patch_v2/meow_launcherd/notif_center.c | 1363 +++ patch_v2/meow_launcherd/power_menu.c | 406 + patch_v2/meow_launcherd/settings_page.c | 1322 +++ patch_v2/meow_launcherd/statusbar.c | 111 + patch_v2/meow_launcherd/test_notif_panel | Bin 0 -> 75112 bytes patch_v2/meow_launcherd/util.c | 1099 ++ patch_v2/sdk/meow_ui_gfx.c | 6 +- patch_v2/sdk/meow_ui_internal.h | 2 +- patch_v2/sdk/meow_ui_vol.c | 2 +- patch_v2/sdk/meow_ui_vol.h | 2 +- 33 files changed, 9798 insertions(+), 9804 deletions(-) create mode 100644 patch_v2/meow_launcherd/about.c create mode 100644 patch_v2/meow_launcherd/app_launcher.c create mode 100644 patch_v2/meow_launcherd/audio.c create mode 100644 patch_v2/meow_launcherd/backlight.c create mode 100644 patch_v2/meow_launcherd/fb_compositor.c create mode 100644 patch_v2/meow_launcherd/font_theme.c create mode 100644 patch_v2/meow_launcherd/keys.c create mode 100644 patch_v2/meow_launcherd/main_menu.c create mode 100644 patch_v2/meow_launcherd/meow_launcher_internal.h delete mode 100644 patch_v2/meow_launcherd/meow_launcherd.c create mode 100644 patch_v2/meow_launcherd/net.c create mode 100644 patch_v2/meow_launcherd/notif_center.c create mode 100644 patch_v2/meow_launcherd/power_menu.c create mode 100644 patch_v2/meow_launcherd/settings_page.c create mode 100644 patch_v2/meow_launcherd/statusbar.c create mode 100755 patch_v2/meow_launcherd/test_notif_panel create mode 100644 patch_v2/meow_launcherd/util.c 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 63730e18032d7cf3b67c1cd888645e8552765fd4..4426663fd0f58e7dbec24cd151fa435fef4f5bb5 100755 GIT binary patch delta 193657 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 O#rEKzG$-BG;{OB0+f9=I delta 197803 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

%`mX-q9O1BefN> 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 跨午夜;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 0000000000000000000000000000000000000000..984a0f0343f7e18013df105583014689ca073428 GIT binary patch literal 75112 zcmeIb3!Gd3(>DwsdZ( zP9)mv8s0g7W=HGHIi?*c%gV#;TfWL*wGCs3vX`mTKokW@@;Zc-Ls?Z*5jyb{;;%u1 zS3pjz2>*kiPcFbuy%&wTk_Rf~NL^`Fwo@gR(yj?YKOr}%KZIsoNYEkXY$wp1YzpZT`#N%)fbrjj2jyES7 zTU1+nDv<<#5+Kxuw)V!B^acYZ(UGcysGB-kgc^_6ZArv8G`1v~8b6dGlL6kGYVJs- ziMu1EHf(Nh1Sljjb36`~mex2irW;#ZRAPN=ds;Phq*AB?V59>TtnW;;Clw*mXT4Ju z_GBwpU$JyqeD2IS7b|-^Z(ja(zKUP5ZcQ8&O4T=Zq*Lwd)+}pkZAq<5tZ$+|>zi9! zL<4cdT96T~#VZPbOaB%v_#a1+VkuFKwkAyrg5Qm(vtWDlM;`v}?z#)ZN4c`YW&|aj zEa}YgZJj%XPh)b~YB1m(Kp;eLm1$9Cp8;Lt}K5&GY(LwEb_D-Qh>hyP87KFOi) z|BI(Jf#(JKypq$B^RLwD=-rb9o`;a7r_c*Z#NQ4ZZ5S}Pp- zNe=%+hb~i0o$5JsXavjclw z@6b=TM1-{teWF8eaOh_^^frfnrbFN2(Cyrp$r*=!w!?p`LqEr%?{erIlkIhnL-#BZ zVYfrCa_A2@ba!5|$DvPg`1d;WsSf?JDP=!7zU_$zkC&;gZcO`+Z+r5ACqU~hi~rPN zQ%AvbRH>sUZ&*24G5K9xUUbq=CwXN*v41-4Mx-q@X*%A5_gzp`>LgFG5j zd2DbE>T&>op32nJKBX4^eoSoe__n9~e#N>_Kfgd7bIa6o*^z3nzf28+zY_5-;>|rh z>bc|FPJ8h9EhDgr$Si(ob7t|&+cJxRald+FB3*}|f_l`_&P)ZETR~2hsL1lppa>kFv6dZUhhOZ!sPGN8|t1<1w!QaWb*$!jPpR89pXl(=_}E|+cZp*F!+Vk;+8 ziMiyG8fUvqrhK4U9S!*9Yq`=P6Yv=#6{H?P%>_4V|c z!hH_dX{!pXOE^K4TU~Zk&x2_}gBz3%%b;3RJJ(v9(den9RjMOdk>Gy5x5B>hIZ0vK{S!jm~ z(QX`X+_C8s(l2JAz0hZ~w?r|1L>}5PD$}=<{e$#fpnr|y3h3z1AF=eKpdUKH&?_>1 zTYNfvN!f{pJ~7kRZs~1hkW*&pV>5kCmc9$);Ia`ueS^@;UV&|({BqbYL$!9*!e5uE z3wS>&yTs7et_G1m$b=}hOwpQ zWz_Q}U_4AcybJws9>(Jf@dtdmJy0+90oI#7rSpt1d1^k-JOy|$Uw9@v4|3)~9_$`| zuSegb@4ORv*q-b|Rq%P)+%0|7-LsHw=^p9uW7MNkJNtN_Jyh0p2zd^|r)f;UUdB3K zjI@i55Bg0x{1trW$;3sUh&*0H?Se_-PuQPHhy2^nHrfU}>JRv^SD^D~I%M>Nk8+9a zVfcTY7jfp*K7cqReItCaPDj1SKbm9BS*9Jj-^{HGkAu3NrmsJf#oUp$3R~`Dd)#$Q zx6f;sjA>mzggD#h65z|BzgHvdp8{;6x7*3bIQ)UHFWl=f1~v#C-v%GlrLxc;?Cc=y z+3N4xkez*(8qB`S=x+_?0{ytR{PYo)cQwWy+Kt7vQ*fbyCg^^Gd|EfasPUP&t{?Fs z?`y07l=qc759)g#F(7O+@@i1t1z9^+{Alfv{<5C@WswV{Z`_t2Z}aPm+|!lM73?$D zMa{ec{e^fE&>eM)erxQw+t@tQfMFf}jw+$)aS8Red2R^hd4ab0wP;r~gZT*O&L~6A zFU0S=<*fiUevhB3gki|dvS zv5Oa39_#{b-(}Z}W*%ndJC$45Ye{GM+j476SMsIF^JM>~fw*kwMy%&cu`OT-HdW}eMC zEy}BvIQxB!`l=n#)sHf(V6&c5gPgPa>*0_rn^%vq6T16!KD5=4e7A5N7QvbV{!;p- zo`Yk~sD0Kc#uj2sQzv$vE%p#$8;>-4`37)ej^2a(N7Y*FIVkAN%~yM3*SNoxueLzG z-V?jjU(dH+wMjnW(`9QM&=uAWoQseS9=6RP*cj@uM|q>Wj-s7p&cBCi|AS})*4tfY zUk0Az+m>L>V%Ds7j#e8%J$v;UOWQ>)>W{utGY#z&y*1Ovd5bPLHoEH_kP%fk_f1ya zulw}cv${Oc4bNF!XlsKrrn;*1SfP4!8Q^2u-I!z9GHv=&&bwH*ZsXHvbCHTnO^o%+ zj_%qE`NWj>pJvWsU$a)uZs2utM_JJ|BXL>lFQAf5v&wT^E zPXHWMs4L_B@Bt^HY|35&8~I!O*pr{YKBdal$-1wxPBEifjH5l5DYbEybYZLu@cwj`0DQ{=oJ{ybxxt$*9jtSD$Zw)Ym8HW|ZM!Z0B0zpCQ|} z6?HW_tTXVcOsvP(7v?i)$I5)!X6&@Ob6q>sZ8}BuU=G(amF;@o(6Y3B0SEhuwO3nq#*E#A3Tqv< zb;QOij4jNU`m#^auCU9a*P%0ozJu7O2xj2G`o!P|9^Mmw^Ph8?CvO){{}$}9{kM%h zX0ZzPCoM*-=~x%GpT;b8iM$QW-xz{f_Fd5TsdL(S7Hd9)cW@oSesv-15cX4O>wx}c z@5SoBmU9BE39SD6StrH?^9zHfuT)vO=>JDTPq2>U$2yq%GUy|({s-GcXnKuU{VAp}PHWNZo>c+_pit z>3@T&Hos4Ry4Sn|b=BimCcm%Q3tmf$VBDrp@UVBuP>+$dqZaCP-QQw85%_l-k8r#N zAM)uiiuy8MhBjrWuun07xq{`V&%-ksGk1uM?dpeKJjd4@-zN2O*5_l4E_K^J0^8`H zlnIWTA$yE2H)F|}kWF94xe1?%aE^=l$8+%AJy`qp;9032x3oViAk+0{S=yxaeZ_cD zH;gsfrzecP+O@LrAGS`g(KTiquFTJe{>AKpN?**;@4%JihVb|21EKF+Rdb6KaKS`8tIB+-^`}JX4AQ^#&m?- zZ@1~cw&}CWO*%q1KW$FS--Ep^=11tJ|JL&&iUb8)V1 zF&96n>mo0HS?(T-2;F;qDQ*V>b zd`}q}J-F94qSz;?1IS<)1rG9+Wq_CS#IwpXTVUHTWH@@pT#1C0NgWL;j9e`Qe9q{}Z*nK=$Gf~E6m~+3% zv4Zt=c&msF<15E6*6A71XF0KfcLaESj0MJDi;?TFgJmPI_f&3hX2*BkPMnbY4V2Nv zcDoqmTx`m@pkUiqbbVCX9*dR<{TC0_Nk8;eW#?Piw~C67<=$=Y|DX)EIo4dPbAJA> z^!&dcJRZjhgj)C11?m~){9b&*F=A#q>n*x+ea0I${iviPbbZDVKfSQe7_jL@e8!75 zy@=0v!KN4S8PD4EB0ggu(@`(&Gf>Zf&tTm38H~F=gK_!{vH$GIubhe3?klyeF%ABs z2J*Eo8Gl6$Zj-n=xU;W{`R0PJkcmIh}pO8FOwl zzMxXCnG26SdwkoJwd?n2_n6!3I^KXfa-I*H)B8y%1N~>0@+!NYr0rr(e-Qizuh~!X z=ZwJHk3H{Vm@fuq?6ZR>){_~Vsfq83Z1VPJ&WxZw?ANM$$CYg?h;xIMC|+RigMwy>XLOxULTyy@>d=v(2l9s-T~1K6uKbU#9(`!%Z+U~cVdH(6H%?jxui*}v?Ih?a_&Bnc(B6Ea%eWacI`6}zdHj;Hu zmH0#G@HLca{HN^Y!Pf-VK)ru7>j_=gzp-vxQ)~?cJbY%$_YjKJaWU#;-$h6mo2*&D zdZSHq&^_LxF6KM)^$x9-GNE-ugmLJ>uQzq#)|+EK?3m9`ZkBp~mHI&&EAIOMi{8(M zuc0l(|bi12OGw^e~f;H$Mrb+nQi-{;5iZHYg>XJBfswJYC_ll zg1+o}fb$Stci`o{Uej}J1=-;>eYnoucpg86enXzuRkOZly}kPCc4ynl`pmYwI)XJX zo=04UHSaC14xn4siO-UV(XTW74xddGtaI;#u1~sk2CuF&{F#2{#&w9(-uZf)HCVXb zF|?6>22P*o`;k+-+H@I#ec3$ zUcAPV!M@W-j!%X6>orez5uRI|_kCoIg}%Ms9%HOy<^xBJ5N*8WDu$P9hYc<3K{ zdQIX3K2F=eT5aW+iI>jJyKdmY{d?IT750+xe%n_1h!)z)#ec(AWUg*~W9=A}iTS1V zjs2rFhP;p9e983@ZRJGdxh}S~AAK6OwQ&q(Ag=ei;lJrCu4LOVFUNn( zO*z((4t?uT&)t~5E4ZHfG}@nhWo@WC(~kz{W*_s@F*nocSkH0KQs#aimUO(i*VT{o zD)f1jxsWo|3qr$O!>1h(8paBr_M*@*|Ej_s2IzyjzL4Q@{zJbj^+)_YdK|twww3)s zEmC|BaqC<0kJYa`&t;Kc=1E(#xYxc3Wq8}wS!nx{bzJQ05avjDh7x#}?@w<)Z+H&p zzRQJoy6_B1-anMF0`2#jwmZ}X>ma-rqMu>y((eo*u3)pAGvR$#u7z~p(sMX{kG4R2 zYMVx%WZQC{Hc%G50MEFwb~xdJ{tG!ypjBh1b$u8%kb95pyS^u6r+kl*a}xC95!`ph zeH8co1^6KEHdzb)RUWl7*26iZUc*=%>h!Mn(-z+&{t9?(y@eJ)!(~GYjoM3-%=Y(=o0g5!ZS`sOPo__1qR=Ic!+-%e+?K!v?jEkaGu3zqH>|fFU!iOI`UG*G7xdY=>&ta5}{?aq)2IH4t6E$Z*FZSJv zhvBO@N2Twg?lvM$o(k&j6wt>QI_IRMk7iyxn}+ReQoA#M=lcWjGJp0T%o+rE{P$)H z;p#_um^1Zw>~ph#ah8$kVLrk!g!rm-8bb6Nmg$Y{J(Id24r0?|{e`GA)<2ZR@`;o8 z+!xSvsTZF~v5t1Xz{e;1e*M%F>N=_1=!v|QtSe}d?USws&M|6x+yw2EQBSao$7S+r2zKFX0}eUlw#|$BHb<*E)eLzaF?JKe0^0`hj)eTEf!o zGZMU~0pBq4!Q)SJjcfMo58~PKp^?bzj$;>_am+po_vq_K8omDz;{fV0h_VJz{;TVa zz6OD7(9)rUXZTD-zo+w_FY9-XR&kCgGJb=7l>U(!y^e_XPQ?4OvBB^DK6m#*+7jj{ z*&lzoulI!R*C~HG<}d@`<=iE5Y{m25Z+G>nBY*#THu>Y%dvD9W?&)`*S3EC!Si7Rx ziP%pm*YAYf51Cv`5NqVvvgfmd_x07@_}J^+>E~b1OdOEB%b(9;9@Y;#%;MSd$B<`| ztQm+A@9Xp~vdJ2MxIdS__Np91emlrQyKLz}eqsx>@2z%yiA5M4tq{YT(AH_Jc1 zZRLY#E7LC=yF**?xp|=db-$#&0t4+q`eo%P>XLNWhVCC_#s;nJ`L-64PaCzeXmfra z7rO3;v{%|1>{8n=ZIxxghKWJ)Wl9k2{^!|E;_`O3cf$SO#&wO5Az9jIngq--GY@TlH}3tewcBd{FHwPeh+Ir_|#0zN706{-prW_ z&)8X}eHJm97x z9}B0*vNNOh{cYR)b$Ujn^~rQv?o4iC0}+{1~v&FJ02m%4vc~KqW$Wem-!SB#pZBA#dcK-nsOPJb7MXWVi1+V+ z$4%p2a{~K`Rp6bBb&CJ4iG5eXyj$}O->m`Po~msAoq_1A{5vP(U≮|$k69ILqI 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 cx5Q?h*4cCO9_5c6? literal 0 HcmV?d00001 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(不进负值)。