阶段B 通知中心UI(agent-123) + 进程模型/文档完整(agent-124) + 对外README(agent-125)

- 通知中心=launcher侧系统级叠加层(§6.1③ 仅内部): meow_notif_panel.h(纯逻辑) + meow_launcherd.c(内联 draw/run_notif_center + run_menu顶部热区下滑接入) + test_notif_panel.c(56单测)
- 通知中心零旁路走 meow_settings_*; 通知列表读 meow_notif; 左缘返回复用 meow_edge_back; 局部脏区; 系统级控件无公共API(App无法自建悬浮层)
- meow_ui.a 零改动(d249c6e6); meow_launcherd=4652901a(0warn)
- FRAMEWORK_DESIGN §10 进程模型/Supervisor树(svcd服务子进程化隔离防级联崩) ; MEOW_APP_SDK §6.1 系统UI归launcher+SDK/APP进程隔离; NOTIFICATION_SYSTEM §9 归属; SYSTEM_SETTINGS/SPEC_REF/DEVELOPMENT_PLAN 补齐
- README 重写为面向第三方(进度/架构/构建/部署/SDK开发; 固件打包标注本地工具不入库, 无敏感)
- 遗留: SDK(App进程)侧通知中心/activity跳转 依赖 M3 导航栈(独立里程碑); 真机待验证
This commit is contained in:
MeowOS committed 2026-08-30 15:59:23 +08:00
1 parent b6fe812251
commit 2b17f69f24
11 files changed
+1498 -60

No files matched your search

+15
View File
@@ -2,6 +2,21 @@
> 目标:把 R3 Pro II 打造成可扩展的"第二系统"。**MeowOS 作为主控层优先启动、拥有屏幕与输入;HibyOS 播放器降级为 MeowOS 启动的一个内置系统应用**(Android home/launcher 模型)。MeowLauncher(即 MeowOS 核心)、MeowAPPs、MeowPanel 均为其组成部分。 > 目标:把 R3 Pro II 打造成可扩展的"第二系统"。**MeowOS 作为主控层优先启动、拥有屏幕与输入;HibyOS 播放器降级为 MeowOS 启动的一个内置系统应用**(Android home/launcher 模型)。MeowLauncher(即 MeowOS 核心)、MeowAPPs、MeowPanel 均为其组成部分。
> 本文档是路线图与分阶段计划。当前技术档案见 `ANALYSIS.md`,应用规范见 `APPS.md`,框架协议见 `FRAMEWORK_DESIGN.md`。 > 本文档是路线图与分阶段计划。当前技术档案见 `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**(审后)。
--- ---
+57
View File
@@ -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.<id>.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(用户追加):后台运行 / 后台播放权限 ## 附录 A(用户追加):后台运行 / 后台播放权限
**需求**:允许应用声明后台运行、后台播放等能力,避免播放类应用切到后台被直接终止。hiby 播放器作为系统应用单独适配。 **需求**:允许应用声明后台运行、后台播放等能力,避免播放类应用切到后台被直接终止。hiby 播放器作为系统应用单独适配。
+9 -1
View File
@@ -543,7 +543,15 @@ void meow_ui_global_remove(int id);
- **对象复用(非销毁)**:§6.1 的「Activity 内对象池复用」本轮**从简落地**——控件对象不随 pop 销毁,SDK 不维护独立对象池;App 自行持有控件指针(hello 用静态全局),弹栈回来自动恢复命中(零重建)。注册表按**指针去重**(`live_btn_register` 查重),故反复 push/pop 不重复注册、不泄漏。 - **对象复用(非销毁)**:§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` 幂等。 - **② 为可用骨架**: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,既有状态栏/返回滑条逻辑原样保留。 - **③ 系统级(状态栏/通知中心/返回滑条/电源菜单)**:未开放任何公共悬浮/叠加层 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.<id>.fullscreen`)、**内容区域**(inset)与**前台 activity**;系统 UI 据此做叠画 + 命中分流(§6.1 交互分层:③ 系统全局 → ② App 全局 → ① 当前 Activity 页面控件)。
--- ---
+7 -2
View File
@@ -66,8 +66,13 @@ target_activity(可选, 来自 hints) / progress(可选 int) / urgency(0-3, 缺
- **启动时清板载 + 清内存通知表**:开机(含异常关机后)清理板载残留通知数据,并**清空内存通知表**(在既有「异常关机:开机时清理板载残留」基础上**补充内存表一并清**);正常关机/重启前先转储未上卡通知再清板载(沿用既有规则)`[源:用户 2026-08-30]`。 - **启动时清板载 + 清内存通知表**:开机(含异常关机后)清理板载残留通知数据,并**清空内存通知表**(在既有「异常关机:开机时清理板载残留」基础上**补充内存表一并清**);正常关机/重启前先转储未上卡通知再清板载(沿用既有规则)`[源:用户 2026-08-30]`。
- **无 TF 卡**:系统可运行(内存态通知即可),不依赖卡。 - **无 TF 卡**:系统可运行(内存态通知即可),不依赖卡。
## 9. 通知中心 UI(阶段 B) ## 9. 通知中心 UI(阶段 B)—— 归属定案 [源:2026-08-30]
- 状态栏下滑打开通知中心(用户已定),列表显示通知(app 图标 + summary + body + target Activity),点击按路由表解析跳转(M3 导航栈),下拉关闭/清除。**此部分依赖 M2 存储 + M3,后置。**
- **渲染主体 = 系统进程(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. 分步实施建议 ## 10. 分步实施建议
1. **M2a 通知存储 + 原生 IPC**:按上面的配额(category 级)+ TTL 10s 转储钩子 + 系统事件源(系统作 App)。→ 可先落地。 1. **M2a 通知存储 + 原生 IPC**:按上面的配额(category 级)+ TTL 10s 转储钩子 + 系统事件源(系统作 App)。→ 可先落地。
+291 -55
View File
@@ -1,67 +1,303 @@
# MeowOS for HiBy R3 Pro II # 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/<name>.conf`(Data 区,拔卡保留,权威优先)+ TF `MeowOS/services.d/<name>.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 <name|all>` / `pid`。
- **meow_mdns(mDNS 应答器,自研)**:`/usr/bin/meow_mdns`,**IPv4-only**(该设备内核无 IPv6,完整 mDNSResponder/SAResponder 均无法运行),应答 `<host>.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/<family>/<weight>/`**(每套三件套:`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/<pkg>/fonts/<handle>/`,多句柄 `_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>/`(`package` = `app.json` 的 `package`,如 `os.meow.hello`):`app.json` + 静态 MIPS 二进制 + 资源;数据在 `MeowData/<package>/{data,cache}`。
- **热更新**:把构建产物放入 `MeowAPPs/os.meow.hello/` 即可;launcher 每次打开菜单重扫卡,立即可见。App 二进制/资源在卡上;拔卡会优雅降级(应用列表"未插卡"/字体回退/日志回退内存环),不崩系统。
- 应用日志:`MeowOS/logs/apps/<package>/logs/<ts>_<pid>.log`(每次启动新文件,单文件 2MB 轮转、每 App 总量上限)。包管理 CLI `nyai`(放 `MeowOS/opt/bin/nyai`):`nyai install <pkg>/<file.mpk>` 等。
**TF 卡目录(关键)**
```
/usr/data/mnt/sd_0/
├── Music/ # 音乐库
├── MeowData/<pkg>/{data,cache}
├── MeowAPPs/<pkg>/ # 应用(app.json + 二进制 + 资源)
└── MeowOS/
├── fonts/<family>/<weight>/ # 多字体库
├── config/fonts/index # 字体列表索引
├── notifications/ # 通知转储(10MB 滚动)
├── logs/ # 系统 / 应用日志
├── services.d/<name>.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.<id>.fullscreen`) |
| `fonts` | — | 字库档位声明 `"full" | "gbk" | "common"`(默认 `common`≈4k) |
其他扩展(launcher 极简解析、不校验 JSON):`icon` / `framework` / `immersive` / `route.<id>`(路由表)/ `awake.<id>`(keep_awake)/ `service.<svc>`。应用数上限 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/<package>/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.<id>=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` | 权威速查总索引(首选入口;常量/路径照抄原文 + 出处 + 矛盾清单) | | `docs/MEOWOS_SPEC_REF.md` | **权威速查总索引**(首选入口):路径 / 常量 / 接口 / 约定 + 「已知矛盾 / 待确认」清单 |
| `devdocs/ANALYSIS.md` | 主技术档案(**本地开发档,gitignore 不入库**):固件逆向、启动链、fb 双缓冲、HGL、一切踩坑与纪律(§ 章节持续累积) | | `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(AF_UNIX socket、路由表、导航栈、后台权限、媒体会话、freedesktop/MPRIS/XDG 对齐) |
| `DEVELOPMENT_PLAN.md` | 路线图与分阶段计划(**自身已过时,以实际进度为准**;阶段 0 双系统 / A 稳定化 / B 状态栏通知中心 / B2 系统设置 / C MeowPanel / D Activity 框架 / E 网易云) | | `MEOW_APP_SDK.md` | SDK 设计:架构分层、绘制/控件/图片/输入/主题/字体/通知 API、三层控件模型、Activity 栈、动画与性能 |
| `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(IPC、路由表、导航栈、后台权限、媒体会话) | | `APPS.md` | MeowAPPs 应用规范:目录、`app.json` 字段、生命周期、边界、返回键、`.mpk` 包与 `nyai` |
| `SYSTEM_SETTINGS.md` | 系统设置子系统设计(统一存储 + C API + 实体 UI + web 复用 + 导入导出) | | `NOTIFICATION_SYSTEM.md` | 通知系统:freedesktop 标准、配额、TTL/转储、存储分层 |
| `MEOWOS_API.md` | 统一 API 蓝本(命名规范、返回码、设置 API、HTTP 映射、扩展指南) | | `SYSTEM_SETTINGS.md` | 设置子系统:统一存储 + `meow_settings_*` C API + 变更通知 + 导入导出 |
| `APPS.md` | MeowAPPs 应用规范(目录、app.json、生命周期、边界、日志、返回键) | | `TF_CARD_LAYOUT.md` | TF 卡目录布局 + 「固件 / 卡」分头原则(拔卡降级) |
| `DEVELOPMENT_PLAN.md` | 路线图与分阶段计划(**部分已过时,以实际进度为准**) |
| `GESTURE_BACK_DESIGN.md` / `KEY_CONTROL_DESIGN.md` / `PERF_SCHEDULER.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/<app_id>/logs/<ts>_<pid>.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 桥为后续任务。 - **无 GPU,全 CPU 软渲染**;480×720 双缓冲(虚拟高 1440、yoffset 翻页)。
- **TF 卡多字体库 + 字重结构(M2.5x/M2.58)**:`MeowOS/fonts/<family>/<weight>/`(`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 套到 `<family>/regular/`(default / google_sans / meslo / misans / source_han_sans / source_han_serif / ubuntu_mono / ztest,位深 4)。 - **性能铁律:局部脏区刷新,不整屏重绘**。渲染走 `back_buf` + `meow_present(dirty)`(脏区矩形合并集、相交吸 bbox、超过阈值转全屏、裁可视区);状态栏时钟 / 格按需局部标脏;等垂直同步 + 单次原子 memcpy。刷新率调度 `display.refresh_rate` 60/30/15 + 自适应降档(连续 12 帧超预算降一档、远低预算升一档)。
- **全屏机制(M2.5x)**:`app.json fullscreen`(app 级)+`route.<id>.fullscreen`(activity 级)+env `MEOW_FULLSCREEN` 注入,launcher 启动 app/切 activity 前预设 SDK 全屏;进入/退出全屏=**状态栏上收出屏顶只剩提示线(4px)**+自然过渡;全屏下保留左缘返回手势+顶部伸缩滑条+顶部下滑开通知中心(热区缩小)。⚠️ 全屏滑条仍有已知问题在跟进(agent-112)。 - **rootfs 上限 45,088,768 B(≈45MB)**;`/usr/bin` 各组件静态 MIPS32r2。
- **包名/目录安卓化**:`app.json` `package`(反域名**权威 app_id**,==目录名,缺省回退目录名)→ 示例 `os.meow.hello`;目录 `MeowAPPs/os.meow.hello`;数据 `MeowData/<pkg>/{data,cache}`。 - **板载存储极小**(`/data` 约 16MB):只写 KB 级设置(`/usr/data/meow/settings.conf`);日志 / 字库 / 媒体 / 应用一律写 TF 卡;无卡优雅降级(不崩不死)。
- **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/<name>.conf`,同名板载优先+WARN;目录式 `key=value`,未知键忽略/前向兼容);服务=svcd **独立子进程**(崩溃隔离:服务挂不影响 svcd/supervisor);事件触发 `boot`/`card-mount`/`wifi-up`(1s 轮询);`-f` 前台便于 supervisor `waitpid`;状态机 DISABLED/STOPPED/RUNNING/STOPPING/FAILED + CLI。 - **mtd 分区**(勿写 NAND):mtd0=uboot、mtd1=kernel、mtd2=rootfs、mtd3=kernel2、mtd4=rootfs2、mtd5=ota(8M 标记)、mtd6=userdata。
- **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 <host>.local` 超时(网络层限制,非缺陷),主机需 DNS 兜底。**`MeowOpt` 已废弃并入 `MeowOS/opt`**。 - **安全红线**:不要远程 kill 常驻 `meow_launcherd`(整机重启);`hiby_player` 只能 SIGSTOP/SIGCONT,严禁 killall / 对其发未注册信号(如 SIGUSR1 导致整机重启)/ 走 `hiby_player.sh`;严禁触碰 NAND / `recovery_all`;播放器不能「杀+重拉」(HGL 连续内存)。
- **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/<id>/logs/<ts>_<pid>.log`;服务定义 `MeowOS/services.d/<name>.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` 纪律章节。
+1 -1
View File
@@ -27,7 +27,7 @@
| **电源** | `power.longpress_menu_ms` | int | 3000 | 长按电源弹电源菜单阈值 | | **电源** | `power.longpress_menu_ms` | int | 3000 | 长按电源弹电源菜单阈值 |
| | `power.forceoff_ms` | int | 8000 | 长按电源强关阈值 | | | `power.forceoff_ms` | int | 8000 | 长按电源强关阈值 |
| | `power.defeat_hotkey` | string | `power_voldown` | 切系统/切 MeowOS 热键组合 | | | `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.menu_enabled` | bool | true | 是否显示引导选单 |
| | `boot.timeout_ms` | int | 5000 | 选单超时(有输入暂停重计) | | | `boot.timeout_ms` | int | 5000 | 选单超时(有输入暂停重计) |
| **输入** | `input.hotkey_seq` | string | `volup,volup,volup` | 应用列表热键序列 | | **输入** | `input.hotkey_seq` | string | `volup,volup,volup` | 应用列表热键序列 |
+3
View File
@@ -11,6 +11,7 @@
- **板载服务目录**:`/usr/data/meow/services.d/<name>.conf`(M2.54,Data 区,恒定存在、权威优先;首启由 `S89meow_svcd` 预置 `mdnsd.conf`)。与 TF `MeowOS/services.d/` 双源,同名板载优先 + WARN;板载 `trigger=boot` 服务不依赖卡(拔卡保留)`[meow_svcd.md][meow_svcd.c]` - **板载服务目录**:`/usr/data/meow/services.d/<name>.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 应答器公布 `<host>.local` `[SYSTEM_SETTINGS §1][mips-repo/bins/meow_mdns]` - **主机名**:设置键 `system.hostname`(SET_STR,默认 `meowos`;RFC1035 首标签),`S89meow_svcd` 开机读 settings `sethostname`;供自研 mDNS 应答器公布 `<host>.local` `[SYSTEM_SETTINGS §1][mips-repo/bins/meow_mdns]`
- `/etc/meowos_version`:版本唯一来源(launcher 运行时 `load_meowos_version` 读;`MEOWOS_VERSION` 宏仅作 dev 占位兜底)`[DEV_NOTES #4]` - `/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`: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]` - 日志启动缓冲 `/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]` - 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]` `[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/<pkg>/{data,cache,config})`[APPS 附录 §2][FRAMEWORK_DESIGN 附录 D2]` - 环境注入(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/<pkg>/{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 划分原则(拔卡降级) ### 1.3 板载 vs TF 划分原则(拔卡降级)
- 板载只放**稳定框架+KB 级设置**;日志/字库/媒体等大件一律 TF 卡 `[TF_CARD_LAYOUT §0][SYSTEM_SETTINGS §2]` - 板载只放**稳定框架+KB 级设置**;日志/字库/媒体等大件一律 TF 卡 `[TF_CARD_LAYOUT §0][SYSTEM_SETTINGS §2]`
@@ -116,6 +118,7 @@
--- ---
## 4. SDK 之外系统组件 ## 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]`): - 通知(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` - 常量:`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` - 分类:`CAT_GENERAL/MESSAGE/EMAIL/EVENT/PROGRESS/MEDIA(=5)/COUNT`;紧急度 `URG_LOW/NORMAL/HIGH/CRITICAL`
Binary file not shown.
+763 -1
View File
@@ -325,7 +325,7 @@
#include <time.h> #include <time.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/mman.h> #include <sys/mman.h>
#include <sys/poll.h> #include <poll.h> /* 0-warning:musl 把 <sys/poll.h> 重定向到 <poll.h>,用后者消除 -W#warnings */
#include "meow_touch.h" /* M2.25:Launcher 统一触摸接口(电源菜单/确认框复用) */ #include "meow_touch.h" /* M2.25:Launcher 统一触摸接口(电源菜单/确认框复用) */
#include "pm_icons.h" /* M2.32:Material Symbols rounded 图标(电源菜单圆钮,alpha 位图+主题色) */ #include "pm_icons.h" /* M2.32:Material Symbols rounded 图标(电源菜单圆钮,alpha 位图+主题色) */
#include "sb_icons.h" /* MD3 状态栏/通知中心图标(Material Symbols rounded,32x32 单通道 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_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.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_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
/* ---- paths ---- */ /* ---- paths ---- */
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */ #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); 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 touch_fd = -1;
static int t_last_x = -1, t_last_y = -1; static int t_last_x = -1, t_last_y = -1;
static int t_down = 0; 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; t_down_valid = 0;
logmsg("menu tap at %d,%d (up %d,%d, d=(%d,%d))", tx, ty, logmsg("menu tap at %d,%d (up %d,%d, d=(%d,%d))", tx, ty,
t_last_x, t_last_y, dx, dy); t_last_x, t_last_y, dx, dy);
/* 阶段B:顶部热区下滑打开通知中心。判定在「滑动」分支内先于左缘返回——
* 左缘(down_x<MEOW_EDGE_W)起点交给 meow_edge_back 优先消费;
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
logmsg("menu: top-edge swipe-down -> 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、 /* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、
* 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */ * 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */
if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) { if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) {
@@ -6715,6 +6758,725 @@ done:
logmsg("settings page closed"); 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: /* 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 * 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 * then FBIOBLANK(4); on this device bl_power stays 0, so brightness==0 is
+116
View File
@@ -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 头,仅 <stdint.h> <string.h> <stdio.h> <time.h> —— 纯宿主可编。
*/
#ifndef MEOW_NOTIF_PANEL_H
#define MEOW_NOTIF_PANEL_H
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
/* ---- 手势判定 ---- */
/* 左缘宽度:与 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 */
+236
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
/* 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;
}