diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md index 0ee5456..0c42c4a 100644 --- a/DEVELOPMENT_PLAN.md +++ b/DEVELOPMENT_PLAN.md @@ -2,6 +2,21 @@ > 目标:把 R3 Pro II 打造成可扩展的"第二系统"。**MeowOS 作为主控层优先启动、拥有屏幕与输入;HibyOS 播放器降级为 MeowOS 启动的一个内置系统应用**(Android home/launcher 模型)。MeowLauncher(即 MeowOS 核心)、MeowAPPs、MeowPanel 均为其组成部分。 > 本文档是路线图与分阶段计划。当前技术档案见 `ANALYSIS.md`,应用规范见 `APPS.md`,框架协议见 `FRAMEWORK_DESIGN.md`。 +> **⚠️ 本文档为公开路线图,自身已过时**:里程碑/阶段划分以**实际进度为准**(权威现状见 `devdocs/WORK_STATE.md`,本仓库 gitignore 不入库)。截至 **M2.61(0.7.56)** 已达成;下方「当前主线」为实际推进序列,**按依赖因果排列,非平行**;旧「阶段拓扑序」仅作背景参考,勿按它安排开发。 + +--- + +## 当前主线(按依赖因果 [源:2026-08-30]) + +> 每项必须在其依赖落地后才推进(不在依赖未满足时抢先实现上层)。✅=已达成。 + +1. **阶段 B 通知中心 UI(进行中)** —— 依赖 M2 通知存储 + SDK 控件基座 + B2a 后端。顶部快捷按钮/亮度滑条走 `meow_settings_*` 零旁路;通知列表读 launcher `meow_notif`;手势(状态栏下拉开、上滑/点外关、左缘返回关)。 +2. **启动器 SDK 改造** —— 依赖上游 SDK 控件基座(阶段 B 沉淀的 MD3 基座)。状态栏收敛单一 SDK 全局(launcher 退役 `draw_statusbar`);主界面/设置页/电源菜单迁到 SDK 控件;MD3 化、日/夜主题、全局 UI 统一。(见 FRAMEWORK_DESIGN §10.4) +3. **系统基建线:meow_svcd 隔离 + meow_sysui 总监** —— 服务全部子进程化(fork+exec+setpgid)+ svcd 崩溃隔离;新增 meow_sysui 系统进程总监统一监管 launcher/svcd/mdns/媒体服务/MeowPanel(见 FRAMEWORK_DESIGN §10.1)。**与 1/2 串行**(共享 launcher/SDK 文件,避免并发改同批文件)。 +4. **M3 导航栈 + M5 通知整合 + 媒体会话** —— M3 依赖阶段 0 让位退役;M5 依赖 M2 存储 + B 的 UI + M3;媒体会话(MPRIS)依赖 M3+M4+阶段 0。 +5. **SDK 缺失件**:`meow_config_*`(app 配置读写)/ `meow_media_*`(媒体会话)/ 崩溃捕获·看门狗。 +6. **蓝牙/WiFi 子页 + MeowPanel** —— 均消费 B2a 后端 / 通知后端。 +7. **文档一致性收尾 + 推 main**(审后)。 --- diff --git a/FRAMEWORK_DESIGN.md b/FRAMEWORK_DESIGN.md index e8a6d1d..1ec4cf5 100644 --- a/FRAMEWORK_DESIGN.md +++ b/FRAMEWORK_DESIGN.md @@ -111,6 +111,63 @@ M0 IPC 骨架(含 meow_ipc 测试钩子)→ M1 路由表+解析 → M2 通 --- +## 10. 进程模型 / Supervisor 树 [源:2026-08-30 定案] + +> 定位:本文档 §1-§9 聚焦「MeowAPP ↔ 启动器」的 Activity/通知 IPC;本节补齐**系统层面进程架构**——各系统进程的职责边界、监管关系与故障隔离,落实「一个崩不全部崩」。与 §5 后台服务保活**互补**:§5 管 MeowAPP 注册的**后台服务**,本节管 **MeowOS 自身系统进程**。跨文档:MEOW_APP_SDK §6.1、NOTIFICATION_SYSTEM §9、docs/MEOWOS_SPEC_REF §3.1/§5.1。 + +### 10.1 进程角色与 Supervisor 树 + +| 进程 | 职责 | 监管关系 | 崩溃隔离(核心诉求) | +|---|---|---|---| +| **meow_sysui**(新增,meow_svcd 托管) | **系统进程总监**:监管所有系统进程,监控存活 + 逐个拉起 | 监管 launcher / meow_svcd / meow_mdns / 未来媒体服务 / MeowPanel | 自身**最小依赖、不渲染、不承载重逻辑** → 最稳;**一个进程崩→只拉起它,绝不级联、绝不拖垮自身** | +| **meow_launcherd** | **系统 UI 渲染宿主**:SDK 全局状态栏、通知中心(下拉)、电源菜单、全局左缘返回/滑条;另含启动器主界面、设置页 | 被 sysui 监管;与 APP 进程独立 | 与 APP 渲染进程独立(见 §10.2) | +| **meow_svcd** | **服务管理器**:每个服务 = 独立子进程(fork+exec+setpgid) | 被 sysui 监管;其服务子进程由 svcd 托管 | 服务崩→svcd 捕获(waitpid)+只重启该子进程;**svcd 自身绝不因服务崩而崩**(信号/超时/僵死隔离) | +| **APP 进程** | 只渲染自己内容(经 SDK 语义事件) | 框架(IPC/launch)管理 | 崩→不连累 launcher / sysui / svcd | + +**Supervisor 树(目标架构):** + +``` +meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最稳) + ├─ meow_launcherd 系统 UI 渲染宿主(主界面/设置页/电源菜单/全局状态栏/通知中心/全局手势) + ├─ meow_svcd 服务管理器(服务=独立子进程;逐个子进程崩溃隔离) + │ ├─ meow_mdns mDNS 应答器(板载 /usr/data/meow/services.d/mdnsd.conf,trigger=boot) + │ └─ 未来媒体服务 / 其它 services.d 服务(TF MeowOS/services.d/…) + ├─ 未来媒体服务 / MeowPanel(后置) +``` + +> **托管关系澄清**:定案中的「meow_sysui(新增,meow_svcd 托管)」指 sysui 由**开机启动链**拉起(现网 S89 init.d/S90 bootmenu 归属,后续归 sysui 自身),并居于 svcd **之上**为总监——**不是** svcd 的子进程。监管方向固定:**sysui 总监 → svcd 被监管 → 服务为 svcd 子进程**。即便将来引导顺序先起 svcd 再起 sysui,此方向不变。 + +**监管语义(权威)**: +- **一个崩、只拉起它**:任一被监管进程崩溃(非零退出/被信号杀)→ 所属 supervisor **只重启该进程**,不级联重启、不影响相邻进程、不拖垮 supervisor 自身。 +- **svcd 崩 → sysui 拉起**;launcher 崩 → sysui 拉起;mdns 崩 → svcd 拉起(或 sysui 兜底)。 +- **「一个崩不全部崩」= 每进程独立 + 总监逐个拉起 + 单项故障不扩散**;「服务崩溃不导致 meow_svcd 崩溃」= **服务全部子进程化**(fork+exec+setpgid,svcd 只 waitpid 感知,绝不 dlopen/共享内存接服务)。 + +**现网 vs 定案(迁移说明,避免矛盾)**: +- 现网:launcher 由 bootmenu `execv` 起动(S90meow_bootmenu);svcd 由固件 `etc/init.d/S89meow_svcd` 自启(`-f` 前台)。二者**当前互不监管**。 +- 定案:改为 **meow_sysui 统一监管** launcher/svcd/mdns/媒体服务/MeowPanel。属**系统基建线**(渐进迁移,非一次性推翻现网 S89/S90)。在此落地前,现网的「bootmenu+init.d 各自拉起」仍有效,但**不得**新增依赖打破「进程独立 + 崩溃隔离」约束。 + +### 10.2 系统 UI 归属与进程隔离 [源:2026-08-30] + +- **系统组件渲染归系统进程(launcher)**:状态栏 / 通知中心(下拉)/ 电源菜单 / 全局左缘返回与滑条等**全局叠加层**,一律由系统进程(meow_launcherd)负责渲染;**APP 进程不渲染系统 UI**(跨引用 MEOW_APP_SDK §6.1 ③「系统级控件仅系统内部」)。 +- **SDK 提供系统组件 API 仅供系统进程链接调用**:SDK 内保留系统级控件的内部接口位(§6.1 ③);**用户 App 拿不到**、**不可自建悬浮层/全局叠加**(会引入遮挡、冲突、性能与安全风险)。App 这类需求只能走 SDK 标准能力。 +- **进程隔离**:SDK 负责**系统组件渲染**的进程(launcher)与负责 **APP 渲染**的进程**相互独立**——APP 卡死/崩溃时,系统 UI(launcher)**不崩**,始终可息屏/返回/下拉通知中心(对应「物理键全局接管,APP 禁映射」APPS §3.4.1)。 +- **APP 侧职责**:只画自己的内容;经 IPC 上报**全屏声明**(`fullscreen`/`route..fullscreen`)、**内容区域**(inset)与**前台 activity**;系统 UI 据此做叠画(复用 fb 双缓冲 `fb_backup/restore/pan`)与 hits 命中分流(MEOW_APP_SDK §6.1 交互分层:③ 系统全局 → ② App 全局 → ① 当前 Activity 页面控件)。 + +### 10.3 通知中心归属 [源:2026-08-30] + +- **通知中心 UI 渲染主体 = 系统进程(launcher),非 APP**(跨引用 NOTIFICATION_SYSTEM §9)。 +- **数据**:通知列表经 IPC 从通知存储/通知服务读(`MSG_NOTIFY_LIST` 重同步等),launcher 只做 UI 呈现与手势。 +- **交互**:状态栏下滑展开、上滑/点面板外关、左缘返回关(与全局左缘返回/顶部滑条按起始位置分流,见 docs/MEOWOS_SPEC_REF §5.1)。 +- **协议/约束**:遵守 freedesktop 字段、配额(每 App 4 类型×每类型 4、媒体单例)、TTL、转储(10s+10MB ring)、urgency/icon/progress(见 NOTIFICATION_SYSTEM §5-§8)。 + +### 10.4 启动器 SDK 改造 [源:2026-08-30] + +- **状态栏收敛为单一 SDK 全局**:launcher 现状私有 `draw_statusbar()` **退役移除**,统一走 SDK 全局状态栏(`meow_ui.h` §3.1;28px,默认显示,APP 声明全屏/`fullscreen` 才隐藏/收合)。 +- **launcher 主界面 / 设置页 / 电源菜单全部迁到 SDK 控件**:逻辑/渲染走 SDK(MEOW_APP_SDK §6.1 ② App 跨 Activity 控件、§5.4 A① 分组设置表单、§5.7 B④ 面板等模板),MD3 化、日/夜主题(`display.theme_mode`)、全局 UI 统一。 +- **不让 app 自建叠加**:app 一律经 SDK 标准能力;系统级叠加(状态栏/通知中心/电源菜单/全局手势)只在系统进程内实现。迁移后 launcher 与 APP 用同一套 SDK 渲染地基,观感/生命周期统一。 + +--- + ## 附录 A(用户追加):后台运行 / 后台播放权限 **需求**:允许应用声明后台运行、后台播放等能力,避免播放类应用切到后台被直接终止。hiby 播放器作为系统应用单独适配。 diff --git a/MEOW_APP_SDK.md b/MEOW_APP_SDK.md index e6e0cfb..2b28101 100644 --- a/MEOW_APP_SDK.md +++ b/MEOW_APP_SDK.md @@ -543,7 +543,15 @@ void meow_ui_global_remove(int id); - **对象复用(非销毁)**:§6.1 的「Activity 内对象池复用」本轮**从简落地**——控件对象不随 pop 销毁,SDK 不维护独立对象池;App 自行持有控件指针(hello 用静态全局),弹栈回来自动恢复命中(零重建)。注册表按**指针去重**(`live_btn_register` 查重),故反复 push/pop 不重复注册、不泄漏。 - **② 为可用骨架**:SDK 维护全局控件列表(`meow_ui_global.c`),渲染帧 `meow_ui_global_render()` 在 active activity 内容之上、全局状态栏之下调用 render_cb;触摸 TAP 先 `meow_ui_global_hit()`(返回 1=消费),否则落 `on_tap`。无 z 序细分/无按压态管理/无命中区排布(待 B2 细化);同 id `global_add` 幂等。 - **③ 系统级(状态栏/通知中心/返回滑条/电源菜单)**:未开放任何公共悬浮/叠加层 API,既有状态栏/返回滑条逻辑原样保留。 -``` + +#### 系统 UI 归属与进程隔离(源:2026-08-30 定案,跨引用 FRAMEWORK_DESIGN §10.2) + +> 把「系统组件归谁渲染」在 SDK 规范层面钉死,明确 **APP 进程不渲染系统 UI**、**SDK 负责系统组件的进程与负责 APP 的进程相互独立**。 + +- **系统组件渲染归系统进程(launcher)**:状态栏 / 通知中心(下拉)/ 电源菜单 / 全局左缘返回与滑条等**全局叠加层**,由系统进程(meow_launcherd)负责;**APP 进程不渲染系统 UI**。 +- **SDK 提供系统组件 API 仅供系统进程链接调用**:SDK 保留系统级控件内部接口位(上面 ③);**用户 App 拿不到**、**不可自建悬浮层/全局叠加**——App 这类需求只能走 SDK 标准能力。 +- **进程隔离(核心)**:SDK 负责**系统组件渲染**的进程(launcher)与负责 **APP 渲染**的进程**相互独立**——APP 卡死/崩溃时,系统 UI(launcher)**不崩**,始终可息屏/返回/下拉通知中心(对应 APPS §3.4.1 物理键全局接管、APP 禁映射)。 +- **APP 侧职责**:只画自己的内容;经 IPC 上报**全屏声明**(`fullscreen`/`route..fullscreen`)、**内容区域**(inset)与**前台 activity**;系统 UI 据此做叠画 + 命中分流(§6.1 交互分层:③ 系统全局 → ② App 全局 → ① 当前 Activity 页面控件)。 --- diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md index c0a26b9..1080b84 100644 --- a/NOTIFICATION_SYSTEM.md +++ b/NOTIFICATION_SYSTEM.md @@ -66,8 +66,13 @@ target_activity(可选, 来自 hints) / progress(可选 int) / urgency(0-3, 缺 - **启动时清板载 + 清内存通知表**:开机(含异常关机后)清理板载残留通知数据,并**清空内存通知表**(在既有「异常关机:开机时清理板载残留」基础上**补充内存表一并清**);正常关机/重启前先转储未上卡通知再清板载(沿用既有规则)`[源:用户 2026-08-30]`。 - **无 TF 卡**:系统可运行(内存态通知即可),不依赖卡。 -## 9. 通知中心 UI(阶段 B) -- 状态栏下滑打开通知中心(用户已定),列表显示通知(app 图标 + summary + body + target Activity),点击按路由表解析跳转(M3 导航栈),下拉关闭/清除。**此部分依赖 M2 存储 + M3,后置。** +## 9. 通知中心 UI(阶段 B)—— 归属定案 [源:2026-08-30] + +- **渲染主体 = 系统进程(meow_launcherd),非 APP**:通知中心 UI 由系统进程负责渲染(复用 SDK 控件/状态栏基底);**APP 进程不渲染通知中心**(系统级控件仅内部,见 MEOW_APP_SDK §6.1 ③、FRAMEWORK_DESIGN §10.2)。 +- **数据来源**:通知列表经 IPC 从通知存储/通知服务读(如 `MSG_NOTIFY_LIST` 重同步、`target_activity`/`progress`/`icon` 等字段),launcher 只做 UI 呈现与手势,不各画各的。 +- **交互**:**状态栏下滑展开**(用户已定);**上滑 / 点面板外关**;**左缘返回关**(与全局左缘返回/顶部滑条按起始位置分流,见 spec-ref §5.1)。列表显示通知(app 图标 + summary + body + target Activity),点击按路由表解析跳转(M3 导航栈),下拉关闭/清除。 +- **协议/约束**:遵守 freedesktop 字段(summary/body/app_icon/actions/hints/expire_timeout/replaces_id)、配额(每 App 4 类型×每类型 4、媒体单例)、TTL、转储(10s + `MeowOS/notifications/` 10MB ring)、`urgency`/`icon`/`progress`(见 §5-§8)。 +- **依赖**:M2 存储 + M3 导航栈(点击跳转)+ SDK 控件基座。**阶段 B 进行中。**(叠加/与播放器帧备份的互斥边界见 DEV_NOTES #16。) ## 10. 分步实施建议 1. **M2a 通知存储 + 原生 IPC**:按上面的配额(category 级)+ TTL 10s 转储钩子 + 系统事件源(系统作 App)。→ 可先落地。 diff --git a/README.md b/README.md index d3c4206..26246a7 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,303 @@ # MeowOS for HiBy R3 Pro II -把 R3 Pro II(君正 X1600E,HiBy OS / Linux 4.4)打造成可扩展的"第二系统":MeowOS 作为主控层,双系统互斥(引导选单),HibyOS 播放器由 MeowOS 托管,MeowAPPs / MeowPanel / Activity 框架持续扩展。 +> 把 HiBy R3 Pro II 便携播放器(君正 X1600,MIPS32r2 / mipsel / musl 静态软浮点,480×720,无 GPU 软渲染)变成一个可扩展的「第二系统 + 应用框架」。 +> MeowOS 与官方 HibyOS 共存于同一台设备:开机由引导选单二选一;启动后 MeowOS 作为主控层,提供自己的桌面 UI、应用框架、服务管理与系统组件。 +> +> 许可证:Apache-2.0(见 [`LICENSE`](LICENSE))。本仓库为非官方第三方项目。 -## 核心文档(先读这些) +--- -> **总索引(单一来源)**:`docs/MEOWOS_SPEC_REF.md` —— 各路径/接口/功能/约定的权威速查,含「已知矛盾/待确认」清单。 +## 1. 项目简介 + +**MeowOS 是什么**:一个运行在 HiBy R3 Pro II 上的自制系统与运行时,它提供: + +- **双系统共存**:官方 HibyOS 原样保留;开机引导选单决定本次进入哪个系统(MeowOS / HibyOS),两者互斥运行,任一时刻只跑一个。 +- **应用框架**:一套 C 应用 SDK(`meow_ui`),第三方 App 用统一控件 / 输入 / 主题 / 字体 / 通知 API 开发,无需自读帧缓冲、自读输入、自管返回——把每个 App 都要踩一遍的坑收口到框架里。 +- **系统组件**:状态栏、通知中心、电源菜单、全局左缘返回手势、主界面(应用列表)、设置页、服务管理器、mDNS 广播。 +- **进程隔离**:系统 UI 渲染进程与 App 进程相互独立,App 卡死不会冻结整个 UI。 + +**面向谁**: + +- **R3 Pro II 玩家**:刷入后获得可切换的「第二系统」,与官方系统互切使用。 +- **开发者**:用 `meow_ui` SDK 编写 MeowAPP,部署到 TF 卡即可热更新,无需重刷固件;也可参与固件 / 框架源码的开发。 + +**硬件基线(贯穿全文)** + +- SoC 君正 X1600;CPU MIPS32r2(little-endian,`mipsel`);libc musl(**静态**链接,soft-float)。 +- 屏幕 **480×720**、24-bit RGB(内部转 RGB565);**无 GPU**,全部 CPU 软渲染。 +- 帧缓冲 `fb0` 为**双缓冲**(虚拟高 1440 = 2×720,显示帧由 `yoffset` 决定,需 `FBIOPAN_DISPLAY` 翻页)。 +- 板载存储紧张(`/data` 约 16MB):所有大件(日志/字库/媒体/应用)一律写 TF 卡,板载只存 KB 级设置。 + +--- + +## 2. 核心架构 + +### 2.1 进程模型(Supervisor 树)——「一个崩不连累全部」 + +每个系统组件都是一个独立的静态进程(固件 `/usr/bin`,只读 squashfs): + +``` +init (rcS) + ├─ /etc/init.d/S89meow_svcd 开机自启 meow_svcd(服务管理器) + ├─ /etc/init.d/S90meow_bootmenu Supervisor / 开机引导选单 + │ ├─(选 MeowOS)拉起 meow_launcherd ← 系统 UI 渲染宿主 + │ │ └─ 用户点 App → fork/exec MeowAPP + meow_watchdog(每 App 一个) + │ └─(选 HiByOS)exec hiby_player + └─ meow_svcd 托管的服务(如 meow_mdns)= 独立子进程 +``` + +- **当前 supervisor = `meow_bootmenu`**:开机引导选单(决定进入哪个系统),并接管关机 / 升级;系统组件由它 / init 逐个拉起。 +- **目标架构(设计定案,见 `docs/MEOWOS_SPEC_REF.md` §4,属「系统基建线」)**:新增统一的 `meow_sysui` 系统进程总监,监管 launcher / svcd / mdns / 媒体服务 / MeowPanel——一个进程崩只拉起它,绝不级联、绝不拖垮自身。这是定案与渐进迁移方向,**当前固件仍以 `meow_bootmenu` 为 supervisor**。 +- **隔离保证(现已落地)**:系统组件各自独立进程;App 进程与系统 UI 渲染进程独立;svcd 的**每个服务是独立子进程**(服务崩只重启该服务,不拖垮 svcd)。任何 App / 服务崩溃都不冻结系统 UI。 + +### 2.2 双系统引导(bootmenu) + +- `meow_bootmenu`(`/usr/bin/meow_bootmenu`,只读 squashfs)开机绘制轻量选单(fb 绘制 + 触摸/按键),选中后 `exec` 对应系统。 +- **boot.default 权威文件 = 板载 `/data/meow_boot_default`**(单值、O_TRUNC、不累积):`0 = MeowOS`、`1 = HiByOS`。bootmenu 读它决定默认高亮项;`boot.default` 设置变更同步写该文件;`meow.conf` 不再承担 boot_default。 +- **双系统互斥**:任一时刻只跑一个。HibyOS 播放器需**开机连续内存**(`sa_hgl_dma` 1.32MB DMA)才能启动,不能"杀+重拉"。因此: + - **HibyOS → MeowOS**:用热键切换(杀播放器即可,MeowOS 界面不需要 HGL,接管无缝)。 + - **MeowOS → HibyOS**:只能**重启回到选单**(播放器需开机连续内存再次启动)。 + +### 2.3 launcher —— 系统 UI 渲染宿主 + +`/usr/bin/meow_launcherd`(源码 `patch_v2/meow_launcherd/`)是 MeowOS 的核心进程,负责渲染**系统级 UI**: + +- 状态栏(28px:时钟 / 电量 / WiFi / 蓝牙 / 音量); +- 通知中心(顶部下滑打开); +- 电源菜单、全局左缘返回手势、全屏提示线; +- 主界面(应用列表)、设置页。 + +**渲染与 App 进程隔离**:launcher 是系统进程,App 是独立进程。App 卡死/崩溃只影响该 App,系统 UI(状态栏/返回/通知中心)始终可用。 + +> ⚠️ `meow_launcherd` 本身就是系统进程,**不要远程 kill 它**——否则会整机重启。 + +### 2.4 SDK(meow_ui)—— App 开发框架 + +`patch_v2/sdk/` 构建出 **`meow_ui.a`**(mipsel 静态库,内含 `meow_font.o` + `stb_image` 图片解码)。App 只链它 + `libz.a` + `-lpthread`,用 API 写业务,不重复造轮子。 + +- App **不** `open /dev/fb0`、不 mmap、不 ioctl、不 `open /dev/input/event*`;屏幕/输入由 SDK 在 App 启动时建立、退出时释放。 +- 渲染统一走 `back_buf`(RGB565)+ `meow_present(dirty)`:App 画到离屏缓冲区,`meow_ui_flush()` 时才单次原子 blit 到 fb0(等垂直同步 + 脏区,无撕裂、无频闪、不整屏重绘)。 +- 分层:绘制层 / 控件层 / 图片层(stb_image 解 PNG/JPEG/BMP)/ 输入层(语义事件)/ 主题 / 字体。 + +**三层控件模型(Activity 栈 + 控件归属)**——控件严格归属创建它的 Activity,不跨页面共享(根治「跨页残留」:旧按钮已不可见但仍被触摸命中): + +| 层 | 谁负责 | 谁能用 | 生命周期 | +|---|---|---|---| +| **① 页面控件** | 归属唯一 owner Activity | App | 随 Activity 栈;owner 在栈顶(active)才绘制 + 响应触摸 | +| **② App 跨 Activity 控件** | **SDK 托管**(逻辑 + 渲染) | App 可声明使用 | 钉在 App 的 Activity 栈**之上**,跨 Activity 常驻 | +| **③ 系统级控件** | MeowOS / SDK 框架内部 | **仅系统组件,用户 App 禁调用** | 随系统,不开放 | + +- **交互命中分流**:③ 系统全局 → ② App 全局 → ① 当前 Activity 页面控件;某层命中即消费,否则落到下一层。 +- **系统 UI 归属与进程隔离(核心)**:状态栏 / 通知中心 / 电源菜单 / 全局返回滑条等**系统级组件由系统进程(launcher)渲染,App 进程不渲染系统 UI**;SDK 系统组件 API **仅供系统进程链接调用**,App 拿不到、不可自建悬浮层 / 全局叠加。 +- 控件 API:`meow_ui_activity_push/pop`(Activity 栈)、`meow_btn/slider/switch/list_create`、`meow_ui_global_add/remove`(② 层)。对象不随 pop 销毁(复用),弹栈回来自动恢复命中。 + +### 2.5 服务 + +- **meow_svcd(服务管理器)**:类 systemd/systemctl,`/usr/bin/meow_svcd`,`etc/init.d/S89meow_svcd` 开机自启。**每个服务 = svcd 的独立子进程**(fork+execv),服务崩只重启它,不拖垮 svcd。 + - **双源 services.d**:板载 `/usr/data/meow/services.d/.conf`(Data 区,拔卡保留,权威优先)+ TF `MeowOS/services.d/.conf`(随卡存在/消失)。同名**板载优先**(TF 被屏蔽 + WARN)。 + - 目录式 `key=value`:`exec`(必填)/`args`/`env`/`enabled`/`restart`/`backoff_ms`/`max_restart`/`trigger`(`boot`/`card-mount`/`wifi-up`,默认 `card-mount`)。 + - 状态机 `DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;CLI(SSH 直呼):`status` / `reload` / `start|stop|restart ` / `pid`。 +- **meow_mdns(mDNS 应答器,自研)**:`/usr/bin/meow_mdns`,**IPv4-only**(该设备内核无 IPv6,完整 mDNSResponder/SAResponder 均无法运行),应答 `.local` 的 A 记录与 `_ssh._tcp` 的 PTR/SRV/TXT。由 `meow_svcd` 托管(板载 `services.d/mdnsd.conf`,`trigger=boot`)。 + - **hostname = `meowos`**(设置键 `system.hostname`,缺省 `meowos`;`S89meow_svcd` 开机 `sethostname`)→ 即 `meowos.local`。 + - 注:手机热点会丢弃设备→主机的组播应答,`avahi-resolve meowos.local` 在上网热点下会超时(网络层热点隔离,非应答器缺陷),主机可用 DNS 兜底。 + +### 2.6 通知系统 + +后端 = 通知存储 + 分发,遵循 **freedesktop.org Desktop Notifications**(`org.freedesktop.Notifications`)标准字段:`app_name` / `replaces_id` / `app_icon` / `summary` / `body` / `actions[]` / `hints{}` / `expire_timeout`,返回通知 id。MeowOS 扩展:`hints` 内 `meow.target_activity`(点击跳转)与 `meow.progress`(进度)。 + +- **来源**:MeowAPP(原生 IPC `MSG_NOTIFY`)+ 系统事件(抽象为 `app_id=system`,走同一配额)+ 外部桌面 App(D-Bus 桥 `meow_notify_dbus`,规划)。 +- **配额**:每 App 最多 **4 个**通知类型(category);每类型活跃 **≤4**;媒体通知单例(每 App 仅 1 条,可多 App)。系统事件同配额。 +- **TTL / 转储**:通知创建 **10s** 后追加写入 TF `MeowOS/notifications/`(CSV 行,滚动保留**最新 10MB**,不被电源事件清);正常关机先转储再清板载;开机清板载残留 + 清内存表;**无 TF 卡可运行**(仅内存态)。`urgency`(0-3)、`icon`、`progress`(实时进度仅改内存活跃槽,里程碑落盘,不每帧写卡)。 +- 通知数据模型补全(SDK→IPC→存储→dump 全链路):`meow_notify_send` 返回 id、`meow_notify_update_progress`、`urgency`/`icon` 字段。通知中心 UI 由系统进程(launcher)渲染,数据经 IPC 从存储读取(该部分/阶段 B/D,部分为规划)。 + +### 2.7 字体系统 + +- 多字体库在 TF 卡 **`MeowOS/fonts///`**(每套三件套:`font.png` + `font.idx` + `font.json`),由 `font/gen/gen_font.py` 生成(`--weight` 写字重、`--charset` 档位)。 +- 字体列表索引 **`MeowOS/config/fonts/index`**:launcher 扫描后落盘(`family,weight,path,label,tier,glyph_count` CSV);SDK 读该文件枚举字库,**不自行发现**。 +- `font.file` 设置键选默认套(缺省 `MeowOS/fonts/default/regular/font.png`);`font.json` 标 `weight` / `charset`(`common`≈4k / `gbk`≈20k+ / `full`)/ `glyph_count`;缺失件 → 该套跳过 + 日志。 +- 字库档位:默认用小字库(`common`≈4k);App 声明需大字库(`app.json "fonts":"full"`)才加载大字库;`font.load_full` 强制全系统大字库。 +- 多字体(`display.multi_font`):按字符类别(CJK/LATIN/NUM/…)逐字符路由,可各选一套。 +- **App 未自带字库 → 跟随系统 `font.file`**:launcher 启动 App 前把当前生效字体注入为 env `MEOW_FONT_PNG`/`MEOW_FONT_IDX`(绝对路径),SDK 优先读 env;无 env 回退卡默认 → 固件内置。SDK `meow_font_has_glyph(cp)` 预判当前字库是否含某码位(缺字回退/提示)。 +- 应用自带字库(`MeowAPPs//fonts//`,多句柄 `_h` API)为**草案/未实现**;当前 App 只能跟随系统字体。 + +### 2.8 设置后端 + +`meow_settings_*`(C API:`get/set` int/str/enum/bool + listener 变更通知)读写**板载 `/usr/data/meow/settings.conf`**(UBIFS,几十行、<10KB),跨换卡/掉卡保留配置。板载只存 KB 级设置;日志/字库/媒体等大件一律 TF 卡(无卡优雅降级)。 + +贯穿子系统的键(详见 `SYSTEM_SETTINGS.md` / `docs/MEOWOS_SPEC_REF.md`):开机 `boot.default`、刷新率 `display.refresh_rate`(60/30/15)、亮度 `display.brightness`(clamp ≥ `display.brightness_min`,下限 **1**)、息屏 `display.screen_off_timeout`(默认 30s)、主题 `display.theme_mode`、主机名 `system.hostname`(=meowos)、字体 `font.file`/`display.multi_font`/`font.load_full`、通知配额 `notify.*` 等。 + +--- + +## 3. 目录结构 + +> 标注「入库」= 本仓库已提交;未标注的为**本地构建 / 开发资产**(gitignore 不入库,需本地生成)。 + +- **`patch_v2/`(入库)**—— MeowOS 核心源码: + - `bootmenu/` 引导选单 + Supervisor(`meow_bootmenu`) + - `meow_launcherd/` launcher(系统 UI 宿主)、`meow_ipc`、`meow_settings`、`meow_font`、`meow_notif`、`meow_card_path.sh` + - `sdk/` meow_ui SDK 源码 + `build_sdk.sh`(→ `meow_ui.a`) + - `hello_app/` 示例 App(`hello.c` + `build_hello.sh`) + - `meow_svcd/` 服务管理器;`meow_watchdog/` 应用看门狗;`meowpkg/` 包管理;`uinput_tap/` +- **`mips-repo/`(入库)**—— mipsel 交叉编译软件仓库:`bins/`(`meow_mdns` / `mdnsd` 等二进制工具 + `build.sh` + `manifest.md`)、`mpk/`(`.mpk` 应用包)。 +- **`docs/`(入库)**—— `MEOWOS_SPEC_REF.md` 权威速查总索引。 +- **`font/`(入库)**—— 字形系统:`gen/gen_font.py` 生成器、`meow_font.c/.h` 渲染库、`demo/`、`src/`(字体源文件)。 +- **`bootlogos/`(入库)**—— 启动 / 关机 logo。 +- **`LICENSE`** —— Apache-2.0。 +- **顶层文档(入库)**—— `APPS.md`、`MEOW_APP_SDK.md`、`FRAMEWORK_DESIGN.md`、`SYSTEM_SETTINGS.md`、`NOTIFICATION_SYSTEM.md`、`TF_CARD_LAYOUT.md`、`DEVELOPMENT_PLAN.md`、`GESTURE_BACK_DESIGN.md`、`KEY_CONTROL_DESIGN.md`、`PERF_SCHEDULER.md`、`MEOWOS_API.md`。 +- **本地构建资产(gitignore,不入库)**—— `scripts/build/`(固件打包脚本)、`fw_apps/`(固件组装产物)、官方固件基线 rootfs、交叉工具链、本地开发文档、设备密钥等。详见「构建」与「部署」。 + +--- + +## 4. 构建 + +整条构建是 **mipsel-linux-muslsf 静态交叉编译**链,需要本地交叉工具链(`zig-mipsel-cc`,目标 `mips-linux-musl`)+ `mipsel-linux-muslsf` sysroot(这类工具链为本地大件,gitignore 不入库)。 + +**1. 构建 SDK**(入库脚本) +```sh +patch_v2/sdk/build_sdk.sh # → patch_v2/sdk/meow_ui.a +``` +> 注:SDK 编译期引用 launcher 的 `meow_ipc.h` 共享协议头(`meow_notify_ipc.c` 复用同一份,杜绝两端漂移),属**已知设计**;运行时 App 只链 `meow_ui.a`(含 `meow_font.o`),不会链入 launcher 符号。SDK 库**不进固件**,只随 App 链接。 + +**2. 构建示例 App**(入库脚本) +```sh +patch_v2/hello_app/build_hello.sh # 链 meow_ui.a + libz.a + -lpthread → hello(静态 MIPS32r2) +``` +产物放 TF 卡 `MeowAPPs/os.meow.hello/`(见「部署」)。 + +**3. 构建固件(本地流程)** + +固件 = 官方 v1.4 固件基线 rootfs + SSH 工具集 + MeowOS 系统组件(`meow_bootmenu` / `meow_launcherd` / `meow_watchdog` / `meow_svcd` / `meow_mdns` + 相关 init 脚本 + `/etc/meowos_version`)。特点: + +- 打包脚本 `scripts/build/build_fw_apps_m2NN.sh` 是**本地构建工具(gitignore 不入库)**——本仓库只提交源码与文档,需本地交叉工具链 + 官方固件基线 rootfs 才能复现。 +- **rootfs 上限 45,088,768 B(≈45MB)**;产物 `fw_apps/r3proii_m2NN.upt`。 +- `/usr/bin` 各组件为静态 MIPS32r2;版本号以 `/etc/meowos_version` 为单点。 +- 外围 CLI(curl/socat/tmux/jq/…)已迁 TF 卡 `MeowOS/opt/bin/`(不进固件);固件**不含** SDK 库(`meow_ui.a` 只随 App 链接),`hiby_player` 保持官方原样。 + +> 第三方开发者通常**不需要**重打包固件:用 `build_hello.sh` 类似方式构建 SDK App 后直接部署到 TF 卡即可热更新(见「部署」)。 + +--- + +## 5. 部署 / 刷机 + +**刷机(整机刷新 MeowOS 固件)** + +- 打包产物名 `r3proii_m2NN.upt`;要刷机时把它**复制为 TF 卡根目录的 `r3proii.upt`**。 +- 关机(failsafe)后按住**音量加 + 电源**进入升级模式,从卡根 `r3proii.upt` 升级。避免使用播放器 UI 里那个会卡死的升级按钮。 +- 回退:用官方固件重刷,或从 `backups/`(NAND 全量备份,本地)恢复。 + +**部署 / 热更新 App(无需重刷固件)** + +- TF 卡布局见 `TF_CARD_LAYOUT.md`。App 目录 `MeowAPPs//`(`package` = `app.json` 的 `package`,如 `os.meow.hello`):`app.json` + 静态 MIPS 二进制 + 资源;数据在 `MeowData//{data,cache}`。 +- **热更新**:把构建产物放入 `MeowAPPs/os.meow.hello/` 即可;launcher 每次打开菜单重扫卡,立即可见。App 二进制/资源在卡上;拔卡会优雅降级(应用列表"未插卡"/字体回退/日志回退内存环),不崩系统。 +- 应用日志:`MeowOS/logs/apps//logs/_.log`(每次启动新文件,单文件 2MB 轮转、每 App 总量上限)。包管理 CLI `nyai`(放 `MeowOS/opt/bin/nyai`):`nyai install /` 等。 + +**TF 卡目录(关键)** +``` +/usr/data/mnt/sd_0/ +├── Music/ # 音乐库 +├── MeowData//{data,cache} +├── MeowAPPs// # 应用(app.json + 二进制 + 资源) +└── MeowOS/ + ├── fonts/// # 多字体库 + ├── config/fonts/index # 字体列表索引 + ├── notifications/ # 通知转储(10MB 滚动) + ├── logs/ # 系统 / 应用日志 + ├── services.d/.conf # 服务配置(TF 源,与板载双源) + └── opt/bin/ … # 外围工具(nyai/curl/…) +``` + +--- + +## 6. 开发 SDK App + +### 6.1 app.json + +```json +{ + "name": "Hello App", + "exec": "/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello", + "package": "os.meow.hello", + "version": "0.1", + "author": "whoever" +} +``` + +| 字段 | 必填 | 说明 | +|---|---|---| +| `name` | ✓ | 列表显示名(v2 仅支持 ASCII) | +| `exec` | ✓ | 可执行文件绝对路径(必须为**静态** MIPS32r2 ELF) | +| `package` | — | 反域名权威 app_id(= 目录名;缺省回退目录名,两者不同 → WARN) | +| `version` / `author` | — | 保留字段 | +| `fullscreen` | — | app 级全屏:状态栏**收合至提示线** + 保留左缘返回 / 顶部提示线 / 下滑通知中心(activity 级用 `route..fullscreen`) | +| `fonts` | — | 字库档位声明 `"full" | "gbk" | "common"`(默认 `common`≈4k) | + +其他扩展(launcher 极简解析、不校验 JSON):`icon` / `framework` / `immersive` / `route.`(路由表)/ `awake.`(keep_awake)/ `service.`。应用数上限 8,扫描深度 1 级。 + +### 6.2 链接与入口 + +- 链接:`meow_ui.a` + `libz.a` + `-lpthread`(见 `patch_v2/hello_app/build_hello.sh`)。 +- 事件循环:`meow_ui_init(&opts)` → 注册控件 → `meow_ui_run(&ev)`(阻塞);`meow_ui_request_exit()` 退出;`meow_ui_time_ms()` 单调时间(动画用)。 + +### 6.3 事件回调(meow_ui_events) + +| 回调 | 触发 | +|---|---| +| `on_key(keycode, is_down)` | SDK 已消化系统语义的按键;**不要**用它实现「电源 = exit」这类映射 | +| `on_tap(x, y)` | 触摸抬起落点 | +| `on_swipe(dir, dx, dy)` | 左缘返回手势 = `MEOW_SWIPE_LEFT`;中部滑动 = `RIGHT/UP/DOWN` | +| `on_swipe_progress(x_start, dx)` | 左缘返回手势实时进度(SDK 自动画顶部提示条 `meow_ui_swipe_hint`) | +| `on_tick()` | 空闲周期回调(动画 / 时钟) | + +返回语义:`on_swipe(LEFT)` 里调用 `meow_ui_request_exit()` 回应用列表,或页内处理不退出。 + +### 6.4 控件 / 主题 / 字体 / 通知 API + +- **控件**:`meow_btn_create` / `meow_slider_create` / `meow_switch_create` / `meow_list_create(count, src, ctx)` + `meow_widget_draw/hit`;`meow_ui_activity_push/pop`(Activity 栈,控件归属当前 Activity)、`meow_ui_global_add/remove`(② 层 App 跨 Activity 全局控件)。 +- **图片**:`meow_img_load_file/mem` + `meow_ui_img/scaled/alpha/cover`(stb_image 解 PNG/JPEG/BMP;`cover` = 居中裁方)。 +- **IME**:`meow_ui_text_input` / `_async`(仅英文/数字 ASCII,键盘占底部)。 +- **主题**:`meow_theme_set_mode(light/dark/auto)` + `meow_theme{bg,fg,accent,dim}`,切换即时生效。 +- **字体**:`meow_font_get_available_count/enumerate`(读 `MeowOS/config/fonts/index`)、`meow_font_select_by_id`、`meow_font_has_glyph(cp)`;多字体 `meow_font_config_multifont` / `meow_font_load_slot`。 +- **通知**:`meow_notify_register_type(cat)`(每 App ≤4 类)/ `meow_notify_send(const meow_notify*)`(返回 id)/ `meow_notify_cancel(id)` / `meow_notify_update_progress`;常量 `MEOW_NOTIFY_MAX_TYPES=4`、`MEOW_NOTIFY_MAX_PER_TYPE=4`、`MEOW_NOTIFY_MAX_MEDIA=1`。 +- **日志**:`meow_log(lvl, fmt, ...)`(`MEOW_LOG_DEBUG/INFO/WARN/ERROR`)→ `MeowOS/logs/apps//logs/`。 + +### 6.5 物理键规范(红线) + +- **物理按键是系统资源,语义由 MeowOS 统一管理,App 禁止映射。** 任何非系统组件不得把物理键映射为任何行为(如电源→OK/exit、音量→确认/选择、播放→返回)。 +- 语义只有一份:**电源 = 息屏 / 亮屏**(长按 = 电源菜单,8s = 强关);**音量 = 调音量**;**播放 / 上下曲 = 媒体控制**。返回走**触摸**(左缘左滑 / 虚拟返回键),不是物理键。 +- App **不读** `/dev/input/event*`、不 open/mmap fb0、不处理物理 keycode;想返回/退出用 `meow_ui_request_exit()` 或左滑。SDK `on_key` 回调只做语义事件,不承载"电源 = exit"类映射。 +- 屏幕导航一律走触摸(返回按钮 / 左缘左滑);任何 MeowOS 界面(含设置页)也不可私自定义按键行为。 + +### 6.6 Activity 用法(进阶) + +- 冷启动跳转 / 逻辑深度由框架按**路由表**解析:`app.json` 扁平行 `route.=path`(如 `route.nowplaying=home;search;nowplaying`),经 env `MEOW_ACTIVITY` / `MEOW_CHAIN` 注入;App 用 `meow_ui_activity_push/pop` 管理栈。详见 `FRAMEWORK_DESIGN.md` / `MEOW_APP_SDK.md`。 + +--- + +## 7. 文档索引 | 文档 | 内容 | |---|---| -| `docs/MEOWOS_SPEC_REF.md` | 权威速查总索引(首选入口;常量/路径照抄原文 + 出处 + 矛盾清单) | -| `devdocs/ANALYSIS.md` | 主技术档案(**本地开发档,gitignore 不入库**):固件逆向、启动链、fb 双缓冲、HGL、一切踩坑与纪律(§ 章节持续累积) | -| `DEVELOPMENT_PLAN.md` | 路线图与分阶段计划(**自身已过时,以实际进度为准**;阶段 0 双系统 / A 稳定化 / B 状态栏通知中心 / B2 系统设置 / C MeowPanel / D Activity 框架 / E 网易云) | -| `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(IPC、路由表、导航栈、后台权限、媒体会话) | -| `SYSTEM_SETTINGS.md` | 系统设置子系统设计(统一存储 + C API + 实体 UI + web 复用 + 导入导出) | -| `MEOWOS_API.md` | 统一 API 蓝本(命名规范、返回码、设置 API、HTTP 映射、扩展指南) | -| `APPS.md` | MeowAPPs 应用规范(目录、app.json、生命周期、边界、日志、返回键) | +| `docs/MEOWOS_SPEC_REF.md` | **权威速查总索引**(首选入口):路径 / 常量 / 接口 / 约定 + 「已知矛盾 / 待确认」清单 | +| `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(AF_UNIX socket、路由表、导航栈、后台权限、媒体会话、freedesktop/MPRIS/XDG 对齐) | +| `MEOW_APP_SDK.md` | SDK 设计:架构分层、绘制/控件/图片/输入/主题/字体/通知 API、三层控件模型、Activity 栈、动画与性能 | +| `APPS.md` | MeowAPPs 应用规范:目录、`app.json` 字段、生命周期、边界、返回键、`.mpk` 包与 `nyai` | +| `NOTIFICATION_SYSTEM.md` | 通知系统:freedesktop 标准、配额、TTL/转储、存储分层 | +| `SYSTEM_SETTINGS.md` | 设置子系统:统一存储 + `meow_settings_*` C API + 变更通知 + 导入导出 | +| `TF_CARD_LAYOUT.md` | TF 卡目录布局 + 「固件 / 卡」分头原则(拔卡降级) | +| `DEVELOPMENT_PLAN.md` | 路线图与分阶段计划(**部分已过时,以实际进度为准**) | | `GESTURE_BACK_DESIGN.md` / `KEY_CONTROL_DESIGN.md` / `PERF_SCHEDULER.md` | 左缘返回 / 按键契约与信号 / 帧率调度 | +| `mips-repo/README.md` | mipsel 软件仓库(`bins/` 工具 + `mpk/` 应用包)与 meow_svcd CLI | -> 一次性/阶段性设计文档(已过时或未实施)移至 `devdocs/plan/` 归档(移动不删、保留历史;`devdocs/` 为本地开发档,不入库):`BOOT_SELECTOR_DESIGN`、`CHARGING_SCREEN`、`MAIN_MD3_PLAN`、`MD3_UI_SPEC`、`MEOW_TOUCH_DESIGN`、`MIGRATION_AC`、`BLUETOOTH_MANAGER_PLAN`、`APP_LOG_MANAGER`。 +> 文档收敛规则:新功能在对应权威文档(`MEOW_APP_SDK.md` / `APPS.md` / `SYSTEM_SETTINGS.md` 等)补小节;新增决策/检查点记录在本地开发文档;过时的一次性设计归档。 -**文档收敛规则**:不再为每个功能新增设计 md;新功能在对应权威文档(`MEOW_APP_SDK.md`/`APPS.md`/`SYSTEM_SETTINGS.md` 等)补小节;新增决策/检查点写 `devdocs/DEV_NOTES.md`;过时一次性设计进 `devdocs/plan/`。 +--- -## 当前架构与组件(2026-08-30 快照) +## 8. 约束 / 硬件 -- **SDK 统一日志 + 通知(M2.52/53 已落地 + 模型补全)**:`meow_log()` 按 app 分目录、每次启动新文件 `MeowOS/logs/apps//logs/_.log`,单文件 2MB 轮转、每 App 总量 50MB 删最旧;通知 API `meow_notify_*`(4 类型×每类型 4、媒体单例);**#5 SDK↔IPC 桥**(`meow_notify_ipc.c/.h`:app→launcher 中央存储,send/cancel 接入、回填 launcher `nid`,无 socket 降级不崩)。**通知模型补全(agent-107)**:SDK 补 `icon`+`meow_notify_update_progress`,IPC `m_notify_t` 补 `urgency`(复用 pad)+`MSG_NOTIFY_PROGRESS`,存储补 urgency + icon 回退 + progress(内存实时/里程碑落盘),dump CSV 加 3 列;**#6 TF 转储**已落地(`MeowOS/notifications/` appended CSV + 10MB 滚动 + 开机清板载/内存表)。通知中心 UI / D-Bus 桥为后续任务。 -- **TF 卡多字体库 + 字重结构(M2.5x/M2.58)**:`MeowOS/fonts///`(`font.png`+`font.idx`+`font.json` 三件套),默认 `MeowOS/fonts/default/regular`;`font.file` 设置键选默认套(**family 点开→字重子菜单**,两级扫描 fam/wt/name 数组);`gen_font.py --weight` 写 `font.json.weight` + `--charset` 档位(common/gbk/full);SDK `load_font` **优先读 env `MEOW_FONT_PNG/IDX`**(launcher 每次 `launch_app` 注入),app 未自带字库时**跟随系统 `font.file`**;多字体 6 类(按字符类别路由) + `font.load_full` 大字库开关 + 档位按需加载;≤16 套卡内(防爆);§5.7 字体管理 + 应用字库(多句柄,**草案未实现/**字体索引 agent-113 进行中)。已迁移卡上 8 套到 `/regular/`(default / google_sans / meslo / misans / source_han_sans / source_han_serif / ubuntu_mono / ztest,位深 4)。 -- **全屏机制(M2.5x)**:`app.json fullscreen`(app 级)+`route..fullscreen`(activity 级)+env `MEOW_FULLSCREEN` 注入,launcher 启动 app/切 activity 前预设 SDK 全屏;进入/退出全屏=**状态栏上收出屏顶只剩提示线(4px)**+自然过渡;全屏下保留左缘返回手势+顶部伸缩滑条+顶部下滑开通知中心(热区缩小)。⚠️ 全屏滑条仍有已知问题在跟进(agent-112)。 -- **包名/目录安卓化**:`app.json` `package`(反域名**权威 app_id**,==目录名,缺省回退目录名)→ 示例 `os.meow.hello`;目录 `MeowAPPs/os.meow.hello`;数据 `MeowData//{data,cache}`。 -- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`,mipsel 静态 ~99KB,进固件不入卡):类 systemd/systemctl;**已接入固件开机链**(`etc/init.d/S89meow_svcd` 开机自启,`-f` 前台 + 重挂循环,置于 S84ssh 后、S90 前;bootmenu/launcher `fork/exec`+`waitpid` 托管仍为备选);**双源 services.d**(板载 `/usr/data/meow/services.d/` + TF `MeowOS/services.d/.conf`,同名板载优先+WARN;目录式 `key=value`,未知键忽略/前向兼容);服务=svcd **独立子进程**(崩溃隔离:服务挂不影响 svcd/supervisor);事件触发 `boot`/`card-mount`/`wifi-up`(1s 轮询);`-f` 前台便于 supervisor `waitpid`;状态机 DISABLED/STOPPED/RUNNING/STOPPING/FAILED + CLI。 -- **mDNS(自研 `meow_mdns`,IPv4-only)+ `meow_svcd` 双源服务**:完整 mDNSResponder/SAResponder 均因内核无 IPv6(`socket(AF_INET6)` 失败)被否,改**自研 `meow_mdns`**(mipsel 静态 ~79KB)→ **进固件 `/usr/bin/meow_mdns`**(不入卡),由 `meow_svcd` 托管。`meow_svcd` **双源 services.d**(板载 `/usr/data/meow/services.d/` + TF `MeowOS/services.d/`,同名板载优先+WARN),`etc/init.d/S89meow_svcd` 开机自启并预置 `mdnsd.conf`(trigger=boot);hostname=**meowos**(`system.hostname`,S89 应用)。⚠️ **手机热点组播隔离**:设备→主机组播被热点丢弃 → 该热点下 `avahi-resolve .local` 超时(网络层限制,非缺陷),主机需 DNS 兜底。**`MeowOpt` 已废弃并入 `MeowOS/opt`**。 -- **boot 默认→板载**:**`/data/meow_boot_default`**(O_TRUNC 单值、不递增、权威);bootmenu 读它定默认;**meow.conf 不再承担 boot_default**(其 O_APPEND 累积 bug 已修,设备已清)。 -- **状态栏 40→28px**:SDK `MUI_SBAR_H=28`(字号 20)+ launcher `SB_BAR_H=28`/`SB_FONT=20`;状态栏两条实现(SDK `meow_ui_sys_statusbar_render` / launcher `draw_statusbar`)**待 MD3 收敛为单一 SDK 全局状态栏**(launcher 退役 draw_statusbar 复用 SDK)。 -- **系统修复(agent-104/106,M2.56)**:亮度开机被播放器覆盖→launcher 接管压回用户值;SDK 状态栏音量读取修复(空格匹配 + clamp `volume_max`);设置开关 `meow_settings_get_switch/set_switch` 修复(SET_INT{0,1});**「关于本机」**(设置最底,只读弹窗:hostname=meowos/版本/内存/存储/WiFi);bootmenu 版本显示读 `/etc/meowos_version`(替换硬编码 0.7.9);boot 默认板载 `/data/meow_boot_default`;SDK 拆分触发线(>1200 行才拆,现状 meow_ui_gfx.c=839 行保持扁平)。 -- **键码/阈值修正**:`MEOW_KEY_PREVIOUS=165`(19 是另一码,[A-6] 定案);左缘返回阈值 **65px**([A-4] 90vs65 定案 65);亮度下限 1([A-3]);热键窗口下限 60ms([A-1]);日志路径统一 `MeowOS/logs`([A-7])。 -- **TF 卡资产**:包管理 CLI `nyai`(部署 `MeowOS/opt/bin/nyai`,同 launcher 包语义);应用日志 `MeowOS/logs/apps//logs/_.log`;服务定义 `MeowOS/services.d/.conf`。 -- **mips-repo/**(宿主侧 mipsel 软件仓库):`bins/`(二进制工具:交叉编译产物 + `build.sh` + `manifest.md`)+ `mpk/`(`.mpk` 软件包),供后续固件/TF 复用(见「目录速览」)。 -- **仓库公开净化**:净化后基线单提交 `245bee2` + 本开发批次 8 提交(领先 origin/main 8),HEAD 敏感 0 命中;`devdocs/`(本地开发文档 + WORK_STATE/DEV_NOTES)、`scripts/build/`(固件打包脚本)**gitignore 不入库**;`mips-repo/`(mipsel 软件仓库)入库。 - -## 目录速览 - -- `patch_v2/` — meow_launcherd(MeowOS 核心)、bootmenu(引导选单)、watchdog、meow_svcd(服务管理器)、sdk(meow_ui SDK)、meowpkg(包管理);源码与构建脚本 -- `mips-repo/` — mipsel 交叉编译软件仓库(`bins/`=二进制工具=`meow_mdns`、`mdnsd` 等;`mpk/`=`.mpk` 软件包),供后续固件/TF 复用 -- `font/` — 预渲染字形系统(gen 生成器、meow_font 渲染库、atlas 产物) -- `fw_apps/` — 固件组装产物(`.upt` 不入库,由 `scripts/build/build_fw_apps*.sh` 重新打包) -- `bootlogos/` — 用户定制启动图/关机 logo -- `scripts/build/` — 固件打包脚本 `build_fw_apps_m*.sh`(**本地 gitignore 不入库**) -- `devdocs/` — 本地开发文档(ANALYSIS / WORK_STATE / DEV_NOTES / OPEN_QUESTIONS / FLASH_SSH / TEST_ON_DEVICE / device_test / plan 归档;**gitignore 不入库**) -- `ssh_keys/`, `.tools/` — SSH 密钥、交叉工具链(不入库,按需) - -## 构建/刷机 - -- 固件打包:`scripts/build/build_fw_apps_m258.sh`(**现 M2.58 / 0.7.53**;需交叉工具链 `zig-mipsel-cc` + musl.cc,见 `devdocs/ANALYSIS.md` 第 10 节) -- 升级锁根:SD 卡根 `r3proii.upt`(固件由 `build_fw_apps*.sh` 打包成 `.upt` 再刷) -- 刷机:failsafe(关机后按住 音量加+电源)从 SD 卡根 `r3proii.upt` 升级;勿用播放器 UI 卡死的升级按钮 -- 组件归属:**launcher / bootmenu / watchdog 为固件组件**(`/usr/bin`,只读 squashfs);`/usr/data` 下的同名文件仅 dev 手动 exec 快速下发,开机不用。**真机验证字体/包名/通知须重打固件 + 刷机 + reboot**。 -- 回退:官方固件 / `backups/`(NAND 全量备份)重刷 - -## 关键纪律(红线) - -- 严禁对 `hiby_player` 发未注册信号(SIGUSR1 导致整机重启);不 killall;禁经 `hiby_player.sh` -- 勿远程 kill `meow_launcherd`(整机重启) -- 播放器不能"杀+重拉"(HGL 连续内存);双系统互斥,MeowOS→HibyOS 只能重启 -- 日志/字库/媒体等大数据只写 TF 卡;板载只写 KB 级 `meow/settings.conf` -- 绝对不对挂载点 `rm -rf` - -详见 `devdocs/ANALYSIS.md` 纪律章节。 +- **无 GPU,全 CPU 软渲染**;480×720 双缓冲(虚拟高 1440、yoffset 翻页)。 +- **性能铁律:局部脏区刷新,不整屏重绘**。渲染走 `back_buf` + `meow_present(dirty)`(脏区矩形合并集、相交吸 bbox、超过阈值转全屏、裁可视区);状态栏时钟 / 格按需局部标脏;等垂直同步 + 单次原子 memcpy。刷新率调度 `display.refresh_rate` 60/30/15 + 自适应降档(连续 12 帧超预算降一档、远低预算升一档)。 +- **rootfs 上限 45,088,768 B(≈45MB)**;`/usr/bin` 各组件静态 MIPS32r2。 +- **板载存储极小**(`/data` 约 16MB):只写 KB 级设置(`/usr/data/meow/settings.conf`);日志 / 字库 / 媒体 / 应用一律写 TF 卡;无卡优雅降级(不崩不死)。 +- **mtd 分区**(勿写 NAND):mtd0=uboot、mtd1=kernel、mtd2=rootfs、mtd3=kernel2、mtd4=rootfs2、mtd5=ota(8M 标记)、mtd6=userdata。 +- **安全红线**:不要远程 kill 常驻 `meow_launcherd`(整机重启);`hiby_player` 只能 SIGSTOP/SIGCONT,严禁 killall / 对其发未注册信号(如 SIGUSR1 导致整机重启)/ 走 `hiby_player.sh`;严禁触碰 NAND / `recovery_all`;播放器不能「杀+重拉」(HGL 连续内存)。 +- 本仓库为**非官方第三方项目**,修改并与官方固件共存;刷机有一定的变砖风险(可用官方固件刷回),请自行评估。所有「真机验证」项请以实机为准。 diff --git a/SYSTEM_SETTINGS.md b/SYSTEM_SETTINGS.md index 246199e..c9dd9ab 100644 --- a/SYSTEM_SETTINGS.md +++ b/SYSTEM_SETTINGS.md @@ -27,7 +27,7 @@ | **电源** | `power.longpress_menu_ms` | int | 3000 | 长按电源弹电源菜单阈值 | | | `power.forceoff_ms` | int | 8000 | 长按电源强关阈值 | | | `power.defeat_hotkey` | string | `power_voldown` | 切系统/切 MeowOS 热键组合 | -| **开机** | `boot.default` | enum | `meow` | 开机默认进入 | +| **开机** | `boot.default` | enum | `meow` | 开机默认进入。**权威文件 = 板载 `/data/meow_boot_default`(O_TRUNC 单值:0=MeowOS / 1=HiByOS);`boot.default` 每次保存同步写该文件**(launcher `meow_settings_save` 同步;bootmenu `set_boot_default_meow`/launcher `set_boot_default_hiby` 同文件同语义);init 读入不触发、不反向覆盖。`meow.conf` 不再承担 boot_default [源:M2.61 同步修复][源:2026-08-30] | | | `boot.menu_enabled` | bool | true | 是否显示引导选单 | | | `boot.timeout_ms` | int | 5000 | 选单超时(有输入暂停重计) | | **输入** | `input.hotkey_seq` | string | `volup,volup,volup` | 应用列表热键序列 | diff --git a/docs/MEOWOS_SPEC_REF.md b/docs/MEOWOS_SPEC_REF.md index dcf67d0..5e3e72f 100644 --- a/docs/MEOWOS_SPEC_REF.md +++ b/docs/MEOWOS_SPEC_REF.md @@ -11,6 +11,7 @@ - **板载服务目录**:`/usr/data/meow/services.d/.conf`(M2.54,Data 区,恒定存在、权威优先;首启由 `S89meow_svcd` 预置 `mdnsd.conf`)。与 TF `MeowOS/services.d/` 双源,同名板载优先 + WARN;板载 `trigger=boot` 服务不依赖卡(拔卡保留)`[meow_svcd.md][meow_svcd.c]` - **主机名**:设置键 `system.hostname`(SET_STR,默认 `meowos`;RFC1035 首标签),`S89meow_svcd` 开机读 settings `sethostname`;供自研 mDNS 应答器公布 `.local` `[SYSTEM_SETTINGS §1][mips-repo/bins/meow_mdns]` - `/etc/meowos_version`:版本唯一来源(launcher 运行时 `load_meowos_version` 读;`MEOWOS_VERSION` 宏仅作 dev 占位兜底)`[DEV_NOTES #4]` +- **`/data/meow_boot_default`:开机默认权威标记(O_TRUNC 单值:`0`=MeowOS / `1`=HiByOS)**。由 launcher `meow_settings_save()` 在保存 `boot.default` 时同步写;bootmenu `set_boot_default_meow()`(写 0)/ launcher `set_boot_default_hiby()`(写 1)同文件同语义;bootmenu 开机读它定默认;init 读入不触发、不反向覆盖;`meow.conf` 不再承担 boot_default `[M2.61 同步修复][README][meow_settings.c][meow_bootmenu.c]` - `/tmp`:tmpfs,开机清空;开机模式标记 `/tmp/meow_boot_mode`、电源菜单 `/tmp/meow_power_menu{,_active}`、app 沉浸 `/tmp/meowapp_state` `[BOOT_SELECTOR §1][KEY_CONTROL §9][APPS §3.5]` - 日志启动缓冲 `/tmp/meow_boot.log`(256KB 滚动,卡出现后自动迁卡)`[APPS §4.5]` - IPC socket:`/usr/data/meow_ipc.sock`(AF_UNIX SOCK_STREAM,0666,backlog 4)`[FRAMEWORK_DESIGN §1][WORK_STATE]` @@ -37,6 +38,7 @@ ``` `[TF_CARD_LAYOUT §1][APPS 附录][NOTIFICATION_SYSTEM §5]` - 环境注入(launcher 每次启动 app 前):`MEOW_DATA_DIR`/`MEOW_CACHE_DIR`/`MEOW_LOG_DIR`/`MEOW_LOG_APP`(app_id=package)/`MEOW_LOG_LEVEL`/`MEOW_ACTIVITY`/`MEOW_CHAIN`;XDG:`XDG_DATA_HOME`/`XDG_CACHE_HOME`/`XDG_CONFIG_HOME`(=MeowData//{data,cache,config})`[APPS 附录 §2][FRAMEWORK_DESIGN 附录 D2]` +- **升级/刷机契约**:升级镜像 = 卡根固定名 **`r3proii.upt`**(`build_fw_apps*.sh` 打包);升级模式=uboot 读 mtd5=`ota:kernel2` 引导 kernel2 自动刷(非传统 UI 升级按钮);failsafe(关机按住 音量加+电源)从卡根 `r3proii.upt` 升级;**`/data/recovery_all`=恢复出厂,危险勿触发**(调试/部署前必须 `rm -f /data/recovery_all`;发现即删+告警——highest-priority 红线)。刷 rootfs 分区不丢卡数据 `[WORK_STATE][APPS §1.3/§6]` ### 1.3 板载 vs TF 划分原则(拔卡降级) - 板载只放**稳定框架+KB 级设置**;日志/字库/媒体等大件一律 TF 卡 `[TF_CARD_LAYOUT §0][SYSTEM_SETTINGS §2]` @@ -116,6 +118,7 @@ --- ## 4. SDK 之外系统组件 +- **系统进程 supervisor 树(源:2026-08-30 定案)**:`meow_sysui`(新增,meow_svcd 托管)= 系统进程总监,监管 launcher/svcd/mdns/未来媒体服务/MeowPanel,**一个崩→只拉起它,绝不级联**;`meow_launcherd`=系统 UI 渲染宿主(状态栏/通知中心/电源菜单/全局手势,与 APP 进程独立);`meow_svcd`=服务管理器(服务全子进程化,svcd 自身不死);APP 进程只渲染自己内容,崩不连累系统 UI。**系统组件渲染归系统进程(launcher),APP 进程不渲染系统 UI**(SDK 系统组件 API 仅系统进程可链,App 不可自建悬浮层);通知中心 UI 渲染主体=系统进程,数据经 IPC 从通知存储读。现网(bootmenu execv launcher + S89 svcd 自启)→ 定案(sysui 统一监管)为渐进迁移,属系统基建线。`[FRAMEWORK_DESIGN §10][MEOW_APP_SDK §6.1][NOTIFICATION_SYSTEM §9]` - 通知(SDK `meow_notify_*`,M2.53 最小版 `[meow_ui.h §9.1]`): - 常量:`MEOW_NOTIFY_MAX_TYPES=4`/`MEOW_NOTIFY_MAX_PER_TYPE=4`/`MEOW_NOTIFY_MAX_MEDIA=1`/`MEOW_NOTIFY_TIMEOUT_ONGOING=-1`/`MEOW_NOTIFY_TIMEOUT_NEVER=0` - 分类:`CAT_GENERAL/MESSAGE/EMAIL/EVENT/PROGRESS/MEDIA(=5)/COUNT`;紧急度 `URG_LOW/NORMAL/HIGH/CRITICAL` diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 3f39389..5be96a2 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/meow_launcherd.c b/patch_v2/meow_launcherd/meow_launcherd.c index 7019d0b..d9c0ead 100644 --- a/patch_v2/meow_launcherd/meow_launcherd.c +++ b/patch_v2/meow_launcherd/meow_launcherd.c @@ -325,7 +325,7 @@ #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) */ @@ -343,6 +343,7 @@ #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 */ @@ -3286,6 +3287,11 @@ static int launch_app(struct app *a); * 「系统设置」行时调用,返回后由调用方重置按键防抖基线防面板误切换)。 */ 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); + static int touch_fd = -1; static int t_last_x = -1, t_last_y = -1; static int t_down = 0; @@ -4432,6 +4438,43 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, 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"); + menu_arm_touch(TAP_ARM_DELAY_MS); + int want_launch = -1; + run_notif_center(apps, napps, &want_launch); + /* 重播按键防抖基线,防面板内电源/音量边沿被 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) { @@ -6715,6 +6758,725 @@ done: 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 +#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_LIST_TOP 266 +#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 /* 面板一次缓冲的通知行上限(防御) */ + +/* chip 索引 */ +enum { NCP_CHIP_WIFI=0, NCP_CHIP_BT=1, NCP_CHIP_THEME=2, NCP_CHIP_BRIGHT=3, + NCP_CHIP_VOL=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; + +/* 短串拷贝(同 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; /* 满:中止遍历 */ + 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; +} + +/* 通知列表是否变化(简单按活跃数比;新条/撤下/TTL 释放都反映在 count)。 */ +static int ncp_rows_changed(void) +{ + int c = meow_notif_count(NULL); + if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS; + return c != g_nrows; +} + +/* 把文本按宽度截断(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 位置(5 个,居中排布)。返回下标对应 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); +} + +/* 面板当前 5 个 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_VOL) { + int v = 0; meow_settings_get_int("audio.volume", &v); + if (val && vlen > 0) snprintf(val, vlen, "%d%%", v); + *on = (v > 0); /* 非静音=开 */ + } +} + +/* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。 + * 返回 1=主题变化(需重刷状态栏);0=其它。 */ +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_VOL) { + /* 快捷音量:静音<->恢复上一次非零音量。 */ + static int last_nz = 60; + int cur = 0; meow_settings_get_int("audio.volume", &cur); + if (cur > 0) { last_nz = vol_clamp(cur); meow_settings_set_int("audio.volume", 0); } + else { meow_settings_set_int("audio.volume", vol_clamp(last_nz)); } + g_sys.vol = settings_volume_get(); + paint_sb_right(); + meow_dac_vol_flush(); /* 抬手=无条件直写真实 DAC */ + } + meow_settings_save(); + return 0; +} + +/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。 */ +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; /* 视口外整行跳过 */ + const ncp_row_t *r = &g_rows[i]; + meow_gfx_fill_round(NCP_ROW_X, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container); + /* 图标 */ + int icx = NCP_ICON_X, 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, 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), ry + 12, tbuf, th->on_surface_variant, 16); + /* 标题 + 内容 */ + ncp_text(NCP_TEXT_X, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W); + ncp_text(NCP_TEXT_X, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W); + /* 进度(progress>=0):细条 */ + if (r->progress >= 0) { + int px = NCP_TEXT_X, 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); + } + } + /* 滚动条 */ + 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); + } +} + +/* 绘制整个通知中心面板(画进 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; + fill_rect(0, 0, W, H, th->surface); + /* 面板体(状态栏之下 surface_variant) */ + 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); + + /* 快捷开关行 */ + 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_VOL) ncp_sb_icon(SB_I_VOL, icon_cx, icon_cy, fg, 22); + char label[24]; + snprintf(label, sizeof label, "%s", chip_lbl[c]); + if (val[0] && (c == NCP_CHIP_BRIGHT || c == NCP_CHIP_VOL)) + 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); + + /* 通知列表 */ + ncp_draw_list(scroll); + + /* 底部提示 */ + ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭", + th->on_surface_variant, 16); + + /* 状态栏画在面板体之上(0-27 单独由 draw_statusbar 维护/标脏,不落面板体脏区)。 + * 仅 redraw_sb=1(打开/主题变化)才重绘,交互时跳过以保持脏区只限面板体。 */ + 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、音量 clamp)。 */ +static void 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); + meow_settings_set_int("display.brightness", v); + write_brightness(v); + g_sys.brightness = v; + paint_sb_right(); + if (save_now) meow_settings_save(); + } else if (which == 2) { + int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0, + vol_clamp(settings_volume_max)); + meow_settings_set_int("audio.volume", v); + g_sys.vol = settings_volume_get(); + paint_sb_right(); + if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } + else meow_dac_vol_write_throttled(); + } +} + +/* 通知点击: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) +{ + 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=音量 */ + int closed = 0; /* 本次触发放置:关面板(跳回 run_menu) */ + 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; /* 打开/主题变化时重绘状态栏;其余交互跳过(脏区只限面板体) */ + + /* 首次绘制 */ + ncp_refresh_rows(); + list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, NCP_LIST_BOT - NCP_LIST_TOP); + draw_notif_center(list_scroll, 1); + meow_dirty_full(); meow_ui_flush(); + need_sb = 0; + + for (;;) { + unsigned long long now = now_ms(); + if (sigterm_flag) break; + int need_paint = 0; + + /* 自动息屏(同设置页语义) */ + 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; + draw_notif_center(list_scroll, 1); + meow_dirty_full(); meow_ui_flush(); + 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) { + 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; + /* 按下原点落在滑条带 → 开始拖动 */ + if (drag_slider == 0) { + if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; ncp_slider_apply(1, tlx, 0); } + else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; ncp_slider_apply(2, tlx, 0); } + } + } + if (drag_slider == 1 && td_ok) { ncp_slider_apply(1, tlx, 0); need_paint = 1; } + else if (drag_slider == 2 && td_ok) { ncp_slider_apply(2, tlx, 0); 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_slider = 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)) { closed = 1; break; } + if (adx < 60 && ady < 60) { /* tap */ + int tx = tdx, ty = tdy; + int hit = 0; + for (int c = 0; c < NCP_CHIP_COUNT; c++) + if (ncp_chip_hit(c, tx, ty)) { + if (ncp_chip_apply(c)) need_sb = 1; /* 主题变化 → 重刷状态栏 */ + hit = 1; need_paint = 1; break; + } + if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { ncp_slider_apply(1, tx, 1); hit = 1; need_paint = 1; } + else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { ncp_slider_apply(2, tx, 1); 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; + closed = 1; /* 点击通知:dismiss + 记录启动 → 关面板 */ + break; + } + } + if (!hit) { closed = 1; break; } /* 点空白(标题/把手/提示/分隔线/列表外)→ 关闭 */ + } else if (mnp_vertical_up(dx, dy, MENU_SWIPE_THRESH)) { + closed = 1; break; /* 面板内上滑 → 关闭 */ + } + /* 其余横滑/下滑忽略(不关闭不动作) */ + } + 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()) { + 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 状态栏」两带,绝不整屏重绘。 */ + if (need_paint) { + last_act = now; + draw_notif_center(list_scroll, need_sb); + need_sb = 0; + 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(); + } + /* ~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_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 diff --git a/patch_v2/meow_launcherd/meow_notif_panel.h b/patch_v2/meow_launcherd/meow_notif_panel.h new file mode 100644 index 0000000..4912891 --- /dev/null +++ b/patch_v2/meow_launcherd/meow_notif_panel.h @@ -0,0 +1,116 @@ +/* meow_notif_panel.h — 通知中心面板的纯逻辑(header-only,宿主可单测)。 + * + * 定位:把通知中心面板里「无副作用、可独立判定」的部分抽成 static inline, + * 供 launcher(meow_launcherd.c 内嵌面板)与宿主单测(test_notif_panel.c)共用同一份实现, + * 避免两端漂移;绘制/触摸循环等依赖 launcher static 符号的部分留在 meow_launcherd.c 内。 + * + * 不依赖 launcher 头 / SDK 头,仅 —— 纯宿主可编。 + */ +#ifndef MEOW_NOTIF_PANEL_H +#define MEOW_NOTIF_PANEL_H + +#include +#include +#include +#include + +/* ---- 手势判定 ---- */ + +/* 左缘宽度:与 launcher meow_touch.h MEOW_EDGE_W(24) / SDK 全局左缘返回同口径。 + * 顶部下滑打开通知中心时,左缘起点保留给「左缘返回」,不与其抢占(spec-ref §5.1)。 */ +#ifndef MNP_EDGE_W +#define MNP_EDGE_W 24 +#endif + +/* 顶部热区下滑 → 打开通知中心(纯函数)。 + * down_x/down_y=抬手原点;dx/dy=抬手位移(当前-原点,即 t_last - t_down);hotzone=顶部热区高 + * (launcher=SB_BAR_H=28);thresh=下滑触发位移阈值(launcher=MENU_SWIPE_THRESH=65)。 + * 返回 1=应打开。约束:起点不在左缘(down_x>=MNP_EDGE_W,左缘保留给返回)+ 起点落在顶部热区 + * (down_y ∈ [0,hotzone))+ 向下主导(dy>=thresh 且 |dy|>|dx|)。调用方通常先判左缘返回 + * (meow_edge_back)+ 本函数,两者互斥。 */ +static inline int mnp_top_swipe_down(int down_x, int down_y, int dx, int dy, + int hotzone, int thresh) +{ + if (down_x < MNP_EDGE_W) return 0; /* 左缘保留给返回手势 */ + if (down_y < 0 || down_y >= hotzone) return 0; + if (dy < thresh) return 0; /* 未达下滑阈值 / 向上 */ + int ax = dx < 0 ? -dx : dx; + return ax < dy; /* 向下主导(横向位移小于竖向) */ +} + +/* 竖向主导上滑(面板内关闭):dy<=-thresh 且 |dy|>|dx|。返回 1=应关闭。 */ +static inline int mnp_vertical_up(int dx, int dy, int thresh) +{ + if (dy > -thresh) return 0; + int ax = dx < 0 ? -dx : dx; + return ax < -dy; +} + +/* ---- 滑条 ---- */ + +/* 滑条值映射:px(屏幕坐标)→ 值,clamp 到 [min,max]。sx=track 左缘、sw=track 宽。 + * 与 launcher 设置页 s_slider_val_from_x 同构(此处参数化,供面板亮度/音量滑条与单测用)。 */ +static inline int mnp_slider_val_from_x(int px, int sx, int sw, int min, int max) +{ + int range = max - min; + int v = min + (range ? (px - sx) * range / sw : 0); + if (v < min) v = min; + if (v > max) v = max; + return v; +} + +/* ---- 亮度 clamp ---- */ + +/* 亮度 clamp:下限 vmin(=display.brightness_min,≥1 防黑屏)、上限 100(防闪屏)。 + * vmin<1 时钳到 1。与 SYSTEM_SETTINGS §1 / spec-ref A-3 一致(旧文档 25 为陈旧值)。 */ +static inline int mnp_brightness_clamp(int v, int vmin) +{ + if (vmin < 1) vmin = 1; + if (v < vmin) v = vmin; + if (v > 100) v = 100; + return v; +} + +/* ---- 时间显示 ---- */ + +/* 通知时间(created_ms 墙钟 epoch ms)→ 显示串。 + * <60s「刚刚」;<60min「N分钟前」;否则本地「HH:MM」。now_ms=当前墙钟 epoch ms。 */ +static inline void mnp_time_ago(uint64_t created_ms, uint64_t now_ms, char *buf, int len) +{ + if (!buf || len <= 0) return; + if (now_ms < created_ms) now_ms = created_ms; /* 时钟回拨防御 */ + uint64_t diff = now_ms - created_ms; + if (diff < 60000u) { if (len >= 1) snprintf(buf, (size_t)len, "刚刚"); return; } + if (diff < 3600000u) { + snprintf(buf, (size_t)len, "%llu分钟前", (unsigned long long)(diff / 60000u)); + return; + } + time_t t = (time_t)(created_ms / 1000u); + struct tm tm; + if (localtime_r(&t, &tm)) + snprintf(buf, (size_t)len, "%02d:%02d", tm.tm_hour, tm.tm_min); + else if (len >= 1) snprintf(buf, (size_t)len, "--:--"); +} + +/* ---- 列表命中 ---- */ + +/* 通知列表行命中:y 为绝对纵坐标;list_top/list_bot 为列表区上下缘;row_h 为行高。 + * 返回行号(0 起);越界返回 -1。 */ +static inline int mnp_row_at(int y, int list_top, int list_bot, int row_h) +{ + if (y < list_top || y >= list_bot) return -1; + int r = (y - list_top) / row_h; + return r; +} + +/* 列表滚动 clainmp:把 scroll 钳到 [0, total-view]。content 高度=total(总行数*row_h)。 */ +static inline int mnp_scroll_clamp(int scroll, int total, int view) +{ + if (total <= view) return 0; + int mx = total - view; + if (scroll < 0) scroll = 0; + if (scroll > mx) scroll = mx; + return scroll; +} + +#endif /* MEOW_NOTIF_PANEL_H */ diff --git a/patch_v2/meow_launcherd/test_notif_panel.c b/patch_v2/meow_launcherd/test_notif_panel.c new file mode 100644 index 0000000..2355798 --- /dev/null +++ b/patch_v2/meow_launcherd/test_notif_panel.c @@ -0,0 +1,236 @@ +/* test_notif_panel.c — 宿主单测:阶段B 通知中心面板(纯逻辑 + 面板读取的存储契约)。 + * + * 直接 #include "meow_notif.c" + "meow_settings.c" 于同一 TU(同 test_notif_tf.c 手法), + * 以「真实 meow_notif 存储 + 真实 meow_settings 后端」验证面板真正读取的字段契约; + * 再测 meow_notif_panel.h 的纯逻辑(滑条映射/亮度 clamp/顶部下滑判定/时间显示/行命中/滚动钳位)。 + * 不依赖 launcher 其它 static / fb / 触摸。 + * + * 运行:gcc -D_GNU_SOURCE -o test_notif_panel test_notif_panel.c && ./test_notif_panel + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +/* stub:meow_notif.c 的 read_quota() 经 meow_settings_get_int 读 notify.* 配额。 + * <0 => 键缺失 => 用默认(MEOW_NOTIF_CATS_DEF=4 / CAT_QUOTA_DEF=4 / MEDIA_QUOTA_DEF=1)。 + * meow_notif.c 对 meow_settings 的唯一依赖就是这一个函数。 */ +int meow_settings_get_int(const char *key, int *out) +{ + (void)key; (void)out; + return -2; /* MEOW_ERR_NOENT */ +} + +/* 先把 tf 转储路径指向临时目录,避免污染真实卡路径(meow_notif_init 会 clear tf)*/ +static char g_tflog[600]; + +#include "meow_notif.c" /* 真实通知存储:post/list/count/dismiss/update_progress/... */ +#include "meow_notif_panel.h" /* 面板纯逻辑(header-only) */ + +static int g_fail = 0; +#define CHECK(cond, msg) do { \ + if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \ + else fprintf(stderr, "ok: %s\n", msg); \ +} while (0) + +/* 收集列表到固定槽(回调式,模拟面板 ncp_collect_cb 的字段读取) */ +#define MAXG 16 +typedef struct { uint32_t id; char summary[96]; char body[96]; char icon[64]; + char app[48]; char tgt[48]; int progress; uint8_t urgency; uint64_t ms; } gslot_t; +static gslot_t g_got[MAXG]; static int g_nslots = 0; +static int list_grab_cb(const meow_notif_t *n, void *ctx) +{ + (void)ctx; + if (g_nslots >= MAXG) return 1; + gslot_t *s = &g_got[g_nslots++]; + memset(s, 0, sizeof *s); + s->id = n->id; + snprintf(s->summary, sizeof s->summary, "%s", n->summary); + snprintf(s->body, sizeof s->body, "%s", n->body); + snprintf(s->icon, sizeof s->icon, "%s", n->app_icon); + snprintf(s->app, sizeof s->app, "%s", n->app_id); + snprintf(s->tgt, sizeof s->tgt, "%s", n->target_activity); + s->progress = n->progress; + s->urgency = n->urgency; + s->ms = n->created_ms; + return 0; +} +static int grab_list(void) +{ + g_nslots = 0; + meow_notif_list(NULL, list_grab_cb, NULL); + return g_nslots; +} +static gslot_t *find_slot(const char *summary) +{ + for (int i = 0; i < g_nslots; i++) + if (!strcmp(g_got[i].summary, summary)) return &g_got[i]; + return NULL; +} + +static uint64_t wall_now_ms(void) +{ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; +} + +static void test_pure_logic(void) +{ + fprintf(stderr, "--- P1 pure logic ---\n"); + /* mnp_top_swipe_down */ + CHECK(mnp_top_swipe_down(40, 5, 3, 90, 28, 65) == 1, "top hotzone down-dominant -> open"); + CHECK(mnp_top_swipe_down(10, 5, 3, 90, 28, 65) == 0, "left-edge (down_x<24) reserved -> no open"); + CHECK(mnp_top_swipe_down(40, 30, 3, 90, 28, 65) == 0, "origin below hotzone -> no open"); + CHECK(mnp_top_swipe_down(40, 5, 3, 40, 28, 65) == 0, "below swipe threshold -> no open"); + CHECK(mnp_top_swipe_down(40, 5, 100, 70, 28, 65) == 0, "horizontal-dominant -> no open"); + CHECK(mnp_top_swipe_down(40, 5, -3, 90, 28, 65) == 1, "down with minor left drift still opens"); + + /* mnp_slider_val_from_x */ + CHECK(mnp_slider_val_from_x(20, 20, 440, 1, 100) == 1, "slider x=left -> min"); + CHECK(mnp_slider_val_from_x(460, 20, 440, 1, 100) == 100, "slider x=right -> max"); + int mid = mnp_slider_val_from_x(240, 20, 440, 1, 100); + CHECK(mid >= 1 && mid <= 100, "slider x=mid within [min,max]"); + CHECK(mnp_slider_val_from_x(0, 20, 440, 1, 100) == 1, "slider x out-left clamped to min"); + CHECK(mnp_slider_val_from_x(999, 20, 440, 1, 100) == 100, "slider x out-right clamped to max"); + + /* mnp_brightness_clamp */ + CHECK(mnp_brightness_clamp(0, 1) == 1, "brightness clamp below min -> min"); + CHECK(mnp_brightness_clamp(150, 1) == 100, "brightness clamp above 100 -> 100"); + CHECK(mnp_brightness_clamp(50, 1) == 50, "brightness 50 kept"); + CHECK(mnp_brightness_clamp(3, 5) == 5, "brightness below custom min -> custom min"); + CHECK(mnp_brightness_clamp(-7, -3) == 1, "brightness clamp forces vmin>=1"); + + /* mnp_time_ago */ + uint64_t now = wall_now_ms(); + char tb[32]; + mnp_time_ago(now - 5000, now, tb, sizeof tb); + CHECK(!strcmp(tb, "刚刚"), "time_ago <60s -> 刚刚"); + mnp_time_ago(now - 5 * 60000u, now, tb, sizeof tb); + CHECK(!strcmp(tb, "5分钟前"), "time_ago 5min -> 5分钟前"); + mnp_time_ago(now - 3 * 3600000u, now, tb, sizeof tb); + CHECK(strlen(tb) == 5 && tb[2] == ':', "time_ago >1h -> HH:MM"); + + /* mnp_row_at */ + CHECK(mnp_row_at(250, 252, 696, 92) == -1, "row_at above list -> -1"); + CHECK(mnp_row_at(300, 252, 696, 92) == 0, "row_at first row -> 0"); + CHECK(mnp_row_at(400, 252, 696, 92) == 1, "row_at second row -> 1"); + CHECK(mnp_row_at(700, 252, 696, 92) == -1, "row_at below list -> -1"); + + /* mnp_scroll_clamp */ + CHECK(mnp_scroll_clamp(0, 2000, 444) == 0, "scroll clamp lower -> 0"); + CHECK(mnp_scroll_clamp(9999, 2000, 444) == 1556, "scroll clamp upper -> total-view"); + CHECK(mnp_scroll_clamp(-5, 2000, 444) == 0, "scroll clamp negative -> 0"); + CHECK(mnp_scroll_clamp(5, 400, 444) == 0, "content fits view -> 0"); +} + +static void test_storage_contract(void) +{ + fprintf(stderr, "--- P0 storage contract (panel reads) ---\n"); + /* meow_notif_init:应清内存表 + 清 tf 文件(路径已指临时);配额用默认(stub 返回 NOENT) */ + CHECK(meow_notif_init() == NOTIF_OK, "meow_notif_init ok"); + CHECK(meow_notif_count(NULL) == 0, "init clears memory table"); + CHECK(g_max_cats == 4 && g_cat_quota == 4 && g_media_quota == 1, + "quota defaults from meow_settings backend (stub NOENT -> defaults)"); + + meow_notif_t n; memset(&n, 0, sizeof n); + snprintf(n.app_id, sizeof n.app_id, "os.meow.hello"); + snprintf(n.category, sizeof n.category, "general"); + snprintf(n.summary, sizeof n.summary, "你好世界"); + snprintf(n.body, sizeof n.body, "这是正文第二行"); + snprintf(n.app_icon, sizeof n.app_icon, "apps/os.meow.hello/icon.png"); + snprintf(n.target_activity, sizeof n.target_activity, "activity_main"); + n.expire_timeout = 0; n.progress = -1; n.urgency = 1; + uint32_t id1 = 0; + CHECK(meow_notif_post(&n, &id1) == NOTIF_OK, "post general ok"); + CHECK(id1 != 0, "post assigns id"); + + /* 媒体通知:ongoing + 进度 */ + memset(&n, 0, sizeof n); + snprintf(n.app_id, sizeof n.app_id, "os.meow.hello"); + snprintf(n.category, sizeof n.category, "media"); + snprintf(n.summary, sizeof n.summary, "正在播放"); + n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 20; n.urgency = 2; + uint32_t id2 = 0; + CHECK(meow_notif_post(&n, &id2) == NOTIF_OK, "post media ok"); + + /* 高紧急 + 进度 */ + memset(&n, 0, sizeof n); + snprintf(n.app_id, sizeof n.app_id, "system"); + snprintf(n.category, sizeof n.category, "event"); + snprintf(n.summary, sizeof n.summary, "系统提醒"); + snprintf(n.body, sizeof n.body, "电量低"); + n.progress = 50; n.urgency = 3; + uint32_t id3 = 0; + CHECK(meow_notif_post(&n, &id3) == NOTIF_OK, "post event(high) ok"); + + CHECK(meow_notif_count(NULL) == 3, "count == 3 after posts"); + + /* 遍历(面板 ncp_collect_cb 同款字段读取)*/ + int got = grab_list(); + CHECK(got == 3, "list returns 3 active notifs"); + + gslot_t *s1 = find_slot("你好世界"); + CHECK(s1 != NULL, "panel sees summary 你好世界"); + if (s1) { + CHECK(!strcmp(s1->app, "os.meow.hello"), "app_id"); + CHECK(!strcmp(s1->body, "这是正文第二行"), "body"); + CHECK(!strcmp(s1->icon, "apps/os.meow.hello/icon.png"), "app_icon"); + CHECK(!strcmp(s1->tgt, "activity_main"), "target_activity"); + CHECK(s1->progress == -1, "general progress=-1 (none)"); + CHECK(s1->urgency == 1, "general urgency=NORMAL(1)"); + CHECK(s1->ms > 0, "created_ms set"); + } + gslot_t *s2 = find_slot("正在播放"); + CHECK(s2 != NULL && s2->progress == 20 && s2->urgency == 2, "media progress=20 urgency=HIGH(2)"); + gslot_t *s3 = find_slot("系统提醒"); + CHECK(s3 != NULL && s3->progress == 50 && s3->urgency == 3, "event progress=50 urgency=CRITICAL(3)"); + + /* dismiss → 面板列表减一(点通知=阅读)*/ + CHECK(meow_notif_dismiss(id1) == NOTIF_OK, "dismiss general ok"); + CHECK(meow_notif_count(NULL) == 2, "count == 2 after dismiss"); + grab_list(); + CHECK(find_slot("你好世界") == NULL, "dismissed notif no longer listed"); + + /* 里程碑 100% → 释放(非 ONGOING):面板列表再减一 */ + CHECK(meow_notif_update_progress(id3, 100) == NOTIF_OK, "update_progress(100) ok"); + CHECK(meow_notif_count(NULL) == 1, "milestone released event slot -> count 1"); + grab_list(); + CHECK(find_slot("系统提醒") == NULL, "100% milestone notif no longer listed"); + + /* 剩余媒体通知仍活跃(ONGOING 常驻)*/ + CHECK(find_slot("正在播放") != NULL, "ongoing media stays listed"); + + /* 配额:单 app 媒体单例(media_quota=1)——再发一条 media 顶掉旧媒体 */ + memset(&n, 0, sizeof n); + snprintf(n.app_id, sizeof n.app_id, "os.meow.hello"); + snprintf(n.category, sizeof n.category, "media"); + snprintf(n.summary, sizeof n.summary, "下一首"); + n.expire_timeout = 0xFFFFFFFFu; n.ongoing = 1; n.progress = 60; + uint32_t id4 = 0; + CHECK(meow_notif_post(&n, &id4) == NOTIF_OK, "post second media ok"); + CHECK(meow_notif_count(NULL) == 1, "media quota=1 -> old media replaced, count stays 1"); + grab_list(); + CHECK(find_slot("下一首") != NULL && find_slot("正在播放") == NULL, + "media singleton replaced oldest"); +} + +int main(void) +{ + char tmp[] = "/tmp/meow_notif_panel_XXXXXX"; + char *dir = mkdtemp(tmp); + if (!dir) { perror("mkdtemp"); return 2; } + snprintf(g_tflog, sizeof g_tflog, "%s/notif.log", dir); + meow_notif_set_tf_path(g_tflog); + meow_notif_set_tf_max(1024 * 1024); + + test_pure_logic(); + test_storage_contract(); + + unlink(g_tflog); rmdir(dir); + + fprintf(stderr, "\n%s (failures=%d)\n", g_fail ? "== TEST FAILED ==" : "== TEST PASSED ==", g_fail); + return g_fail ? 1 : 0; +}