MeowOS for HiBy R3 Pro II — 公共发布版本(历史已重置)

• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC)
• 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库)
• 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo
增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
This commit is contained in:
MeowOS Dev committed 2026-08-30 01:12:34 +08:00
commit 245bee2946
136 files changed
+32730

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
# === 大文件 / 可再生产物(不入库) ===
# 固件镜像(可由 build_*.sh 从 rootfs 树重新打包)
*.upt
# TF 卡/NAND 全量备份
backups/
# 固件组装的 rootfs 树(可重新打包,勿入库;.upt 已排除)
fw_apps/
fw_ssh/
fw_ssh*/
# repack/round-trip 测试产物
repack_test/
# Rust 交叉编译工程(target/ 等体积大;如需保留源码可手动最小化)
ncm_mips_smoke/
# 旧 patch 演示/遗留超大树
patch_demo/
# 解包的 squashfs / rootfs
unpacked/
unpacked_v14/
# QEMU 全系统仿真(含 qemu-src、rootfs 镜像)
qemu_env/
# 交叉工具链(zig/musl.cc 大件,从 .tools 下的小脚本引用,重装可复原)
.tools/
# 设备 fb 原始帧 dump
*.raw
# 外部克隆的社区仓库(各自独立 git)
hiby_os_crack/
hiby-mods/
CNMPlayer/
# 旧 patch 实验/遗留(含大量 5.4MB hiby_player 变体二进制,可重新生成)
patch_v2_me/
patch_demo/
# 播放器二进制变体(stock/patched,可由 apply_patch 脚本+stock 重生成;勿入库)
patch_v2/hiby_player_*
patch_v2/cave_4b_*
# 临时/编译中间物
__pycache__/
*.pyc
*.o
*.a
# 大图(字库 atlas 生成物,可从 font/gen 重新生成)
font/atlas/
# SSH 私钥/认证(绝不入库;.pub 公钥可留但这里一并忽略,按需手动 add)
ssh_keys/
# 二进制备份(*.bak 由 build/升级流程生成,勿入库)
*.bak
*.orig
*.rej
# === 编译中间物 / 临时文件(不入库) ===
*.new
*.tmp
*.o
*.a
# 宿主测试产物 / 真机截图(device_test 下可再生产物)
device_test/*.png
device_test/*.rgb565_*.png
device_test/host_*.c
device_test/gesture_host_test.c
device_test/meow_ui_demo.c
device_test/meow_ime_demo.c
# SSH 密钥/认证(绝不入库)
ssh_keys/
# 本地开发文档(Analysis/Notes/Test/device_test/docs-plan,仅本地保留,不入库)
devdocs/
scripts/build/
+438
View File
@@ -0,0 +1,438 @@
# R3 Pro II 第三方应用加载器设计文档(APPS.md)
> 状态:**v2 定稿(Plan B:零 patch 看门狗启动器)**,2026-08-28
> 对应实现:`patch_v2/meow_launcherd/`(启动器守护进程)、`patch_v2/meow_watchdog/`(应用看门狗)、`patch_v2/hello_app/`(示例应用)
> 前置知识:ANALYSIS.md 第 9/11/12/16/17/18 节
> 变更记录:v1(2026-08-27)为 hiby_player 二进制 patch 路线(Qobuz 入口触发),cave 代码在真机不稳定,
> 已整体废弃;v2 改为零 patch 的独立守护进程 + 热键触发。
---
## 1. 目标与边界
### 1.1 目标
1. 硬件热键(**按住电源 + 单击音量减**,默认;可配 meow.conf `hotkey=power_voldown|seq`)→ meow_launcherd 打开应用列表(fb0 直绘,列出 TF 卡 `/usr/data/mnt/sd_0/MeowAPPs/` 下的应用)
2. 触摸/按键导航选择某个应用 → 守护进程 **SIGSTOP 冻结 hiby_player**(让位),fork/execve/waitpid 启动该应用的静态 MIPS 二进制
3. 应用进程退出 → **SIGCONT 恢复 hiby_player**(自动回到原界面)
4. 应用运行期间 meow_watchdog 提供虚拟返回键/沉浸手势(SIGTERM→1s→SIGKILL 兜底)
### 1.2 边界(v2 刻意不做)
- 不做应用沙箱/权限模型(应用以 root 运行,与 hiby_player 同权)
- 不做应用安装/卸载 UI(仅支持手动往 TF 卡放目录)
- 不做动态链接应用(只支持**静态** MIPS32r2 ELF)
- 不做应用内音频接入(见 §5,未来经 socket 通道)
- 不做滚动列表(应用 ≤ 8 个,单屏显示)
- 菜单内虚拟返回键不做(菜单只有「退出」语义;应用运行期的返回由看门狗提供)
### 1.3 安全红线
- 只允许触碰:TF 卡(`/usr/data/mnt/sd_0`)、`/usr/data`、bind-mount 覆盖点
- **绝不写 NAND 分区**;调试全程 bind-mount/直接替换可写卷文件,重启即回退,变砖风险为零
- hiby_player.sh 看门狗:`hiby_player 退出 → sleep 1 → reboot`。**SIGSTOP 不触发**(进程未退出);**严禁 killall hiby_player**,须先杀 hiby_player.sh 父 shell 再手动拉起
- **若 meow_launcherd 意外死亡(SIGKILL/崩溃)且播放器被冻结**:手动恢复 `kill -CONT $(pidof hiby_player)`(守护进程自身 SIGTERM 路径会自动 SIGCONT)
---
## 2. TF 卡目录规范(MeowAPPs)
挂载点:`/usr/data/mnt/sd_0`(TF 卡,exfat;hiby_player 内部虚拟盘符 `a:\` 映射到该目录)。
```
/usr/data/mnt/sd_0/ # TF 卡挂载点
└── MeowAPPs/
├── <app_id>/
│ ├── app.json # 应用描述(必填)
│ ├── <binary> # 静态 MIPS32r2 ELF(exec 指向)
│ └── icon.png # 预留:列表图标(v2 未用)
└── ...
```
### 2.1 app.json 字段
```json
{
"name": "Hello App",
"exec": "/usr/data/mnt/sd_0/MeowAPPs/hello/hello",
"version": "0.1",
"author": "whoever"
}
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `name` | ✓ | 列表显示名。**v2 仅支持 ASCII**(UTF-8 中文留待 v3,需完整 UTF-8→UTF-16 转换) |
| `exec` | ✓ | 应用可执行文件绝对路径(必须是**静态** MIPS32r2 ELF,soft/hard-float 均可) |
| `version` / `author` | — | 保留字段,v2 不解析 |
- 目录扫描深度 1 级:只枚举 `MeowAPPs/` 下的直接子目录
- 应用计数上限 **8**(超出忽略;单屏 8 行)
- app.json 解析器极简(launcher 内实现):仅匹配 `"name":` 与 `"exec":` 后的引号字符串,不做 JSON 校验(多行/单行均可)
- 列表刷新时机:每次菜单打开时重新扫描(TF 卡热插拔后立即可见)
### 2.2 边界情况行为(launcher 实现,全部真机验证)
| 场景 | 行为 |
|---|---|
| TF 卡未挂载/未插入 | 菜单提示「NO STORAGE CARD / INSERT TF CARD」(state=1,/proc/mounts 无 sd_0) |
| 卡挂载但无 MeowAPPs 目录 | 提示「NO APPS FOUND」+ 底部示例行 `MeowAPPs/<id>/app.json`(state=2) |
| MeowAPPs 为空目录 | 同上(扫描计数 0 → state=2) |
| app.json 损坏/缺 name 或 exec 字段 | 跳过该应用,列表其余项正常 |
| 二进制不存在/不可执行 | 行尾标 `[MISSING]`;点击/确认该行提示 toast「CANNOT START: <name>」(access X_OK 检查,不 fork;`launch_app` 返回 -1) |
| 二进制存在但 execve 失败 | v6.8 起 `launch_app` 用 CLOEXEC 管道检测 execve 失败(子进程写 errno),返回 -1 → toast;**不再把 127 误当「应用跑完」**。toast「CANNOT START」**仅在返回 -1(真启动失败)时显示**,应用干净退出(返回 0)不显示(Bug 3,旧版无条件显示) |
| 扫描读取出错(opendir/fopen/fgets 失败) | 各自跳过;最终无应用时按 state=2 提示 |
状态机:state 0=正常 / 1=未检测到存储卡 / 2=无可用应用。
---
## 3. 应用生命周期契约
### 3.1 启动流程(meow_launcherd 侧)
```
热键(按住电源 + 单击音量减;电源按下后 300ms~2.5s 内收到音量减按下边沿触发)
→ meow_launcherd 检测(EVIOCGKEY 状态轮询,10ms;电源按下即 arm,窗口外或松开即 disarm)
→ SIGSTOP hiby_player(/proc/N/exe == /usr/bin/hiby_player;等待状态 T)
→ 背光仅在面板灭时拉回(brightness==0 才写 saved/60,绝不无脑拉亮度)+ unblank 面板(先 SIGSTOP 再 unblank)→ fb0 直绘应用列表
→ 菜单循环(10ms):
· 触摸(event1 事件读)点击行 → 启动;点击「BACK TO PLAYER」行 → 退出菜单
· 按键(EVIOCGKEY 状态轮询):音量减/加=移动光标,播放键=启动
· **电源 = 纯息屏/亮屏(launcher 不干预亮度/面板,不退出菜单)**;退出菜单仅「BACK TO PLAYER」触摸行(或 launcher SIGTERM)
· 应用启动:fork → execve(exec[i])(失败 exit 127);同时 fork meow_watchdog <pid>
· waitpid(app) 阻塞;应用内强杀 = 音量减×3,退出回菜单
→ 应用退出 → 菜单退出 → SIGCONT hiby_player → 播放器恢复重绘
```
### 3.2 让位机制(守护进程方案,2026-08-28 真机验证)
**结论:SIGSTOP 整进程冻结 hiby_player(UI+音频+全部线程),菜单期间播放器完全不动作;SIGCONT 后原样恢复。hiby_player.sh 看门狗只盯进程退出,SIGSTOP 不触发 reboot。**
依据(真机实证,见 ANALYSIS.md §18):
1. **SIGSTOP 不触发看门狗**:进程存活(状态 T),`hiby_player.sh` 的 `sleep 1; reboot` 不执行。
2. **恢复确定可靠**:菜单每条退出路径(含 SIGTERM 处理、错误路径)都会 SIGCONT;`meow_launcherd` 自身被 SIGTERM 时也先 SIGCONT 再退出。
3. **崩溃安全**:应用崩溃/被杀 → waitpid 返回 → 自动 SIGCONT 恢复。若守护进程本身被 SIGKILL,播放器会冻结——手动 `kill -CONT $(pidof hiby_player)` 恢复(详见 §1.3)。
4. **菜单期间播放器完全不刷新**(SIGSTOP),fb0 归守护进程独占绘制,无抢绘。
5. **音频**:SIGSTOP 连音频线程一起冻结(应用运行期无声音;如需出声见 §5 未来方案——v2 的 SIGSTOP 方案下播放器无法代播,需改 waitpid-in-callback 让位或音频线程解冻,留待 v3)。
6. **应用退出 → SIGCONT → 播放器首帧整页重绘**,无需应用清屏。
### 3.3 应用该做什么 / 不该做什么
**应用必须:**
| # | 规则 | 说明 |
|---|---|---|
| 1 | 干净退出 | 任意退出码均可;守护进程 waitpid 立即返回,SIGCONT 恢复。`exit(0/1)` 即可,不要 `abort()` 留下僵尸 |
| 2 | 独占屏幕自绘 | 自行 `open("/dev/fb0")` + `mmap` 后绘制;设备帧缓冲 480×720(分辨率/色深运行时用 FBIOGET_VSCREENINFO/FSCREENINFO 探测,RGB565 或 RGBA8888 自适应)。**注意:本设备 ioctl 号 VSCREENINFO=0x4600、FSCREENINFO=0x4602(勿颠倒,见 §4.3)** |
| 3 | 自己读输入 | `open("/dev/input/event*")` 逐个打开,读到任意 key/touch 事件即处理。按任意键退出是推荐约定(物理事件对所有读者多播,播放器已冻结,无需担心抢事件) |
| 4 | 及时退出 | 建议空闲超时(如 30s 无操作)自动退出,避免长期占屏(hello 60s 看门狗) |
**应用不该做:**
| # | 规则 | 原因 |
|---|---|---|
| 1 | 不要尝试"恢复" fb | SIGCONT 后播放器首帧即整页重绘,无需你清屏。你清屏反而闪一下 |
| 2 | 不要 kill hiby_player | 触发 hiby_player.sh 看门狗 → 整机重启 |
| 3 | 不要继承占用声卡 | SIGSTOP 期间声卡仍被冻结的播放器持有,直接打开会失败/挂起(v2 无音频) |
| 4 | 不要 fork 出长期后台进程 | 你的退出应"真正退出",否则 SIGCONT 后后台进程仍占屏/抢输入 |
| 5 | 不要碰 NAND 分区/固件 | 只写 TF 卡与 /usr/data |
### 3.4 退出与恢复
- 应用退出 → waitpid 返回 → 菜单关闭 → SIGCONT 播放器 → 播放器恢复原界面
- 菜单内「BACK TO PLAYER」行 → 直接 SIGCONT(不启动任何应用);**电源键在菜单内不再退出菜单(v6.5 起电源=纯息屏/亮屏,仅 sys_server/内核处理背光)**
- 整机重启 → 一切回退原版(守护进程由 S90 随开机自启)
### 3.4.1 物理按键语义 —— 强制规范(系统统一管理,APP 禁映射)
> ⚠️ **红线**:物理按键是**系统资源**,语义由 MeowOS(launcher/watchdog/内核)**统一管理**。**任何非系统组件(MeowAPP)不得将物理按键映射成任何行为**,包括但不限于:电源键→OK/确认/exit/翻页,音量键→确认/选择,播放键→确认/返回。
**为什么**:用户在任何界面都必须能靠物理键完成系统级操作——**电源键=息屏/亮屏**(MeowOS 界面也必须能息屏)。若 APP 抢走电源键当"OK/exit",用户一按电源键,屏没息反而退出了 APP,**无法息屏**;音量键/播放键同理。物理键必须留给系统。
**约定(MeowOS 界面 = launcher 主菜单/选单/电源菜单,是唯一允许映射物理键的地方)**:
> ⚠️ **适用范围**:**除了电源键菜单(`show_power_menu`,允许"电源=确认、音量=导航")以外,所有按键信号由 MeowOS 全局接管**——**任何界面(含 MeowOS 自身的设置页 / 应用列表 / 选单、以及 MeowAPP)都不得私自定义按键行为**。物理键的语义只有一份:电源=息屏/亮屏/长按菜单/8s强关,音量=调系统音量,播放/上下曲=媒体控制(未落地前「抓住」)。任何界面(如系统设置页)**不得**把电源键/播放键映射成"返回/确认"这类自定义行为;界面导航一律走触摸(返回按钮/左缘左滑),按键语义统一由系统判。
| 键 | 系统语义(APP 收到的是语义事件,不是裸 keycode) |
|---|---|
| 电源 | 息屏/亮屏、长按弹电源菜单;**APP 内=纯息屏/亮屏**(绝不 exit/OK) |
| 音量 +/− | 调系统音量;**APP 内=调音量**(绝不选中/确认/翻页) |
| 播放/暂停 | 全局媒体控制(播放/暂停当前媒体会话);**APP 内=媒体控制**(绝不确认/打开) |
| 返回 | 触屏左滑/虚拟返回键 → SDK 语义 `on_swipe(LEFT)` / watchdog 信号,**非物理键** |
**MeowAPP 侧**:
- APP **不读 `/dev/input/event*`**、**不 open/mmap fb0**、**不处理任何物理 keycode**——一切走 SDK 的语义回调(`on_tap`/`on_swipe`/`on_tick`;`on_key` **仅用于 SDk 内部状态,不含电源/音量/播放的确认/exit 语义**)。
- APP 想"返回/退出"→ 用 `meow_ui_request_exit()` 或左滑,**绝不监听物理键映射 exit**。
- SDK 的 `on_key` 回调:**禁止 APP 用它实现"电源=exit"这类映射**。SDK 内部消化物理键的系统语义(息屏/音量/媒体),**只把触摸返回/手势等语义事件给 APP**。
**落地**:hello.c 当前 `on_key(KEY_POWER)→request_exit()` 违反此规范,需删除(改由 SDK/系统处理息屏);APP 退出走左滑/`request_exit`/系统返回。**MeowOS 系统设置页(run_settings_page)曾私自定义"电源/播放键=返回",M2.45 已按本规范改为全局语义(电源=息屏/亮屏/长按菜单/8s强关,播放=抓住),退出仅靠触摸(底部返回/左缘左滑)**——任何 MeowOS 界面新增时须遵守本规范,不得私自定义按键。
### 3.5 虚拟返回键 / 沉浸模式 / 看门狗
应用启动时,launcher **同时 fork 看门狗进程** `/usr/data/meow_watchdog <app_pid>`(固件 S99ssh 首启从 `/usr/bin/meow_watchdog` 拷到可写卷;launcher 先试 /usr/data 再试 /usr/bin):
| 特性 | 行为 |
|---|---|
| 虚拟返回键 | 看门狗独立 mmap `/dev/fb0`(RGB565 自适应),每 100ms 重画左上角 48×48 返回按钮(深色底+白箭头);读 `/dev/input/event1` 触摸,按下落在按钮区 → `SIGTERM` 应用 → 1s 后 `SIGKILL`(应用卡死也能退出)。**v6.4 防抖**(针对"启动误杀"):`last_x/y=-1` 起始(无坐标的按下丢弃)、BTN_TOUCH 按实际值匹配边沿(消除失同步翻转)、仅短按 ≤600ms 有效、启动后先 drain 一次、**冷却期 2.5s**(实测幽灵触摸窗口 1.2–2.25s;此窗内返回键不响应,改 `COOLDOWN_MS` 可调) |
| 沉浸模式 | 应用写 `/tmp/meowapp_state` 内容为 `1` → 看门狗不画按钮、只识别「左缘 24px 内起点向右滑 >50px」手势 → 发 `SIGUSR1`;写 `0` 或删除 → 恢复虚拟键 |
| 看门狗自退出 | 应用进程消失(`kill(pid,0)` ESRCH)或 launcher SIGTERM → 退出 |
| 强制退出兜底 | 应用运行期(launcher 处于 waitpid):**音量减连按 3 次**(间隔 < `hotkey_seq_gap`,与主序列同阈值)→ SIGTERM → 0.5s → SIGKILL → 恢复。⚠️ 响应音量键的应用(如示例 hello)会先自行退出(`exit:key`),强杀序列是**不响应音量键/卡死应用**的兜底 |
| 长按电源 | 硬件级强制关机(设备固有),最终兜底 |
| 测试模式 | `meow_watchdog <pid> --sim-tap N` / `--sim-swipe N`,无触摸设备也可验证 |
**应用协议约定**:
- 默认(非沉浸)由看门狗画返回键,应用**不必**处理返回手势
- **⚠️ 返回键信号 = `SIGTERM`**(看门狗 `handle_touch` 按钮命中 → `kill(app_pid, SIGTERM)`→1s→`SIGKILL`);`SIGUSR1` **仅**是沉浸左缘滑动手势,不是返回键。com 与 SIGTERM 默认终止会**不画 BYE** 直接死,故示例 hello 安装 `SIGTERM` 处理器:画 BYE + `exit(0)`,使返回键呈现确定性的「bye → 立即退出」(v6.8,Bug 2)。
- **⚠️ 应用启动后须防 goodix 幽灵触摸**(v6.8,Bug 2 根因):goodix 在启动触摸的尾部发出一段幽灵触摸(实测启动后 1.2-2.25s,§18.16),无防护时应用会**误把幽灵当下按而立即退出画 BYE**,且 launcher 菜单会把同一幽灵重放成行点击而**重开应用**(用户看到「bye 后又回 hello 主界面、需再点一次」)。hello v6.8 增加 `COOLDOWN_MS=2500`:启动后前 2.5s 忽略一切按下边沿,只有用户**真正**的下按才触发退出。launcher 侧同步在应用退出后 drain + 冷却(见 §4.7)。
- 沉浸应用写 `/tmp/meowapp_state`,处理 `SIGUSR1` 作为「返回事件」
- 应用异常卡死 → 虚拟返回键(若可见)→ SIGKILL;沉浸态卡死 → 音量减连按 3 次(强杀序列)
- **应用日志**:launcher 通过环境变量 `MEOW_LOG_DIR` 告知 TF 卡日志目录(无卡时不设),应用自行决定文件名(示例 hello 写 `hello.log`);启动时先 drain 输入队列、只响应"按下边沿"退出(释放事件/轨迹事件忽略)
### 3.6 应用运行期卡移除(M2.11 最佳努力)
**场景**:前台 MeowAPP 的二进制/资源位于 TF 卡(`exec` 指向 `/usr/data/mnt/sd_0/MeowAPPs/…/`)。运行中卡被拔出 → 应用访问卡上代码页/资源可能触发 **SIGBUS** 崩溃。
**launcher 侧(已实现,`launch_app` waitpid 循环内)**:
- 启动应用时记录 `app_on_card = (exec 路径在 MEOWAPPS 下)`;若是,则进入「运行期卡移除监测」。
- waitpid 主循环每 ~100ms 采样 `/dev/mmcblk0` 存在性。一旦发现卡设备节点消失(被拔)且当前应用在卡上 → **主动 SIGTERM 应用**(干净退出,避免等 SIGBUS 崩),`usleep(300ms)` 内未退则 SIGKILL 兜底,然后走正常「应用退出→回菜单」路径。菜单/日志提示「卡已移除,应用已结束」。
- 不做:把应用二进制拷到内存/板载 staging 再 exec(该方案权衡大小/内存,本阶段先记录不做)。
**应用侧规范(给第三方 MeowAPP 的约定)**:
1. **别依赖卡上资源当运行必需**:字体/封面/贴图等用 RAM 缓存到内存再渲染;运行期避免持续从卡 mmap/读大文件。
2. **读错误容错**:`open/read/mmap` 返回失败(卡没了)时别 `abort()`/崩溃,优雅退出。
3. **卡掉能干净退出**:收到 `SIGTERM`(launcher 因卡移除而发,或用户虚拟返回键)一律画 `bye` + `exit(0)`;`mmap` 到卡文件失败时视为可恢复/可退出,不 SIGBUS 宿命。
---
## 4. meow_launcherd 实现细节
### 4.1 架构:被动式守护进程(零 patch、零 grab、零注入)
**关键内核实证(2026-08-28,见 ANALYSIS.md §18.2)**:
1. **hiby_player 不持有 EVIOCGRAB**:播放器运行时对 event0/event2/event1 的 EVIOCGRAB 探测全部成功(非 EBUSY)——此前"播放器独占按键"的观察不成立或仅为瞬态。
2. **注入事件永不投递**:`write()` 到任何 event 节点都成功、且更新设备键状态(EVIOCGKEY 可见),但**任何 evdev 读者(含 grabber 自身)都收不到**——厂商输入子系统修改了投递路径。⇒「grab + write 回写转发」方案在真机不可行(原方案 B 的前半段作废)。
3. **物理事件多播**:播放器自身两个 fd 同时读 event1(触摸正常),证明物理事件对所有读者多播(标准语义)。
因此 launcher 采用:
- **按键 = EVIOCGKEY 状态轮询**(10ms,只读 ioctl,与 grab/投递无关,播放器抓不抓都不影响)——音量/播放(114/115/164/163, event2)、电源/上一曲(116/165, event0)。
- **触摸 = event1 事件读**(未被抓、物理多播)——菜单点击。
- **不 grab、不注入、不改播放器**——"按键无感"由构造保证(播放器按键路径完全没被碰)。
### 4.2 热键与菜单键
| 键 | 设备 | code | 用途 |
|---|---|---|---|
| 音量加 | event2 | 115 | 菜单光标上 |
| 音量减 | event2 | 114 | **热键第二击(电源按住后单击)** / 菜单光标下 / **应用内强杀序列键** |
| 播放/暂停 | event2 | 164 | 菜单确认 |
| 电源 | event0 | 116 | **热键第一击(按住)** / 菜单/应用内=纯息屏亮屏(不再退出菜单) |
| 下一曲 | event2 | 163 | (未用) |
| 上一曲 | event0 | 165 | (弃用——GPIO PC28 硬件链断,§18.12) |
**热键判定(v6.5 定案)**:**按住电源 → 电源按下后 300ms~2.5s 内单击音量减**(在窗口内收到音量减的**按下边沿**且电源仍被按住)⇒ 开菜单。电源按下会触发播放器息屏流程(**已接受的副作用**,用 v6.2/v6.3 已验证时序保证菜单可见:SIGSTOP 播放器 → unblank 面板 → 仅灭屏时拉背光);音量减单击副作用 = **音量 -1**(可接受)。电源松开或窗口超时(>2.5s)即 disarm;音量减在窗口外(<300ms 或 >2.5s)按下视为普通音量键,不触发。**纯单键序列**(v6 volup×3)与组合键(上一曲+音量加,GPIO 断,§18.12)均已退役,仅保留在 `meow.conf hotkey=seq`(历史备选,不推荐)。
热键 `meow.conf` 配置:
```
hotkey=power_voldown # 默认:电源按住 + 单击音量减(300-2500ms 窗口)
hotkey=seq # 历史序列热键(单键连按 N 次,不推荐)
```
(`hotkey_seq=`/`hotkey_seq_gap=` 仅在 `hotkey=seq` 时生效。)
⚠️ **v6.1 修复**:早期版本 hotkey_seq 解析在行尾 `\n` 处死循环(外层 while 不消费 EOL),配了该行 daemon 即卡死——已修复并在真机复测。该解析器同样用于 `hotkey=` 行(无此问题,逐行 `strncmp`)。
**音量副作用与恢复**:v6.5 主热键(电源+音量减)的副作用是**音量 -1**(单击音量减,播放器收到,直接作为音量调节——**无恢复需求,接受**)。序列热键(`hotkey=seq`,默认已不使用)的每次按下仍会音量+1×N,launcher 在第 1 次按下快照 CS43198 音量、序列完成触发时写回(**⚠️ 实测该 control 写无效,恢复为尽力而为**)——因已退役,不再展开。
**热键演变记录**:v1 电源+音量减 → v2 电源+上一曲 → v3 双音量同按(物理不可能:event2 单 ADC 通道)→ v4 上一曲+音量加(上一曲 GPIO PC28 硬件链断,§18.12)→ v6 单键序列(volup×3,单键物理必可注册)→ **v6.5 电源+单击音量减(定案)**:回到"跨设备组合键"能力(电源在 event0、音量减在 event2,两键不同 ADC/GPIO,物理可同按),利用电源按下必然触发播放器息屏的既有行为,以"按住电源 + 窗口内单击音量减"作热键,同时**移除菜单/应用内电源=逐级返回**(电源在菜单/应用内改为纯息屏/亮屏,见 §3.1/§3.4)。
### 4.3 fb0 绘图
- 480×720 RGB565(运行时 ioctl 探测,RGB565/RGBA8888 自适应)
- **⚠️ 本设备 fb ioctl 号(实测)**:`FBIOGET_VSCREENINFO=0x4600`、`FBIOGET_FSCREENINFO=0x4602`(旧式无 size 编码号)。hello.c/meow_watchdog.c 曾沿用标准头文件常量写反(0x4602/0x4600),导致 ioctl 返回垃圾、mmap EINVAL——已全部修正并用探针实测通过。
- **⚠️ 双缓冲(v6.6)**:本设备 fb0 为**双缓冲**(实测 `yres_virtual=1440=2×720`、`smem_len=2 帧`、`ypanstep=1`),**面板显示哪个缓冲由 fb_var.yoffset 决定,`FBIOPAN_DISPLAY=0x4606` 可翻页**。launcher 与 hello 等 app 都只画 mmap 偏移 0(buffer0),若 yoffset 停在 buffer1(播放器电源/息屏重绘后),菜单画在 buffer0 而面板显示 buffer1 → 菜单不可见、屏幕冻结(v6.5 bug)。v6.6 起 `fb_open` **mmap 整个虚拟帧缓冲**(含两帧),菜单打开时 `fb_backup_player` 快照当前 yoffset 处缓冲 → `fb_norm_buf0`(FBIOPAN_DISPLAY 把 yoffset 归 0,菜单/app 共同绘制目标)→ 回退时 `fb_restore_player` 把备份拷回原 yoffset 缓冲并 pan 回原 yoffset(播放器事件驱动不主动重绘,必须恢复 yoffset)。**面板 blank/亮度状态仍归播放器**(§18.11 教训),launcher 只在 fb 缓冲/翻页层面介入。
- **重绘纪律(v6.9,修复列表 ~3fps 频闪)**:菜单只在**内容真正变化**时整屏重绘——光标移动(输入)、应用退出/进入(重写了 buffer0)、toast 起止、未来通知徽标变化时置 `dirty`;**绝无周期整屏重绘**。v6.8 的「500ms 安全重绘」会在 500ms 定时把**整个菜单**重写进正在显示的 buffer0(就地改写产生撕裂/脉冲,且 `draw_statusbar` 每次调用都 `amixer` popen 拖慢主循环),正是频闪根因。若真需周期刷新(时钟每秒),用**只刷时钟数字**的轻量路径 `draw_clock()`(不弹 amixer、不碰应用列表),其余状态(电量/WIFI/BT/音量)仅在事件(dirty)时随整屏刷新。
- 布局:标题 MEOWAPPS / 高亮行(光标)/ 状态提示 / `BACK TO PLAYER` 行 / 底部按键提示 / toast 区(y≈584-617);**右下角开发版本号**(v6.9)——`MeowOS v<MEOWOS_VERSION> dev`,右对齐画在底部行 y=700,与 `BACK TO PLAYER`(y≤688)和左侧按键提示(至 x≈254)均不重叠。
### 4.4 命令行(含测试钩子)
```
meow_launcherd # 守护模式
meow_launcherd --scan # 打印应用列表与状态,退出
meow_launcherd --dumpkeys N # 轮询 N 毫秒,记录所有按键状态边沿(真机抓 key code 用)
meow_launcherd --sim-hotkey N # N 毫秒后模拟热键(默认=电源按住→N+800ms 单击音量减;hotkey=seq 时=音量加 ×3 快速连按,每次 300ms 间隔)
meow_launcherd --sim-tap N # 菜单打开 N 毫秒后模拟点击第 0 行
meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
```
测试钩子用于无物理输入时的全链路自动化验证(注入事件不投递、无法远程按物理键)。
### 4.5 统一日志系统(MeowAPPs 组件共用)
- **默认位置**:TF 卡 `/usr/data/mnt/sd_0/MeowAPPs/logs/meow_<boot时间戳>.log`(如 `meow_20260828_020123.log`;boot 时间戳 = 当前墙钟 − /proc/uptime,同一 boot 会话内多次启动共用同一文件)
- **无 TF 卡 / 卡晚挂载(S90 早于卡挂载)**:**开不了卡日志**——日志落 **/tmp 启动缓冲**(`/tmp/meow_boot.log`,上限 256KB 滚动),**绝不写板载存储**(/usr/data 只剩 ~10MB);**卡出现后自动迁移**(主循环 ~1s 周期重试 + 输入设备重试循环内也检查;无需开菜单)——`log_poll_card()` 打开卡日志并把 /tmp 缓冲整段搬入后删除 /tmp 文件
- **容量上限**:logs/ 目录总量 1GB,开机时超限按 mtime 删最旧
- **组件环境变量**:
- `MEOW_LOG_FILE` → meow_watchdog:追加写同一 boot 日志文件(无该变量则走 stderr)
- `MEOW_LOG_DIR` → 应用:日志目录(示例 hello 写 `<dir>/hello.log`)
- 内容:时间戳 + 全部按键边沿/菜单/应用事件(真机按键证据)
- PID 文件:`/usr/data/meow_launcherd.pid`;播放器发现:扫描 `/proc/N/exe` == `/usr/bin/hiby_player`
### 4.6 UI 规范(应用列表与 MeowAPPs 界面)
- **纯黑背景为默认**(`menu_bg = 0x000000`)
- **可配置**:TF 卡 `MeowAPPs/meow.conf` 支持 `bg_color=RRGGBB`(如 `bg_color=10284f`),菜单打开时重新加载
- **40px 状态栏**(v6.8 由 20px 翻倍;v6.9 时钟加秒):第 1 行 = 左侧时钟(**HH:MM:SS,每秒刷新**,v6.9;用 `draw_clock()` 只刷时钟数字,不弹 amixer、不碰应用列表——见 §4.3 重绘纪律)、右侧 `电量% WIFI BT`(电量读 `/sys/class/power_supply/battery/capacity`、WIFI 读 `/sys/class/net/wlan0/operstate`、蓝牙读 `/sys/class/bluetooth/hci0` 存在性);第 2 行 = 左侧 `VOL nn%`(音量读 CS43198 / amixer numid=1,值 0-255);下方 1px 分隔线(移至 y=40)。⚠️ 音量 amixer control 实测为**死镜像**(§18.13:播放器走 sa_hgl_dma 硬件直通,ALSA control 读回恒 0),故展示值可能恒为 0%,为已知设备限制。
- **开发版本号**(v6.9):右下角绘制 `MeowOS v<ver> dev`(字体无小写,实际渲染为大写 `MEOWOS V0.6.9 DEV`)。单一来源 = `meow_launcherd.c` 的 `MEOWOS_VERSION` 宏;`build_fw_apps.sh` 打包时用 `sed` 从该宏抓取写入 rootfs 的 `etc/meowos_version`,屏显与固件文件**同源,不会两处不一致**(版本号改一处即可)。
- 示例应用 hello 同样黑底 + 40px 状态栏(应用内左上角 48×48 返回键由 watchdog 绘制,允许盖住状态栏左侧)
### 4.7 应用退出后的菜单触摸守卫 + 启动失败检测(v6.8,Bug 2/3/4)
**Bug 2/4 根因(应用退出后菜单重放残留触摸)**:应用运行期间 launcher 的菜单触摸 fd(`touch_fd`)**不轮询**(被 `launch_app` 的 waitpid 阻塞),期间收集的触摸事件(goodix 幽灵触摸 + 真正退出应用的那次点击/按键)**全部排队**。应用退出回到菜单后,菜单下一轮 poll 会把这些残留事件**重放成新的行点击**——落在应用行则**重开应用**(症状:bye 后回 hello 主界面、需再点一次,Bug 2);落在「BACK TO PLAYER」行则**关菜单 → SIGCONT 播放器**(整屏闪回播放器,Bug 4)。
**修复(`run_menu` + `menu_after_app_exit()`)**:
1. **drain**:应用退出后立即读完并丢弃 `touch_fd` 的全部残留事件。
2. **冷却**:应用**真正跑过**(`launch_app` 返回 0)时置 `menu_touch_cooldown_until = now + 2500ms`,冷却窗内忽略一切触摸按下边沿(与 watchdog 的同值 COOLDOWN_MS 对齐,覆盖实测幽灵窗 1.2-2.25s);启动失败(返回 -1)不置冷却,避免无谓锁菜单。
3. **菜单重绘**:v6.8 曾把 `last_redraw` 从绝对时间戳 `t` 改为相对量 `el`(使 500ms 安全重绘生效)以覆盖应用退出后的残帧(如 BYE)。**v6.9 起摒弃周期整屏重绘**(它在 500ms 定时把整个菜单重写进正在显示的 buffer0,造成列表 ~3fps 频闪):改为**仅 dirty 时整屏重绘**,应用退出/进入路径显式置 `dirty=1` 主动覆盖应用帧;时钟每秒用轻量 `draw_clock()` 只刷数字。
**Bug 3 根因与修复**:旧代码在所有 `launch_app` 调用点**无条件**置「CANNOT START: <name>」toast,即使应用**正常跑完**。修复:仅当 `launch_app` 返回 **-1(真启动失败)** 才置 toast;应用干净退出(返回 0)不显示。`launch_app` 用 **CLOEXEC 管道**检测 execve 失败(子进程写 errno → 父进程读 >0 判失败并返回 -1),不再把子进程 `_exit(127)` 误当「应用跑完」。
**副作用(已知取舍)**:应用退出后 2.5s 内菜单触摸不响应(返回键误触/退出后紧急再操作需等待;与 watchdog 相同值,实测幽灵窗 1.2-2.25s)。物理触摸的幽灵无法注入复现(§18.2 注入不投递),以上防抖逻辑在注入/sim 链路已验证,物理最终确认需用户。
---
## 5. 音频通路(未来应用的出声方案)
参考 ANALYSIS.md 第 11 节结论:
- 声卡 = 内部唯一 card0(CS43198 双 DAC),**无混音**,谁打开谁独占;hiby_player 常驻持有 `hw:0,0`。
- **v2(SIGSTOP 让位)下播放器完全冻结,无法代播**。应用直接开 ALSA 会失败。v3 候选:
1. **改让位机制为"仅 UI 冻结"**(如让播放器走 waitpid-in-callback 或独立音频线程),应用经 unix socket 汇入通道(0x73d320 区,`AIRPLAY:CONNECT` 类消息 + 16-bit PCM)出声——采样率定 44.1k/16bit(shairport 路径)。
2. 应用退出后由播放器继续(现状 v2)。
- 音量/通路切换由 hiby_player 管理(硬件音量寄存器),应用只管喂 PCM。
---
## 6. 测试流程
> 设备已刷 SSH 固件:`ssh -i ssh_keys/<device_key> root@<IP>`(登录口令见私密记录,勿入库)。
> 所有设备操作遵守纪律:动播放器前数实例;重启前 `rm -f /data/recovery_all`;息屏测试后恢复背光;结束恢复设备干净。
0. **轮前检查**:`ls /data/recovery_all`(存在即删+告警);`ps | grep hiby_player | wc -l` 应=1。
1. **推送**:`meow_launcherd/meow_launcherd` → `/data/meow_launcherd`(测试期直接跑 `/data/...`);`meow_watchdog` → `/usr/data/meow_watchdog`;`hello` → `/usr/data/mnt/sd_0/MeowAPPs/hello/hello`。
2. **验证输入路径**(无物理按键时的替代证据):
- `meow_launcherd --scan` → 列出应用
- `meow_launcherd --dumpkeys 6000` + 物理按键 → 日志出现 `key VOL_DOWN/VOL_UP DOWN/UP`
- 或注入状态模拟:`evtool send /dev/input/event2 1 114 1`(仅更新键状态,无投递,供 EVIOCGKEY 观察)
3. **全链路**(自动化):`meow_launcherd --sim-hotkey 2000 --sim-tap 4000`;期间 `cat /proc/<pid>/stat` 应 T(冻结);fb0 截图验证菜单/应用画面(黑底+状态栏);注入上一曲(165)+音量加(115)按住 2s 强杀应用 → 日志 SIGCONT → 播放器回 R、实例 1、背光 101。
4. **边界 ×5**:无卡(umount sd_0 → state=1 → 重挂)、无目录(mv MeowAPPs → state=2)、空目录(state=2)、坏 app.json(跳过)、二进制缺失(`[MISSING]` + toast),各拍 fb0 截图存 `device_test/`。
5. **稳定性**:守护进程跑 ≥3 分钟,播放器 R、实例 1、无 recovery_all;SIGTERM 干净退出。
6. **固件**:`build_fw_apps.sh` → `fw_apps/r3proii_ssh_apps.upt`,回验 0 diff、rootfs ≤ 45,088,768 B。
7. **回退**:重启即回退原版(守护进程不装则无热键);设备侧删除 `/data/meow_launcherd` 等测试文件。
---
## 7. 已知风险与未决项
0. **⚠️ `/data/recovery_all` 恢复触发(设备会被出厂重置,最高优先红线)**:hiby_player 内部存在写该标志的代码路径(`0x4823d0` 无限循环 / `0x56d1c0` 消息处理器)。调试期间设备已被出厂重置两次。防护规矩:**每次重启播放器前 `rm -f /data/recovery_all`;部署后确认稳定才允许设备重启;发现该文件立即删除并告警**。Plan B 全程未触发(SIGSTOP 不崩溃播放器)。
1. **守护进程被 SIGKILL/崩溃 → 播放器冻结**:SIGTERM 路径会自动 SIGCONT,但 SIGKILL 无法拦截。恢复:`kill -CONT $(pidof hiby_player)`。v3 可加双重守护(两个进程互保)。
2. **热键副作用**:v6.5 主热键(电源+单击音量减)会触发播放器息屏流程(菜单打开用后置时序 unblank+仅灭屏拉背光,见 §3.1)、音量 **-1**(可接受);**不再碰电源键"逐级返回"**。仅当 `hotkey=seq` 时才有音量 +N(amixer 音量 control 写无效无法恢复,见 §4.2;可配 `hotkey_seq=play,play,play` 换取播放停顿)。
3. **菜单触摸依赖 event1 物理多播**:播放器不抓触摸(已实测),若未来固件抓取,菜单仍有按键导航兜底。
4. **UTF-8 应用名**:v2 仅 ASCII;中文名需实现 UTF-8→UTF-16 转换。
5. **应用挂死且虚拟返回键不可见(沉浸态)**:音量减连按 3 次强杀兜底;再不行长按电源硬件关机。
6. **SIGSTOP 冻结音频线程**:应用运行期无声(v3 方向见 §5)。
7. **开机时序**:S90 在 S92 播放器之前启动;输入设备由 S11 加载完成,launcher 启动时重试打开(30 次 × 1s)。
---
## 附录:MeowAPPs 开发规范 v2(包结构与数据目录/软件包)
### 1. 应用目录结构
```
MeowAPPs/<包名>/
├── app.json # 应用配置(名称/exec/路由/图标/框架声明等)
├── meow # 程序主体(静态 MIPS 二进制;也可多文件/脚本)
└── <资源文件/图标…> # 只读程序资源
MeowData/<包名>/
├── data/ # 持久应用数据(配置、播放列表、登录态…)
└── cache/ # 缓存(可清)
```
> 注:TF 卡为 exFAT,**不支持符号链接**,故数据目录不用链接,改由 MeowOS 通过**环境变量**注入(见 §2)。
### 2. 数据目录(MeowOS 负责,用环境变量注入)
- **程序主体 + 必要配置文件** 放 `MeowAPPs/<包名>/`(可替换/可升级)。
- **软件数据** 放 `MeowData/<包名>/{data,cache}`(独立于程序,重装/升级保留数据)。
- **MeowOS 每次启动应用前**:验证 `MeowData/<包名>/{data,cache}` 存在(否则建)。
- 启动时注入环境变量给应用:
- `MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/<包名>/data`
- `MEOW_CACHE_DIR=/usr/data/mnt/sd_0/MeowData/<包名>/cache`
- (与已有 `MEOW_LOG_DIR`/`MEOW_ACTIVITY`/`MEOW_CHAIN` 一致)
- **应用写数据用 `$MEOW_DATA_DIR`/$MEOW_CACHE_DIR**,无需链接;exFAT 也能跑。
- 好处:数据与程序分离;大体积数据在 MeowData(卡上);换程序不丢数据。
### 3. 软件包 `.mpk`
- **格式**:`.mpk` = tar.gz(含 `app.json` + `meow` 二进制 + 资源),**不含 data/cache**(运行时按设备生成)。
- **导出**:打包 `MeowAPPs/<包名>/`(**不含 data/cache 目录**)→ `<包名>.mpk`。
- **导入/安装**(MeowOS 软件包安装程序 `meow_pkg_install()`):
1. 校验 `.mpk`(app.json 合法、包名不冲突/是否覆盖、校验哈希);
2. 解压到 `MeowAPPs/<包名>/`(若已有同名,确认升级/覆盖);
3. 触发**首次启动数据初始化**(建 `MeowData/<包名>/{data,cache}`);
4. 登入应用列表。
- **后续**:Web 后台(阶段 C)提供上传/安装/卸载/导出 `.mpk` 界面;本地文件管理器可调用安装程序 UI(`meow_pkg_install` 的交互入口)。
### 4. 实现挂点(MeowOS/launcher)
- 启动应用入口统一调 `meow_app_prepare(pkg)`:建 `MeowData/<包名>/{data,cache}` + 设 MEOW_DATA_DIR/CACHE_DIR,失败回退提示。
- `meow_pkg_install/export`:包安装/导出(本期可先做 CLI/接口,UI 后续)。
- 这些为阶段 C(Web)/文件管理器预留。
### 5. 软件包管理器 CLI `nyai`(静态 MIPS,apt/yum 风格,含镜像源)
`nyai` 是 MeowOS 的包管理 CLI(静态 MIPS32r2/musl,无外部依赖;tar.gz 用 `gzip -dc | tar -x` 管道,与 launcher 一致)。**部署**:`MeowOS/opt/bin/nyai`(卡,随 TF_CARD_LAYOUT 外围工具目录)。
**调用**:当前固件(M2.11,无 `/etc/profile.d/meow_card_path.sh`)SSH 需绝对路径
`/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai` 或 `export PATH=/usr/data/mnt/sd_0/MeowOS/opt/bin:$PATH`;
M2.12+ 固件该目录已由 `meow_card_path.sh` 前插,`nyai` 可直接调用。
**命令**:
| 命令 | 说明 |
|---|---|
| `nyai update` | 读 sources.list 各源:本地读 `<源>/Packages`;http 用卡上 curl `-fsSL <url>/Packages` 缓存到 `MeowOS/opt/nyai/cache/`;无网正确提示、不硬失败 |
| `nyai install <包或文件.mpk>` | ① 参数是 `.mpk` 路径→直接装;② 是包名→按缓存索引取 `.mpk`(本地文件或 http 拉取)装;自动解析 Depends(递归)、校验 SHA256+app.json、同名先备份 `.bak_<pid>`、数据 init(建 MeowData/<pkg>/{data,cache}) |
| `nyai remove <pkg> [--purge]` | 卸载 MeowAPPs/<pkg>(数据默认保留);`--purge` 连 MeowData/<pkg> 删;清理 `.bak_` 残留 |
| `nyai list` | 列出 MeowAPPs/* 已装应用(名称/版本/占用),跳过 fonts/logs/.bak |
| `nyai info <pkg>` | 读 app.json 显示 name/version/exec/size/routes/services/depends/description |
| `nyai search [pattern]` | 扫索引列出可装包(含版本/描述) |
| `nyai list-upgradable` | 索引版本 > 已装版本的包 |
| `nyai index <dir>` | 为 `<dir>` 生成 `Packages` 索引(读每个 .mpk 的 app.json 提取 version/description,算 SHA256/Size/Filename) |
| `nyai export <pkg> [out.mpk]` | 打包 MeowAPPs/<pkg>/(不含 data/cache)→ `<pkg>.mpk` |
| `nyai scan [dir]` / `nyai upgrade <pkg> [file.mpk]` | 列出 .mpk / 升级(保留数据) |
| `nyai --help` | 帮助;退出码 0=成功 / 非0=错误 |
**源配置** `MeowOS/opt/nyai/sources.list`(卡;无此文件则默认本地源 `MeowOS/opt/repo`):
```
# 每行一个源,支持多源与 # 注释
local /usr/data/mnt/sd_0/MeowOS/opt/repo # 本地目录
http http://10.0.0.1/nyai # http(s) 源(url 下挂 Packages)
deb [trusted=yes] /usr/data/mnt/sd_0/MeowOS/opt/repo # apt 风格别名
file:///usr/data/mnt/sd_0/MeowOS/opt/repo # file:// 等价本地
```
**索引 `Packages`**(`nyai index <dir>` 生成,行式、空行分隔记录):
```
Package: simtest # 包名 = .mpk 文件名茎
Version: 1.2.3 # 来自该 .mpk 内 app.json
Filename: simtest.mpk
Size: 388 # 字节
SHA256: 3fea0b73… # 可选;有则 install 时校验
Depends: depapp # 可选,逗号/空格分隔,递归安装
Description: A simulated app
```
**与 launcher 一致**:install/export 复用 M2.12 `meow_pkg_install/export` 同套语义(tar.gz、gzip 管道、包名=文件名茎、同名先备份、校验 app.json、数据 init);exFAT 无符号链接,数据目录由 MeowOS 经 `MEOW_DATA_DIR/CACHE_DIR` 注入,CLI 只建目录不建链接。
+159
View File
@@ -0,0 +1,159 @@
# MeowOS 开发计划(HiBy R3 Pro II)
> 目标:把 R3 Pro II 打造成可扩展的"第二系统"。**MeowOS 作为主控层优先启动、拥有屏幕与输入;HibyOS 播放器降级为 MeowOS 启动的一个内置系统应用**(Android home/launcher 模型)。MeowLauncher(即 MeowOS 核心)、MeowAPPs、MeowPanel 均为其组成部分。
> 本文档是路线图与分阶段计划。当前技术档案见 `ANALYSIS.md`,应用规范见 `APPS.md`,框架协议见 `FRAMEWORK_DESIGN.md`。
---
## 0. 总览与已达成里程碑
**已达成(真机验证):**
- ✅ 官方固件 v1.4 逆向 / 解包 / 无损重打包链路
- ✅ SSH 固件(dropbear 双认证 + 14 工具)—— 已刷入,长期可用
- ✅ 全量 NAND 备份(TF 卡 `firmware_backup_1.4/`)
- ✅ MeowLauncher 零 patch 架构:热键(按住电源+音量减,窗口 60–2500ms)→ 应用列表菜单
- ✅ 应用列表、5 种边界处理、虚拟返回键、沉浸手势(左缘滑动)、统一日志系统(TF 卡,无卡回退内存缓冲)
- ✅ 纯黑 UI + 状态栏;fb0 双缓冲翻页机制(菜单可见性根因)
- ✅ 应用启动/退出/回列表全链路(让位机制:SIGSTOP/SIGCONT + fb 备份恢复)
**关键约束(教训已固化为纪律):**
- 严禁对 `hiby_player` 发未注册信号(SIGUSR1 实测导致整机重启)
- `hiby_player` 只能 SIGSTOP/SIGCONT,不能 killall(其 `hiby_player.sh` 看门狗会整机 reboot)
- 播放器为**事件驱动绘制**(无持续刷屏循环)→ 恢复画面必须"备份→还原+翻页→SIGCONT"
- fb0 为**双缓冲**(虚拟高 1440,2×720),显示帧由 `yoffset` 决定,须 `FBIOPAN_DISPLAY` 翻页
- 厂商内核**触摸/按键注入不投递**(`input_inject_event` 需 grab==handle);uinput 是阉割版
- 播放器「背光时间」会自动息屏(亮度 0 + FBIOBLANK(4))——任何亮度/面板干预都必须尊重它,只做"兜底+恢复"不做"对抗"
- 板载存储紧张(/data 仅 ~16MB)——日志/资源一律放 TF 卡,无卡绝不用板载
---
## 1. 分阶段路线图
### 阶段 0:双系统引导模型(用户定案,Phase 0 最终方案)—— **隐藏的地基,优先于应用模型**
> 战略方向:开机由 MeowOS 引导选单决定运行哪个系统;两系统互斥运行,任一时刻只管一个,内存充足,绕开 HGL 碎片化问题(MeowOS 界面只用普通 fb0 不需要 sa_hgl_dma,只有播放器需要且须开机连续内存才能启动)。
**双模式**:
- **HibyOS 模式**:选单后尽早拉起播放器(内存连续,双拿 fb0+HGL),全程官方体验。热键(电源+音量减) → **弹确认框** → 「是」**完全杀死播放器** → MeowOS 接管(MeowOS 界面不需要 HGL,接管无缝);「否」留在 HibyOS。(M2 已实现:**MeowOS 与 HibyOS 永不并发**,MeowOS 不控制 HibyOS 播放器媒体。)
- **MeowOS 模式**:选单后完全不碰播放器,只跑 MeowOS(64MB 全给 MeowOS;音乐由 MeowAPPs 直接出)。
- **切换规则**:HibyOS→MeowOS 用热键(杀播放器即可);MeowOS→HibyOS 只能重启回到选单(播放器需开机连续内存才能再次启动)。
**关键事实(§18.21 修正应用)**:
- MeowOS 界面路径:/dev/fb0 mmap + pan(不需要 sa_hgl_dma)。
- 播放器路径:需 /dev/fb0 + /dev/sa_hgl_dma(1.32MB 连续 DMA),仅在开机内存连续时可获取;重拉失败。
- ⇒ 单系统互斥运行:无并发内存竞争,无 SIGSTOP 让位需求(该机制退役)。
**实现要点**:
- init 改造:S90 meow_launcherd 变为**引导选单**(轻量 fb 绘制 + 触摸/按键选择)。选 HibyOS → exec/拉起播放器;选 MeowOS → 进入 MeowOS 主屏(不拉起播放器)。
- 参考播放器启动契约(§18.21):直接 exec `/usr/bin/hiby_player`(勿经 hiby_player.sh,其 `sleep 1; reboot` 看门狗);cwd=`/`;env -i 注入 11 变量。
- 热键进入 MeowOS(HibyOS 模式):播放器运行中按 电源+音量减 → 选单守护唤醒 → kill 播放器(SIGKILL?或 SIGTERM——注意 §18.21:SIGTERM 仅运行态有效,T 态需先 SIGCONT;严禁 SIGUSR1)→ MeowOS 接管。
- 待办:引导选单 UI → init 改造 → 两种模式的生命周期 → 热键切换 → 退役旧让位机制。
- 遗留:热键切换时播放器的音频/声卡释放需真机确认;HGL 碎片化 vs rmem 池单次分配未彻底区分(本方案下两种解释均不再关键)。
### 阶段 A:稳定化(当前 bug 批次,基本完成)—— 与各链并行,随时收尾
目标:把 MeowLauncher 日常用的核心链路做到零崩溃、交互顺手。
| 项 | 状态 |
|---|---|
| 热键窗口下限 60ms | ✅ v6.7 |
| 状态栏翻倍 + 加音量/蓝牙 | 🔨 agent-16(v6.8) |
| 应用内返回键"一次即退" | 🔨 agent-16 |
| 列表 "Cannot start" 误标 | 🔨 agent-16 |
| 回列表闪退 | 🔨 agent-16 |
| 物理按键/触摸全面验收 | ⏳ 需用户 |
---
## 阶段拓扑序(按因果依赖,非平行摆放)
> 说明:以下四条链按**依赖因果**排序——每个里程碑/阶段必须先于依赖它的项落地。**决策:按序推进,不在依赖未满足时抢先实现上层**。已达成项标记 ✅。
### 依赖链 A【系统启动模型】(隐藏地基)
```
[阶段0 双系统引导] ← 决定 MeowOS 默认开机、HibyOS 是否降级为系统app、SIGSTOP 让位机制是否退役
```
### 依赖链 B【UI 基建】
```
[渲染地基 M2.16 ✅]
└─ [SDK/控件层 meow_ui, B1] ← 几乎全部 UI 的底座(设置页/通知中心/媒体播放界面/网易云都依赖它)
└─ [B2a 设置后端 meow_settings_* + settings.conf + listener] ← 设置的单一数据源
└─ [B2b 实体设置页 UI] ──┐
└─ [C MeowPanel Web 面板] ─┘(B2b 与 C 都消费同一 B2a 后端,天然镜像)
```
### 依赖链 C【应用模型】
```
[M0 IPC 骨架 ✅]
└─ [M1 路由表+解析 ✅]
└─ [M2 通知存储+分发]
└─ [M3 导航栈+前台会话+看门狗返回] (依赖 M2;且依赖阶段0——SIGSTOP 让位应已退役)
└─ [M4 后台服务保活] (依赖 M3 导航栈 + 阶段0)
└─ [媒体会话(hiby适配/元数据/后台播放)] (依赖 M3+M4+阶段0)
└─ [M5 通知中心 UI 整合] (依赖 B 的 UI + M2 存储)
```
### 依赖链 D【应用主体】
```
[E 网易云客户端] ← 跑在链C 全链路(M3/M4/媒体会话)+ 链B(SDK)之上
```
### 关键因果修正(相对旧平行表)
1. **B2a 设置在 SDK 之后**——设置控件(开关/滑条/选择器)本身就是 SDK 控件的一种,SDK 是前置。
2. **SDK/控件层(B1)是被低估的底座**——应排在链B 最前(渲染地基之后)。
3. **媒体会话不是独立里程碑,是链C 收官**——依赖 M3 导航栈 + M4 服务保活 + 阶段0(HibyOS 降级)。
4. **M3/M4 依赖阶段 0**——文档强调"单系统互斥,SIGSTOP 让位机制退役";阶段0 不落地,M3"前台会话"与 M4"服务保活"会建立在新引入的让位机制上并返工。**这是原计划最大的隐性依赖。**
5. **B(通知中心 UI)在 B2a + M2 之后**——它消费设置后端(快捷键)+ 通知存储(通知列表),两头依赖。
### 依赖链 B【UI 基建】详细(旧阶段 B 归并于此)
- **阶段 B(状态栏+通知中心 UI)**:在 B2a 设置后端 + M2 通知存储 + SDK 控件之后。下拉通知中心消费阶段 C 链(M2)的通知存储;顶部快捷按钮行(WiFi/蓝牙/亮度/音量/主题)、亮度滑条(clamp ≥25)、通知列表(应用图标+标题+目标 Activity)——**全部走 `meow_settings_*` 后端,零旁路**。现代 UI 化:MD3(Material You)优先,MIUI/HyperOS 备选。
- **阶段 B2a(设置后端)**:`meow_settings_*` API + `settings.conf` 板载 UBIFS + listener 变更通知。SYSTEM_SETTINGS.md/MEOWOS_API.md 就绪。**为什么先于 B**:B 的快捷按钮/亮度滑条/主题开关全是设置项,后端不定就做 B 会借旧 `settings_load` 手写、后返工。listener 是"实体 UI ↔ Web 镜像"桥梁,也早于 C。
- **阶段 B2b(实体设置页 UI)**:分组列表+开关/滑条/选择器,只走 `meow_settings_*` API,不手写存取。音量跳步/上限/字体/主题/息屏模式等可配。
- **架构**:独立模块 `meow_settings_page.c` 编译进 launcher(复用渲染/全局按键/meow_settings 后端,无 IPC);逻辑层(分组/控件/读写 meow_settings_*)稳定,绘制层将来 MD3 重写时整块替换。
- **MD3 规范**:见 `MD3_UI_SPEC.md`(色调角色/圆角卡片/开关滑条选择器/轻量特效,dark/light 两套接 display.theme)。
- **入口(用户定案)**:后期 **MD3 重写启动器 UI** 后,在**启动器主页设左滑侧边栏**放设置入口;**当前先做过渡**——应用列表加"系统设置"builtin 项。MD3 重写启动器为独立后续里程碑(阶段 B 之后)。
- **阶段 C(MeowPanel)**:后端内嵌启动器(HTTP/JSON API + 静态文件服务),前端资源放 TF 卡。功能:实时性能、亮度/音量、文件管理、MeowAPPs 管理(.mpk 安装/导出)、设置镜像、实时画面、远程控制。设置页调 `meow_settings_*`;通知管理调链 C 的通知后端。
### 阶段 D(Activity/通知框架)—— 即链 C 的集合
> 通知中心/MeowPanel/应用生态的前置。协议设计已就绪(FRAMEWORK_DESIGN.md)。实现:M0 IPC 骨架 → M1 路由表 → M2 通知存储 → M3 导航/后台服务 → M4 媒体会话。M0/M1 已完成(0.7.10,M2.15)。
### 阶段 E:网易云客户端(链接 C + 链 B 之后,跑在框架上)
- 播放器「背光时间」需在应用内管理;应用退出按阶段 D 的导航语义。
### 阶段 F(可选项/远期)
- HiByLink 协议逆向 → 播放器级无线遥控;音乐库数据库管理(sqlite3 CLI 已具备);更多应用(播客/TTS/时钟等)。
- 更多应用(播客/TTS/时钟等)
---
## 2. 产品设计要点(已确认需求汇总)
- **让位机制**:应用启动 = SIGSTOP 播放器 + fb 备份/翻页/绘制;退出 = 还原备份 + 翻回 + SIGCONT
- **返回语义**:默认回应用列表;接阶段 D 后按导航栈回上一层
- **虚拟返回键**:启动器看门狗在左上角绘制 48×48;**沉浸模式**(应用声明)隐藏之,改左缘滑动返回(SIGUSR1);物理强杀组合保留
- **亮度安全**:任何亮度写入 clamp ≥25;不干扰播放器自动息屏
- **日志**:TF 卡 `MeowAPPs/logs/`,开机新文件,1GB 滚动;无卡不写板载
- **应用目录**:TF 卡 `MeowApps/<app>/{app.json, 二进制}`;app.json 含名称/图标/执行路径/路由表(阶段 D)
---
## 3. 开发与测试流程
- 迭代回路:改代码 → 交叉编译(zig + musl.cc)→ SSH 推 `/data` 或 bind-mount 部署 → 真机验证(注入 + 用户物理确认)→ repack 固件刷入
- 注入仅能测逻辑路径(触摸注入不可投递)→ **物理项一律需用户确认**
- 每次部署前后核对:播放器实例数、背光、bind 列表、recovery_all 标志
- 崩溃/异常优先看 `MeowAPPs/logs/` 与 `/proc/interrupts`(区分软件/硬件)
- 刷机走官方 SD 卡升级通道;变砖用官方固件刷回 + TF 卡全量备份兜底
---
## 4. 当前 backlog(按优先级)
0. [新] 阶段 0:可行性验证(播放器子进程化)→ init 改造 → MeowOS 主屏 + 播放器生命周期
1. [进行中] 阶段 A bug 批次(agent-16)
2. 阶段 B:状态栏升级 + 通知中心 + 快捷按钮 + 亮度滑条 + MD3 化
3. 阶段 C:MeowPanel(含实时画面 + 远程控制菜单)
4. 阶段 D:Activity/通知 IPC 框架(先协议文档 → FRAMEWORK_DESIGN.md 已就绪)
5. 阶段 E:网易云客户端
6. 阶段 F:HiByLink 逆向、更多应用
> 用户优先级:系统框架(阶段 0 + D)优先,MeowPanel 与网易云随后。
+223
View File
@@ -0,0 +1,223 @@
# MeowOS 阶段 D:Activity / 通知 IPC 框架设计文档
> 状态:设计稿 v1(2026-08-28)。先出协议与架构,本阶段只设计、不实现。
> 前置:ANALYSIS.md §18(Launcher 全架构)、§11(播放器 unix socket 先例)、APPS.md、DEVELOPMENT_PLAN.md 阶段 0/D。
> 实现语言:启动器 C(musl 静态 MIPS32r2,zig-mipsel-cc);应用同为 C 静态 MIPS。
> 补充:本文件由 plan 子代理产出设计,主会话落盘,并附「后台运行/后台播放权限」(用户追加)。
## 0. 目标与一句话概括
让 MeowOS 成为"单机迷你 Activity 框架":
- 应用经 **AF_UNIX socket** 向主守护进程(meow_launcherd,即 MeowOS 核心)推送通知、注册后台服务;
- 通知绑定 target Activity;通知中心点击后按路由表解析启动链路,逐层拉起应用、压入按应用隔离的导航栈;
- 返回语义:应用内逐屏回退到"上一层",回到栈根才退出到应用列表(回到 MeowOS 主屏 / 播放器)。
**核心架构决策**:
1. **IPC 线程只读帧+解析+入队;所有状态变更由主循环单写者应用**——主循环对时序敏感(10ms 热键轮询、SIGSTOP 让位、fb pan),绝不在 socket IO 阻塞。
2. **导航栈:应用管屏内上翻,启动器管"回到根→退出到列表"边界** + 冷启动跳转的栈初始化。
3. **后台服务永远无头(不碰 fb0/输入)、永不被 SIGSTOP**——SIGSTOP 只用于抢占用 fb/输入的前台会话。
## 1. IPC 通道(AF_UNIX socket)
- 类型 `SOCK_STREAM`,路径 `/usr/data/meow_ipc.sock`(unlink 后 bind),`chmod 0666`,backlog 4。
- 帧格式:定长头(16B) + 变长负载,**全部小端 LE**(mipsel 原生)。
```c
struct ipc_hdr { /* LE */
uint32_t magic; /* 0x4D4F5701 = 'MEOW'|1 */
uint16_t version; /* 1 */
uint16_t type; /* msg_type */
uint32_t req_id; /* 请求id,响应回显 */
uint32_t len; /* payload 字节数 */
};
```
- `len` 上限 `MEOW_IPC_MAX=4096`;读满 16B 头→校验 magic/version→读 len→分发。
- 列表编码:`[u16 count]` + count 个 NUL 结尾字符串。
### 消息类型
| type | 名称 | 方向 | 用途 |
|---|---|---|---|
| 0x0001 | MSG_HELLO | ↑ | 声明 app_id/版本/能力 |
| 0x0002 | MSG_ACK | ↓ | 握手/应答 |
| 0x0003 | MSG_ERR | ↓ | 错误码 |
| 0x0010 | MSG_REGISTER | ↑ | 注册后台服务(保持连接) |
| 0x0011 | MSG_SERVICE_OK | ↓ | 服务登记成功 |
| 0x0012 | MSG_SERVICE_STOP | ↑ | 请求停止服务 |
| 0x0020 | MSG_NOTIFY | ↑ | 推送/更新通知 |
| 0x0021 | MSG_NOTIFY_ACK | ↓ | 落库结果 |
| 0x0022 | MSG_NOTIFY_DISMISS | ↑ | 撤下通知 |
| 0x0023 | MSG_NOTIFY_LIST | ↓ | 下发通知清单(重同步) |
| 0x0030 | MSG_ACT_DECLARE | ↑ | (重)声明路由表 |
| 0x0031 | MSG_ACT_QUERY | ↑ | 解析 activity 链路 |
| 0x0032 | MSG_ACT_RESULT | ↓ | 返回链路 |
| 0x0040 | MSG_NAV | ↑ | 上报导航/请求退出 |
| 0x0041 | MSG_SHOW_ACTIVITY | ↓ | 令前端应用显示某屏 |
| 0x00F0 | MSG_SYS | ↓ | 系统事件(守护关闭/服务死亡/播放器状态) |
负载结构体见设计稿正文(m_hello/m_notify/m_register/m_act_* 等,定长多字节 LE)。
## 2. Activity 路由表
- `app_id` = `MeowAPPs/<dir>/` 目录名(app.json 可覆盖)。
- `activity_id` 全局唯一为 `app_id/activity_id`。
- **启动链路**:根屏→目标屏的 activity_id 序列(`;` 分隔),链长=逻辑深度。
- 静态路由用 **app.json 扁平行**(不引入 JSON 解析器):
`route.<id>=path`(如 `route.nowplaying=home;search;nowplaying`);`framework=true`(看门狗返回发 SIGUSR1);`service.<svc>=mode:restart:max:window_ms`。
- 运行时 `MSG_ACT_DECLARE` 并集覆盖静态。
- 校验:`route` 缺失→深度 1 兜底;activity 未注册→ERR_UNKNOWN_ACTIVITY。
## 3. 导航栈
- `struct nav_stack`(每应用一个,≤16 深度),`active` 前台标记。v1 同一时刻单前台应用。
- push/pop;**回退**:应用弹自己的屏并上报 `MSG_NAV{REPORT,depth,current}`;应用在根屏再按返回→`MSG_NAV{EXIT}`→启动器终止应用、恢复播放器、回列表。
- **冷启动通知跳转**:`resolve_chain` → 设栈深 → `launch_activity(app, chain)`,环境 `MEOW_ACTIVITY=<target>` + `MEOW_CHAIN=<id0>;<id1>;...;<top>`。
- 向下兼容:应用若不发 MSG_NAV,启动器保持初始深度;应用在根屏 `exit(0)` 即退出到列表(旧版行为)。
## 4. 通知生命周期
- 内存态(配额 ≤16/应用,满丢最旧);ttl 过期(1s 粒度);ONGOING 服务常驻通知(服务死→随清)。
- 点击按 route_status:0 跳转(tap_notification 逐层 launch);1/2 友好 toast(NO ROUTE / SOURCE NOT FOUND),不崩。
- 通知仅内存,不跨守护重启持久化(服务重启后重发重建)。
## 5. 后台服务保活
- `MSG_REGISTER` 登记;`service_spawn/stop/reap`;`SIGCHLD`→eventfd 回收;退避重启 1/2/4/10s(cap);`max_restart` 熔断→清通知+广播 MSG_SYS{2}。
- **服务无头契约**:禁止 open /dev/fb0、禁读 /dev/input/*、禁碰亮度/面板;**永不被 SIGSTOP**。
- SIGTERM 时依次关服务再 SIGCONT 播放器。
## 6. 与现有启动器整合
- `main()` 起 IPC 线程 + eventfd;主循环 `poll(eventfd,0)` 排空 pending。
- `run_menu` 的 poll 集合加 eventfd(通知徽标 + 阶段 B 下拉面板预留 API:`notify_count/at/dismiss`)。
- `launch_app` → `launch_activity(a, chain)` + env;waitpid 循环 poll eventfd 收 NAV{EXIT/REPORT}。
- 看门狗:framework 应用虚拟返回键发 SIGUSR1(沉浸手势同信号),非框架保持 SIGTERM。
- **所有 IPC/通知触发的 launch 必须走统一安全出口**(fb 备份→归 0→绘制→...→fb 恢复→SIGCONT),提前返回也要确保恢复。
## 7. 边界(不崩启动器,给友好提示)
未注册/路由缺失/二进制缺失/exec 失败/秒崩/服务熔断/配额/坏帧/应用死锁(看门狗强杀兜底)等,逐一对应 toast / MSG_ERR / 日志,见设计稿正文表。
## 8. 安全
单机信任模型,AF_UNIX 0666 无认证不防恶意;只做健壮性防御(帧长/配额/服务数/熔断/越界检查)。明确不做沙箱与恶意应用防护。
## 9. 实现里程碑
M0 IPC 骨架(含 meow_ipc 测试钩子)→ M1 路由表+解析 → M2 通知存储+分发 → M3 导航+前台会话改造+看门狗返回 → M4 后台服务保活 → M5 阶段 B 通知中心 UI 整合 → M6 加固+边界+稳定性验收。建议先落 M0+M1(纯内存/socket,SSH 全验证,不动播放器),再进 M3(触碰 SIGSTOP)。
### 进度(阶段 D)
- ✅ **M0 IPC 骨架**(0.7.10,M2.15):`meow_launcherd` 加 IPC 线程 + AF_UNIX `/usr/data/meow_ipc.sock`(unlink+bind+chmod0666+listen4)+ 帧解析(16B 头 magic=0x4D4F5701/version=1/type/req_id/len,LE)+ `MSG_HELLO/ACK/ERR` + eventfd 唤醒主循环 + 无锁 SPSC 内存环(`__sync`)+ 日志埋点。遵守 §1/§6.2:IPC 线程只读帧+解析+入队,主循环 `meow_ipc_process()` 单写者改状态,跨线程 eventfd,主循环 10ms 热键轮询每圈非阻塞排空(eventfd 失败时 10ms 轮询兜底)。测试钩子:`meow_ipc`(`patch_v2/meow_launcherd/meow_ipc.c`,静态 MIPS,与 evtool 同定位)+ launcher `--ipc-only N [--ipc-sock <path>]`。验证:`meow_ipc hello`→ACK、播放器不动(MeowOS 模式下无播放器即满足)。
- ✅ **M1 路由表 + 解析**(0.7.10,M2.15):`app.json` 扁平行 `route.<id>=path` / `framework=true` / `service.<svc>=mode:restart:max:window_ms` / `icon` 解析入 `g_accts` 注册表(主循环单写者);`MSG_ACT_DECLARE`(运行时并集覆盖静态)/`MSG_ACT_QUERY`/`MSG_ACT_RESULT`(深度+链路);`resolve_chain(app,activity)` 返回 `[root..target]` 链路与深度——`route` 缺失(app 未声明路由)→ 深度 1 兜底 `chain=activity`;声明了路由但 activity 非 route 键 → `MEOW_ERR_NOENT`(activity 未注册)。验证:`meow_ipc query <app> <activity>` 返回链路(qemu + 真机 MeowOS 模式均通)。
- ⬜ **M2 通知存储+分发**、**M3 导航+前台会话改造**、**M4 后台服务保活**、**M5 通知中心 UI**、**M6 加固**:后续里程碑,本轮未做。
- ✅ **渲染地基(0.7.11,M2.16,前置基座)**:所有 UI 统一走 **RAM 离屏后备缓冲 `back_buf`(480×720×2=691,200B) + `meow_present`(dirty)** = `FBIO_WAITFORVSYNC` 等垂直同步 + 单次原子 memcpy(脏区或全帧)到 fb0(buffer0,yoffset=0)——无渐进/无中间态/无撕裂。脏区 `meow_dirty_add`(矩形合并集、相交吸 bbox、>12 转全屏、裁可视区);状态栏时钟/右侧格按需局部标脏。整合铺垫:框架后续 LAUNCH/NOTIFY/前台会话的绘制复用该机制(而非旧「全屏 clear→重绘到 buffer0」路径)。详情见 ANALYSIS.md §18.43。刷新率调度(display.refresh_rate 60/30/15 + refresh_always_max + 自适应降档 governor)归口 PERF_SCHEDULER.md。
- 测试钩子说明:`meow_launcherd --appinfo <app.json>`(打印单个 app.json 解析结果)、`--ipc-only N`(免 fb/输入跑 IPC)、`--ipc-sock <path>`(覆盖 socket 路径供 qemu sim);`meow_ipc hello|ping|query|declare`(客户端)。
---
## 附录 A(用户追加):后台运行 / 后台播放权限
**需求**:允许应用声明后台运行、后台播放等能力,避免播放类应用切到后台被直接终止。hiby 播放器作为系统应用单独适配。
**设计**:
- 应用经 `MSG_HELLO`/`MSG_REGISTER` 声明 **声明式权限**:`PERF_BACKGROUND`(可后台常驻)、`PERF_AUDIO_BACKGROUND`(后台音频/播放,要求系统**不因前台切换而杀掉**)。
- 权限粒度在 `MSG_HELLO.flags`/`MSG_REGISTER.flags` 扩展位:`MF_BACKGROUND`、`MF_PLAYBACK`。
- **后台播放的保活路径**:真正后台播放音源 = 注册**后台服务**(§5)+ 绑定一条 ONGOING 通知(用户可见"正在播放")+ 声明 `PERF_AUDIO_BACKGROUND`。守护进程据此**不因前台应用切换杀掉它**;退出前台会话时若该应用声明了后台能力,只结束其前台会话(SIGSTOP 播放器释放 fb/输入),**保留服务进程与音频**。
- **hiby 播放器单独适配**:作为 `app_id=hiby` 的内置系统应用(阶段 0),MeowOS 管理其启停。它天然是"后台播放"——MeowOS 主屏/通知中心使用时,hiby 播放器作为后台服务/音频源继续播放(符合其"音乐播放器"角色,不因用户切到 MeowOS 界面而停歌)。实现上:hiby 播放器注册为系统级后台音频,MeowOS 在任何前台会话下都不 SIGSTOP/终止它,仅在其自身退出时回收。
- **前台/后台切换规则**(框架统一):
- 前台应用(占用 fb/输入)切后台 → 结束其前台会话(恢复 fb/播放器),除非其声明 `MF_PLAYBACK` 且音频在播——此时仅释放视图,音频服务保留。
- 后台服务从不被 SIGSTOP;只有前台会话才 SIGSTOP。
- 缺省(未声明)应用:切后台即终止(现行为),最保守。
- **注意**:真正"切后台还在放"仍受硬件约束——音频只能走 hiby 播放器 socket 代播(44.1k/16bit)或应用直开 hw(需先确认 SIGSTOP 后播放器是否释放声卡,阶段 E 实测)。框架只保证生命周期(不被杀),不保证声卡空出;声卡占用问题是独立事项,记录为阶段 E 待测。
(完)
---
## 附录 B(用户追加):默认播放控制界面 / 正在播放(Media Session)
**需求**:MeowOS 启动器内置一个**默认播放控制界面**(Now Playing)。其他播放类应用(如网易云)在后台播放时,通过接口提供**实时播放数据**(元数据、歌词、封面等),供启动器的播放界面显示;启动器界面也能向应用下发**播放控制指令**(播放/暂停/上一首/下一首/进度)。
**设计**(在 §1 IPC 基础上新增"媒体会话"消息):
- **上报(应用→启动器)`MSG_META`**:实时元数据。负载:
```c
struct m_meta {
char title[128];
char artist[128];
char album[128];
uint32_t pos_ms; /* 当前播放位置 */
uint32_t dur_ms; /* 总时长 */
uint32_t state; /* 0=停止 1=播放 2=暂停 3=缓冲 */
uint32_t flags; /* MF_ART / MF_LYRIC / MF_COVER */
char artwork[192]; /* 封面: 本地路径或 MeowOS 存储槽位 */
char lyric[192]; /* 歌词: 路径或文本槽位 */
char app_id[32];
};
```
- 流式更新(播放位置/进度)用高频 `MSG_META`(或拆出轻量 `MSG_PROGRESS`,由应用按 ~1s 推送);元数据变化低频。
- **指令(启动器→应用)`MSG_PLAYCTL`**:
```c
struct m_playctl { uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek */ int64_t seek_ms; };
```
- 启动器播放界面上的按钮/滑条 → 通过该应用的**服务连接**下发。
- **注册**:播放应用声明 `MF_PLAYBACK` 并注册**媒体会话**(`MSG_REGISTER` 带 `svc=media_session` 或 `HELLO.flags` 含 PLAYBACK)→ 成为当前"正在播放"提供的源。
- **启动器表现**:
- 播放界面在**主屏/通知中心**有入口(默认一个 Now Playing 卡片):封面(缩略图,从 artwork 路径/槽位读)、标题/歌手/专辑、进度条、播放/暂停/上下首按钮。
- 歌词/封面若应用提供(MF_LYRIC/MF_COVER),启动器可显示(滚动歌词、封面大图)。
- 无音频源时显示"未在播放"占位。
- **hiby 播放器适配**:`app_id=hiby` 作为系统应用上报元数据(其自带曲目信息),启动器播放界面可显示官方播放器的歌名/进度,并下发放置控制。实现时从 hiby 播放器读元数据(其 SQLite 库 / 运行时状态)或后续再经其 socket 对接;阶段 0/D 落地后作为系统应用接入。
- **边界**:无应用上报 → 播放界面空态;多源 → 以最近活跃媒体会话为准(可扩展为"媒体源选择");应用退出/服务死亡 → 清 Now Playing 并回空态;进度/歌词缺省则相应区域留空。
- **与后台播放权限的关系**:MF_PLAYBACK 声明 + 媒体会话上报 → 框架视为后台播放源;启动器在主屏/通知中心时**不因用户离开该应用前台而停其音频**(附录 A),保证"切到 MeowOS 界面歌还在播、界面还能显示和控制"。
(完)
---
## 附录 C(用户追加):通知接口适配 freedesktop.org 桌面通知规范
**需求**:为最大化兼容既有通知生态,通知中心接口适配常见 X11 桌面应用使用的接口规范。
**决定**:通知后端提供**双接口**——
1. **轻量 IPC**(本框架 §1 的 AF_UNIX,MeowAPPs 用,带 Activity 路由等定制特性)。
2. **`org.freedesktop.Notifications`(freedesktop.org Desktop Notifications 规范)**——设备已跑 dbus-daemon 系统总线 + glib/gio/libdbus 可用,新增独立守护 `meow_notify_dbus` 把该 D-Bus 接口桥接到 MeowOS 通知中心后端。
**`org.freedesktop.Notifications` 方法/信号**(标准):
- `Notify(app_name, replaces_id, app_icon, summary, body, actions[], hints{}, expire_timeout)` → 存入通知后端(映射 summary/body/app_name/icon + 可选 target Activity via hints)
- `GetCapabilities()` / `GetServerInformation()` / `CloseNotification(id)` / `GetCapabilities`
- 信号:`NotificationClosed(id, reason)`、`ActionInvoked(id, action_key)`
**实现要点**:
- `meow_notify_dbus`(独立小守护,链 libdbus/GDBus)在系统总线注册 `org.freedesktop.Notifications`;收到 Notify → 经 MeowOS IPC 后端入通知存储;后端关闭/动作 → 回发信号。
- 任意遵循该规范或用 libnotify 的客户端(含未来移植的 X11/桌面应用)→ D-Bus 推送 → MeowOS 通知中心显示。
- MeowAPPs 可继续用轻量 IPC;第三方兼容走 D-Bus。
- 阶段 D 实现里把本兼容层并进(M4 附近),通知中心(阶段 B)消费统一存储。
---
## 附录 D(用户定案):与成熟标准统一(全部采纳)
> 最大化生态兼容;阶段 D(及媒体会话/设置/应用规范)实现时一并落地。
### D1. MPRIS(机制:`org.mpris.MediaPlayer2`)
- 对齐媒体会话(附录 B):MeowAPPs 播放器暴露 **`org.mpris.MediaPlayer2` / `.Player`** D-Bus 接口(设备已有 `mpris-proxy`);MeowOS Now Playing(默认播放控制界面)经 MPRIS 读取元数据/进度/封面 + 下发播放/暂停/上下首/seek。
- 方法/属性:`Play/Pause/Next/Previous/Stop/PlayPause`、`Seek/SetPosition`、`Metadata`(title/artist/album/artUrl/length)、`PlaybackStatus`、`Position`;信号 `Seeked`/`PropertiesChanged`。
- MeowOS 通知中心/媒体卡片显示 MPRIS 源;多个源时按最近活跃选择。
### D2. XDG Base Directory(数据目录兼容)
- 启动应用时,在注入 `MEOW_DATA_DIR`/`MEOW_CACHE_DIR` 基础上,**同时设置**:
- `XDG_DATA_HOME=<MeowData/<pkg>/data>`、`XDG_CACHE_HOME=<MeowData/<pkg>/cache>`、`XDG_CONFIG_HOME=<MeowData/<pkg>/config>`(config 目录首次启动若缺则建)。
- 遵循 XDG 的移植应用直接复用我们数据目录,零改码。
### D3. freedesktop Desktop Entry(应用元数据对齐)
- `app.json` 字段与 `.desktop` 对齐:`Name`/`Icon`/`Exec`/`Categories`/`Comment`/`StartupNotify`;可额外映射 `org.freedesktop` 相关。
- 图标可选遵循 freedesktop 图标主题(hicolor 等);MeowOS 应用列表读取这些元数据。
### D4. UPower / sysfs(电源标准,天然兼容)
- 已读 `/sys/class/power_supply`(电量/充电/电压),对齐 UPower 语义;MeowPanel/状态栏用同名概念。保留。
### D5. syslog 日志输出(可选)
- MeowOS 日志除 TF 卡/内存环外,**追加输出到 syslog**(设备 busybox syslog;`log.level` 控制),让 MeowOS 关键日志进标准系统日志流,便于与系统日志合并排查。
### D6. ALSA mixer 控制命名(音频兼容)
- 音量已对齐 CS43198 标准 control(`numid=1 Left Playback Volume`/`numid=2 Right Playback Volume`),MeowOS 音量键/面板用同名字节,与 ALSA 生态一致。保留。
## 实现归入
- D1/D2/D3 → 阶段 D 媒体会话/应用规范/应用启动(FRAMEWORK_DESIGN §1-§6 扩展)。
- D5 → 日志系统升级(SYSTEM_SETTINGS log.*)。
- D4/D6 → 状态/音频子系统(已有,确认保留)。
+115
View File
@@ -0,0 +1,115 @@
# MeowOS 应用返回手势与物理按键语义 —— 专项方案
> 状态:设计稿(2026-08-29)。目标:修三件事——① 去掉 APP 内的虚拟返回键(watchdog 顶部 48×48);② 左滑返回**抬手才触发**;③ 左滑过程加**视觉反馈条**(顶部横条,长度=手指距左边缘)。并落实「物理按键语义系统统一管理、APP 禁映射」规范(见 APPS.md §3.4.1)。
---
## 0. 背景与问题定位
用户实测 SDK 版 hello(B1)反馈 3 个问题:
1. **左上角返回键太丑**——由 `meow_watchdog` 的 `paint_button()`(`meow_watchdog.c`)绘制,仅在 **非沉浸模式**(`/tmp/meowapp_state != "1"`)时每 100ms 重画。
2. **左滑返回"没松手就触发"**——实际看代码,watchdog 手势(`handle_touch` release edge 判 `SWIPE_DX`)与 SDK 手势(`meow_ui_input.c` `touch_release` 判 `SWIPE_THRESH`)**都是抬手触发**。但 hello 的 `on_swipe(LEFT)→request_exit()` 无条件处理左滑,加上 SDK 的 `SWIPE_THRESH=60px` 偏小 → 用户在"准备滑动"时 SDK 提前判成 swipe 发出 → 显得没抬手就返回。
3. **左滑无视觉反馈**——手势过程中没有任何可视化。
**根因链**:当前 hello 走「watchdog 非沉浸模式」(画返回键、watchdog 管手势);但应用现在用 **SDK**(B1),SDK 有自己的手势。**两套手势/返回键并存**是乱象来源。**正确方向:应用返回/手势统一由 SDK 负责,watchdog 只留安全退出兜底,虚拟返回键删除。**
---
## 1. 核心设计:返回手势统一收到 SDK
**明确分工**:
| 组件 | 职责 | 改什么 |
|---|---|---|
| **SDK(meow_ui)** | APP 内返回手势(左滑)、视觉反馈条、触摸语义事件 | 手势抬手判定、新增 `on_swipe_progress` 回调、画顶部反馈条 |
| **watchdog(meow_watchdog)** | 仅安全兜底(卡死强杀)、沉浸前后台切换 | 删除返回键绘制(或仅保留"无返回键");APP 手势不再走 SIGUSR1 |
| **launcher** | app.json `immersive` → 写 `/tmp/meowapp_state` | 解析 `immersive` 字段写入状态文件 |
| **hello(示例)** | 删除 `on_key(KEY_POWER)→request_exit()` | 遵守 §3.4.1:物理键不映射 |
**沉浸模式**:APP 声明 `immersive:1` → launcher 写 `/tmp/meowapp_state=1` → watchdog 不画返回键 → 手势归 SDK。
---
## 2. 具体改动
### 2.1 虚拟返回键:删除(watchdog)
- `meow_watchdog.c`:**删除 `paint_button()` 的调用与返回键绘制逻辑**(顶部 48×48 按钮、`draw_arrow`、按钮区命中逻辑——211-217 行)。
- watchdog 保留:**app 卡死兜底强杀**(音量减×3 已存在)、app 存活检测(`kill(pid,0)`)。
- 删除后 watchdog 不再画任何 UI;APP 返回 100% 由 SDK 负责。**注意**:非沉浸模式下也从"画返回键+SIGTERM"退化为"无按钮",仅触摸左滑返回(与沉浸统一)。若担心某些 APP 不用 SDK,它们仍走原 SIGUSR1/SIGTERM 兜底,但**helloS DK 版走 SDK 手势**。
### 2.2 左滑返回:抬手触发(SDK)
- `meow_ui_input.c` 的 `touch_release()`:**手势判定逻辑保持抬手触发**,但调优阈值与判定:
- `SWIPE_THRESH` 从 60 → **80~100**(更大容错,防止"摸索滑动"被误判)。
- 增加**方向限定**:左滑返回只认「**以左边 24px 边缘为起点**」(`t_dx < 24` 且 `mdx < -SWIPE_THRESH`),避免屏幕任意处左滑都退出。
- **抬手才 push_ev(MEOW_EV_SWIPE)**(已是);补注释明确"release edge 才触发,按下/移动过程不触发"。
- `hello.c`:`on_swipe(LEFT)` 保留 `request_exit()`(这是 SDK 语义,不违反规范);但判定由 SDK 收紧(左缘起点+大阈值)。
- **删除 `on_key(KEY_POWER)→request_exit()`**(违反 §3.4.1):hello 不再因电源键退出;电源键在 APP 内=纯息屏(系统处理)。APP 退出走左滑/SDK 返回。
### 2.3 视觉反馈条(SDK 新增)
- 新增 SDK API:`void (*on_swipe_progress)(int x_start, int dx);`(触摸**按下后移动过程**实时回调,x_start=按下 X,dx=当前相对位移)。
- 输入线程在 `BTN_TOUCH` 按下后、且 `ABS_X` 更新时,若满足"`t_dx < EDGE_W(24)` 且当前 `t_cx < t_dx`(右滑/左滑方向)"→ 实时 `push_ev(MEOW_EV_SWIPE_PROGRESS, t_dx, t_cx)`。
- SDK 绘制层新增 `meow_ui_swipe_hint(int x)`:在**屏幕顶部**画一个「N 像素高的横条,从最左边缘向内延伸,长度 = 手指当前 X」:
- 形状:`fill_rect(0, 0, x, several_px, accent_color)`,高约 4~6px,用主题 accent 色(夜间高对比)。
- **实时联动**:每次 `on_swipe_progress` 更新画条(`flush_region` 只刷顶部条区域),抬手达标触发返回时条消失(触发行消失,回到 APP)。
- **视觉约束**:条仅"滑动过程中"可见;手指回到原点/抬手未达标 → 条消失。不做复杂动画(CPU 软渲),就是实时长度的横条。
### 2.4 app.json immersive 传递(launcher)
- launcher 解析 `app.json` 的 `immersive` 字段(布尔),应用启动时写 `/tmp/meowapp_state` = `1`/`0`;退出时清理。
- 当前 launcher 源码 grep `immersive` 无实现 → **新增**。SDK 应用默认声明 `immersive:1`(hello)。
---
## 3. 验收(宿主已验证项 + 真机待验证项)
> 本次为宿主验证(2026-08-29),未重启/刷真机。宿主验证通过项打 ✅;需真机确认的标「待真机」。
**已实现(宿主验证 ✅)**:SDK `meow_ui.a` 重编译通过;hello 链 `meow_ui.a` 重编译通过(MIPS32r2 静态);launcher 编译+链接通过。
宿主 demo(`device_test/gesture_host_test.c`)验证:
- ✅ **视觉条**:`meow_ui_swipe_hint(x)` 在屏幕顶部画 5px、从 0 到 x 的 accent 横条;左滑过程 `MEOW_EV_SWIPE_PROGRESS` 实时上报 → SDK 自动画条(长度=当前 X),抬手达标清条。
- ✅ **抬手才返回**:`MEOW_EV_SWIPE`(release 边沿才 push)才触发 `on_swipe(LEFT)`;`MEOW_EV_SWIPE_PROGRESS` 只报进度不触发返回。
- ✅ **hello 不再因电源键退出**:`on_key(KEY_POWER)` 不再映射 `request_exit()`;宿主注入 POWER 键 → 仅 `on_key` 被调、app 未退出(随后注入左滑才退出)。
**待真机确认**:
1. **无返回键**:SDK hello(`app.json immersive:1`)左上角不再有 48×48 返回按钮 —— 待真机。
2. **抬手才返回**(真机手感):左縁向内滑 ≥90px、松手后退出 —— 待真机。
3. **视觉条**(真机观感):左滑过程中顶部横条随手指增长、抬手达标消失 —— 待真机。
4. **电源键息屏**:SDK hello 内按电源键=息屏(不退出 APP、不触发 OK);再按=亮屏 —— 待真机。
5. **音量键**:SDK hello 内音量键=调音量(不选中/不返回)—— 待真机。
6. 触摸点按钮计数正常(回归)—— 待真机。
### ⚠️ 实现期定案(与设计稿 §2.2 的偏差,需知悉)
设计稿 §2.2 写「`t_dx < 24` 且 `mdx < -SWIPE_THRESH`(左滑)」。**该组合物理上不可达**:手指若从 ≤24px 的左缘启动,向左最多再走 ~24px 就到 x=0 边缘,位移永远达不到 90px。故实际采用 **「左縁起点 + 向右(向内)划 ≥90px」**(与 `meow_watchdog.c` immersive 兜底——左縁 rightward movement >50px——方向一致,二者才判定一致):
- 事件上报:该左缘返回手势由 SDK 统一报为 **`MEOW_SWIPE_LEFT`**(`on_swipe` 的 dir=LEFT,hello 用 `on_swipe(LEFT)` 退出;「左滑=返回」契约保持)。
- 进度回调:`on_swipe_progress(x_start, dx)`,`dx = 当前X - 按下X`(dx>0=向内划,为返回手势方向);SDK 自动画条长度=当前 X。
- 屏幕中部左/右滑**不是**返回手势(中部右滑→`MEOW_SWIPE_RIGHT`,中部左滑→落 TAP),避免任意处滑动误退出。
若真机实测发现手势方向相反(应为向左划),只需翻转 `touch_release()` 的 `mdx` 符号并同步 `on_swipe_progress` 说明即可,其余逻辑不变。
---
## 4. 关键约束(代理必读)
- **绝不**把 APP 内的物理键(电源/音量/播放)映射成任何"确认/exit/返回/翻页"。物理键语义看 APPS.md §3.4.1:电源=息屏/亮屏、音量=调音量、播放=媒体。**只有 MeowOS 界面(launcher 菜单)允许处理物理键语义。**
- 渲染走 `back_buf + meow_present`(SDK 现有机制),无闪烁;视觉条用 `fill_rect` + `flush_region` 刷顶部一小条即可,不做全屏重绘。
- 不碰 launcher 主循环的物理键处理(那是 MeowOS 界面,允许);只改 SDK 的手势 + watchdog 返回键绘制 + launcher 的 app.json immersive 解析。
- `meow_watchdog` 是独立进程,**改它不影响 SDK**;但确认删除返回键后它仍能兜底(app 卡死强杀)。
---
## 5. 涉及文件
| 文件 | 改动 |
|---|---|
| `patch_v2/sdk/meow_ui_input.c` | 抬手触发收紧(左缘起点+阈值 80-100)、新增 `on_swipe_progress` 上报 |
| `patch_v2/sdk/meow_ui.h` | 新增 `MEOW_SWIPE_PROGRESS` 事件 + `on_swipe_progress` 回调签名 |
| `patch_v2/sdk/meow_ui_gfx.c` | 新增 `meow_ui_swipe_hint(x)` 顶部横条绘制 |
| `patch_v2/sdk/meow_ui_widget.c`(如有输入/绘制分发) | 分发 `on_swipe_progress` |
| `patch_v2/meow_watchdog/meow_watchdog.c` | 删除 `paint_button()` 返回键绘制/按钮命中;保留兜底 |
| `patch_v2/meow_launcherd/meow_launcherd.c` | 解析 app.json `immersive` → 写 `/tmp/meowapp_state` |
| `patch_v2/hello_app/hello.c` | 删 `on_key(KEY_POWER)→exit`;声明 immersive;处理 `on_swipe_progress` 画条 |
| `patch_v2/apps_assets/apps/hello/app.json` | 加 `"immersive":1` |
| `APPS.md` / `MEOW_APP_SDK.md` | 已写 §3.4.1 规范;SDK 文档补 `on_swipe_progress`/`meow_ui_swipe_hint` |
+101
View File
@@ -0,0 +1,101 @@
# MeowOS 全面按键接管设计(Key Control)
> 状态:已落地(M2.24,2026-08-29,见 §9 落地记录)。目标:**所有按键信号全部由系统接管**,任何界面(launcher 菜单 / MeowAPP / 息屏)都统一响应系统语义。应用侧绝不碰物理键(APPS.md §3.4.1 规范)。本设计是「全面按键接管专项」的蓝本。
---
## 1. 原则
1. **按键是系统资源**,语义由系统组件(launcher / watchdog / 内核)统一决定。
2. **任何界面按键行为一致**:电源=息屏/长按菜单、音量=系统音量、播放/上下曲=活跃媒体会话(现阶段先"抓住"不路由)。
3. **应用绝不映射物理键**:MeowAPP 只接收 SDK 语义事件(tap/swipe/tick),不读 `/dev/input`、不把任何物理键映射成确认/exit/返回/翻页。只有 MeowOS 界面(launcher 菜单/选单/电源菜单)允许处理物理键语义。
## 2. 按键 → 系统行为映射表(所有界面统一)
| 键 | code | 系统行为 | 现阶段 | 依赖 |
|---|---|---|---|---|
| 电源 单击 | 116 | 息屏/亮屏 | ✅ 实现(watchdog 接管) | 无 |
| 电源 长按 3s | 116 | 打开电源菜单(关机/重启/回HibyOS/取消) | ✅ 实现 | 无 |
| 电源 长按 8s | 116 | 强制关机 | ✅ 实现 | 无 |
| 音量 + | 115 | 调系统音量(CS43198 DAC) | ✅ 实现 | 无 |
| 音量 − | 114 | 调系统音量 | ✅ 实现 | 无 |
| 播放/暂停 | 164 | 活跃媒体会话 播放/暂停 | 🔵 **先抓住(不泄漏给应用)** | 阶段D媒体会话 |
| 上一曲 | 165 | 活跃媒体会话 上一曲 | 🔵 **先抓住** | 阶段D媒体会话 |
| 下一曲 | 163 | 活跃媒体会话 下一曲 | 🔵 **先抓住** | 阶段D媒体会话 |
> 🔵 “先抓住”:watchdog 捕获该键信号并**消费掉**(记录日志、暂不执行动作),确保不泄漏给 MeowAPP(防止应用误用/误响应),也不产生"按键被吞"的观感空洞。等阶段 D 媒体会话落地,在**同一点**直接转给活跃会话(无需改键处理层)。
## 3. 实现:统一监听点
**关键**:所有界面的按键语义集中在**一个常驻系统组件**里判断,不分散。
- **launcher 菜单/列表界面**:已有 `run_menu` 主循环读键(音量/电源/播放)。**保留**,作为 MeowOS 界面的键处理。
- **MeowAPP 运行界面**:launcher 卡在 `waitpid(app)`,主循环不跑 → **由 `meow_watchdog` 接管按键**(watchdog fork 出来陪跑 app、读 `/dev/input/event*`)。
- **共同原则**:watchdog 与 launcher 用**同一套按键语义逻辑**(电源单击=息屏、长按=菜单、音量=调音量、播放/上下曲=抓住/路由),从代码层面保证"任何界面一致"。
### watchdog 按键语义(应用运行期,重点新增)
watchdog 主循环(`for(;;)`)加按键轮询(`read /dev/input/event0/event2`):
- **KEY_POWER(116)**:
- 按下计时,≤某阈值单击 → 息屏/亮屏(复用 launcher `meow_panel_off/on`:背光 0 + FBIOBLANK(4) / FBIOBLANK(0)+亮度恢复);
- 按住 ≥3s → 电源菜单(复用 launcher 的电源菜单 UI/逻辑——但 watchdog 是独立进程,需把电源菜单绘制/交互自包含,或经 IPC 通知 launcher);**为避免双进程抢屏,建议:watchdog 把"长按电源"事件经 IPC 上报 launcher,由 launcher 弹电源菜单**(launcher 仍在 waitpid 但可被 eventfd 唤醒处理菜单)。
- 按住 ≥8s → 强制关机(`syscall(SYS_reboot, ...POWER_OFF)`)。
- **KEY_VOLUMEUP(115) / KEY_VOLUMEDOWN(114)** → 调系统音量(复用 launcher 的 `settings_volume_adj` / CS43198 DAC 写路径)。
- **KEY_PLAYPAUSE(164) / KEY_PREVIOUSSONG(165) / KEY_NEXTSONG(163)** → **先抓住**:捕获、logmsg、消费,暂不路由;媒体会话落地后转活跃会话。
- **KEY_PREVIOUS(19)**:`KEY_PREVIOUSSONG` 是 165;19 是另一码(`KEY_PREVIOUSSONG` 统一用 165,见 launcher `DEV_POWER` 注释 event0 含 POWER/PREVIOUS)。
## 4. 电源菜单在应用内如何弹出(关键设计决策)
应用运行时 watchdog 捕获长按电源。电源菜单**由谁画**?两方案:
- **方案 A(推荐)**:watchdog 长按 3s → 写 `/tmp/meowapp_state`/或 SIGUSR1 通知 → **launcher 从 waitpid 被 eventfd 唤醒 → SIGSTOP app → 弹电源菜单**(复用 launcher 现有电源菜单代码,不重复实现)。菜单关闭 → SIGCONT app。
- 方案 B:watchdog 自绘电源菜单(独立实现,重复代码,且与 launcher 抢屏时序复杂)。
**选 A**:电源菜单始终由 launcher 统一绘制(一处实现),watchdog 只负责"检测到长按电源→唤醒 launcher"。这符合"系统接管要统一、不分散"。
## 5. 与应用/将来会话的衔接
- **现阶段**:播放/上下曲"抓住"(消费日志,不路由)。应用拿不到这些键 → 不会误操作。
- **阶段 D**:媒体会话(`MSG_REGISTER svc=media_session` / `MF_PLAYBACK`,见 FRAMEWORK_DESIGN §5/附录 B)落地后,**在 watchdog 抓住键的同一点**直接 `route_to_active_media_session()`。键处理层结构不变,只加路由调用。
## 6. 涉及文件
| 文件 | 改动 |
|---|---|
| `patch_v2/meow_watchdog/meow_watchdog.c` | 主循环加按键轮询:电源(单击息屏/长按菜单/8s关机)、音量(调音量)、播放/上下曲(抓住);长按电源 → 唤醒 launcher |
| `patch_v2/meow_launcherd/meow_launcherd.c` | 应用运行时(waitpid 中)能被 eventfd 唤醒处理电源菜单;复用现有电源菜单/panel/音量逻辑 |
| `patch_v2/sdk/meow_ui_input.c` | 不接管物理键语义(仅触摸 tap/swipe),排除误读事件1以外的键 |
| `APPS.md §3.4.1` | 已写规范(应用禁映射物理键)——保持 |
| `FRAMEWORK_DESIGN.md` | 阶段 D 媒体会话:播放/上下曲路由到活跃会话(附录 B) |
## 7. 真机验收(用户)
1. 应用内:电源键**单击=息屏、长按 3s=电源菜单、长按 8s=关机**;
2. 应用内:音量 ± = 调音量(状态栏可见);
3. 应用内:播放/上一曲/下一曲**不引起应用任何反应**(被系统抓住);
4. launcher 菜单 / 应用列表:同样电源/音量行为一致;
5. 电源菜单在任何界面都能弹出(含应用内);
6. 息屏后任意键唤醒。
## 8. 依赖与顺序
- **前置**:agent-46(应用输入接管修复,电源键息屏奠基)完成。
- **本轮**:全面按键接管专项(✅ 已落地 M2.24:watchdog 全键接管 + launcher 唤醒弹电源菜单 + 播放键抓住)。
- **下期**:阶段 D 媒体会话 → 播放/上下曲路由。
*本设计为蓝本,实现以代码注释+KEY_CONTROL_DESIGN.md 为准;与 APPS.md §3.4.1、FRAMEWORK_DESIGN §5/附录 B 对齐。*
## 9. 落地记录(M2.24,2026-08-29,MeowOS 0.7.19)
完成项(相对本蓝本):
- ✅ 电源 单击=息屏/亮屏:`meow_watchdog.c handle_power`(沿用 M2.22,短按<3s)。
- ✅ 电源 长按≥3s=电源菜单:`meow_watchdog.c wd_power_tick` 写 `/tmp/meow_power_menu` → `meow_launcherd.c` 应用 waitpid 轮询消费 → `SIGSTOP app` + 写 `/tmp/meow_power_menu_active` → `show_power_menu()`(launcher 一处绘制)→ 关菜单清标记 → `SIGCONT app`。方案 A。
- ✅ 音量±:`meow_watchdog.c wd_vol_adj` 自实现(写 CS43198 amixer numid=1/2 + 持久化 `/usr/data/meow/settings.conf` 与 launcher 同源);launcher `launch_app` 末尾 `settings_load()` 刷新。
- ✅ 播放/上下曲(164/165/163/19):`meow_watchdog.c` 捕获+日志+消费,不路由;SDK `meow_ui_input.c` 吞噬全部物理键,应用绝不碰。
- ✅ 应用内电源菜单弹出中断恢复:`show_power_menu` 复用(含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)。
偏差(实现以稳妥为准,记录如下):
1. **8s 强关拆分**:设计给 watchdog,但为避免与菜单内 `pm_interacted` 冲突(用户正点选时被强关抢先),落地为「launcher 菜单内 8s 强关(pm_interacted 保护)+ watchdog 8s 兜底(仅当菜单未接管,即 `/tmp/meow_power_menu_active` 未出现)」。
2. **音量跳步**:watchdog 每按下边沿按 `audio.volume_step` 一步(不做 launcher 的线性渐变);短按与 launcher 菜单一致,长按不连续爬(媒体落地后可补)。launcher 应用返回时 `settings_load()` 同步。
3. **左右滑切屏**:菜单触摸段已留 `menu swipe (dx,dy) — no launch` 判定,未实现切换逻辑。
4. **媒体键**:仅抓消费(日志 `media key captured (code N) -> consumed, no route`),路由待阶段 D。
真机验收要点(§7):应用内电源单击/长按菜单/8s 强关、音量±调音量、播放/上下曲无反应(被抓住)、launcher 菜单同样一致、电源菜单任何界面可弹(含应用内)、息屏后任意键唤醒。
+73
View File
@@ -0,0 +1,73 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2026 AskaEth
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+181
View File
@@ -0,0 +1,181 @@
# MeowOS API 参考(MEOWOS_API.md)
> 状态:设计稿 v1(2026-08-28)。作为后续开发(系统设置 / 通知中心 / MeowPanel / Activity 框架)的**统一 API 蓝本**。
> 原则:**命名统一、类型明确、返回码一致、可扩展**。所有新增 API 必须按本文命名与返回约定,保证多端(实体 UI / Web / CLI/MCP)一致。
---
## 0. 命名约定(全局)
| 范畴 | 规则 | 例 |
|---|---|---|
| C API 前缀(后端) | `meow_<域>_<动作>`(下划线,小写) | `meow_settings_get_int`、`meow_notify_push` |
| 域(domain) | 全局命名空间:`settings` / `notify` / `act` / `svc` / `nav` / `media` / `sys` / `panel` | — |
| 设备属性 | 属 `sys` 域子域 `device`(`meow_sys_devattr` + `meow_devattr_*`);只读属性 + 可控 kcontrol 白名单 | `meow_devattr_set("out.port", 3)` |
| 结构体 | `meow_<域>_t`(typedef 别名),字段下划线 | `struct meow_settings_bool { int v; }` |
| 枚举 | `MEOW_<域>_<NAME>` | `MEOW_BOOT_MEOW` |
| 常量/宏 | `MEOW_<DOMAIN>_<NAME>` | `MEOW_LOG_LEVEL_INFO`、`MEOW_SET_MAX_LEN` |
| 键名(设置/配置) | 小写点分:`域.子.名` | `display.brightness`、`boot.default`、`input.hotkey_seq` |
| REST 路径 | `/api/<域>/<资源>`(kebab-case 资源) | `/api/settings`、`/api/settings/export` |
| HTTP 方法 | GET=读、POST=写/动作 | — |
| 返回值(C 函数) | `int`,0=成功,负=错误码 | 见 §4 |
## 1. 返回码(统一错误码)
| 宏 | 值 | 含义 |
|---|---|---|
| `MEOW_OK` | 0 | 成功 |
| `MEOW_ERR_INVAL` | -1 | 参数/取值非法 |
| `MEOW_ERR_NOENT` | -2 | 键/资源不存在 |
| `MEOW_ERR_EXISTS` | -3 | 已存在(如重复注册) |
| `MEOW_ERR_PERM` | -4 | 无权限(本机信任模型下少用,保留) |
| `MEOW_ERR_IO` | -5 | 读写(存储/文件)失败 |
| `MEOW_ERR_NOMEM` | -6 | 内存不足 |
| `MEOW_ERR_NOTINIT` | -7 | 未初始化(如 settings 未 init) |
| `MEOW_ERR_BUSY` | -8 | 忙(如服务已运行) |
| `MEOW_ERR_TIMEOUT` | -9 | 超时 |
| `MEOW_ERR_NODEV` | -10 | 设备不存在(如无卡) |
| `MEOW_ERR_INTERNAL` | -255 | 内部错误 |
> C 函数返回上述;HTTP 层映射为 HTTP 状态码 + `{"code": <meow_err>, "msg": "..."}`。
## 2. 设置子系统 C API(`meow_settings.h`)
### 2.1 类型与初始化
```c
typedef enum { SET_INT, SET_STR, SET_ENUM, SET_BOOL } meow_set_type;
int meow_settings_init(void); /* 加载存储,注册默认;重复调用幂等 */
int meow_settings_shutdown(void); /* 保存 + 清理 */
void meow_settings_dump(const char *log_prefix); /* 调试:打印当前全部键 */
```
### 2.2 存取(按类型)
```c
int meow_settings_get_int (const char *key, int *out);
int meow_settings_get_str (const char *key, char *buf, int buf_len);
int meow_settings_get_enum(const char *key, int *out);
int meow_settings_get_bool(const char *key, bool *out);
int meow_settings_set_int (const char *key, int v); /* clamp 到取值域 */
int meow_settings_set_str (const char *key, const char *v);/* 长度截断 MEOW_SET_MAX_LEN(256) */
int meow_settings_set_enum(const char *key, int v); /* 校验枚举合法 */
int meow_settings_set_bool(const char *key, bool v);
```
- **所有 clamp/校验集中在 set**(亮度 clamp ≥ `display.brightness_min` 默认 25、枚举越界拒绝、str 截断)。
- set 成功后**自动 `settings_save()` 落盘**(板载 /usr/data/meow/settings.conf)并**通知 listener**。
### 2.3 枚举辅助(供 UI/web 显示中文标签)
```c
int meow_settings_enum_label(const char *key, int idx, char *buf, int buf_len); /* 中文标签 */
int meow_settings_enum_count(const char *key); /* 选项数 */
int meow_settings_enum_index_of_name(const char *key, const char *name); /* 名字→索引 */
void meow_settings_enum_names(const char *key, const char **out_names, int *n); /* 全名数组 */
```
### 2.4 变更通知
```c
typedef void (*meow_settings_listener)(const char *key); /* key 变化时回调,UI 据此重绘 */
int meow_settings_add_listener(meow_settings_listener cb);
int meow_settings_remove_listener(meow_settings_listener cb);
```
### 2.5 枚举/列表
```c
int meow_settings_list(int (*cb)(const char *key, meow_set_type t, void *ctx), void *ctx);
int meow_settings_get_default(const char *key, char *buf, int len); /* 默认值(复位用) */
int meow_settings_reset(const char *key); /* 单键复位默认 */
int meow_settings_reset_all(void); /* 全量复位 */
```
### 2.6 导入/导出(备份)
```c
int meow_settings_export(const char *path); /* 写一份 .conf 到 path(默认 MeowOS/backups/settings/setting.bak) */
int meow_settings_import(const char *path); /* 从 .conf 读入覆盖;逐键校验,记录损坏键(不中断) */
```
### 2.7 设置项注册(扩展规范——新增设置照此)
```c
typedef struct meow_setting_def {
const char *key; /* 如 "display.brightness" */
meow_set_type type;
const char *def; /* 默认值字符串(int/str/enum/bool 统一字符串) */
int min, max; /* SET_INT 取值域;其他 0 */
const char *enum_names; /* SET_ENUM:逗号分隔选项名,如 "panel,black" */
const char *enum_labels; /* 对应中文标签,如 "面板下电,背光全黑双击" */
} meow_setting_def;
int meow_settings_register(const meow_setting_def *def); /* 注册一个设置(启动时批量注册) */
```
- 新增设置 = 加一个 `meow_setting_def` 注册 + 存储自动处理;实体 UI / web / CLI 依据 `meow_settings_list` 与 `enum_*` 自动生成控件,**四端零新增**。
## 3. 设置存储格式
文件(板载 `/usr/data/meow/settings.conf`):
```
# meow-settings 1
display.brightness = 101
display.brightness_min = 25
display.screen_off_mode = panel
boot.default = meow
input.hotkey_seq = volup,volup,volup
log.level = info
...
```
- 单行 `key = value`;`#` 注释;未知键忽略;读入时 `clamp/校验` 与 set 一致。
## 4. Web(MeowPanel)HTTP API 映射
REST 路径,HTTP 层调 C API(本机)。
| 方法 | 路径 | C API | 说明 |
|---|---|---|---|
| GET | `/api/settings` | `meow_settings_list`+`get_*` | 返回全部键值 |
| POST | `/api/settings` | `meow_settings_set_*` | body `{key,value}` |
| GET | `/api/settings/keys` | `meow_settings_list` | 键/schema 描述(供前端生成控件) |
| POST | `/api/settings/reset` | `meow_settings_reset`/`reset_all` | body `{key?}` |
| GET | `/api/settings/export` | `meow_settings_export(path)` | 下载 settings.conf |
| POST | `/api/settings/import` | `meow_settings_import(path)` | 上传覆盖 |
| GET | `/api/status` | `meow_sys_status()` | 实时性能/电量/亮度/WiFi |
| GET | `/api/device` | `meow_sys_devattr_refresh()` | 设备属性域全量(输出口/3.5·4.4 插入/按输出口分级音量上限(增益代理)/音量/DAC/蓝牙/电源;采样率·DSD·CPU温度=`MEOW_ERR_NOENT`,见 SYSTEM_SETTINGS 附B) |
| POST | `/api/device` | `meow_devattr_set()` | body `{field,value}`;白名单可控(out.port 1-4、vol、filter/nos/dre/dop/mute、bt.powered) |
| GET | `/api/device/outport-label` | `meow_devattr_enum_label()` | `out.port` 1-4 中文标签 |
| GET | `/api/notify` | `meow_notify_list` | 通知列表 |
| POST | `/api/notify/dismiss` | `meow_notify_dismiss` | 清一条 |
| GET | `/api/screen` | `meow_screen_capture()` | 实时画面(fb → PNG) |
| POST | `/api/key` | `meow_input_inject()` | 远程按键(注:播放器级注入受内核限制,MeowOS 菜单/应用可用) |
响应统一:`{"code":0,"data":...}` 或 `{"code":-1,"msg":"..."}`。
## 5. CLI/MCP(调试/自动化)
`meowcli settings get|set|list|export|import <key> [value]`;`meowcli notify list|dismiss`;`meowcli status`。与 C API 一一对应。
**launcher 自测钩子(M2.16 渲染/刷新率调度/防抖)**:`meow_launcherd` 增加 `--scan`(`state=1 napps=0`)、`--sysdump`(含 `render: eff_rate cfg always_max frames`,eff_rate=有效帧率 / cfg=display.refresh_rate / always_max=refresh_always_max / frames=累计呈现帧数)、`--sim-vol N`(音量渐变 N=1/2/5,长按 clamp)、`--sim-hotkey/-tap/-exit[-tap]/-power-*/-no-exec`、`--appinfo <app.json>`、`--ipc-only N --ipc-sock <path>`(配合 `meow_ipc` 验 HELLO→ACK / ACT_QUERY→链路)。渲染为 **RAM 离屏后备缓冲 `back_buf` + `meow_present(vsync+原子 blit 脏区)`**,刷新率调度入 PERF_SCHEDULER.md(`meow_sched_fps()` 归口);设置键 `display.refresh_rate`(60/30/15)/`display.refresh_always_max` 见 SYSTEM_SETTINGS.md。
> 软件包管理另有独立 CLI `nyai`(与 launcher `meow_pkg_install/export` 同语义,支持镜像源):用法见 APPS.md 附录 §5;部署于卡 `MeowOS/opt/bin/nyai`。
## 6. 扩展指南(新增功能的必读)
1. **命名单一**:新域前缀 `meow_<域>_`;键名小写点分。
2. **设置项**:只加 `meow_setting_def`(§2.7),勿动手写存取分支。
3. **返回码**:复用 §1,不造新码;扩展加在表尾。
4. **线程/内存**:设置域单写者(主循环)+ listener;跨线程用 eventfd(参考 §18.29 并发模型);字符串拷贝进 `MEOW_SET_MAX_LEN` 缓冲。
5. **持久化**:只写板载 `meow/settings.conf`(小文件);大数据(日志/字库/媒体)留 TF 卡。
6. **多端一致性**:任何设置/接口改动,同步文档本文件 + 四端(实体 UI/web/CLI)。
7. **单元可用性**:新 API 写进 `ANALYSIS.md` 对应 §,提供 `--sim`/`meowcli` 测试钩子。
8. **line-out 保守默认(无实测,2026-08-29)**:`out.port=1`(line-out) 按**固定满幅电平**处理——MeowOS 侧不响应音量键、不套 lock_vol 上限,UI 标注"固定电平输出";仅设置显式开启"line-out 可调"才放开。`out.gain`(高/低) 标记 **Unsupported/只读**(选择值存 `user.ini`,读不干净、写有完整性风险,禁写);信息透出只用 `out.vol_max_headset/balance`(=lock_vol,可读)响应度分级代理。详见 SYSTEM_SETTINGS 附 B。实现前再读该节,避免与 HibyOS 播放器设定冲突。
## 7. 待实现清单(按此文档开发)
- `meow_settings` 存储+核心 API+注册机制(阶段 B2a 后端,先于阶段 B)
- 实体系统设置页(分组列表+控件,调 API)
- `meowcli` settings 命令
- MeowPanel `/api/settings*`(阶段 C)
- 后续 notify/act/svc/media 域按本规范补充(阶段 D)
+377
View File
@@ -0,0 +1,377 @@
# MeowOS 应用开发框架 SDK(meow_ui)设计
> 状态:设计稿(2026-08-29)。路线 B:**框架承载**——MeowAPPs 不再裸 `mmap("/dev/fb0")` 自绘、自读输入,而是链接统一静态库 `meow_ui.a`,用 API 开发业务;渲染/控件/图片/输入/导航由框架收口。
> 目标:应用只写业务,不重复造轮子;观感、交互、生命周期统一;避免每个应用踩一遍 fb 双缓冲 / ioctl 号 / 返回键 / 息屏交互这些坑。
---
## 1. 问题与定案
**现状(APPS.md §3.3)**:应用必须"独占屏幕自绘"——自行 `open("/dev/fb0")`+`mmap` 绘制;自行 `open("/dev/input/event*")` 读输入;自行管返回。全部 480×720、RGB565、双缓冲 yoffset、ioctl 号 0x4600/0x4602,每个应用都要重写一遍,观感必然乱。
**定案:路线 B(框架承载)**。理由:
- 现状只有 1 个示范应用能凑合;要开放多应用/第三方,裸自绘 = 爆炸。
- 渲染基建已就绪(`back_buf` + `meow_present` 无闪烁、`meow_font` 字库、`blend565` 混叠、脏区追踪),把它开放成 app 可链接的 API,是低成本高收益。
**分层决策**(分两步落地,不一次做完):
- **B1 渲染/控件/图片 SDK**(本文档主体,优先):应用链接 `meow_ui.a`。依赖现成 `meow_font.a` + 内建 `stb_image`(单头文件、静态编入、解 PNG/JPEG/BMP)。**不依赖阶段 D,可独立先行。**
- **B2 输入/生命周期/导航**(衔接阶段 D):把应用从"自读 event / 自管返回"解放,走 IPC + M3 导航栈。本文档 §5 给衔接设计,B2 实现归阶段 D。
---
## 2. 架构分层
```
┌─────────────────────────────────────────────┐
│ MeowAPP(业务逻辑,只调 meow_ui API) │
├─────────────────────────────────────────────┤
│ meow_ui 静态库(meow_ui.a) │
│ ├─ 绘制层 meow_draw_* │
│ ├─ 控件层 meow_widget_*(按钮/列表/滑条…)│
│ ├─ 图片层 meow_img_*(stb_image 解码) │
│ ├─ 输入层 meow_input_*(语义事件) │ ← B1:只收事件;B2:接 M3 导航
│ └─ 主题/配置 meow_theme_*(颜色/间距/字体) │
├─────────────────────────────────────────────┤
│ 渲染后端 meow_ui_gfx(复用 launcher 机制) │
│ back_buf(RGB565) + meow_present(dirty) │
│ + meow_font(位图字库) + blend565 │
└─────────────────────────────────────────────┘
```
**关键约定**:
- 应用**只调 API**,绝不 `open fb`、`mmap`、`ioctl`、`open event`。屏幕/输入由 meow_ui 在应用 `meow_app_start()` 时建立、退出时释放。
- 渲染统一走 `back_buf` + `meow_present`:应用画到离屏,只在 `meow_ui_flush()` 时单次原子 blit 到 fb0(无撕裂无频闪,复用 M2.16 地基)。
- 颜色统一 24-bit RGB(`0xRRGGBB`),meow_ui 内部做 `rgb565()` 转换;字体绘制用 `meow_font`,字号约定 28(标题/状态栏)/20(正文/按钮)。
---
## 3. 绘制层 API(B1)
所有绘制先画进离屏 back_buf;调用 `meow_ui_flush()` 才上屏。坐标 0,0 = 左上。
```c
/* 初始化/销毁(应用 main 里 meow_app_start 的懒加载) */
int meow_ui_init(const meow_ui_opts *opts); /* 开 fb、读几何、载字库、建 back_buf */
void meow_ui_shutdown(void);
/* 帧缓冲几何(应用需自适应时读取) */
int meow_ui_width(void); /* 480 */
int meow_ui_height(void); /* 720 */
/* 基本绘制(全部画进 back_buf) */
void meow_ui_clear(uint32_t color); /* 整屏清(填背景色) */
void meow_ui_put_px(int x, int y, uint32_t color);
void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t color);
void meow_ui_draw_rect(int x, int y, int w, int h, uint32_t color); /* 空心框 */
void meow_ui_draw_rect_round(int x, int y, int w, int h, int r, uint32_t color); /* 圆角框 */
/* 文本 */
void meow_ui_text(int x, int y, const char *utf8, int size, uint32_t color); /* size=20/28 */
void meow_ui_text_right(int right_x, int y, const char *utf8, int size, uint32_t color);
void meow_ui_text_center(int cx, int y, const char *utf8, int size, uint32_t color);
int meow_ui_text_width(const char *utf8, int size);
int meow_ui_text_height(int size); /* = meow_font_line_height */
/* 上屏:vsync + 原子 blit 脏区(内建脏区追踪,应用无需关心) */
void meow_ui_flush(void);
void meow_ui_flush_region(int x, int y, int w, int h); /* 只刷新指定区 */
/* 半透明(已有 blend565:前景按 alpha 与背景混叠) */
void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alpha); /* 0-255 */
```
> 说明:`meow_ui_text_*` 底层即 `meow_font_drawtext` + `meow_font_width_text`;`meow_ui_fill_rect_alpha` 复用 `blend565`。**模糊/毛玻璃不在 B1 承诺**(硬件无 GPU、CPU 软渲;仅在静态小区域可考虑,见 §7)。
### 3.1 全局状态栏(系统级组件,A+C 方案并入)
**定位**:状态栏是**系统级全局组件**,由 SDK 统一绘制——所有界面(launcher 菜单 + MeowAPP 画面)默认都显示,**仅当 APP 声明全屏(fullscreen)才隐藏**。数据由 SDK 内置状态读取器抓系统快照(与 launcher 同源单点一致),非各画各的。
```c
/* 状态栏显隐(默认显示;APP 声明全屏则隐藏——沉浸) */
void meow_ui_set_statusbar(int visible); /* 1=显示(默认) 0=隐藏(全屏) */
int meow_ui_statusbar_visible(void);
/* 全屏声明:等价 app.json `fullscreen:1`(外观上隐藏状态栏 + 隐藏返回键)。 */
void meow_ui_set_fullscreen(int on);
/* 状态栏高度(px,供 APP 布局上移内容,避免被盖——status bar inset) */
int meow_ui_statusbar_height(void);
```
**绘制规则**:
- `meow_ui_flush()` 上屏前,SDK 自动在顶部画状态栏(slim ~20-40px),内容:时钟 / 电量 / WiFi / 蓝牙 / 音量(与 launcher 一致)。
- 状态栏数据由 SDK 内置读取器周期刷新(~1s 轻刷,读 `/sys/class/power_supply/*`、WiFi 状态、时钟、音量寄存器——与 launcher 同一批 source)。
- **z 顺序**:状态栏是最顶层。APP 内容需 `meow_ui_statusbar_height()` 下移(inset),否则被状态栏盖住(有状态栏 OS 的通用做法)。
- **全屏(fullscreen)**:`meow_ui_set_fullscreen(1)` 或 app.json `fullscreen:1` → 隐藏状态栏 + 隐藏返回键(沉浸);退出全屏恢复。
**与 launcher 统一(A+C)**:launcher 迁移到 SDK 后,其状态栏(现私有 `draw_statusbar()`)改走 SDK 这一套,全局统一、单点一致。
**注意(诚实)**:这是 SDK 的较大增强——需引入系统状态读取器 + 顶部留白(inset)协调 + 与 APP 内容 z 顺序。非全屏 APP 必须给顶部 inset,否则内容被盖。API 名实现时以 `meow_ui.h` 为准。
---
## 4. 图片层 API(B1)
内建 `stb_image`(单头文件,静态编入 `meow_ui.a`),解 PNG/JPEG/BMP,统一转 RGBA。**这是对"图片渲染"的直接答案**(背景/封面/图标)。
```c
/* 加载图片(从 RAM 缓冲或 TF 卡文件)。返回句柄,失败 NULL。
* mem: 应用可先读文件到 RAM 再喂给解码器;meow_ui 不解读自身以外的 IO。 */
meow_img *meow_img_load_file(const char *path); /* 内部读文件 + stb 解码 */
meow_img *meow_img_load_mem(const void *buf, int len);
void meow_img_free(meow_img *img);
/* 查询 */
int meow_img_w(meow_img *img);
int meow_img_h(meow_img *img);
/* 绘制:整图 / 缩放 / 带 alpha */
void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */
void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img); /* 最近邻缩放 */
void meow_ui_img_alpha(int x, int y, int w, int h, meow_img *img, int alpha);
/* 最常用:封面/图标九宫格或圆形裁剪(封面/专辑图等) */
void meow_ui_img_cover(int x, int y, int size, meow_img *img); /* 居中正方形裁剪成 size×size */
```
> **裁剪/缩放**:软渲用最近邻(快),不做高质量插值(省 CPU)。封面用 `meow_ui_img_cover` 居中裁方 + 缩放到目标尺寸即可。
---
## 5. 控件层 API(B1)
内置常用控件,观感统一(MD3 优先 / MIUIx 备选,见 §6 主题)。控件状态由 meow_ui 维护,绘制走 back_buf。
```c
typedef struct meow_widget meow_widget;
/* 创建/销毁;控件 layout 由应用每次 flush 前调用 meow_widget_draw 绘制 */
meow_widget *meow_btn_create(int x, int y, int w, int h, const char *label);
meow_widget *meow_slider_create(int x, int y, int w, int min, int max, int val);
meow_widget *meow_switch_create(int x, int y, int on);
meow_widget *meow_list_create(/* 数据源回调 */);
void meow_widget_free(meow_widget *w);
/* 绘制 */
void meow_widget_draw(meow_widget *w);
/* 状态/取值 */
void meow_slider_set(meow_widget *s, int val);
int meow_slider_get(meow_widget *s);
void meow_switch_set(meow_widget *s, int on);
```
> 控件命中(触摸点是否落在控件内)由输入层语义事件驱动,应用在 `meow_ui_on_tap(x,y)` 回调里做分发——见下节。
> **B1 先做最小控件集**(按钮/滑条/开关/列表四件套),其余(下拉、进度圈、通知卡片)随阶段 B 通知中心 / 网易云需要用再加。
### 5.1 输入法(IME)—— 内置触屏软键盘
**背景**:R3 Pro II 无独立键盘设备(仅触摸 goodix-ts + 物理按键 md-gpio-keys / jz adc keyboard),无 USB/蓝牙 HID 键盘栈。**任何需要文本输入的应用(输入框/搜索/设置项)都依赖 IME,且必须由 SDK 内置**——应用绝不能自己实现键盘,否则观感/行为不一、重复造轮子。
**形态**:系统级单例软键盘,作为控件层的一个特例(`meow_ime`),应用调 `meow_ui_text_input()` 弹出,键盘覆盖在应用之上,由 SDK 绘制与分发(复用 `back_buf` + `meow_present` 无闪烁地基,不干扰业务渲染)。
**B1 范围**:**英文 QWERTY 键盘**(数字+字母+符号),覆盖字符输入、退格 Backspace、空格、回车/完成。
**不在 B1**:中文拼音/词库选字——复杂度高、依赖词库,留到后期独立 IME(网易云/中文搜索需要时再上)。B1 的 IME 只保证"能输入 ASCII 文本"。
```c
/* 弹出软键盘,输入到应用提供的缓冲。返回 1=确认(fn=enter),0=取消(fn=esc/背景点)。
* 回调驱动:输入变化由 SDK 内部循环处理,应用只需在 done 时读 out。 */
typedef enum { MEOW_IME_ASCII, MEOW_IME_NUM } meow_ime_mode; /* 全键盘/数字键盘 */
typedef struct {
meow_ime_mode mode;
char buf[256]; /* 应用提供的输入缓冲(初始值可预填,如 Wi-Fi 密码) */
int maxlen;
/* 回调:每次字符变化回调一次(用于实时预览/校验) */
void (*on_change)(const char *text, void *ctx);
void *ctx;
} meow_ime_ctx;
/* 阻塞:弹出键盘直到确认/取消。返回 1=确认(buf 已回填),0=取消。 */
int meow_ui_text_input(meow_ime_ctx *ctx);
/* 非阻塞变体:返回后应用在 on_tick 里轮询 meow_ime_active()/meow_ime_text() */
void meow_ui_text_input_async(meow_ime_ctx *ctx);
int meow_ime_active(void);
const char *meow_ime_text(void);
```
**布局(480×720 竖屏)**:键盘占底部约 40% 高(~290px),上方为输入预览行 + 候选区。键位 40×40(够 touch target),触屏点按输入。
**物理键辅助**(兼容未来硬件键/可访问性):方向键移动光标、播放/确认键=选择、音量=翻页。B1 以触摸为主,物理键作为可选加分项(SDK 内实现)。
**[B1 后续 / 不阻塞]**:中文拼音 IME、词库、候选词联想、剪贴板、长按重复——作为独立里程碑,等首个中文输入场景(网易云搜索)出现再做。
---
## 6. 输入层 / 生命周期(B1 部分 + B2 衔接)
**B1(本阶段)**:应用不再自读 `/dev/input/event*`,meow_ui 建立输入线程,把按键/触摸翻译成语义事件回调:
```c
/* 应用注册事件回调(meow_app_start 时传入) */
typedef struct {
void (*on_key)(int keycode, int is_down); /* KEY_VOLUP/VOLDOWN/PLAYPAUSE/POWER…(SDK 已消化系统语义) */
void (*on_tap)(int x, int y); /* 触摸抬起落点 */
void (*on_swipe)(int dir, int dx, int dy); /* 左缘返回手势=LEFT;中部滑动=RIGHT/UP/DOWN */
void (*on_tick)(void); /* 空闲周期回调(动画/时钟用,见 §7) */
void (*on_swipe_progress)(int x_start, int dx);/* 左缘返回手势过程实时回调;x_start=按下X, dx=当前X-按下X */
} meow_ui_events;
void meow_ui_run(const meow_ui_events *ev); /* 阻塞:事件循环,直到 meow_app 请求退出 */
void meow_ui_request_exit(void); /* 应用调它请求退出(相当于触返回/按退出键) */
/* 左缘返回手势的顶部视觉条(SDK 已自动绘制,应用一般无需自画)。
* 画屏幕顶部 5px、从 x=0 到 x 的横条(主题 accent 色);x<=0 清条。应用可在
* on_swipe_progress 里用它自画(长度=当前 X=x_start+dx),或留空全靠 SDK。 */
void meow_ui_swipe_hint(int x);
```
- **返回语义**:B1 下按"返回"(左缘返回手势/虚拟返回键)→ 由应用决定:`on_swipe(LEFT)` 是返回手势,应用自行 `meow_ui_request_exit()` 回应用列表;若想在页内返回,可在 `on_swipe(LEFT)` 里处理并 `return` 不退出。
- **左缘返回手势(抬手触发)**:手指从**屏幕左缘(≤24px)向内划 ≥90px**,**抬手(release 边沿)后才触发** `on_swipe(LEFT)`;过程以 `on_swipe_progress` 实时上报并自动画顶部进度条(`meow_ui_swipe_hint`,长度=当前 X)。中部左/右滑不触发返回(中部右滑→`on_swipe(RIGHT)` 内容导航;中部左滑→落 TAP)。**与设计稿 §2.2 的偏差**:§2.2 写「左縁 + 向左 mdx<-90」物理不可达(左縁起点再向左最多走 ~24px),故取「左縁向内(右)划 ≥90px」实现,事件仍报 `MEOW_SWIPE_LEFT`(保持「左滑=返回」契约),方向与 watchdog immersive 兜底一致。
- **沉浸模式**:应用声明(app.json `immersive=1`)→ launcher 写 `/tmp/meowapp_state=1` → watchdog 隐藏虚拟返回键,返回手势归 SDK。这是 B1 可做、B2 转正的一部分。
**B2(衔接阶段 D)**:把 `on_key`/`on_swipe` 换成走 IPC 的语义导航(`MSG_NAV` 上报 + 启动链路),应用不再自己决定"返回回哪层",而是上报当前 Activity,由框架导航栈统一回退。届时:
- `meow_ui_run` 改为从 IPC 收启动链路(`MEOW_CHAIN`/`MEOW_ACTIVITY` 环境变量)初始化导航栈;
- 返回 = 栈回退,到根才 `meow_ui_request_exit()`。**本文档 §5 的控件与 §3/§4 绘制在 B2 不变**,只换输入/导航后端。
---
## 7. 动画/时间(B1 最小可用)
当前无动画框架。B1 提供**帧驱动回调 + 内置时间**,应用据此做低成本的帧序列/淡入淡出:
```c
/* on_tick(ev) 里用 meow_ui_time_ms() 做时间轴动画 */
uint64_t meow_ui_time_ms(void); /* 单调毫秒时间戳 */
/* 动画 = 应用在 on_tick 里根据时间更新状态 → 调绘制 → meow_ui_flush_region 只刷变化区 */
```
**约束**:
- 只做**状态时间轴动画**(如滑条拖动回馈、列表项滑动、淡入淡出用 `fill_rect_alpha` 叠加),**不做实时 tween 每帧重算几何**(CPU 软渲扛不住)。
- 动图/视频:**B1 不提供**(无解码器/无 GPU)。若确需"动画封面",用**预渲染位图帧序列**(应用持有若干帧,`on_tick` 按时间切帧 + `flush_region`),这是唯一低成本的动图方案。
- 毛玻璃/模糊背景:**支持廉价版**(HibyOS 同款降采样模糊,见 §11)——用 `meow_ui_img_scaled` 缩 1/8 铺底 + `fill_rect_alpha` 遮罩;**仅在切歌/换封面时算一次,非每帧**。真高斯卷积/全屏动态模糊不做。
---
## 8. 主题/配置(B1)
观感统一的核心,也是回应"控件样式效果"。**内置日间 / 夜间两套主题,跟随 MeowOS 系统设置的主题模式自动切换。**
```c
typedef enum { MEOW_THEME_LIGHT, MEOW_THEME_DARK, MEOW_THEME_AUTO } meow_theme_mode;
typedef struct {
uint32_t bg; /* 背景色 */
uint32_t fg; /* 前景/正文 */
uint32_t accent; /* 强调色(按钮/选中) */
uint32_t dim; /* 次要文字 */
int font_title; /* 28 */
int font_body; /* 20 */
/* MD3 优先 / MIUIx 备选 */
} meow_theme;
/* 当前生效主题(应用只读)。AUTO 时按系统主题设置(display.theme_mode)解析,
* 日间取 light 表、夜间取 dark 表;由框架在主题模式变化时刷新。 */
const meow_theme *meow_theme_get(void);
/* 设置主题模式(AUTO 时联动系统设置键 display.theme_mode)。 */
void meow_theme_set_mode(meow_theme_mode m);
meow_theme_mode meow_theme_get_mode(void);
/* 主题变化通知:应用注册后,AUTO 切换日/夜时收到回调(据此重绘)。 */
typedef void (*meow_theme_listener)(meow_theme_mode m);
void meow_theme_add_listener(meow_theme_listener cb);
void meow_theme_remove_listener(meow_theme_listener cb);
```
**双主题 + 跟随系统设置**:
- **日间/夜间**:SDK 内置两套标准色板(`meow_theme_light` / `meow_theme_dark`),日间高亮(浅底深字)、夜间低亮度(深底浅字,护眼省电)。框架所有控件/绘制默认读当前生效主题,应用 `meow_theme_get()` 只读、不自配色。
- **跟随系统设置**:主题模式 `display.theme_mode`(`light`/`dark`/`auto`)是 `meow_settings` 的一个设置键。`auto` 时按系统时间(如 6:00-18:00 日间,其余夜间)或系统亮度/环境判定切换(B1 先用固定时段,后续可接入环境光,见遗留)。框架监听该键,变时对注册的 listener 广播,实体 UI / Web / 应用统一重绘。
- **主题切换即时生效**:`meow_theme_set_mode` 非"只改设置",会立即广播 listener 并让当前绘制的控件用新主题重绘(应用在 `on_tick` 里据 listener 触发 `meow_ui_flush_region`)。
> 主题随 `meow_settings` 的 `display.theme_mode` 键分发;框架统一,应用不必自配色——这正是"实体 UI / Web 面板 / 应用全程一致观感"的保证。
---
## 9. 目录/构建约定
```
patch_v2/apps_assets/apps/<pkg>/app.json # 应用描述(新增 SDK/immersive 字段,见下)
patch_v2/hello_app/hello.c # 应用源码(改造为调 meow_ui 的示例)
patch_v2/sdk/ # SDK 源码
├── meow_ui.h # 对外 API(本文档 §3-§8)
├── meow_ui_gfx.c # 渲染后端(back_buf+present+font)
├── meow_ui_widget.c # 控件
├── meow_ui_img.c # stb_image 解码
├── meow_ui_input.c # 输入线程+语义事件
├── meow_ui_theme.c
├── stb_image.h / stb_image.c # 单头文件(vendor)
patch_v2/meow_launcherd/meow_font.a # 复用(静态链接)
```
构建出 `patch_v2/sdk/meow_ui.a`;应用编译链(zig-mipsel-cc)额外链 `meow_ui.a meow_font.a libz.a -lpthread`。
**app.json 扩展字段**(向后兼容,缺省即旧行为):
```json
{
"name": "Hello App",
"exec": "...",
"version": "0.1",
"framework": 1, // 1=走 meow_ui(框架承载);缺省/0=裸自绘(旧)
"immersive": 0, // 1=沉浸模式(隐藏虚拟返回键,左缘滑动返回)
"icon": "apps/hello/icon.png" // 可选:列表图标(供图片层)
}
```
---
## 10. 与阶段关系 / 落地顺序
- **B1(本文档,先行)**:`meow_ui.h` + gfx/widget/img/input/theme 最小实现 + `meow_ui.a`;**改造 hello.c 为 SDK 版**作为示例;出包 `r3proii_m2_20.upt` 或独立应用包验证。**不依赖阶段 D。**
- **B2(衔接阶段 D)**:输入/导航后端换成 IPC + M3 导航栈,应用框架字段 `framework=1` 的应用自动升级为"导航栈承载",裸应用保持兼容。
- **依赖**:`meow_present/back_buf/dirty`(M2.16 地基,✅)、`meow_font`(✅)、`blend565`(✅)——B1 全部基于现有能力,**无需新的硬件依赖**;仅新增 stb_image(单头文件)与输入线程。
---
## 11. 硬件约束(务必让应用作者知道)
| 能力 | 可行性 | 说明 |
|---|---|---|
| 图片(PNG/JPEG/BMP 静态图) | ✅ 支持 | stb_image 解码 + 最近邻缩放;封面居中裁方 |
| 半透明叠加 | ✅ 支持 | `fill_rect_alpha`(blend565),面板/遮罩可用 |
| 圆角/描边 | ✅ 支持 | 圆角框 + 字体描边 |
| 毛玻璃/模糊背景 | ✅ 支持(廉价版) | **HibyOS 同款「降采样模糊」**:封面缩到 1/8 再放大铺底(天然低通滤波抹掉高频),叠一层 `fill_rect_alpha` 半透明遮罩,清晰小封面叠其上。**背景只在切歌/换封面时算一次,非每帧**;不是真高斯卷积、不需 GPU。用 `meow_ui_img_blur_bg()` 生成背景 cache。**已实测依据**:设备 `playing_plane/` 无模糊背景图 + 无 GPU/无 SIMD/14MB 内存 → 运行时模糊只能走降采样近似,成本微秒级 |
| 控件动画(时间轴/帧序列) | ⚠️ 有限 | 只做状态式动画;实时 tween 不做;动图=预渲染帧序列 |
| 动图(GIF/WebP 动画) | ⚠️ 弱支持 | 用预渲染位图帧序列模拟;不承诺实时 GIF 解码 |
| 视频 | ❌ 不做 | 无解码器/无 GPU/内存 14MB 会 OOM;如需"视频"用预渲染帧序列 |
| 刷新率 | 上限 60(默认)/30/15 | 应用动画别贪高帧率,软渲耗 CPU |
---
## 12. 遗留 / 待实现
1. `meow_ui` 渲染后端需从 launcher 里**抽公共绘制实现**(`back_buf`/`meow_dirty_*`/`meow_present`/`meow_font_bind` 目前是 launcher 静态函数——需提取为可链接的 `meow_ui_gfx` 或改为共享 obj,注意 launcher 与 app 是**不同进程**,不能共享同一 back_buf;`meow_ui.a` 是**独立库**,内部自建 fb 上下文,不复用 launcher 的全局 `g_dirty`)。
2. 输入线程优先级:vs 主循环 10ms 热键轮询的竞态,需在 B1 实现时定义清楚(应用进程内独立事件循环,不与 launcher 抢)。
3. 网易云跑通后迭代控件集(播放页/列表/歌词),非一次性补齐。
4. **`meow_ui_img_blur_bg`(降采样模糊背景,HibyOS 同款)API 待加**:已在 §11 确认可行性(缩 1/8 放大铺底 + α 遮罩,切歌算一次);函数签名与实现留到 B1 之后(或并入网易云播放页时)。当前 SDK B1 的图片层先用 `meow_ui_img_scaled` + `fill_rect_alpha` 手动组合即可达到同等观感。
5. 视频**已定不做**:无解码器/无 GPU/内存 14MB 会 OOM;"视频"一律用预渲染帧序列模拟。若确需播放具体小视频,宿主机离线转帧序列 PNG → 打包到 TF 卡 → SDK 按帧播放(规避设备一切解码负担)。
### B1 落地状态(2026-08-29)
- **B1.0 渲染后端** ✅ `meow_ui_gfx.c`:自建 fb 上下文(自 mmap `/dev/fb0`、自建 `back_buf`、独立脏区集 + vsync+原子 blit);无 fb 时(宿主/qemu)离屏 480×720,`MEOW_UI_DUMP=<path>` 可把 back_buf 导出 PPM 截图。
- **B1.1 绘制层** ✅ §3 全量:`meow_ui_clear/put_px/fill_rect/draw_rect/draw_rect_round/fill_rect_alpha/blend565` + `text/text_right/text_center/text_width/text_height`。字库(真机)优先,回退内建 5×7 点阵。
- **B1.2 图片层** ✅ `meow_ui_img.c` + 内建 `stb_image`(PNG/JPEG/BMP→RGBA):`meow_img_load_file/mem/free/w/h`、`meow_ui_img/scaled/alpha/cover`(最近邻 + 居中裁方)。宿主已用 test.png 验证预览/裁剪/缩放。
- **B1.3 控件层** ✅ `meow_ui_widget.c`:`meow_btn/slider/switch/list` 最小集(绘制 + 命中 + 取值)。
- **B1.4 输入层** ✅ `meow_ui_input.c`:输入线程读 `event*`(触摸 goodix-ts / md-gpio-keys / ADC 键),翻译成 `on_key/on_tap/on_swipe/on_tick` 语义事件;`meow_ui_run/request_exit`。手势专项(GESTURE_BACK):左缘返回手势=抬手触发 + `MEOW_EV_SWIPE_PROGRESS` 进度上报 + `on_swipe_progress` 回调;SDK 自动画顶部进度条 `meow_ui_swipe_hint`。
- **配套:Launcher 统一触摸接口 `meow_touch`**(M2.25,launcher 内部,应用不依赖)✅ `patch_v2/meow_launcherd/meow_touch.{h,c}`:Launcher 自身 UI(应用列表 / 电源菜单 / 确认框)的低性能触摸解析收敛为一份,goodix 时序免疫(`BTN_TOUCH=1` 不锁原点,等按下后首个有效 `ABS` 才锁;抬手位移 ≥65px 判滑动不命中)。应用侧**不**用它——应用触摸由本 SDK 输入层(`meow_ui_input.c`)翻译成 `on_tap/on_swipe` 回调,两者互不干扰;与 SDK 触摸保持一致的 goodix 时序免疫语义(详见 `meow_touch.h` 注释)。
- **B1.5 主题** ✅ `meow_ui_theme.c`:日间/夜间两套色板 + `get/set_mode/get_mode/add/remove_listener`;跟随 `display.theme_mode`(`/usr/data/meow/settings.conf`)。AUTO 按本地 6:00-18:00 日间。
- **B1.6 IME** ✅ `meow_ui_ime.c`:英文 QWERTY / 数字软键盘(ASCII),`meow_ui_text_input`(阻塞)+ `meow_ui_text_input_async/meow_ime_active/meow_ime_text`;覆盖底部约 300px,背景点=取消、OK=确认。
- **构建** ✅ `patch_v2/sdk/meow_ui.a`(含 meow_font.o 内联,应用只需链 `meow_ui.a libz.a -lpthread`);`build_sdk.sh` 一键重编。宿主 + `qemu-mipsel` 均跑通;示例 `hello` 改造为 SDK 版(见 §9 目录与 `patch_v2/hello_app/build_hello.sh`)。
- **验证**:`meow_ui.a` mipsel 静态编译链接 ✅(宿主 demo + qemu 均导出可见画面到 `device_test/`);真机 /dev/fb0 几何与输入触屏**未实测**(设备本次不可达,逻辑参照 launcher 既有实现)。
**实现期 API 微调(相对本文档)**:
- [D1] `meow_ui_opts` 补充 `width/height`(0=自动读 fb;离屏缺省 480×720)。
- [D2] `meow_list_create` 数据源回调定案为 `int (*)(int idx, char *label, int len, void *ctx)`。
- [D3] 新增内部 `meow_gfx_fill_round/blit_rgba/mark_dirty`(控件/图片用,非公共 API)。
- [D4] 新增 `meow_theme_defaults(mode)`(脱机取内置色板)。
- [D5] 新增 `on_swipe_progress` 回调 + `meow_ui_swipe_hint(x)`(左缘返回手势视觉条)+ 内部 `MEOW_EV_SWIPE_PROGRESS` / `meow_input_inject`(宿主验证用)。
- `meow_ui.h` 为对外权威签名;本文件 §3-§8 作为设计蓝本。
---
*文档基于 2026-08-29 现有渲染基建(M2.16 无闪烁地基 + meow_font + blend565)编写;API 名会随 B1 实现微调,实现时以 `meow_ui.h` 为准。*
+72
View File
@@ -0,0 +1,72 @@
# MeowOS 通知系统设计(M2 通知存储 + 分发,阶段 B/D 前置)
> 状态:设计稿定稿讨论中(2026-08-29,用户已给定多数决策)。按 FREWORKS §4/附C + 用户约定,遵循 freedesktop.org 通知规范。**实施前仅剩"应用配额"细节待深入讨论**(见 §7)。
## 1. 定位
- MeowOS 通知中心的后端(存储+分发),是阶段 B 通知中心 UI + 阶段 D(M2 通知存储)的前提。
- 生产者 = MeowAPP(原生 IPC)+ **系统事件(系统抽象为 App)** + 外部桌面 app(D-Bus 桥)。
## 2. 通知标准(freedesktop.org Desktop Notifications,用户约定)
遵循 `org.freedesktop.Notifications`(D-Bus 接口),字段严格按标准:
- **app_name**(来源应用名)
- **replaces_id**(更新/替换通知 id;0=新建)
- **app_icon**(图标,指 icon name 或 path)
- **summary**(标题,单行)
- **body**(正文,可多行)
- **actions[]**(按钮,可选;动作回调)
- **hints{}**(提示,含 `x-canonical-*`/progress/图片数据等;我们用 `hints` 传 target Activity、进度 percent 等)
- **expire_timeout**(超时 ms;-1=常驻/ONGOING,0=不自动过期)
- 返回新版通知 **id**。
**MeowOS 扩展**:`hints` 内加 MeowOS 专有键(如 `meow.target_activity`、`meow.progress`)用于通知中心点击跳转/进度条显示。媒体通知走 MPRIS(`org.mpris.MediaPlayer2`,另行)。
## 3. 双接口(FREWORKS 附C,已定)
1. **原生 IPC**:MeowAPP 经 AF_UNIX socket 发 `MSG_NOTIFY`(FREWORKS §4 通定协议 0x0020)+ `MSG_NOTIFY_ACK`(0x0021)/`DISMISS`(0x0022)/`LIST`(0x0023)。
2. **D-Bus 桥** `meow_notify_dbus`(独立小守护,链 libdbus/GDBus,系统总线注册 `org.freedesktop.Notifications`):收到标准 `Notify()` → 经 MeowOS IPC 后端入通知存储;后端关闭/动作 → 回发信号。libdbus-1/libdbus-glib/libgio 已确认可用。
## 4. 通知数据结构(存内存 + 转储)
每通知字段(对齐 freedesktop + MeowOS):
```
id(uint32) / app_name / app_icon / summary / body / actions[] / hints{}
expire_timeout / created_ms / dismissed / ongoing(bool)
target_activity(可选, 来自 hints) / progress(可选 int)
```
统一从 IPC/D-Bus 两路解析入该结构。
## 5. 存储分层(用户已定 2026-08-29)
- **关键配置优先板载**:设置类(notify.* 配额等)存板载 `/usr/data/meow/settings.conf`(B2a),确保**无卡时系统正常工作**。
- **实时通知 → 本地内存**(板载,动态分配/ring)。
- **超时/转储 → TF 卡** `MeowOS/notifications/`(**系统约定:系统文件统一放 MeowOS/ 下**,非 MeowData/——MeowData 是应用专属数据根)(转储区,如 notifications/<ts>.json 或 appended 日志)。
- **无 TF 卡可运行**(本地内存态即可,不依赖卡;TF 卡在时才转储)。转储/日志类是可降级数据(无卡用 /tmp 回退),配置类永远板载。
- **本地数据清理策略**:
- 关机/重启前 → 清理本地内存通知数据(正常关机路径)。
- 异常关机(重启后)→ 开机时识别并清理本地残留。
- **TF 卡转储区**:保留**最新 10MB**(ring/截断),**不被电源事件清理**(持久留存)。
## 6. 通知生命周期(FREWORKS §4 已定,作参考)
- 内存态:每应用**配额**(待讨论 §7),满丢最旧;ttl 过期(1s 粒度);ONGOING 常驻(服务死随清)。
- 转储:超时/入 TF 卡区。
## 7. 【已定】应用配额(用户拍板,2026-08-29)
- **每个 App 最多申请 4 个通知类型(category)**。
- **每个类型活跃(未过期)通知上限 4 条**。
- **媒体通知除外**:单个 App 只允许保留 1 个媒体通知;允许多个媒体 App 同时(多应用同时播放场景),UI/逻辑后期处理好。
- 数据类型:`notify.category`(App 声明用哪些类型,≤4) / `notify.category_quota`(每类型 4,默认)。
- 系统事件:系统也抽象为 App(`app_id=system`),同样走配额(系统通知也占"系统"App 的类型/条数配额)。
- 配额只管**内存活跃态**;转储到 TF 的历史不受配额限制(只受 10MB 总量)。
## 8. 转储规则(用户拍板,2026-08-29)
- **通知创建 10 秒后自动转储到 TF 卡**(无论是否过期/被清)——通知中心显示"最近 10 秒内活跃" + TF 归档历史。
- **正常电源事件(关机/重启)触发清除板载通知数据之前**:先转储所有未上卡的通知,再清除板载。
- **异常关机**:开机时清理板载残留通知数据。
- TF 卡转储区 `MeowOS/notifications/`:保留**最新 10MB**(ring/截断),**不被电源事件清理**(持久留存)。
- **无 TF 卡**:系统可运行(内存态通知即可),不依赖卡。
## 9. 通知中心 UI(阶段 B)
- 状态栏下滑打开通知中心(用户已定),列表显示通知(app 图标 + summary + body + target Activity),点击按路由表解析跳转(M3 导航栈),下拉关闭/清除。**此部分依赖 M2 存储 + M3,后置。**
## 10. 分步实施建议
1. **M2a 通知存储 + 原生 IPC**:按上面的配额(category 级)+ TTL 10s 转储钩子 + 系统事件源(系统作 App)。→ 可先落地。
2. **M2b D-Bus 桥 meow_notify_dbus**(org.freedesktop.Notifications 桥接)。
3. **M2c 转储 TF 卡 + 10MB 保留 + 关机前转储/开机清理**。
4. M5 通知中心 UI(阶段 B)+ media 通知(MPRIS)。
+50
View File
@@ -0,0 +1,50 @@
# MeowOS 统一系统性能调度器(PERF_SCHEDULER.md)
> 状态:设计蓝图(2026-08-29)。**后续阶段实现**(阶段 B/B2 之后,或与框架并进)。M2.16 的刷新率调度是它的第一个子模块,后续统一收进 meow_sched。
>
> **更新(M2.28 / A-1+A-2,2026-08-29)**:显示刷新率调度(60/30/15 档 + 自适应降档 + always_max)
> **已从 launcher 迁出、归口 SDK 渲染后端**——`sdk/meow_ui_gfx.c` 的 `present()`(`meow_ui_flush()`
> 上屏)内嵌帧率门控 + governor,公共接口 `meow_ui_set_refresh_rate()/meow_ui_set_refresh_always_max()`
> (已入 `meow_ui.h` §7.1)。launcher 与 app 现统一走 SDK 的 present 帧率门控,不再各自内联一套。
> 后续(S1)仍按原计划把该子模块进一步收进 `meow_sched`(`meow_sched_fps()`),SDK present 只作为
> 执行器(消费 `meow_sched` 下发的档位)。
## 0. 目标与一句话
在资源紧张(64MB RAM、单核 X1600E、无 GPU)下,**统一治理"性能 vs 功耗 vs 流畅度"**:用中央调度器按策略(平衡/性能/续航)分配显示刷新率、CPU/频率、前后台应用优先级、温控——避免各模块零散自己降频/跳帧。
## 1. 管辖子域
| 子域 | 内容 | 现状/未来 |
|---|---|---|
| **显示刷新率** | 60/30/15 档 + 自适应降档 + always_max | M2.16 内联在 launcher;**M2.28(A-1/A-2)已归口 SDK present**(`meow_ui_set_refresh_rate/always_max`,`meow_ui_gfx.c`);以后仍计划收进 `meow_sched`(`meow_sched_fps()`) |
| **CPU/频率** | 若 X1600E cpufreq 可控(/sys/devices/system/cpu/cpu0/cpufreq/),按策略设 governor/频率 | 待测(可能无 cpufreq,简化为核数/负载建议) |
| **前后台优先级** | 前台应用/会话 = 满额;后台服务/空闲 = 低优先、省电 | MeowOS 已有前台会话概念,纳入调度 |
| **温控** | /sys/class/thermal 读取,超阈值降档 | 待测(thermal_zone 可能仅 1 个) |
| **功耗策略** | 平衡/性能/续航 三种 policy(settings `power.mode`) | 设置项 + web |
## 2. 设计
- **中央调度**:`meow_sched_tick()`(主循环低频,如 250ms)读取策略 + 各子域信号(渲染负载/后台活跃/温度/电量),决定并下发:
- 有效刷新率档位(60/30/15)
- 前台 vs 后台的调度权重/优先级(nice? 前台应用可 `setpriority`,后台/服务降 nice)
- (若 cpufreq)频率档
- 温控降档(过热强制降)
- **策略 `power.mode`**(enum):`balanced`(默认)/ `performance` / `battery`:
- performance:恒 60、前台满额、不降频、温控阈值放宽
- battery:允许降档、后台更省电、更早温控
- balanced:折中 + 自适应
- **自适应降档**:统一在 meow_sched 里做(M2.16 的内联 governor 迁入),基于渲染负载+温度+电量综合。
- **API**:`meow_sched_*()`(`meow_sched_mode()`/`meow_sched_effective_fps()`/`meow_sched_hint_load(x)`/`meow_sched_set_foreground(pid)`);settings `power.mode`。
- **通知/暴露**:web 面板显示当前策略/负载/温度/刷新档;CLI `meowcli sched status`。
## 3. 实现里程碑(后面)
- S1:把(现归口 SDK present 的)刷新率调度迁为 meow_sched 子模块(`meow_sched_fps()`,SDK present 作执行器消费其档位),加 `power.mode` 三策略。
- S2:前后台优先级(前台应用 setpriority、后台/服务降 nice)+ 空闲省电。
- S3:温控/电量感知降档 + 暴露 web/CLI。
- (cpufreq 若可用再并入)
## 4. 原则
- 任何降档/提权都要**无卡顿感知**(降档只在重负载/过热时,常态满额)。
- 中央单一决策,各模块只上报信号、不各自为政(避免"各管各的跳帧/降频")。
- 低开销(低频调度、轻采样),不引入明显 CPU 负担。
+56
View File
@@ -0,0 +1,56 @@
# MeowOS for HiBy R3 Pro II
把 R3 Pro II(君正 X1600E,HiBy OS / Linux 4.4)打造成可扩展的"第二系统":MeowOS 作为主控层,双系统互斥(引导选单),HibyOS 播放器由 MeowOS 托管,MeowAPPs / MeowPanel / Activity 框架持续扩展。
## 核心文档(先读这些)
> **总索引(单一来源)**:`docs/MEOWOS_SPEC_REF.md` —— 各路径/接口/功能/约定的权威速查,含「已知矛盾/待确认」清单。
| 文档 | 内容 |
|---|---|
| `docs/MEOWOS_SPEC_REF.md` | 权威速查总索引(首选入口;常量/路径照抄原文 + 出处 + 矛盾清单) |
| `ANALYSIS.md` | 主技术档案:固件逆向、启动链、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、生命周期、边界、日志、返回键) |
| `GESTURE_BACK_DESIGN.md` / `KEY_CONTROL_DESIGN.md` / `PERF_SCHEDULER.md` | 左缘返回 / 按键契约与信号 / 帧率调度 |
> 一次性/阶段性设计文档(已过时或未实施)移至 `docs/plan/` 归档(移动不删、保留历史):`BOOT_SELECTOR_DESIGN`、`CHARGING_SCREEN`、`MAIN_MD3_PLAN`、`MD3_UI_SPEC`、`MEOW_TOUCH_DESIGN`、`MIGRATION_AC`、`BLUETOOTH_MANAGER_PLAN`、`APP_LOG_MANAGER`。
**文档收敛规则**:不再为每个功能新增设计 md;新功能在对应权威文档(`MEOW_APP_SDK.md`/`APPS.md`/`SYSTEM_SETTINGS.md` 等)补小节;新增决策/检查点写 `DEV_NOTES.md`;过时一次性设计进 `docs/plan/`。
## 当前架构与组件(2026-08 快照)
- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`):类 systemd/systemctl 的独立守护进程,由 supervisor(meow_launcherd/bootmenu)`fork/exec` 托管。读 TF 卡 `MeowOS/services.d/<name>.conf`(目录式 `key=value`:`exec/args/env/enabled/restart/backoff_ms/max_restart/trigger`,未知键忽略、前向兼容);每个服务是 svcd 的**独立子进程**(崩溃隔离:服务挂不影响 svcd/supervisor);事件触发(`boot`/`card-mount`/`wifi-up`,1s 轮询);`-f` 前台运行便于 supervisor `waitpid`。mipsel 静态 ~99KB,进固件不入卡(拔卡 svcd 不消失)。
- **MeowOpt/(TF 卡)**:板载外部工具/二进制(如 `MeowOpt/mdnsd/mdnsd`),由 MeowOS 启动时从卡加载,拔卡降级不崩。
- **mDNS 方案A(已拍板)**:用**完整 `mdnsd`**(994KB,mipsel 静态)放 TF `MeowOpt/mdnsd/mdnsd`,由 meow_svcd 托管;SAResponder(885KB)因"单服务注册型"无法按名访问设备(`ingenic.local`)被排除。⚠️ 手机热点组播常在客户端间丢失 → mDNS 有风险,主机 DNS 兜底。
- **SDK 已落地**:统一日志 `meow_log()`(按 app 分目录、每次启动新文件、单文件 2MB 轮转、每 App 总量 50MB 删最旧,M2.52)+ 通知 API `meow_notify_*`(4 类型×每类型 4、媒体单例,M2.53)。
- **mips-repo/**(宿主侧 mipsel 软件仓库):交叉编译产物 + `build.sh` + `manifest.md`,供后续固件/TF 复用(见「目录速览」)。
## 目录速览
- `patch_v2/` — meow_launcherd(MeowOS 核心)、bootmenu(引导选单)、watchdog、meow_svcd(服务管理器)、sdk(meow_ui SDK 源码/产物);源码与构建脚本
- `mips-repo/` — mipsel 交叉编译软件仓库(mdnsd 等:`build.sh`+`manifest.md`+`bin/`),供后续固件/TF 复用
- `font/` — 预渲染字形系统(gen 生成器、meow_font 渲染库、atlas 产物)
- `fw_apps/` — 固件组装产物(`.upt` 不入库,由 build_fw_apps*.sh 重新打包)
- `bootlogos/` — 用户定制启动图/关机 logo
- `device_test/` — 真机验证截图/证据(`.raw` 不入库)
- `ssh_keys/`, `.tools/` — SSH 密钥、交叉工具链(不入库,按需)
## 构建/刷机
- 固件打包:`build_fw_apps*.sh`(需交叉工具链 `zig-mipsel-cc` + musl.cc,见 ANALYSIS 第 10 节)
- 刷机:failsafe(关机后按住 音量加+电源)从 SD 卡根 `r3proii.upt` 升级;勿用播放器 UI 卡死的升级按钮
- 回退:官方固件 / `backups/`(NAND 全量备份)重刷
## 关键纪律(红线)
- 严禁对 `hiby_player` 发未注册信号(SIGUSR1 导致整机重启);不 killall;禁经 `hiby_player.sh`
- 勿远程 kill `meow_launcherd`(整机重启)
- 播放器不能"杀+重拉"(HGL 连续内存);双系统互斥,MeowOS→HibyOS 只能重启
- 日志/字库/媒体等大数据只写 TF 卡;板载只写 KB 级 `meow/settings.conf`
- 绝对不对挂载点 `rm -rf`
详见 `ANALYSIS.md` 纪律章节。
+181
View File
@@ -0,0 +1,181 @@
# MeowOS 系统设置子系统设计(阶段 B2a 后端 + B2b UI)
> 状态:设计稿(2026-08-28)。**后端 B2a 前置到阶段 B 之前**(B 的通告栏快捷按钮/亮度滑条/主题开关全是设置项,必须先有统一后端,B 直接消费、零返工);B2b 实体设置页 UI 在 B 之后;阶段 C(MeowPanel)调同一后端。
> 原则:**一套设置后端,多端(实体 UI / Web 面板 / CLI)共用同一 API**,单一数据源,不留旁路。
---
## 0. 目标与一句话
把 MeowOS 全部系统设置抽象成**统一设置子系统**(结构化存储 + C API + 变更通知);阶段 **B2a** 落地这套后端(`meow_settings_*` + `settings.conf` + listener,**先于阶段 B**);阶段 **B** 的通告栏控件直接消费它;阶段 **B2b** 用同一 API 建**实体设置 UI**;阶段 C 的 MeowPanel 设置页**直接调同一后端**(HTTP 转发 C API),实体与 web 镜像、改哪边都同步。
## 1. 设置项清单(覆盖 MeowOS 全部可配置项)
| 分组 | 设置键 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
| **显示** | `display.brightness` | int 0-255 | 101 | 当前亮度(clamp ≥`display.brightness_min`,防黑屏;设置页滑条 clamp 到 1-100,101 会闪屏) |
| | `display.brightness_min` | int | **1** | 亮度安全下限(用户可调;M2.39 由 25 改 1,旧持久 25 视为残留默认自动迁移到 1,使亮度滑条可调到「极暗但可见」) |
| | `display.screen_off_mode` | enum | `panel` | `panel`=面板下电+背光0+FBIOPBLANK / `black`=背光0全黑+双击亮屏 |
| | `display.screen_off_timeout` | int 秒 | 30 | 息屏时长(0=常亮) |
| | `display.refresh_rate` | enum | `60` | 刷新率上限三档 `60`/`30`/`15`(默认 60,不逼 62Hz 瓶颈) |
| | `display.refresh_always_max` | bool | false | 始终最大刷新率(=60 且不自动降档);关=允许自适应降档(60→30→15) |
| | `display.theme` | enum | `dark` | `dark`/`light`(MD3 深浅;manual 模式下 s_ui_theme 直读它) |
| | `display.theme_mode` | enum | `manual` | 主题判定方式:`manual`(=读 `display.theme`)/`auto`(按深色时段)。**B2b 新增** |
| | `display.theme_dark_start` | int 分钟 0-1439 | `0` | 深色时段开始(如 19:00=1140);`start==end` 视为未配置 → auto 回退 manual。**B2b 新增** |
| | `display.theme_dark_end` | int 分钟 0-1439 | `0` | 深色时段结束(如 07:00=420);`start>end`=跨午夜。**B2b 新增** |
| | `display.background` | path/color | `black` | `black` 或图片路径(`MeowAPPs/backgrounds/xx.png`) |
| **电源** | `power.longpress_menu_ms` | int | 3000 | 长按电源弹电源菜单阈值 |
| | `power.forceoff_ms` | int | 8000 | 长按电源强关阈值 |
| | `power.defeat_hotkey` | string | `power_voldown` | 切系统/切 MeowOS 热键组合 |
| **开机** | `boot.default` | enum | `meow` | 开机默认进入 |
| | `boot.menu_enabled` | bool | true | 是否显示引导选单 |
| | `boot.timeout_ms` | int | 5000 | 选单超时(有输入暂停重计) |
| **输入** | `input.hotkey_seq` | string | `volup,volup,volup` | 应用列表热键序列 |
| | `input.hotkey_window_ms` | int | 2500 | 组合/序列窗口 |
| | `input.touch_arm_delay_ms` | int | 700 | 触摸布防延迟(防幽灵) |
| | `input.swipe_invert` | bool | false | 设置页滑动方向(0=手指上滑→往下翻;1=反向) |
| **字体/主题** | `font.file` | path | `MeowAPPs/fonts/font.png` | 字体图集 |
| | `font.size.title` / `font.size.body` | int | 28/20 | 标题/正文字号 |
| | `ui.style` | enum | `md3` | `md3`(Material You)/`miui` |
| **通知** | `notify.center_enabled` | bool | true | 下拉通知中心开关 |
| | `notify.app_quota` | int | 16 | 每应用通知配额 |
| **日志** | `log.level` | enum | `info` | `error/warn/info/debug` |
| | `log.to_card` | bool | true | 写 TF 卡(无卡回退内存环) |
| **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(MeowOS 直写 CS43198 DAC;读回为追踪值) |
| | `audio.volume_max` | int 0-100 | 100 | MeowOS 内部音量上限(clamp;**不与 HibyOS user.ini 上限同步**——其带 CRC 有风险,已定案不做;用户可自设 MeowOS 上限) |
| | `audio.volume_step` | int 1-5 | **1** | 音量键跳步(默认 1%,可配 1-5,最大 5) |
| | `audio.output` | string | `hw:0,0` | MeowAPPs 音频输出设备 |
| **网络** | `network.wifi_enabled` | bool | true | WiFi 开关(写 wlan0;设置页开关经 `wifi_up.sh`/`ip link set wlan0`,`ensure_wifi()` 尊重此值——关=0 则不再自动连网) |
| | `network.bluetooth_enabled` | bool | false | 蓝牙开关(写 `hciconfig hci0 up/down`;默认关=少耗电) |
> 新增设置项时:在清单加一行 + 存储 schema + API 常量/枚举 + 实体 UI 控件 + web 面板控件(四端同步)。
## 2. 统一存储(单一数据源)
- **主持久化位置:板载 `/usr/data/meow/settings.conf`**(UBIFS)。settings.conf 是**小文件**(几十行、<10KB),持久在板载可跨换卡/掉卡保留配置,避免换音乐卡丢失设置。板载仅此小设置文件(日志/字库/媒体等大数据仍留 TF 卡,符合"板载不写大文件"纪律;KB 级设置除外)。
- **导入/导出(备份):**
- **导出默认路径**:TF 卡 `MeowOS/backups/settings/setting.bak`(自动建目录)。用户可在导出时改路径。
- 本地 TF 卡:导入 = 从该 .bak 文件读入覆盖(选择/确认 + 校验)。
- Web 面板(阶段 C):`GET /api/settings/export`(下载 .conf)/ `POST /api/settings/import`(上传覆盖)。
- 二者都调用同一 `meow_settings_export(path)` / `import(path)`。
- **无卡/无板载时**:内存缓存,启动加载;板载在则自动落盘。
- **格式**:`key = value` 扁平文本(收敛旧 meow.conf),支持注释 `#`。
- 处理:`settings_load()`(启动读 + 变更即 `settings_save()` 落到 /data)、`settings_save()`(有变更/定时/掉电前)。避免频繁写(UBIFS 有磨损但 KB 级低频无碍)。
- 版本:文件头 `# meow-settings 1`。
## 3. C API 契约(后端,`meow_settings.h`)
```c
/* 类型 schema:每个设置键注册“名称/类型/默认/取值域”,存取统一走表 */
typedef enum { SET_INT, SET_STR, SET_ENUM, SET_BOOL } meow_set_type;
int meow_settings_init(void); /* 加载存储,注册默认 */
int meow_settings_get_int(const char *key, int *out);
int meow_settings_get_str(const char *key, char *buf, int len);
int meow_settings_get_enum(const char *key, int *out); /* 按枚举名/索引 */
int meow_settings_get_bool(const char *key, int *out);
int meow_settings_set_int(const char *key, int v); /* clamp 到取值域 */
int meow_settings_set_str(const char *key, const char *v);
int meow_settings_set_enum(const char *key, int v);
int meow_settings_set_bool(const char *key, int v);
/* 变更通知:注册回调(实体 UI 重绘 / web 推送) */
typedef void (*meow_settings_listener)(const char *key);
int meow_settings_add_listener(meow_settings_listener cb);
int meow_settings_remove_listener(...);
/* 枚举辅助:名字<->索引,供 UI/web 显示中文标签 */
int meow_settings_enum_label(const char *key, int idx, char *buf, int len);
int meow_settings_enum_count(const char *key);
/* 列表:枚举当前全部键,供 web 面板 / 设置页生成控件 */
int meow_settings_list(int (*cb)(const char *key, meow_set_type t, void*), void *ctx);
/* 导入/导出(备份) */
int meow_settings_export(const char *path); /* 写一份 .conf 到 path */
int meow_settings_import(const char *path); /* 从 .conf 读入覆盖,校验 + 通知 */
```
- **clamp/校验集中在 set**:如 `brightness` 自动 clamp ≥ `brightness_min`(默认 25);`screen_off_timeout >= 0`;枚举越界拒绝。
- **变更通知**在 set 成功后对注册 listener 广播(key 字符串),实体 UI 据此重绘;web 端经 MeowPanel 长连接推送。
## 4. 阶段 B2b:实体系统设置页(Launcher 内,用 B2a 已就位后端)
- 入口:应用列表加一个**系统设置**卡片/图标(或列表项),点进设置页。
- UI:列表式(与应用列表同款渲染),**分组**:显示 / 电源 / 开机 / 输入 / 字体主题 / 通知 / 日志 / 音频。每项 = 标题 + 控件:
- 开关(bool):`display.theme` 深浅、`notify.center_enabled`…
- 滑条(int):亮度(**clamp ≥25**)、音量、息屏时长时间
- 选择器(enum):息屏模式、开机默认、主题、日志级别——选择器用子菜单/对话框,选项中文标签来自 `enum_label`
- 路径(str):字体、背景——文件浏览器选择(可先用内置几个预设,自定义文件选择后期)
- **所有控件只调 `meow_settings_set_*` / `get_*`**,绝不直接改文件;改完 `settings_save()`,UI 按 listener 重绘。保证与 web/CLI 一致。
- 权限/安全:本机信任,设置页不做鉴权。
## 5. 阶段 C 复用(MeowPanel 设置页)
- MeowPanel 后端新增 `/api/settings`:
- `GET /api/settings` → 返回全部键值(调 `meow_settings_list` + get_*)
- `POST /api/settings` (body: key/value)→ 调 `meow_settings_set_*` + save
- `POST /api/settings/restore` → 恢复默认
- web 设置页**与实体设置页同一套 key/控件定义**(前端每 key 渲染对应控件),共用 `meow_settings` 后端。改 web → 实体立即反映(listener/轮询);改实体 → web 下拉刷新。
- 设置 API 也暴露给 **CLI/MCP**(`meowcli settings get/set`)供调试/自动化。
## 6. 与阶段 B / D 的衔接
- **阶段 B**:搭 `meow_settings` 后端 + 状态栏/通知中心期间,逐步把**现有散落的配置**(meow.conf 的 boot_default/hotkey/bg_color/字体路径)**迁移**进 `settings.conf` 卸载旧 meow.conf;状态栏/通知中心读设置(如通知中心开关、字体字号)走 API。
- **阶段 D(Activity/通知框架)**:通知配额/行为等设置进 settings;应用可通过 IPC 读改名(可选暴露只读 `meow_settings_get_*`)。
## 7. 实现里程碑
M1 存储+核心 API(get/set/save/load/listen/list/枚举)→ M2 实体设置页(分组列表+各类控件)→ M3 迁移旧 meow.conf + 状态栏/通知中心走 API → M4 web 设置页对接 → M5 加固(clamp/校验/掉电保存/测试)。
## 8. 风险/注意
- 板载存储不写:settings.conf 只落 TF 卡,未挂载用内存,挂载后落盘;掉电前 save。
- clamp 必须集中在 set(防亮度 0 / 非法枚举)。
- 迁移 meow.conf 时保持向后兼容(旧文件读入,写回新格式),避免把现有用户配置弄丢。
- 字体/背景路径若失效回退默认(不崩)。
## 附:系统状态重抓清单与策略(进 MeowOS 全量快照)
**状态项**(进 MeowOS 时全量抓,HibyOS 改过也同步):
- 电量(百分比/充电状态/电压/温度)、WiFi(开关/SSID/IP/信号/链路)、蓝牙(开关/连设备数)、音量(settings)、亮度(sysfs)、存储(/data 剩余/TF卡挂载容量)、性能(loadavg/meminfo)、温度(thermal_zone)、系统(模式/应用数/版本)、音频输出(后续)
**性能策略**:
- 进 MeowOS 一次性全量 `meow_sys_snapshot()`(阻塞可接受)。
- 主循环 10ms 热键轮询**绝不读系统状态**;单独 1s 轻刷仅时钟/电量/WiFi 信号。
- 性能/温度类用后台定时低频率(1-2s)读,仅 MeowPanel 用,别占 UI。
- 统一封装 `meow_sys_state` + `meow_sys_refresh()`,状态栏/设置/面板共用。
## 附 B:设备属性域(device attributes)——meow_sys 只读+可控子域
> 承接 §18.44 设备全属性只读枚举(任务要求「覆盖设备所有属性:4.4 平衡口插入、3.5 单端口插入、低增益/高增益输出、及所有自带属性」)。在 `meow_sys_state`(`g_sys`)上扩展 `struct meow_sys_devattr`,状态栏/MeowPanel/设置页共用,只读属性与可控(kcontrol)分开标注。**只设计接口,不改 launcher 实现**(落地留框架批次)。
**检测源(只读居多,可控项白名单):**
| 域字段 | 类型 | 读取源 | 可写 | 说明/单位 |
|---|---|---|---|---|
| `out.port` | int 0-7 | `amixer cget numid=9` | **写=切输出**(白名单1-4) | 现读=0(空闲);1=lineout/2=headset(3.5)/3=balance(4.4)/4=Type-C SPDIF;**0/5 危险勿写**。**line-out(1)=固定满幅电平,保守默认不响应音量键、不受 lock_vol 限制**(无实机接功放可测,按最保守处理,见 §附B line-out) |
| `out.headset_present` | bool | `/sys/class/switch/headset/state` | 只读 | 3.5 插入(=1) |
| `out.balance_present` | bool | `/sys/class/switch/balance/state` | 只读 | 4.4 插入(=1) |
| `out.earpods_adc_sw` | bool | `/sys/devices/platform/earpods_adc/earpods_adc/earpods_adc_sw` | 只读 | 耳塞型判定 |
| `out.balance_lineout_en` | bool | `amixer cget numid=8` | 写 | 平衡 Lineout 使能 |
| `vol.left`/`vol.right` | int 0-255 | `amixer cget numid=1/2`(读恒 0,dummy get) | **写(真写 DAC)** | meow_vol_set 已用;get 仅跟踪值 |
| `out.vol_max_headset`/`out.vol_max_balance` | int % | `config.json` volume.`lock_vol` | 只读(玩家态) | 3.5=55 / 4.4=43 ——**按输出口分级音量上限(实际「增益/响度等级」代理)** |
| `out.vol_default_headset`/`out.vol_default_balance` | int % | `volume.default_vol` | 只读 | 20 / 15 |
| `out.vol_warn_headset`/`out.vol_warn_balance` | int % | `volume.warn_vol` | 只读 | 34 / 28 |
| `codec.soft_mute`/`mute_out`/`nos_en`/`dre_en`/`dop_en` | bool | numid=5/3/6/7/10 | 写 | 软静音/硬静音/关过采样/动态范围增强/DoP 编码(numid 8/10 put=存驱动标志,get=dummy) |
| `codec.digital_filter` | int 0-4 | numid=4 | 写 | 滤波模式 |
| `dac.sample_rate` / `dac.is_dsd` | int/bool | **无任何只读路径**:`/proc/asound` 不存在、codec `reg_val/write_reg_val` **只写**(`--w-------`,回读 EACCES)、`current_samplerate` 未导出、dmesg 已滚出 | 只读 | **Unsupported**(标记 `MEOW_ERR_NOENT`);需玩家态或改驱动导出 |
| `usb_dac.mode` | bool | `/sys/class/udc/13500000.otg_new/is_a_peripheral`+`state` + `configfs usb_gadget/*/UDC` | 只读 | 当前 `is_a_peripheral=0`,state=not attached → 非 DAC 模式;=1 当 `is_a_peripheral=1`+gadget pid=0x0004 |
| `usb_dac.vid`/`pid`/`dac_pid` | int | `/usr/resource/config.json` | 只读 | 0x32BB / 0x0101 / 0x0004 |
| `bt.powered` | bool | `bluetoothctl show` `Powered`(或 `hciconfig` UP/DOWN);**勿用** `rfkill/*/soft` | 写(可控开/关) | 当前 no(rfkill soft=0/hard=0 但 HCI 未上电) |
| `bt.mac`/`bt.name`/`bt.codec` | str | `/sys/class/bluetooth/hci0/{address,name}` + 连接后 bluealsa | 只读 | codec 无连接不可读 |
| `batt.capacity`/`voltage`/`status` | int%/µV/enum | `power_supply/battery/*` | 只读 | 100% / 4342000µV / Discharging |
| `chg.online`/`current`/`temp` | bool/µA | `fusb302-typec-source/online` + `mp2731-charger/*` | 只读 | 0 / 0µA / 30.0℃(当前未充) |
| `ac.present` | bool | `power_supply/ac/present` | 只读 | 0 |
| `sys.temperature` | int | 无 thermal_zone | 只读 | **不可读**(x1600 未暴露) |
**关键结论**:① 3.5/4.4 是独立输出口,由 `Output Port Switch`(numid=9) 路由、由 `/sys/class/switch/headset|balance` 判别插入(headset 走 ADC=1 通道、balance 走 GPIO=PC24;`balancelo`/`lineout` 开关**未启用**),**有独立端口检测、无独立"平衡/单端"切换控件**;② **硬件/驱动层无高/低增益接口**——`codec_set_gain` 反汇编=**strcmp**、`codec_set_pregain/lrbalance/dsd_compensate`=**`jr ra` 空操作**、无 gain kcontrol、codec `reg_val` **只写不可回读**;幅度即 Playback Volume(0-255)、动态仅 DRE_EN;**真实的按输出口响度分级在玩家态 `config.json`(lock_vol 55/43、default_vol 20/15、warn_vol 34/28),可读**;③ USB DAC 模式 = `is_a_peripheral=1` + gadget(pid=0x0004)(当前 `is_a_peripheral=0`,state=not attached → 非 DAC 模式);④ CPU 温度、运行时采样率/DSD **无干净只读接口**(采样率/DSD 因 `/proc/asound` 缺失+codec reg 只写+`current_samplerate` 未导出而 **Unsupported**;温度因无 thermal_zone 而不可读),均标记 MEOW_ERR_NOENT。
**API / 暴露**(`meow_sys.h` + 三端复用,同 meow_settings):`meow_sys_devattr_refresh(&out)`、`meow_devattr_set(field,value)`(白名单)、`meow_devattr_enum_label(field,idx,buf,len)`;MeowPanel `GET/POST /api/device`;CLI `meowcli device [set]`。
**安全**:白名单写(out.port 1-4、vol、filter/nos/dre/dop/mute、bt.powered);禁写 0/5 端口;禁写 codec `write_reg_val`(保留为调试钩子);volume 读用跟踪值。
**附 B:line-out 与 gain(2026-08-29 实机更新,无实测保守默认)**
- **`out.gain`(高/低)= 播放器应用层数字增益**,选 `ot_devices.json``VOLUMES.HW.Gains` 的 HDB/LDB(实机仅暴露高/低两档,MDB 未暴露;框架与曲线本有 HDB/MDB/LDB 三档)。选择值存 `user.ini`(位置式 struct + 自定义完整性字 `0x0134fe6e`,读不干净、写有完整性风险)→ **MeowOS 只读标记 `Unsupported`(MEOW_ERR_NOENT)**,不设可写;`ot_devices.json` 增益曲线作只读信息展示(标注"工厂校准,非当前选择态")。**增益(输出电平曲线) ≠ 音量上限(lock_vol)**,二者独立相乘叠加。
- **`out.port=1`(line-out,3.5mm 同口)**:给功放/台式机的满幅固定电平。**保守默认**:MeowOS 切到 line-out 后**不响应音量键**、**不套用 lock_vol 上限**(`out.vol_max_headset` 不作用于 line-out),UI 标注"固定电平输出";仅当用户在设置里显式开启"line-out 可调"时才放开。**该行为未实测**(无功放设备),待补:切 line-out 后音量键是否仍生效、音量滑杆是否影响输出、lock_vol 是否绕过——测后再松开约束。
- **刷新触发**:用户可能经 HibyOS 切增益档 / 切输出口,MeowOS 需在这两个事件后全量重抓 `meow_sys_refresh()`(与"每次进 MeowOS 抓一遍"同逻辑),勿只靠 1s 轻刷。
+62
View File
@@ -0,0 +1,62 @@
# MeowOS TF 卡目录布局与"固件/卡"分头原则
> 状态:设计稿 v1(2026-08-28)。对齐"资源紧张(板载/RAM)"与"TF 卡热插拔"约束。
## 0. 一句话
**固件 rootfs 只装"稳定框架"(运行时零卡依赖);TF 卡承载一切可扩展/大体积内容。** 拔卡 → 框架不死、优雅降级;插卡 → 自动重建。MeowOS 各类非必要文件统一收在 `MeowOS/` 下。
## 1. TF 卡目录规范
```
/usr/data/mnt/sd_0/
├── Music/ # 音乐库(播放器标准索引目录)
├── MeowData/ # 用户数据(歌曲、封面缓存、下载等)
├── MeowAPPs/ # 应用
│ ├── <app>/ # app.json + 二进制 + 资源
│ ├── fonts/ # 预渲染字库 font.png + font.idx(可由 MeowOS 加载)
│ ├── logs/ # 统一日志(meow_<boot>.log;无卡回退内存环)
│ └── backgrounds/ # 可选背景图
└── MeowOS/ # MeowOS 专属(一切非必要文件)
├── backups/
│ └── settings/
│ └── setting.bak # 设置导出默认路径(SYSTEM_SETTINGS 一致)
├── opt/ # 外围可选工具(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd…)
├── lib/ # 可选共享库(opt 工具的依赖 .so)
├── tools/ # 调试/运维(可不设,并入 opt)
└── <其他 MeowOS 非必要资源…>
```
## 2. 固件 rootfs 只保留(稳定框架)
- `meow_launcherd`(MeowOS 核心)、`meow_bootmenu`(引导选单)、`meow_watchdog`
- `dropbear`/`dropbearkey`(SSH 必需)、`sftp-server`(小,保留)
- 调试三件套:`gdbserver`/`strace`/`tcpdump` + `sqlite3`(DB 调试)
- 系统依赖库(libc/libstdc++/libcrypto/glib/asound 等——播放器/服务需要,不能动)
- 启动器只读 `/data`(settings)+ rootfs;**不读卡**才能保证拔卡不死
> 其余 CLI(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd)→ `MeowOS/opt/`(卡);依赖 `.so` → `MeowOS/lib/`(卡)。
## 3. 拔卡/插卡行为
| 事件 | 行为 |
|---|---|
| **拔卡** | 框架核心(launcher/选单/SSH)不受影响(零卡依赖);应用列表"未插卡";日志回退内存环;字体回退点阵;opt/lib 工具不可用(PATH 里卡上的没了);**不崩不死** |
| **插卡** | 侦测 `mmcblk0` → 自动 mount → 重扫 MeowAPPs → 载字库 → 恢复 opt PATH → 日志迁回卡;自动重建 |
## 4. 实现要点
- **移工具**:从 rootfs 移除 `MeowOS/opt/` 对应二进制;MeowOS 启动时若卡在则 PATH 前插 `/usr/data/mnt/sd_0/MeowOS/opt/bin:$PATH`(卡不在跳过)。用 `meowtool` 包装提供绝对路径引用。
- **框架零卡依赖审查**:确认 launcher/bootmenu 没有任何"卡必须在"的硬依赖(settings 在 /data、字体/应用/日志对卡可缺省/回退)。
- **字库**:卡 `MeowAPPs/fonts/`(无卡回退点阵,已有)。
- **日志**:卡 `MeowAPPs/logs/`(无卡内存环,已有)。
- **设置**:板载 `/data/meow/settings.conf`(小文件跨卡保留),导出默认 `MeowOS/backups/settings/setting.bak`。
## 5. 体积收益
移除 MeowOS/opt 工具后 rootfs 压缩预算省约 3-5MB(curl/socat 大头),回到 ~41MB,远离 45MB 上限。
> **M2.12 落地确认**:rootfs `/usr/bin` 的 curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd 已迁至卡 `MeowOS/opt/bin/`(均**静态 MIPS**,无 `.so` 依赖,故 `MeowOS/lib/` 暂为空占位);固件 rootfs 由 44,343,296 B → **37,724,160 B(≈36.98MB)**。MeowOS 启动器在卡在时把 `MeowOS/opt/bin` 前插自身及子进程 PATH;SSH 登录会话由 `/etc/profile.d/meow_card_path.sh` 前插(卡不在跳过)。零卡依赖:launcher/bootmenu 运行时只读 `/data`(settings)+ rootfs,卡不在则字库回退点阵、应用示"未插卡"、日志回退内存环、opt 工具不可用——**均不崩不死**。
## 6. 后续
- 应用/工具管理:MeowAPPs 扫描(已有);`MeowOS/opt`/`lib` 由 MeowOS 或 web 面板管理(下载/更新)。
- 阶段 C MeowPanel 文件管理页直接映射这些目录。
Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

+187
View File
@@ -0,0 +1,187 @@
# MeowOS 核心规范速查(MEOWOS_SPEC_REF)
> 单一速查来源。常量/路径**照抄原文**;矛盾处单列「已知矛盾/待确认」。
> 各条末尾 `[源:XX.md]` 标注出处。
---
## 1. 文件系统路径
### 1.1 板载(NAND rootfs / /data)
- 设置持久化:**板载** `/usr/data/meow/settings.conf`(UBIFS,小文件 <10KB,跨换卡保留)`[SYSTEM_SETTINGS §2][MEOWOS_API §3]`
- `/etc/meowos_version`:版本唯一来源(launcher 运行时 `load_meowos_version` 读;`MEOWOS_VERSION` 宏仅作 dev 占位兜底)`[DEV_NOTES #4]`
- `/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]`
- seccomp/分区(勿写):mtd0=uboot mtd1=kernel mtd2=rootfs mtd3=kernel2 mtd4=rootfs2 mtd5=ota(8M 标记) mtd6=userdata `[WORK_STATE]`
### 1.2 TF 卡(挂载点 `/usr/data/mnt/sd_0`,exFAT)
```
/usr/data/mnt/sd_0/
├── Music/ # 播放器标准索引目录
├── MeowData/<pkg>/{data,cache} # 应用专属数据(exFAT 无软链,用 env 注入)
├── MeowAPPs/ # 应用(<app>/app.json+二进制+资源, fonts/, logs/, backgrounds/)
├── MeowOpt/ # 板载外部工具/二进制(如 mdnsd/,卡上加载,拔卡降级不崩)`[DEV_NOTES][meow_svcd.md]`
└── MeowOS/ # MeowOS 专属(一切非必要文件)
├── backups/settings/setting.bak # 设置导出默认路径
├── logs/ # 系统日志 meow_<bootts>.log + apps/...(见 §3 日志)
├── notifications/(转储区,保留最新 10MB,不被电源事件清)
├── opt/{bin,nyai,sources.list}, lib/, tools/
└── themes/charging/<id>/ # 充电屏主题(theme.json manifest)
```
`[TF_CARD_LAYOUT §1][APPS 附录][NOTIFICATION_SYSTEM §5]`
- 环境注入(launcher 每次启动 app 前):`MEOW_DATA_DIR`/`MEOW_CACHE_DIR`/`MEOW_LOG_DIR`/`MEOW_LOG_APP`/`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]`
### 1.3 板载 vs TF 划分原则(拔卡降级)
- 板载只放**稳定框架+KB 级设置**;日志/字库/媒体等大件一律 TF 卡 `[TF_CARD_LAYOUT §0][SYSTEM_SETTINGS §2]`
- 拔卡:launcher/bootmenu/SSH 不死;应用列表"未插卡"、日志回退内存环、字体回退点阵、opt 不可用——均优雅降级 `[TF_CARD_LAYOUT §3]`
- 关键配置优先板载(无卡系统能跑);转储/日志可降级 → 卡,无卡 /tmp 回退 `[DEV_NOTES #7]`
- rootfs 体积上限:**45,088,768 B(≈45MB)**,回验 0 diff `[BOOT_SELECTOR §1][APPS §6]`
---
## 2. IPC / 接口
### 2.1 launcher↔app env 注入
`MEOW_LOG_DIR`(卡日志目录,无卡不设)/`MEOW_LOG_APP`(app_id)/`MEOW_LOG_LEVEL`(本 app 级别,系统下发)/`MEOW_ACTIVITY`<target>/`MEOW_CHAIN`<id0>;<id1>;...;<top> `[APPS §3.5][APP_LOG_MANAGER §3][FRAMEWORK_DESIGN §3]`
### 2.2 meow_ipc 消息协议(AF_UNIX,全小端 LE)
- 帧头 16B:`magic=0x4D4F5701`('MEOW'|1)/`version=1`/`type`/`req_id`/`len`;`len` 上限 **MEOW_IPC_MAX=4096**;列表编码 `[u16 count]`+NUL 串 `[FRAMEWORK_DESIGN §1]`
- 消息类型:`MSG_HELLO 0x0001↑ / ACK 0x0002↓ / ERR 0x0003↓ / REGISTER 0x0010↑ / SERVICE_OK 0x0011↓ / SERVICE_STOP 0x0012↑ / MSG_NOTIFY 0x0020↑ / NOTIFY_ACK 0x0021↓ / NOTIFY_DISMISS 0x0022↑ / NOTIFY_LIST 0x0023↓ / ACT_DECLARE 0x0030↑ / ACT_QUERY 0x0031↑ / ACT_RESULT 0x0032↓ / NAV 0x0040↑ / SHOW_ACTIVITY 0x0041↓ / SYS 0x00F0↓` `[FRAMEWORK_DESIGN §1]`
- 媒体:`MSG_META`(元数据)、`MSG_PLAYCTL`(cmd 1=播放2=暂停3=上一首4=下一首5=seek)、`MSG_PROGRESS` `[附录 B]`
- IPC 线程**只读帧+解析+入队**;状态变更主循环单写者(eventfd 唤醒,10ms 热键轮询非阻塞排空)`[FRAMEWORK_DESIGN §0][§6]`
### 2.3 watchdog / supervisor 信号
- 应用返回键=**SIGTERM**(1s 后 SIGKILL 兜底);**SIGUSR1 仅**=沉浸左缘滑动手势,不是返回键 `[APPS §3.5]`
- 强杀兜底:应用卡死 = 音量减连按 3 次(间隔 <hotkey_seq_gap)→SIGTERM→0.5s→SIGKILL `[APPS §3.5]`
- 全程禁:`killall hiby_player`/SIGUSR1 发给播放器/走 hiby_player.sh(其 `sleep 1; reboot` 看门狗)/触碰 NAND/recovery_all `[APPS §1.3][WORK_STATE]`
- 播放器信号:SIGSTOP/CONT 安全;SIGTERM 仅运行态(~2s)有效,T 态需先 SIGCONT;SIGKILL 兜底 `[BOOT_SELECTOR §1]`
- supervisor(meow_bootmenu)接管关机/升级:播放器 `exit(0)`→真 poweroff;检测 mtd5=`ota:kernel2`→真 reboot `[WORK_STATE][MIGRATION_AC 无]`
### 2.4 按键信号全局接管(除电源菜单)`[APPS §3.4.1][KEY_CONTROL §1]`
- 红线:**除电源菜单(允许电源=确认、音量=导航)外,所有按键由 MeowOS 全局接管**——任何界面(含 MeowOS 设置页/应用列表/MeowAPP)不得私自定义按键映射;导航走触摸(返回钮/左缘左滑)`[APPS §3.4.1]`
- | 键 | code | 语义 | 阶段 |
|---|---|---|---|
| 电源单击 | 116 | 息屏/亮屏 | ✅ watchdog |
| 电源长按 3s | 116 | 电源菜单(关机/重启/回HibyOS/取消)| ✅ launcher 一处绘制 |
| 电源长按 8s | 116 | 强制关机 | ✅ |
| 音量+ | 115 | 调音量(CS43198)| ✅ |
| 音量− | 114 | 调音量 | ✅ |
| 播放/暂停 | 164 | 活跃媒体会话 | 🔵先抓住 |
| 上一曲 | 165 | 同上 | 🔵先抓住 |
| 下一曲 | 163 | 同上 | 🔵先抓住 |
### 2.5 热键 / 菜单 / 电源菜单 / 音量
- 热键(默认):**按住电源 + 300ms~2.5s 内单击音量减**(`meow.conf hotkey=power_voldown`;历史 `hotkey=seq` 已退役)`[APPS §4.2]`;v6.7 修正窗口下限 **60ms**(见矛盾 §A)
- 电源菜单 5 按钮:关机/重启/回 HibyOS / 取消 /(+「升级模式」按钮)`[WORK_STATE][BOOT_SELECTOR]`
- 音量跳步:`audio.volume_step` int 1-5 默认 **1**;watchdog 每按下边沿一步(无线性渐变)`[KEY_CONTROL §9 偏差2]`
---
## 3. SDK 公共 API(meow_ui.h)—— 以实现定案为准
> 权威签名 = `patch_v2/sdk/meow_ui.h`;MEOW_APP_SDK.md §3-§8 为设计蓝本。
### 3.1 关键常量
- 屏幕:**480×720**,24-bit RGB(`0xRRGGBB`),内部转 RGB565;文本字号 28(标题/状态栏)/20(正文/按钮) `[MEOW_APP_SDK §2]`
- keycode:POWER=116/VOLUMEDOWN=114/VOLUMEUP=115/PLAYPAUSE=164/PREVIOUS=19(注释标 KEY_PREVIOUSSONG,**有疑**)/NEXT=163 `[meow_ui.h][KEY_CONTROL 注:165 才是 prevsong]`
- 事件:`on_key(code,is_down)/on_tap(x,y)/on_swipe(dir,dx,dy)/on_tick/on_swipe_progress(x_start,dx)`;`MEOW_SWIPE_LEFT=0/RIGHT=1/UP=2/DOWN=3` `[meow_ui.h]`
- 主题:`MEOW_THEME_LIGHT/DARK/AUTO`;`meow_theme{bg,fg,accent,dim,font_title=28,font_body=20}` `[meow_ui.h]`
- 控件:`meow_list_source = int (*)(int idx,char *label,int len,void *ctx)`([D2] 定案)`[meow_ui.h]`
- 刷新率:`meow_ui_set_refresh_rate(60/30/15)`/`meow_ui_set_refresh_always_max(1)`/`meow_ui_effective_rate()`/`meow_ui_frames_presented()` `[meow_ui.h §7.1]`
- 状态栏:`meow_ui_set_statusbar(1/0)`/`meow_ui_statusbar_height()`(非全屏=40,全屏=0)/`meow_ui_set_fullscreen(on)`;状态栏高 **MUI_SBAR_H=40** `[MIGRATION_AC §11.1][meow_ui.h]`
- 日志:`MEOW_LOG_DEBUG=0/INFO/WARN/ERROR`;`meow_log(lvl,fmt,...)`/`meow_log_set_level`/`meow_log_get_level` `[meow_ui.h §9]`
- 通知:见 §4。
### 3.2 render/控件/图片/输入/IME 签名(关键)
- `meow_ui_init(const meow_ui_opts*)`(opts.width/height,0=自动读 fb)/`meow_ui_shutdown`/`meow_ui_width`(480)/`meow_ui_height`(720)
- 绘制:`meow_ui_clear/put_px/fill_rect/draw_rect/draw_rect_round/fill_rect_alpha(color,alpha 0-255)/flush/flush_region`
- 文本:`meow_ui_text/text_right/text_center(x,y,utf8,size,color)/text_width/text_height`
- 图片:`meow_img_load_file/mem/free/w/h`/`meow_ui_img/scaled/alpha/cover`(stb_image 解 PNG/JPEG/BMP,最近邻缩放,`cover`=居中裁方)
- 控件:`meow_btn_create/slider_create/switch_create/list_create(count,src,ctx)/widget_free/draw/hit`;`meow_slider_set/get`、`meow_switch_set/get`、`meow_btn_pressed`
- IME:`MEOW_IME_ASCII/NUM`;`meow_ime_ctx{mode,buf[256],maxlen,on_change,ctx}`;`meow_ui_text_input`(阻塞)/`_async`/`meow_ime_active`/`meow_ime_text`(仅英文/数字 ASCII;键盘占底 ~300px)
- 生命周期:`meow_ui_run(&ev)`(阻塞)/`meow_ui_request_exit()`;`meow_ui_time_ms()`(单调 ms)
### 3.3 实现定案(相对设计稿)`[MEOW_APP_SDK §12][meow_ui.h]`
- [D1] `meow_ui_opts` 补 width/height;[D2] `meow_list_source` 定案签名;[D3] 内部 `meow_gfx_fill_round/blit_rgba/mark_dirty`(非公共);[D4] 新增 `meow_theme_defaults(mode)`/`meow_theme_current()`;[D5] `on_swipe_progress`+`meow_ui_swipe_hint(x)`+内部 `MEOW_EV_SWIPE_PROGRESS`/`meow_input_inject`;[D6] 状态栏高=40;[D7] 音量源=settings.conf `audio.volume`;[D8] 蓝牙读 `hciconfig` 30s 缓存;[D9] 状态读取器跑在 `meow_ui_run` 1s 节流(无独立线程);[D10] 全局返回默认语义="APP 未注册 on_swipe 时 SDK 默认退出;已注册交 APP"
---
## 4. SDK 之外系统组件
- 通知(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`
- `meow_notify_register_type(cat)`/`meow_notify_send(const meow_notify*)`(返回 id)/`meow_notify_cancel(id)`/`meow_notify_active_count`/`count_in_type`/`has_active`/`urgency_str`/`category_str`
- **本阶段落点=SDK 内部活动表 + meow_log() 打日志**;真实投递通知中心 UI / D-Bus 是后续任务 `[meow_ui.h 注释]`
- `meow_config_*`(app 配置读写,走 meow_settings 后端)——**SDK 缺失清单第 3,未定** `[WORK_STATE]`
- `meow_media_*`(媒体会话)——**依赖阶段 D 落地,未定** `[WORK_STATE]`
- 崩溃捕获/看门狗——**规划中未定** `[WORK_STATE SDK 缺失清单 #5]`
- `meow_notify_dbus`(org.freedesktop.Notifications 桥)——**已定,待实现** `[FRAMEWORK_DESIGN 附录 C]`
- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`,mipsel 静态 ~99KB 进固件,不入卡)`[meow_svcd.md]`:
- 类 systemd/systemctl,由 supervisor(meow_launcherd/bootmenu)`fork/exec` 托管;**接入 supervisor 启动链为后续**,接入用 `-f` 前台便于 `waitpid`。
- 读 TF 卡 **`MeowOS/services.d/<name>.conf`**(目录式 `key=value`,未知键忽略/前向兼容):`exec`(必填)/`args`/`env`/`enabled`(0=永不自动启动,默认0)/`restart`(`always`/`on-fail`/`never`,默认on-fail)/`backoff_ms`(默认1000)/`max_restart`(默认5)/`cap`(本阶段仅解析不实装)/`trigger`(`boot`/`card-mount`/`wifi-up`,默认card-mount)。
- 崩溃隔离:服务=svcd **独立子进程**(`fork+execv`),svcd 只 `waitpid`/`SIGCHLD` 感知;服务死不影响 svcd/supervisor;重启受 `backoff_ms` 退避防热循环。
- 状态机:`DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;稳定运行 30s 清零 restart_count。CLI:`status/reload/start|stop|restart <name|all>/pid`(AF_UNIX 单行文本 `/tmp/meow_svcd.sock`)。
- **卡上外部工具 = `MeowOpt/`**(TF 根下平级目录,如 `MeowOpt/mdnsd/mdnsd`):MeowOS 启动时从卡加载,拔卡降级不崩 `[DEV_NOTES][meow_svcd.md]`。
- **mDNS 方案A(已拍板)**:用**完整 `mdnsd`**(994KB, mipsel 静态) 放 TF `MeowOpt/mdnsd/mdnsd`,由 meow_svcd 托管(需 root / setcap `cap_net_bind_service,cap_net_raw`);**SAResponder**(885KB) 因"单服务注册型"(只回单服务查询,无法按名访问设备)被排除。⚠️ 手机热点组播常在客户端间丢失 → mDNS 有风险,主机 DNS 兜底 `[DEV_NOTES 检查点]`。
- `meow_settings_*`(设置后端,已定)/`meowcli`/`meow_sys_devattr`/`meow_devattr_set` `[MEOWOS_API]`
- 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`;`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]`
---
## 5. 关键约定 & 约束
### 5.1 按键/手势/状态栏/全屏
- 按键全局接管(除电源菜单),见 §2.4 `[APPS §3.4.1]`
- **左缘返回手势**:手指从**屏幕左缘 ≤24px** 向内(右)划 **≥90px**,**抬手(release 边沿)才触发** `MEOW_SWIPE_LEFT`("左滑=返回"契约);过程以 `on_swipe_progress` 实时上报 + SDK 自动画顶部进度条 `meow_ui_swipe_hint(x)`(5px 高,top,theme accent 色);x<=0 清条。**中部左/右滑不是返回**(中部右滑→SWIPE_RIGHT,中部左滑→落 TAP)`[MEOW_APP_SDK §6][GESTURE_BACK §2.2/§3]`
- 偏差:原「左缘+向左 mdx<-90」物理不可达 → 取「左缘向内(右)划 ≥90px」,事件仍报 `MEOW_SWIPE_LEFT` `[GESTURE_BACK §3]`
- **全局状态栏**:SDK 内置,默认显示(时钟/电量/WiFi/蓝牙/音量),1s 轻刷,z 最顶;仅 APP 声明全屏/`fullscreen:1` 才隐藏;非全屏 APP 须用 `meow_ui_statusbar_height()` 做 inset `[meow_ui.h §3.1][MIGRATION_AC §3]`
- 沉浸:`app.json immersive:1` → launcher 写 `/tmp/meowapp_state=1` → 隐藏虚拟返回键,手势归 SDK `[APPS §3.5][GESTURE_BACK §2.4]`
### 5.2 主题
- **日/夜/自动**:SDK `meow_theme` 内置 `meow_theme_light/dark`;模式键 `display.theme_mode`(SYSTEM_SETTINGS 记为 `manual`/`auto`——**与 SDK 枚举 light/dark/auto 冲突**,见 §A);AUTO 判时段 `[SYSTEM_SETTINGS §1][MEOW_APP_SDK §8]`
- 背景图系统:`display.bg_mode`(color/image)/`display.bg_dark_color`/`display.bg_light_color`/`display.bg_image`/`display.bg_image_dark`/`bg_image_light`/`display.bg_overlay_alpha`(0-255 默认 ~120);图片目录 **`MeowData/backgrounds/`**(480×720 约束);叠加层颜色随主题(深=黑叠/白=白叠)`[MAIN_MD3_PLAN §6][DEV_NOTES #12/#13]`
- 主界面背景:深色用 background(可选色)、浅色用 surface(白) `[DEV_NOTES #13]`
### 5.3 通知配额/转储
- **每 App 最多 4 类型;每类型活动 ≤4;媒体单例(每 App 仅 1 条,可多 App)**;系统事件抽象为 App(`app_id=system`) 走同一配额 `[NOTIFICATION_SYSTEM §7][meow_ui.h][DEV_NOTES #11]`
- 通知创建 **10s 后自动转储到 TF `MeowOS/notifications/`**(保留最新 10MB ring,不被电源事件清);正常关机先转储再清板载;无卡可运行 `[NOTIFICATION_SYSTEM §8]`
### 5.4 配置版本单点 / 刷新率 / 息屏
- /etc/meowos_version 版本单点(见 §1.1)`[DEV_NOTES #4]`
- 刷新率:`display.refresh_rate` enum 60/30/15(默认 60);`display.refresh_always_max` bool false(关=允许 60→30→15 降档)。governor:连续 12 帧超预算降一档,连续 4 批≈48 帧远低预算升一档 `[SYSTEM_SETTINGS §1][PERF_SCHEDULER][MIGRATION_AC §12.1]`
- 息屏:`display.screen_off_timeout` int 秒 默认 **30**(0=常亮);`display.screen_off_mode` enum `panel`(面板下电+背光0+FBIOPBLANK)/`black`(背光0全黑+双击亮屏) `[SYSTEM_SETTINGS §1]`
- 亮度 clamp:`display.brightness` 0-255 默认 101(clamp ≥ `display.brightness_min`);`display.brightness_min` 默认 **1**(M2.39 由 25 改 1)——**与旧文档 25 冲突见 §A** `[SYSTEM_SETTINGS §1]`
### 5.5 MeowAPPs 包结构 / .mpk / 数据
- `<pkg>/` = 程序主体+app.json+资源;`MeowData/<pkg>/{data,cache}` = 数据;首次启动自动建 data/cache 并经 env 注入,exFAT 无软链 `[APPS 附录 §1/§2]`
- `.mpk` = tar.gz(app.json + meow 二进制 + 资源),**不含 data/cache**;导入校验 app.json/哈希→解压→数据 init→登入列表 `[APPS 附录 §3]`
- 包管理 **CLI `nyai`**(静态 MIPS,部署 `MeowOS/opt/bin/nyai`):install/remove/list/info/search/index/export/upgrade/scan;源 `MeowOS/opt/nyai/sources.list`;索引 `Packages`(Package/Version/Filename/Size/SHA256/Depends/Description)`[APPS 附录 §5]`
- 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + 扩展 `framework`/`immersive`/`icon`/`route.<id>`/`service.<svc>`/`fullscreen` `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2]`
### 5.6 冷启动充电屏
- 触发(**三者同时**):冷启动(`poweroff` 写 `/data/meow_cold_boot=1`;`reboot` 写 0)+ 在充电(AC online=1 或 battery status=Charging)+(开机瞬间一次性快照,判定后不回头)`[CHARGING_SCREEN §0]`
- 热启动(reboot、含充电态 reboot)一律**不进**充电屏,走引导选单 `[CHARGING_SCREEN §0]`
- 主题:`theme.charging=<id>`(默认 default);`MeowOS/themes/charging/<id>/theme.json`;加载失败回退内置纯代码默认主题 `[CHARGING_SCREEN §1/§2]`
---
## 6. 标志性 TODO / 未决(文档未定,勿凭猜实现)
- OPEN_QUESTIONS.md 待定 ❓:#1 背景图加载时机/缓存、#2 非 480×720 图如何处(拉伸/裁剪/回退)、#3 叠加层颜色联动、#4 背景色板具体枚举、#5 浅色主题文字对比度、#7 首期是否做应用图标、#8 通知来源(**最大空白**,M2 已定为 系统+IPC+D-Bus 三路)、#9 下拉手势 vs 左滑返回区分、#10 通知存储上限/FIFO、#11 媒体活跃会话判定、#12 多 app 音频混音/焦点、#13 web 实时画面 JPEG/WS、#14 远程输入注入仲裁、#16 主页左滑=侧边栏 vs 返回 `[OPEN_QUESTIONS]`
- DEVELOPMENT_PLAN:阶段 0(bootmenu 双系统)**待实现**(M0-M4);阶段 B/E/F;M2-M6 阶段 D 里程碑未做 `[DEVELOPMENT_PLAN]`
- 未定/待拍板:`meow_config_*`/`meow_media_*`/崩溃捕获(见 §4);MeowOS 音频输出设置(只读还是可配);OTA 依赖 MeowPanel `[WORK_STATE][OPEN_QUESTIONS #17/#18]`
- **meow_svcd 待办**:接入 supervisor 启动链(`-f` 前台 + waitpid,改固件有风险需另确认);`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS。**services.d/svcd 尚未接入 supervisor,勿当作已跑起来。** `[WORK_STATE][meow_svcd.md]`
---
## 7. 已知矛盾 / 待确认(⚠️ 别凭猜)
- **A-1 热键窗口下限**:APPS.md §3.1/§4.2 写「电源按下后 **300ms**~2.5s」;DEVELOPMENT_PLAN §0/阶段A 写「窗口下限 **60ms**(v6.7 修)。→ 以 **60ms** 为准,APPS.md 已过时。
- **A-2 主题模式枚举**:SYSTEM_SETTINGS §1 `display.theme_mode`=`manual`/`auto`(另设 `display.theme`=dark/light);MEOW_APP_SDK §8 / meow_ui.h `meow_theme_mode`=`LIGHT/DARK/AUTO` 且直接跟随 `display.theme_mode`。→ 两套枚举语义不一致;AUTO 时段:SYSTEM_SETTINGS 用可配 `theme_dark_start/end`(分钟),SDK §12 用固定「6:00-18:00 日间」。WORK_STATE/DEV_NOTES 明言「时段判定 launcher/SDK 两份逻辑需抽共享纯函数」。→ **DOM 键值 enum 与 SDK enum 需统一,时段判定需抽共享函数后再实现。**
- **A-3 亮度下限**:`display.brightness_min` 默认 **1**(SYSTEM_SETTINGS §1,M2.39 由 25 改 1);但 MEOWOS_API §2.2「clamp ≥ brightness_min 默认 **25**」、MD3_UI_SPEC §3.2「亮度 clamp ≥25」仍写 25。→ 以 **SYSTEM_SETTINGS(1)** 为准,另两份文档陈旧。
- **A-4 左缘返回阈值**:MEOW_APP_SDK §6 / GESTURE_BACK 取「左缘 ≤24px + 向内划 **≥90px**,抬手触发」;MIGRATION_AC §7 写「左缘 24px + 右滑 **≥65px** 触发」。→ **90 vs 65 不一致**(meow_ui.h 未给阈值,GESTURE_BACK + MEOW_APP_SDK 较权威=90,但确认真机前留疑)。
- **A-5 通知 app_quota**:NOTIFICATION_SYSTEM §7/meow_ui.h = 4 类型×每类型 4=16 条/App;SYSTEM_SETTINGS §1 `notify.app_quota`=16。FRAMEWORK_DESIGN §4「配额 ≤16/应用」。→ **口径可归并(4×4=16)**,但 SYSTEM_SETTINGS 未列 4/4 细分,建议实现以 meow_ui.h 的 `MEOW_NOTIFY_MAX_TYPES/MAX_PER_TYPE` 为准。
- **A-6 KEY_PREVIOUS(code)**:KEY_CONTROL §3 明注「KEY_PREVIOUSSONG 是 **165**;19 是另一码」;meow_ui.h `#define MEOW_KEY_PREVIOUS 19 /* KEY_PREVIOUSSONG */` 注释标错。→ **19 的标签应改**(值 19 非 prevsong;165 才是),属 meow_ui.h 注释误导。
- **A-7 app 日志路径**:APP_LOG_MANAGER §4 写 `apps/<app_id>.log`(单文件);meow_ui.h §9 / DEV_NOTES #9 为 **`apps/<包名>/logs/<启动时间>_<pid>.log`**(每次启动新文件)——用户已纠正,勿用单文件。→ 以 meow_ui.h/DEV_NOTES(M2.51)为准,APP_LOG_MANAGER 目录小节过时。
- **A-8 状态栏高**:MEOW_APP_SDK §3.1「slim ~20-40px」;实际定案 **40px**([D6] MIGRATION_AC §11.1)。→ 取 40。
- **A-9 SDK 通知转储**:NOTIFICATION_SYSTEM §8「创建 10s 后自动转储到 TF」;meow_ui.h 现仅「活动表+meow_log 打日志,真实投递 UI/D-Bus 为后续」。→ 10s 转储/TF 尚未实现,属"已定未做"。
- **A-10 版本来源细解**:DEV_NOTES #4「/etc/meowos_version 唯一来源;MEOWOS_VERSION 宏仅 dev 占位永不动」;APPS.md §4.6「MEOWOS_VERSION 宏=单一来源,build 时 sed 写入 etc/meowos_version」。→ 两者都强调"写入同源",但"谁是源头"措辞矛盾;实践上 build 脚本 echo 生成 /etc/meowos_version,宏作屏显 dev 占位。
- **A-11 `MeowOS/opt/` vs `MeowOpt/`**:§1.2 树中 `MeowOS/opt/{bin,nyai,sources.list},lib/,tools/`(nyai 包管 + 工具)与新增顶层 `MeowOpt/`(板载外部工具/二进制,如 mdnsd)并存,命名仅大小写之差、语义可能重叠。→ **两者是否为同一用途/是否需归并待确认**;当前按各自文档记录,勿混用。
`[各源标注见上]`
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+123
View File
@@ -0,0 +1,123 @@
/*
* meow_font_demo.c — MeowOS 字体子系统主机/真机验证 demo。
*
* 用法:
* host(主机侧验证):meow_font_demo --sim demo.raw
* 渲染中文串到内存 480x720 RGB565 缓冲并写入 demo.raw,主机再用 raw2png.py 转 PNG。
* 真机:meow_font_demo --to-fb (写 /dev/fb0,默认走非显示后备缓冲并暂存,谨慎用 --sim)
*
* 通过 meow_font.h 的公开 API 绘制,验证字形/字号/advance/描边;把 width_text 打印到 stdout。
*/
#include "meow_font.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FB_W 480
#define FB_H 720
static uint16_t *g_fb;
static inline uint16_t rgb565(int r, int g, int b) {
return (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
}
static void fill_bg(uint16_t c) {
for (size_t i = 0; i < (size_t)FB_W * FB_H; i++) g_fb[i] = c;
}
static void fill_rect(int x, int y, int w, int h, uint16_t c) {
for (int yy = y; yy < y + h; yy++) {
for (int xx = x; xx < x + w; xx++) {
if (xx >= 0 && xx < FB_W && yy >= 0 && yy < FB_H) g_fb[(size_t)yy * FB_W + xx] = c;
}
}
}
/* 设备侧写文件 */
static int write_raw(const char *path) {
FILE *f = fopen(path, "wb");
if (!f) { fprintf(stderr, "cannot open %s\n", path); return -1; }
size_t n = fwrite(g_fb, 2, (size_t)FB_W * FB_H, f);
fclose(f);
return (n == (size_t)FB_W * FB_H) ? 0 : -1;
}
int main(int argc, char **argv) {
int to_fb = 0;
const char *rawpath = "font/demo/meow_demo.raw";
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--to-fb")) to_fb = 1;
else if (!strcmp(argv[i], "--sim")) rawpath = (i + 1 < argc) ? argv[++i] : rawpath;
else { fprintf(stderr, "usage: %s [--sim out.raw | --to-fb]\n", argv[0]); return 2; }
}
g_fb = (uint16_t *)calloc((size_t)FB_W * FB_H, 2);
if (!g_fb) return 3;
meow_font_set_fb(FB_W, FB_H, FB_W * 2);
const char *png = "font/atlas/font.png";
const char *idx = "font/atlas/font.idx";
int rc = meow_font_load(png, idx);
if (rc != 0) {
fprintf(stderr, "meow_font_load failed rc=%d\n", rc);
return 4;
}
printf("loaded %s + %s : has_size(28)=%d has_size(20)=%d\n",
png, idx, meow_font_has_size(28), meow_font_has_size(20));
/* ---- 度量核对 ---- */
printf("width_text('MeowOS 主屏',28)=%d line_h28=%d asc28=%d\n",
meow_font_width_text("MeowOS 主屏", 28), meow_font_line_height(28), meow_font_ascent(28));
printf("width_text('返回播放器需重启',20)=%d line_h20=%d\n",
meow_font_width_text("返回播放器需重启", 20), meow_font_line_height(20));
/* ---- 布局 --- */
uint16_t cbg = rgb565(0x0c, 0x0e, 0x12);
uint16_t ctit = rgb565(0xff, 0xb0, 0x30); /* 主题橙 */
uint16_t ctext = rgb565(0xf2, 0xf2, 0xf2); /* 近白 */
uint16_t c_dim = rgb565(0x7a, 0x82, 0x8c); /* 灰 */
uint16_t cselbg = rgb565(0x1e, 0x2a, 0x3a); /* 选中背景 */
fill_bg(cbg);
/* 顶栏:标题 + 状态(版本) */
meow_font_drawtext(g_fb, 16, 10, "MeowOS 主屏", ctit, 28);
meow_font_drawtext(g_fb, 400, 16, "0.6.9", c_dim, 20);
/* 分隔线 */
fill_rect(16, 56, FB_W - 32, 1, rgb565(0x2a, 0x30, 0x38));
/* 应用/功能列表(正文 20px) */
struct item { const char *s; int sel; } items[] = {
{"本地音乐", 0}, {"在线电台", 0}, {"返回播放器需重启", 1},
{"设置", 0}, {"关闭背光", 0}, {"关机", 0},
};
int y = 78;
for (unsigned i = 0; i < sizeof(items) / sizeof(items[0]); i++) {
int lh = meow_font_line_height(20);
if (items[i].sel) {
fill_rect(12, y - 4, FB_W - 24, lh + 6, cselbg);
meow_font_drawtext_outline(g_fb, 24, y, items[i].s, ctext, ctit, 1, 20);
} else {
meow_font_drawtext(g_fb, 24, y, items[i].s, items[i].sel ? ctit : ctext, 20);
}
y += lh + 16;
}
/* 底栏提示 */
meow_font_drawtext(g_fb, 16, FB_H - 46, "按住电源键 3 秒返回", c_dim, 20);
if (to_fb) {
fprintf(stderr, "--to-fb 暂未在本机关联(真机用);先用 --sim 验证\n");
}
if (write_raw(rawpath) != 0) {
fprintf(stderr, "write_raw failed\n");
return 5;
}
printf("wrote %s\n", rawpath);
meow_font_unload();
free(g_fb);
return 0;
}
BIN
View File
Binary file not shown.
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""raw2png.py — 将 meow_font_demo 产出的 480x720 RGB565(raw, little-endian uint16) 转成 PNG。
用法: raw2png.py <in.raw> <out.png>
"""
import sys
from PIL import Image
W, H = 480, 720
def main():
raw = open(sys.argv[1], "rb").read()
assert len(raw) == W * H * 2, f"expect {W * H * 2}, got {len(raw)}"
out = sys.argv[2]
img = Image.new("RGBA", (W, H), (0, 0, 0, 255))
px = img.load()
for y in range(H):
base = y * W * 2
for x in range(W):
o = base + x * 2
v = raw[o] | (raw[o + 1] << 8) # LE uint16
r = ((v >> 11) & 31) * 255 // 31
g = ((v >> 5) & 63) * 255 // 63
b = (v & 31) * 255 // 31
px[x, y] = (r, g, b, 255)
img.save(out)
print(f"saved {out} ({W}x{H})")
if __name__ == "__main__":
main()
+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gen_font.py — MeowOS 预渲染字库生成器(主机侧)· v2:像素进 PNG、索引进 idx。
产出(单份存储,设备与主机共用同一套字节):
<out>/font.png 灰度 PNG(bit depth 4|8),含全部字号的面,每面一个子矩形
<out>/font.idx 二进制索引:头部 + 每面元数据 + 每字符 16B 记录
设计要点:
* 像素只存一次(font.png);idx 只做索引。设备端用 zlib inflate + 自写 PNG 反滤波解码,
无需 libpng(目标根文件系统无 libpng,仅有 zlib)。
* 位深默认 4(16 级 alpha,仍抗锯齿)以把多张高密度汉字面压到 ≤2MB 预算内;8-bit 会超。
--bitdepth 8 可出 8-bit(建议单字号)。
* 绘制模型:drawtext(fb,x,y,str,color,size),(x,y)=行首「升线顶」锚点;baseline=y+ascent;
每字形 blit 于 (x+pen+dx, y+dy),pen 累加 advance。
idx 二进制布局(little-endian, version=2):
Header(16B):
+0 u8[4] magic='MFNI'
+4 u16 version=2
+6 u8 bit_depth (4|8)
+7 u8 face_count
+8 u32 png_w
+12 u32 png_h
FaceHeader ×face_count (20B each):
+0 u16 px_size
+2 u16 plane_x
+4 u16 plane_y (PNG 像素内面左上角)
+6 u16 plane_w
+8 u16 plane_h
+10 i16 ascent
+12 i16 descent (负值)
+14 u16 glyph_count
+16 u32 glyph_offset (文件内本面 glyph 表偏移)
Glyph 表(每面按 codepoint 升序,16B each):
+0 u32 codepoint
+4 u16 x (PNG 像素左上角,已含 offset)
+6 u16 y
+8 u16 w
+10 u16 h
+12 u16 advance
+14 i8 dx
+15 u8 dy
"""
import argparse
import os
import struct
import zlib
from PIL import Image, ImageDraw, ImageFont
MAGIC = b"MFNI"
VER = 2
# --------------------------------------------------------------------------
# PNG 编码(灰度, bit depth 4|8, 全部扫描行 filter=0;规范解码器可读)
# --------------------------------------------------------------------------
def _png_chunk(ctype, data):
c = ctype + data
return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
def gray_png_encode(width, height, depth, packed):
rowbytes = (width * depth + 7) // 8
ihdr = struct.pack(">IIBBBBB", width, height, depth, 0, 0, 0, 0) # gray, compression0 filter0 interlace0
raw = bytearray()
for y in range(height):
raw.append(0) # filter type 0 (None)
raw += packed[y * rowbytes:(y + 1) * rowbytes]
idat = zlib.compress(bytes(raw), 9)
out = b"\x89PNG\r\n\x1a\n"
out += _png_chunk(b"IHDR", ihdr)
out += _png_chunk(b"IDAT", idat)
out += _png_chunk(b"IEND", b"")
return out
def pack_plane_depth(plane_l8, width, height, depth):
"""把 L8 行主序 plane 转为指定 bit depth 的打包字节。"""
if depth == 8:
return bytes(plane_l8)
assert depth == 4
rowbytes = (width + 1) // 2
out = bytearray()
for y in range(height):
base = y * width
row = bytearray(rowbytes)
for x in range(0, width, 2):
a = plane_l8[base + x]
b = plane_l8[base + x + 1] if x + 1 < width else 0
row[x // 2] = ((a >> 4) << 4) | (b >> 4)
out += row
return bytes(out)
# --------------------------------------------------------------------------
# 字符集
# --------------------------------------------------------------------------
def build_charset(extra=""):
cps = []
def add(cp):
if cp not in cps:
cps.append(cp)
for c in range(0x20, 0x7F):
add(c)
for hi in range(0xB0, 0xD8):
for lo in range(0xA1, 0xFF):
try:
add(ord(bytes([hi, lo]).decode("gb2312")))
except UnicodeDecodeError:
pass
for hi in range(0xA1, 0xA4):
for lo in range(0xA1, 0xFF):
try:
add(ord(bytes([hi, lo]).decode("gb2312")))
except UnicodeDecodeError:
pass
for ch in extra:
add(ord(ch))
return sorted(cps)
# --------------------------------------------------------------------------
# 逐字形渲染
# --------------------------------------------------------------------------
class Glyph:
__slots__ = ("cp", "w", "h", "dx", "dy", "adv", "pixels")
def __init__(self, cp, w, h, dx, dy, adv, pixels):
self.cp, self.w, self.h, self.dx = cp, w, h, dx
self.dy, self.adv, self.pixels = dy, adv, pixels
def render_one(font, ch, pad):
asc, desc = font.getmetrics()
# Pillow: ascent & descent 均为正; 字体行高 = asc + |desc|(desc 或为负,用 abs 归一)
line_h = asc + abs(desc)
Wc = int(font.getlength(ch)) + 2 * pad + 16
Hc = line_h + 2 * pad
img = Image.new("L", (Wc, Hc), 0)
ImageDraw.Draw(img).text((pad, pad), ch, font=font, fill=255)
bb = img.getbbox()
if bb is None:
return Glyph(ord(ch), 0, 0, 0, 0, int(round(font.getlength(ch))), None)
l, t, r, b = bb
return Glyph(ord(ch), r - l, b - t, l - pad, t - pad,
int(round(font.getlength(ch))), img.crop((l, t, r, b)).tobytes())
# --------------------------------------------------------------------------
# 打包(decreasing-height 书架式;枚举多个最大宽求最小面积)
# --------------------------------------------------------------------------
def pack_best(glyphs, gap=1):
best = None
for max_w in (256, 384, 512, 640, 768, 1024, 1280, 1536, 2048, 3072):
items = sorted(enumerate(glyphs), key=lambda t: -t[1].h)
placements = {}
x = y = 0
shelf_h = 0
cur_w = 0
for idx, g in items:
if g.w == 0 or g.h == 0:
continue
if cur_w + g.w > max_w:
y += shelf_h + gap
x = 0
cur_w = 0
shelf_h = 0
placements[idx] = (x, y)
x += g.w + gap
cur_w += g.w + gap
if g.h > shelf_h:
shelf_h = g.h
if not placements:
continue
aw = max(placements[i][0] + g.w for i, g in enumerate(glyphs) if i in placements)
ah = y + shelf_h
area = aw * ah
if best is None or area < best[0]:
best = (area, placements, aw, ah, max_w)
if best is None:
return {}, 0, 0, 0
return best[1], best[2], best[3], best[4]
# --------------------------------------------------------------------------
# idx 写入
# --------------------------------------------------------------------------
def write_idx(path, bit_depth, faces, glyph_tabs):
n = len(faces)
png_w = max(f["plane_x"] + f["plane_w"] for f in faces)
png_h = max(f["plane_y"] + f["plane_h"] for f in faces)
header = bytearray(struct.pack("<4sHBBII", MAGIC, VER, bit_depth, n, png_w, png_h))
assert len(header) == 16
face_off = 16
glyph_off = face_off + 20 * n
body = bytearray()
for i, f in enumerate(faces):
body += struct.pack("<HHHHHhhHI", f["px"], f["plane_x"], f["plane_y"],
f["plane_w"], f["plane_h"], f["ascent"], f["descent"],
len(glyph_tabs[i]), 0)
cur = glyph_off
tables = []
offs = []
for tab in glyph_tabs:
offs.append(cur)
tab_sorted = sorted(tab, key=lambda g: g[0])
buf = bytearray()
for cp, x, y, w, h, adv, dx, dy in tab_sorted:
buf += struct.pack("<IHHHHHbB", cp, x, y, w, h, adv, dx, dy)
tables.append(buf)
cur += len(buf)
for i, off in enumerate(offs):
struct.pack_into("<I", body, i * 20 + 16, off)
for buf in tables:
body += buf
with open(path, "wb") as fp:
fp.write(bytes(header))
fp.write(bytes(body))
return len(header) + len(body)
# --------------------------------------------------------------------------
# 主流程
# --------------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(description="MeowOS 预渲染字库生成器 v2")
ap.add_argument("--font", required=True)
ap.add_argument("--sizes", default="28,20")
ap.add_argument("--out", required=True)
ap.add_argument("--name", default="font")
ap.add_argument("--bitdepth", type=int, default=4, choices=(4, 8))
ap.add_argument("--extra", default="")
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
sizes = [int(s) for s in args.sizes.split(",") if s.strip()]
cps = build_charset(args.extra)
print(f"字符集: {len(cps)} 码位")
all_planes = [] # (aw, ah, plane_l8 bytes, tab)
max_w = 0
for size in sizes:
print(f"\n=== 字号 {size}px ===")
font = ImageFont.truetype(args.font, size)
asc, desc = font.getmetrics()
print(f" metrics ascent={asc} descent={desc}")
glyphs = []
miss = []
for cp in cps:
ch = chr(cp)
g = render_one(font, ch, pad=8)
if g.w == 0 and g.h == 0 and cp != 0x20:
miss.append(ch)
glyphs.append(g)
if miss:
print(f" [warn] {len(miss)} 码位无字形(保留advance): {''.join(miss[:20])}")
placements, aw, ah, chosen = pack_best(glyphs)
print(f" packing atlas {aw}x{ah} (max_w={chosen}) glyphs={len(glyphs)}")
plane_l8 = bytearray(aw * ah)
tab = []
for i, g in enumerate(glyphs):
if i in placements:
px, py = placements[i]
for row in range(g.h):
s = row * g.w
d = (py + row) * aw + px
plane_l8[d:d + g.w] = g.pixels[s:s + g.w]
tab.append((g.cp, px, py, g.w, g.h, g.adv, g.dx, g.dy))
else:
tab.append((g.cp, 0, 0, 0, 0, g.adv, 0, 0))
all_planes.append((aw, ah, bytes(plane_l8), tab))
max_w = max(max_w, aw)
# ---- 编排到公共画布(纵向堆叠,公共宽 max_w 偶数化) ----
if max_w % 2:
max_w += 1
total_h = sum(p[1] for p in all_planes)
canvas_l8 = bytearray(max_w * total_h)
face_meta = []
fy = 0
for i, (aw, ah, plane, tab) in enumerate(all_planes):
for y in range(ah):
src = y * aw
dst = (fy + y) * max_w
canvas_l8[dst:dst + aw] = plane[src:src + aw]
face_meta.append({"px": sizes[i], "plane_x": 0, "plane_y": fy,
"plane_w": aw, "plane_h": ah,
"ascent": int(ImageFont.truetype(args.font, sizes[i]).getmetrics()[0]),
"descent": int(abs(ImageFont.truetype(args.font, sizes[i]).getmetrics()[1]))})
fy += ah
# ---- 打包 + PNG ----
packed = pack_plane_depth(canvas_l8, max_w, total_h, args.bitdepth)
png_bytes = gray_png_encode(max_w, total_h, args.bitdepth, packed)
png_path = os.path.join(args.out, f"{args.name}.png")
with open(png_path, "wb") as fp:
fp.write(png_bytes)
# ---- idx ----
idx_path = os.path.join(args.out, f"{args.name}.idx")
idx_size = write_idx(idx_path, args.bitdepth, face_meta, [p[3] for p in all_planes])
png_size = os.path.getsize(png_path)
total = png_size + idx_size
print(f"\nfont.png {png_size} B")
print(f"font.idx {idx_size} B")
print(f"合计 {total} B = {total/1024:.1f} KiB = {total/1048576:.2f} MiB (预算 2MiB)")
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""preview_glyphs.py — 从 atlas/font.png + font.idx 抽取若干字符,拼成带标注的接触表 PNG,
用于人工核对图集中字形正确、无漏字。用法: preview_glyphs.py <atlas_dir> <out.png>"""
import struct
import sys
from PIL import Image, ImageDraw
def load_idx(path):
d = open(path, "rb").read()
assert d[:4] == b"MFNI", d[:4]
depth = d[6]
nf = d[7]
faces = []
for i in range(nf):
fh = d[16 + i * 20:16 + i * 20 + 20]
px, px_, py, pw, ph, asc, dsc, gc = struct.unpack("<HHHHHhhH", fh[:16])
goff = struct.unpack("<I", fh[16:20])[0]
tabs = []
for k in range(gc):
g = d[goff + k * 16:goff + k * 16 + 16]
cp, x, y, w, h, adv, dx, dy = struct.unpack("<IHHHHHbB", g)
tabs.append((cp, x, y, w, h, adv, dx, dy))
faces.append({"px": px, "px_": px_, "py_": py, "pw": pw, "ph": ph, "asc": asc, "dsc": dsc, "tabs": tabs})
return depth, faces
def main():
atlas = sys.argv[1]
out = sys.argv[2]
depth, faces = load_idx(f"{atlas}/font.idx")
im = Image.open(f"{atlas}/font.png").convert("L")
pix = im.load()
# 取样字符
sample = "MeowOS 主屏关机设置播放器音乐电台返回背光时间应用关闭确认取消01472589 ABC.,。!?:;「」¥·—…%"
cell = 44
cols = 12
rows = (len(sample) + cols - 1) // cols
sheet = Image.new("RGB", (cols * cell, rows * cell), (28, 30, 34))
dr = ImageDraw.Draw(sheet)
for idx, ch in enumerate(sample):
cp = ord(ch)
r, c = divmod(idx, cols)
# 找 28 面,找不到用 20 面
box = None
for f in faces:
if f["px"] == 28:
for t in f["tabs"]:
if t[0] == cp and t[3] and t[4]:
box = (f, t)
break
if box is None:
for f in faces:
if f["px"] == 20:
for t in f["tabs"]:
if t[0] == cp and t[3] and t[4]:
box = (f, t)
break
oy, ox = r * cell + 2, c * cell + 4
if box:
f, t = box
_, x, y, w, h, adv, dx, dy = t
for yy in range(h):
for xx in range(w):
a = pix[f["px_"] + x + xx, f["py_"] + y + yy] # PIL: [x, y]
if a:
sheet.putpixel((ox + xx, oy + yy), (255, 255, 255))
dr.text((ox, oy + 26), ch, fill=(255, 180, 60))
sheet.save(out)
print(f"saved {out}")
if __name__ == "__main__":
main()
+532
View File
@@ -0,0 +1,532 @@
/*
* meow_font.c — MeowOS 预渲染位图字体运行时(独立库,不绑定启动器)。
*
* 依赖:
* - libz(musl 静态)仅用于 inflate PNG 像素面。目标系统无 libpng,故模块内自带
* 最小 PNG 解码(灰度 non-interlaced,bit depth 4|8,filter 0-4)。
* - zlib 头文件不在 musl sysroot,故在文件底部声明 ABI 兼容的最小 API(z_stream 布局
* 与 libz.a 一致,已用 qemu-mipsel 验证)。
*
* 文件格式参见 gen/gen_font.py 注释(version=2,magic 'MFNI')。
*
* 绘制模型:drawtext(fb,x,y,utf8,color,size),(x,y)=行首升线顶部;baseline=y+ascent;
* 每字形 blit 于 (x+pen+dx, y+dy);pen 累加 advance。color 为 RGB565(R=bit15..11)。
*/
#include "meow_font.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------------- 最小 zlib ABI 声明(与 libz.a 匹配,mipsel 32-bit unsigned long=4B) ------- */
typedef unsigned char mzf_Bytef;
typedef unsigned int mzf_uInt;
typedef unsigned long mzf_uLong;
typedef void *mzf_voidpf;
typedef unsigned int (*mzf_alloc_func)(unsigned int, unsigned int);
typedef void (*mzf_free_func)(unsigned int, unsigned int);
typedef struct mzf_stream_s {
mzf_Bytef *next_in;
mzf_uInt avail_in;
mzf_uLong total_in;
mzf_Bytef *next_out;
mzf_uInt avail_out;
mzf_uLong total_out;
char *msg;
void *state;
mzf_alloc_func zalloc;
mzf_free_func zfree;
mzf_voidpf opaque;
int data_type;
mzf_uLong adler;
mzf_uLong reserved;
} mzf_stream;
#define MZF_OK 0
#define MZF_STREAM_END 1
#define MZF_NO_FLUSH 0
#define MZF_FINISH 4
/* 函数符号名必须与 libz.a 一致(inflate 等);结构名仅 C 侧,ABI 由布局决定。 */
extern int inflateInit_(mzf_stream *, const char *, int);
extern int inflate(mzf_stream *, int);
extern int inflateEnd(mzf_stream *);
extern const char *zlibVersion(void);
/* ---------------- 内部数据结构 ---------------- */
typedef struct {
uint32_t cp;
uint16_t x, y, w, h, advance;
int8_t dx;
uint8_t dy;
} meow_glyph;
typedef struct {
int px; /* 原生字号 */
unsigned plane_x, plane_y; /* 在大画布(plane_w x plane_h)内的面左上角 */
unsigned plane_w, plane_h;
int ascent, descent; /* descent>=0 */
unsigned glyph_count;
meow_glyph *glyphs; /* 按 cp 升序 */
} meow_face;
typedef struct {
unsigned fb_w, fb_h, fb_pitch;
size_t plane_rowbytes;
unsigned depth; /* 4 或 8 */
unsigned plane_w, plane_h; /* 画布尺寸 */
unsigned char *plane; /* 打包像素面 */
int face_count;
meow_face *faces;
} meow_state;
static meow_state g;
/* PNG 内部为大端;idx(自定格式)为小端。 */
static uint32_t be32(const unsigned char *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
static uint32_t le32(const unsigned char *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
/* ---------------- 内存读文件 ---------------- */
static unsigned char *read_file(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
if (n < 0) { fclose(f); return NULL; }
unsigned char *buf = (unsigned char *)malloc((size_t)n ? (size_t)n : 1);
if (!buf) { fclose(f); return NULL; }
size_t rd = fread(buf, 1, (size_t)n, f);
fclose(f);
if (rd != (size_t)n) { free(buf); return NULL; }
*out_len = (size_t)n;
return buf;
}
/* ---------------- zlib inflate ---------------- */
static int inflate_mem(const unsigned char *src, size_t srclen,
unsigned char *dst, size_t dstlen, size_t *out) {
mzf_stream s;
memset(&s, 0, sizeof(s));
s.next_in = (mzf_Bytef *)src;
s.avail_in = (mzf_uInt)srclen;
s.next_out = (mzf_Bytef *)dst;
s.avail_out = (mzf_uInt)dstlen;
s.zalloc = 0; s.zfree = 0; s.opaque = 0;
if (inflateInit_(&s, zlibVersion(), (int)sizeof(s)) != MZF_OK)
return -1;
int ret = inflate(&s, MZF_FINISH);
*out = (size_t)(dstlen - s.avail_out);
inflateEnd(&s);
return (ret == MZF_STREAM_END) ? 0 : -1;
}
/* ---------------- 最小 PNG 解码(灰度 non-interlaced) ---------------- */
/* 返回打包像素面(调用方释放),并输出 width/height/depth/plane_rowbytes。失败返回 NULL。 */
static unsigned char *png_decode_gray(const unsigned char *png, size_t len,
unsigned *out_w, unsigned *out_h,
unsigned *out_depth, size_t *out_rowbytes) {
if (len < 8 || memcmp(png, "\x89PNG\r\n\x1a\n", 8) != 0)
return NULL;
size_t i = 8;
unsigned w = 0, h = 0, depth = 0, color = 0, interlace = 1, compression = 1, filt = 1;
int have_ihdr = 0;
size_t idat_len = 0;
unsigned char *idat = NULL;
while (i + 8 <= len) {
uint32_t clen = be32(png + i);
const unsigned char *type = png + i + 4;
if (i + 8 + (size_t)clen > len) break;
if (memcmp(type, "IHDR", 4) == 0) {
if (clen < 13) { free(idat); return NULL; }
w = be32(png + i + 8);
h = be32(png + i + 12);
depth = png[i + 16];
color = png[i + 17];
compression = png[i + 18];
filt = png[i + 19];
interlace = png[i + 20];
have_ihdr = 1;
} else if (memcmp(type, "IDAT", 4) == 0) {
unsigned char *na = (unsigned char *)realloc(idat, idat_len + clen);
if (!na) { free(idat); return NULL; }
idat = na;
memcpy(idat + idat_len, png + i + 8, clen);
idat_len += clen;
} else if (memcmp(type, "IEND", 4) == 0) {
break;
}
i += 12 + (size_t)clen;
}
if (!have_ihdr || !idat ||
color != 0 || compression != 0 || filt != 0 || interlace != 0 ||
(depth != 4 && depth != 8) || w == 0 || h == 0) {
free(idat);
return NULL;
}
size_t rowbytes = ((size_t)w * depth + 7) / 8;
size_t scanlen = rowbytes + 1; /* 每扫描行 = 1 filter 字节 + rowbytes 像素 */
size_t rawlen = scanlen * h;
unsigned char *raw = (unsigned char *)malloc(rawlen);
if (!raw) { free(idat); return NULL; }
size_t outlen = 0;
if (inflate_mem(idat, idat_len, raw, rawlen, &outlen) != 0 || outlen != rawlen) {
free(raw); free(idat); return NULL;
}
free(idat);
/* 反滤波(bpp=1:灰度 depth 4|8) → out 为每行 rowbytes 的打包像素 */
size_t outlen_px = rowbytes * h;
unsigned char *out = (unsigned char *)malloc(outlen_px);
if (!out) { free(raw); return NULL; }
const unsigned bpp = 1;
for (size_t y = 0; y < h; y++) {
unsigned char ft = raw[y * scanlen];
const unsigned char *c = raw + y * scanlen + 1;
unsigned char *dst = out + y * rowbytes;
unsigned char *prev = (y == 0) ? NULL : out + (y - 1) * rowbytes;
for (size_t x = 0; x < rowbytes; x++) {
unsigned char left = (x >= bpp) ? dst[x - bpp] : 0;
unsigned char up = prev ? prev[x] : 0;
unsigned char ul = (prev && x >= bpp) ? prev[x - bpp] : 0;
unsigned char v = c[x];
switch (ft) {
case 0: dst[x] = v; break;
case 1: dst[x] = v + left; break;
case 2: dst[x] = v + up; break;
case 3: dst[x] = v + (((unsigned)left + (unsigned)up) >> 1); break;
case 4: {
int p = (int)left + (int)up - (int)ul;
int pa = p - (int)left; if (pa < 0) pa = -pa;
int pb = p - (int)up; if (pb < 0) pb = -pb;
int pc = p - (int)ul; if (pc < 0) pc = -pc;
unsigned char pred = (pa <= pb && pa <= pc) ? left : (pb <= pc ? up : ul);
dst[x] = v + pred;
break;
}
default: free(raw); free(out); return NULL;
}
}
}
free(raw);
*out_w = w; *out_h = h; *out_depth = depth; *out_rowbytes = rowbytes;
return out;
}
/* ---------------- idx 解析 ---------------- */
static int load_idx(const unsigned char *buf, size_t len) {
if (len < 16 || memcmp(buf, "MFNI", 4) != 0) return -1;
unsigned depth = buf[6];
int nfaces = buf[7];
unsigned png_w = le32(buf + 8), png_h = le32(buf + 12);
(void)png_w; (void)png_h;
if (nfaces < 1 || nfaces > 32) return -1;
if (depth != g.depth) return -2; /* 位深须与 PNG 一致 */
size_t face_off = 16;
meow_face *faces = (meow_face *)calloc((size_t)nfaces, sizeof(meow_face));
if (!faces) return -3;
for (int i = 0; i < nfaces; i++) {
const unsigned char *fh = buf + face_off + (size_t)i * 20;
meow_face *f = &faces[i];
f->px = fh[0] | (fh[1] << 8);
f->plane_x = fh[2] | (fh[3] << 8);
f->plane_y = fh[4] | (fh[5] << 8);
f->plane_w = fh[6] | (fh[7] << 8);
f->plane_h = fh[8] | (fh[9] << 8);
int16_t asc = (int16_t)(fh[10] | (fh[11] << 8));
int16_t dsc = (int16_t)(fh[12] | (fh[13] << 8));
f->ascent = asc;
f->descent = dsc;
f->glyph_count = fh[14] | (fh[15] << 8);
uint32_t goff = le32(fh + 16);
if (f->glyph_count > 0 && goff + (size_t)f->glyph_count * 16 <= len) {
f->glyphs = (meow_glyph *)malloc((size_t)f->glyph_count * sizeof(meow_glyph));
if (!f->glyphs) { for (int j = 0; j < i; j++) free(faces[j].glyphs); free(faces); return -4; }
for (unsigned k = 0; k < f->glyph_count; k++) {
const unsigned char *gchr = buf + goff + (size_t)k * 16;
meow_glyph *gr = &f->glyphs[k];
gr->cp = le32(gchr);
gr->x = gchr[4] | (gchr[5] << 8);
gr->y = gchr[6] | (gchr[7] << 8);
gr->w = gchr[8] | (gchr[9] << 8);
gr->h = gchr[10] | (gchr[11] << 8);
gr->advance = gchr[12] | (gchr[13] << 8);
gr->dx = (int8_t)gchr[14];
gr->dy = gchr[15];
}
} else if (f->glyph_count == 0) {
f->glyphs = NULL;
} else {
for (int j = 0; j < i; j++) free(faces[j].glyphs);
free(faces);
return -5;
}
}
g.faces = faces;
g.face_count = nfaces;
return 0;
}
/* ---------------- 面/字形查找 ---------------- */
static meow_face *face_for_size(int size) {
if (g.face_count <= 0) return NULL;
meow_face *best = NULL;
int best_delta = -1;
for (int i = 0; i < g.face_count; i++) {
int d = g.faces[i].px - size;
if (d < 0) d = -d;
if (best_delta < 0 || d < best_delta) { best_delta = d; best = &g.faces[i]; }
}
return best;
}
static meow_glyph *find_glyph(meow_face *f, uint32_t cp) {
unsigned lo = 0, hi = f->glyph_count;
while (lo < hi) {
unsigned mid = (lo + hi) >> 1;
uint32_t c = f->glyphs[mid].cp;
if (c == cp) return &f->glyphs[mid];
if (c < cp) lo = mid + 1; else hi = mid;
}
return NULL;
}
/* ---------------- 像素采样(depth 感知) ---------------- */
static inline unsigned sample_px(unsigned x, unsigned y) {
if (x >= g.plane_w || y >= g.plane_h) return 0;
if (g.depth == 8) return g.plane[(size_t)y * g.plane_w + x];
unsigned char b = g.plane[((size_t)y * g.plane_w + x) >> 1];
unsigned nh = (x & 1) ? (b & 0x0F) : (b >> 4);
return nh * 17;
}
/* ---------------- RGB565 alpha 混合 ---------------- */
static inline uint16_t blend565(uint16_t bg, uint16_t fg, unsigned a) {
unsigned inv = 255 - a;
unsigned br = (bg >> 11) & 31, bgc = (bg >> 5) & 63, bb = bg & 31;
unsigned fr = (fg >> 11) & 31, fgc = (fg >> 5) & 63, fb = fg & 31;
unsigned r = ((fr * a + br * inv) + 127) / 255;
unsigned gc = ((fgc * a + bgc * inv) + 127) / 255;
unsigned b = ((fb * a + bb * inv) + 127) / 255;
return (uint16_t)((r << 11) | (gc << 5) | b);
}
/* ---------------- 字形 blit(含缩放、裁剪、alpha 混合) ---------------- */
static void blit_glyph(uint16_t *fb, meow_face *face, const meow_glyph *gr,
int pen_x, int y, uint16_t color, unsigned scaleq) {
int stride = (int)(g.fb_pitch / 2); /* uint16 元素每行数 */
/* 目标尺寸 = 源尺寸 * scaleq / 4096 */
unsigned gw, gh;
int dx, dy;
if (scaleq == 4096) {
gw = gr->w; gh = gr->h;
dx = gr->dx; dy = gr->dy;
} else {
gw = (unsigned)(((uint64_t)gr->w * scaleq) >> 12);
gh = (unsigned)(((uint64_t)gr->h * scaleq) >> 12);
dx = (int)(((int64_t)gr->dx * scaleq) >> 12);
dy = (int)(((int64_t)gr->dy * scaleq) >> 12);
}
if (gw == 0 || gh == 0) return;
int px = pen_x + dx;
int py = y + dy;
unsigned sw = gr->w, sh = gr->h;
for (unsigned oy = 0; oy < gh; oy++) {
int ay = py + (int)oy;
if (ay < 0) continue;
if (ay >= (int)g.fb_h) break;
int sy = (int)((uint64_t)oy * sh / gh);
int base = face->plane_x + gr->x;
for (unsigned ox = 0; ox < gw; ox++) {
int ax = px + (int)ox;
if (ax < 0) continue;
if (ax >= (int)g.fb_w) break;
int sx = (int)((uint64_t)ox * sw / gw);
unsigned a = sample_px((unsigned)(base + sx),
(unsigned)(face->plane_y + gr->y + sy));
if (!a) continue;
size_t idx = (size_t)ay * (size_t)stride + (size_t)ax;
fb[idx] = blend565(fb[idx], color, a);
}
}
}
/* ---------------- UTF-8 解码 ---------------- */
static int utf8_next(const unsigned char **pp) {
const unsigned char *s = *pp;
unsigned char c = s[0];
if (c < 0x80) { *pp += 1; return c; }
int n;
uint32_t cp;
if ((c & 0xE0) == 0xC0) { n = 2; cp = c & 0x1F; }
else if ((c & 0xF0) == 0xE0) { n = 3; cp = c & 0x0F; }
else if ((c & 0xF8) == 0xF0) { n = 4; cp = c & 0x07; }
else { *pp += 1; return -1; }
for (int i = 1; i < n; i++) {
if ((s[i] & 0xC0) != 0x80) { *pp += 1; return -1; }
cp = (cp << 6) | (s[i] & 0x3F);
}
*pp += n;
return (int)cp;
}
static uint32_t advance_for_missing(meow_face *f) {
/* 缺字:用空格或 人 位 advance;找不到就用 fontSize/2 */
meow_glyph *sp = find_glyph(f, 0x20);
return sp ? sp->advance : (uint32_t)(f->px / 2);
}
static void draw_tofu(uint16_t *fb, int x, int y, int w, int h, uint16_t color) {
int stride = (int)(g.fb_pitch / 2);
for (int oy = 0; oy < h; oy++) {
int ay = y + oy;
if (ay < 0 || ay >= (int)g.fb_h) break;
for (int ox = 0; ox < w; ox++) {
int ax = x + ox;
if (ax < 0 || ax >= (int)g.fb_w) break;
if (oy == 0 || oy == h - 1 || ox == 0 || ox == w - 1) {
size_t idx = (size_t)ay * (size_t)stride + (size_t)ax;
fb[idx] = blend565(fb[idx], color, 255);
}
}
}
}
/* ---------------- 公共 API ---------------- */
void meow_font_set_fb(unsigned fb_w, unsigned fb_h, unsigned pitch_bytes) {
g.fb_w = fb_w; g.fb_h = fb_h; g.fb_pitch = pitch_bytes;
}
void meow_font_unload(void) {
if (g.faces) {
for (int i = 0; i < g.face_count; i++) free(g.faces[i].glyphs);
free(g.faces);
}
free(g.plane);
g.faces = NULL; g.plane = NULL; g.face_count = 0;
g.plane_w = g.plane_h = 0;
}
int meow_font_load(const char *png_path, const char *idx_path) {
meow_font_unload();
size_t plen = 0, ilen = 0;
unsigned char *png = read_file(png_path, &plen);
if (!png) return -1;
unsigned char *idx = read_file(idx_path, &ilen);
if (!idx) { free(png); return -1; }
unsigned w, h, depth;
size_t rowbytes;
unsigned char *plane = png_decode_gray(png, plen, &w, &h, &depth, &rowbytes);
free(png);
if (!plane) { free(idx); return -2; }
g.plane = plane;
g.depth = depth;
g.plane_w = w;
g.plane_h = h;
g.plane_rowbytes = rowbytes;
if (load_idx(idx, ilen) != 0) {
free(idx);
meow_font_unload();
return -3;
}
free(idx);
return 0;
}
int meow_font_has_size(int size) {
for (int i = 0; i < g.face_count; i++)
if (g.faces[i].px == size) return 1;
return 0;
}
int meow_font_ascent(int size) {
meow_face *f = face_for_size(size);
return f ? f->ascent : 0;
}
int meow_font_line_height(int size) {
meow_face *f = face_for_size(size);
return f ? (f->ascent + f->descent) : 0;
}
int meow_font_width_text(const char *utf8, int size) {
meow_face *f = face_for_size(size);
if (!f) return 0;
unsigned scaleq = (size == f->px) ? 4096u : (unsigned)(((uint64_t)size << 12) / (uint64_t)f->px);
const unsigned char *p = (const unsigned char *)utf8;
int total = 0;
while (*p) {
int cp = utf8_next(&p);
if (cp < 0) continue;
meow_glyph *gr = find_glyph(f, (uint32_t)cp);
unsigned adv = gr ? gr->advance : advance_for_missing(f);
if (scaleq == 4096) total += (int)adv;
else total += (int)(((uint64_t)adv * scaleq) >> 12);
}
return total;
}
void meow_font_drawtext(uint16_t *fb, int x, int y, const char *utf8,
uint16_t color, int size) {
meow_face *f = face_for_size(size);
if (!f || !g.plane) return;
unsigned scaleq = (size == f->px) ? 4096u : (unsigned)(((uint64_t)size << 12) / (uint64_t)f->px);
const unsigned char *p = (const unsigned char *)utf8;
int pen = 0;
while (*p) {
int cp = utf8_next(&p);
if (cp < 0) continue;
meow_glyph *gr = find_glyph(f, (uint32_t)cp);
if (gr && gr->w > 0 && gr->h > 0) {
blit_glyph(fb, f, gr, x + pen, y, color, scaleq);
} else if (!gr) {
/* 缺字:画空心方块 */
unsigned adv = advance_for_missing(f);
int tw, th;
if (scaleq == 4096) { tw = f->px / 2; th = f->ascent; }
else { tw = (int)(((uint64_t)(f->px / 2) * scaleq) >> 12); th = (int)(((uint64_t)f->ascent * scaleq) >> 12); }
draw_tofu(fb, x + pen + 1, y + 1, tw - 2, th - 2, color);
pen += scaleq == 4096 ? (int)adv : (int)(((uint64_t)adv * scaleq) >> 12);
continue;
}
int adv = gr ? gr->advance : (int)advance_for_missing(f);
pen += scaleq == 4096 ? adv : (int)(((uint64_t)adv * scaleq) >> 12);
}
}
void meow_font_drawtext_outline(uint16_t *fb, int x, int y, const char *utf8,
uint16_t color, uint16_t outline_color, int ol, int size) {
if (ol <= 0) { meow_font_drawtext(fb, x, y, utf8, color, size); return; }
static const int odx[8] = {-1,1,-1,1,0,0,-1,1};
static const int ody[8] = {-1,-1,1,1,-1,1,0,0};
/* 先将每个字形画 8 方向偏移描边,再画原色:逐字符处理以保证 advance 一致性。 */
meow_face *f = face_for_size(size);
if (!f || !g.plane) return;
unsigned scaleq = (size == f->px) ? 4096u : (unsigned)(((uint64_t)size << 12) / (uint64_t)f->px);
const unsigned char *p = (const unsigned char *)utf8;
int pen = 0;
while (*p) {
int cp = utf8_next(&p);
if (cp < 0) continue;
meow_glyph *gr = find_glyph(f, (uint32_t)cp);
if (gr && gr->w > 0 && gr->h > 0) {
for (int k = 0; k < 8; k++)
blit_glyph(fb, f, gr, x + pen + odx[k] * ol, y + ody[k] * ol, outline_color, scaleq);
}
int adv = gr ? gr->advance : (int)advance_for_missing(f);
pen += scaleq == 4096 ? adv : (int)(((uint64_t)adv * scaleq) >> 12);
}
meow_font_drawtext(fb, x, y, utf8, color, size);
}
+57
View File
@@ -0,0 +1,57 @@
#ifndef MEOW_FONT_H
#define MEOW_FONT_H
/*
* meow_font.h — MeowOS 预渲染位图字体运行时(独立、可复用,不绑定启动器)。
*
* 数据来源:由 gen/gen_font.py 生成的 font.png(灰度 PNG,bit depth 4|8,含各字号面)
* + font.idx(索引:头部 + 每面元数据 + 每字符 16B 记录)。
*
* 设备端加载:模块内建最小 PNG 解码(仅支持灰度非隔行)经 zlib inflate + 反滤波还原像素,
* 无需 libpng;交叉链接 musl 静态 libz.a。像素面按 depth=4|8 打包存储,取像素时按 nibble 解包。
*
* 绘制模型:drawtext(fb, x, y, utf8, color, size);(x,y)=行首「升线(top)」锚点;
* baseline = y + ascent;每字形 blit 于 (x + pen + dx, y + dy);pen 累加 advance。
* color 为 565 位 RGB(R 在 bit15..11,G bit10..5,B bit4..0),与 launcher 一致。
* 无字回退:缺码位画空心方块(tofu)并前进空格宽度。
*
* 字号:字库含若干原生面(如 28、20)。drawtext 请求 size 时先找精确匹配面;
* 若无则就近取一面做最近邻缩放(最优仍用原生字号)。
*/
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 设置 framebuffer 几何:宽/高(像素)与每行字节数(如 480*2)。绘制前调用一次。 */
void meow_font_set_fb(unsigned fb_w, unsigned fb_h, unsigned pitch_bytes);
/* 加载字库(全部字号)。成功返回 0,失败返回负值。可重复调用(先卸载旧的)。 */
int meow_font_load(const char *png_path, const char *idx_path);
/* 释放字库。 */
void meow_font_unload(void);
/* 某字号是否存在(命中原生面返回 1)。 */
int meow_font_has_size(int size);
/* UTF-8 字符串渲染宽度(px)。字面不存在返回 0。 */
int meow_font_width_text(const char *utf8, int size);
/* 行高(px = ascent + descent)。 */
int meow_font_line_height(int size);
int meow_font_ascent(int size);
/* 绘制文本:(x,y)=行首升线顶部;color=RGB565。已按 fb_w/fb_h 裁剪。 */
void meow_font_drawtext(uint16_t *fb, int x, int y, const char *utf8,
uint16_t color, int size);
/* 描边绘制:先以 outline_color 将每字形向 4/8 邻域平移 ol 绘制一圈,再以 color 填充。 */
void meow_font_drawtext_outline(uint16_t *fb, int x, int y, const char *utf8,
uint16_t color, uint16_t outline_color, int ol, int size);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_FONT_H */
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
# mips-repo — MeowOS 板载 mipsel 软件仓库(宿主侧)
目标设备:R3 Pro II / MeowOS 固件(**mipsel-linux-muslsf**,静态,无 glibc 依赖)。
用途:存放针对该设备的交叉编译二进制与构建信息,便于**复用**到后续固件 / TF 卡
(MeowLib / MeowOpt 目录)。
## 仓库结构(每工具一个目录)
```
mips-repo/
README.md # 本文件
<tool>/
build.sh # 交叉编译脚本(可重复执行)
manifest.md # 元信息:来源/版本/arch/校验/产物路径/安装目标
bin/<tool> # 交叉编译产物(静态 mipsel,如存在)
```
## 当前已入库工具
- **mdnsd/**:Apple mDNSResponder-2881.0.25 交叉编译产物(`mipsel-linux-muslsf` 静态)。两种变体同在 `bin/`:
- `bin/mdnsd` = **SAResponder**(mDNSResponderPosix,单服务注册型,`-n/-f` 只发布/回指定服务)strip 后 **885.6 KB**;
- `bin/mdnsd.bin` = **完整 mdnsd** 守护进程(通用宿主响应器,回任意 `.local` 查询)strip 后 **994 KB**。
均远超固件 <200KB 阈值 → 按 mDNS 方案A 用**完整版**部署到 TF `MeowOpt/mdnsd/mdnsd`,由 meow_svcd 托管(需 root / setcap `cap_net_bind_service,cap_net_raw`)。详见 `mdnsd/manifest.md`。
## 目标架构与工具链
- 架构:`mipsel`(MIPS32r2, little-endian)
- 运行库:`musl`(静态链接,避免 libc 版本/动态加载差异)
- 交叉编译器:`~/.tools/zig-mipsel-cc`(zig 目标 mips-linux-musl)
- 目标静态库 sysroot:`~/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf`
## 新增一个工具的流程
1. `mkdir -p <tool>/bin`,写 `<tool>/build.sh`(用 zig-mipsel-cc,`-Os -static`)。
2. 跑 `build.sh`,产出放 `<tool>/bin/<tool>`。
3. 写 `<tool>/manifest.md`:来源(如 Apple-OSS/mDNSResponder 版本)、ARCH、`file`
输出摘要、`ls -l` 大小、构建命令、安装路径(固件 or TF MeowOpt)。
## 部署到哪里
- 体积小(约 <200KB)→ 可直接打进固件(/usr/bin 或由 supervisor 启动)。
- 体积大 → 放 TF 卡 `MeowOpt/<tool>/`,由 MeowOS 启动时从卡加载(拔卡降级不崩)。
- **按需取用**:从 `MeowOpt/<tool>/` 按需部署,不一定要全部入库;若某工具体积仍偏重(如 mdnsd 的 885~994KB),可评估更小方案(OpenWrt linkd / libmdns,或自研极简响应器——仅回 A 记录 + `_ssh` 服务)以塞进固件 <200KB。
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
#
# build.sh — 交叉编译 Apple mDNSResponder 的 POSIX 精简变体 SAResponder(mipsel-linux-muslsf,静态)
#
# 目标架构 : mipsel (MIPS32r2 little-endian), musl, 静态链接, 无 glibc 依赖
# 交叉编译器: zig (目标 mips-linux-musl / mipsel-linux-musleabi),由 ~/Project/R3ProII/.tools/zig-mipsel-cc 包装
# 产物 : bin/mdnsd = SAResponder (mDNSResponderPosix) —— 本脚本构建,无 UDS/IPC 的精简响应器
# bin/mdnsd.bin = 完整 mdnsd 守护进程(上一轮产物,作为对照保留,需重编译见文件尾注释)
# 用途 : MeowOS (R3 Pro II) 板载 mDNS 响应器,回应 .local 查询
#
# 用法 : ./build.sh # 默认构建 SAResponder → bin/mdnsd
#
# 可复现性 : 源码按固定 commit SHA 下载(codeload sha 方式),产物大小/校验值稳定。
set -euo pipefail
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
BIN_DIR="$REPO_DIR/bin"
WORK="$REPO_DIR/.build"
# ---- 工具链(本机实际路径;zig-mipsel-cc 内部也用 $HOME/Project/R3ProII/.tools)----
ZIGCC="${ZIGCC:-$HOME/Project/R3ProII/.tools/zig-mipsel-cc}"
# ---- 固定源码版本:apple-oss-distributions/mDNSResponder 2881.0.25 ----
MDNS_SHA="${MDNS_SHA:-d4658af3f5f291311c6aee4210aa6d39bda82bbe}"
MDNS_VER="${MDNS_VER:-2881.0.25}"
TARBALL_URL="https://codeload.github.com/apple-oss-distributions/mDNSResponder/tar.gz/${MDNS_SHA}"
TARBALL="$WORK/mDNSResponder-${MDNS_VER}.tar.gz"
SRCDIR="$WORK/src"
mkdir -p "$BIN_DIR" "$WORK"
echo "==> [1/4] 获取源码 mDNSResponder-$MDNS_VER (commit ${MDNS_SHA:0:12})"
if [ ! -f "$TARBALL" ]; then
curl -fsSL --retry 4 --retry-all-errors -o "$TARBALL" "$TARBALL_URL"
fi
gzip -t "$TARBALL" >/dev/null 2>&1 || { echo "错误: 源码包校验失败"; exit 1; }
rm -rf "$SRCDIR"
mkdir -p "$SRCDIR"
tar xzf "$TARBALL" -C "$SRCDIR" --strip-components=1
echo "==> [2/4] 交叉编译 SAResponder (zig: mipsel-linux-musleabi, -Os -static, tls=no)"
cd "$SRCDIR/mDNSPosix"
# 说明:
# - os=linux : 选定 Linux 平台分支 (CFLAGS_OS)
# - tls=no : 关闭 TLS 支持,避免链接 mbedtls (嵌入式设备不需要)
# - CC/LD=$ZIGCC : 强制 zig 交叉编译器,不用宿主 gcc
# - CFLAGS="-Os -static" : -Os 体积优化 + 静态链接;-Wno-date-time 关掉 zig/clang
# 默认的 -Werror -Wdate-time (源码用了 __DATE__/__TIME__ 宏)
# - LINKOPTS="-s" : 链接时直接 strip 符号表 + 调试信息,产出部署版。
# 注意 SAResponder 的链接规则里没有 $(STRIP) $@ 这一行,
# 唯一的 strip 来源就是这个 -s(勿依赖 STRIP=true)
# - SAResponder 目标 : 无 UDS/IPC 的独立响应器,产物 build/prod/mDNSResponderPosix
make os=linux tls=no \
CC="$ZIGCC" CFLAGS="-Os -static -Wno-date-time" \
LD="$ZIGCC" LINKOPTS="-s" STRIP=true SAResponder
echo "==> [3/4] 放置产物 $BIN_DIR/mdnsd (SAResponder)"
install -m 755 build/prod/mDNSResponderPosix "$BIN_DIR/mdnsd"
echo "==> [4/4] 校验"
file "$BIN_DIR/mdnsd"
echo "--- 体积 (静态部署版, strip 后) ---"
ls -l "$BIN_DIR/mdnsd"
echo "字节数: $(stat -c %s "$BIN_DIR/mdnsd")"
echo "MD5 : $(md5sum "$BIN_DIR/mdnsd" | cut -d' ' -f1)"
echo "构建完成: $BIN_DIR/mdnsd"
echo
echo "提示: bin/mdnsd.bin 为上一轮的完整 mdnsd 守护进程(1,018,144 B, md5 fcb0a8ae5ad5696253a6ec56b3e585e6),"
echo " 仅作对照保留。如需重编译完整 mdnsd 放到 mdnsd.bin,改目标为 Daemon 即可:"
echo " make os=linux tls=no CC=\"\$ZIGCC\" CFLAGS=\"-Os -static -Wno-date-time\" LD=\"\$ZIGCC\" LINKOPTS=\"-s\" Daemon"
echo " install -m 755 build/prod/mdnsd \"$BIN_DIR/mdnsd.bin\""
+76
View File
@@ -0,0 +1,76 @@
# mdnsd — Apple mDNSResponder 的 POSIX 精简变体 SAResponder(mipsel-linux-muslsf 静态)
> 本目录现以 **SAResponder(mDNSResponderPosix,无 UDS/IPC 的精简响应器)** 为部署目标。
> 其目标名对应上游 `mDNSPosix/Makefile` 的 `SAResponder`,产物 `build/prod/mDNSResponderPosix`。
> 完整 mdnsd 守护进程(上一轮产物)保留为 `bin/mdnsd.bin`(见下"与 mdnsd 对比")。
## 来源
- 项目 : apple-oss-distributions/mDNSResponder
- 版本 : mDNSResponder-2881.0.25
- commit : d4658af3f5f291311c6aee4210aa6d39bda82bbe
- 许可 : Apache-2.0
- 获取 : https://codeload.github.com/apple-oss-distributions/mDNSResponder/tar.gz/<SHA>
- 源码 : mDNSPosix/(Responder.c,独立响应器 main;非 PosixDaemon.c),tls=no(关闭 mbedtls)
## ARCH / 运行库
- ARCH : mipsel(MIPS32r2 little-endian)
- 编译目标 : mipsel-linux-musleabi(soft-float,对应 MeowOS 的 muslsf)
- 运行库 : musl,静态链接(-static),无 glibc 依赖
## file 结果(部署版 = bin/mdnsd)
ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, stripped
- readelf 复核:无 `.debug_*` / `.symtab` / `.strtab`(真正 strip;仅剩 MIPS 必有的 .mdebug.abi32 / .pdr ABI 元数据段)。
## 体积(关键)
- 未 strip(含 -g 调试信息,make 标准链接 `LINKOPTS=""`) : 3,197,588 字节 ≈ 3122.6 KB ≈ 3.05 MB
(file 判:`with debug_info, not stripped`;md5 33f8fcef0a4df5e907f75b6913d2fe23)
- **strip 后部署版(正式部署体积,`LINKOPTS=-s`)** : **906,864 字节 ≈ 885.6 KB ≈ 0.865 MB ← 以此为准**
- md5 (strip 后) : **3361b1a062d39372a30eff0a5a1f1725**
## 构建命令摘要
见同目录 build.sh。要点(目标由上一轮的 `Daemon` 改为 `SAResponder`):
make os=linux tls=no \
CC="$HOME/Project/R3ProII/.tools/zig-mipsel-cc" \
CFLAGS="-Os -static -Wno-date-time" \
LD="$HOME/Project/R3ProII/.tools/zig-mipsel-cc" \
LINKOPTS="-s" STRIP=true SAResponder
- 目标 `SAResponder`:构造对象 = SPECIALOBJ(13 个) + mDNS.c.o + Responder.c.o,**不含** UDS/IPC 相关对象
(去掉了 PosixDaemon.c.o、uds_daemon.c.o、dnssd_ipc.c.o、ClientRequests.c.o;以 Responder.c.o 作 main)。
- os=linux : 平台分支
- tls=no : 关 mbedtls(嵌入式不需要)
- CFLAGS -Os -static: 体积优化 + 静态链接
- -Wno-date-time : 关掉 zig/clang 默认的 -Werror -Wdate-time(源码用了 __DATE__/__TIME__)
- LINKOPTS=-s : 链接时直接 strip,产出部署版。
注意 `mDNSResponderPosix` 的链接规则里**没有** `$(STRIP) $@` 这一行,唯一 strip 来源就是这个 `-s`(勿依赖 STRIP=true)。
- 一次跑通,仅 Responder.c 报告 6 条无害 warning(unused parameter / set-but-not-used,非错误)。
## 功能差异(关键,勿当 mdnsd 的等价替代)
- SAResponder 是**单服务注册型**独立响应器:必须用 `-n <服务名>` 或 `-f <服务文件>` 指定要发布/回应的服务,
默认服务类型 `_afpovertcp._tcp.`、端口 548。它注册**一个**服务并回应针对该服务的 .local 查询。
- mdnsd(bin/mdnsd.bin)是**通用宿主响应器**:读网络接口、监听组播 224.0.0.251:5353,回应宿主下任意 .local 查询。
- 用途选择:若只需"板载发布单服务"→ 用 SAResponder;若需"通用 .local 解析"→ 用 mdnsd(bin/mdnsd.bin)。
## 与 mdnsd 对比(994 KB → 885.6 KB)
| 变体 | strip 后 | 未 strip | file |
|---|---|---|---|
| SAResponder(bin/mdnsd) | **906,864 B = 885.6 KB**(md5 3361b1a0…) | 3,197,588 B = 3122.6 KB | stripped, static |
| 完整 mdnsd(bin/mdnsd.bin) | 1,018,144 B = 994 KB(md5 fcb0a8ae…) | 3,549,496 B = 3466 KB | stripped, static |
- 差值仅 **~109 KB(约 11%)**。去掉 UDS/IPC 并未显著变小,因为体积大头在 mDNS 核心
(mDNS.c、DNSCommon.c、uDNS.c、dso 系),而非 IPC 层。
## 部署建议(结论:TF,不进固件)
- **885.6 KB 远超 <200KB 阈值 → 放 TF 卡 `MeowOpt/mdnsd/mdnsd`**(若只需单服务),由 MeowOS 启动时从卡加载(拔卡降级不崩)。
- 若需通用 .local 回应,部署 `mdnsd.bin`(原完整守护进程,同样放 TF)。
- 启动/参数(SAResponder):`mdnsd -b -n <name> -t _xxxx._tcp. -p <port>`;`-b` 后台,`-P` pidfile,
`-v 0..2` 调试,`-r` 同时绑 53,`-f <file>` 读服务清单。
- 权限:需 root(或 setcap cap_net_bind_service,cap_net_raw)以绑定 5353/53 并加组播。
- 验证:`qemu-mipsel bin/mdnsd -h` → 打印 Usage 后以 exit 1 退出(预期用法退出,程序可运行)。
## bin/ 内容
- `bin/mdnsd` = SAResponder(mDNSResponderPosix)strip 后部署版(906,864 B)
- `bin/mdnsd.bin` = 完整 mdnsd 守护进程 strip 后(1,018,144 B),对照保留
## 备选(评估)
- 若 885 KB 仍偏重:改用 OpenWrt linkd / libmdns 或自研极简响应器(仅响应 .local 查询)。
理由:纯 mDNS 查询响应是轻需求,完整 Bonjour 栈(DNSCommon/uDNS/dso/GenLinkedList 等核心)即占 ~880KB,
在无动态链接的静态 musl 下已很难再压到 <200KB。
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
{"name":"Hello App","exec":"/usr/data/mnt/sd_0/MeowAPPs/hello/hello","version":"0.1","immersive":1}
Binary file not shown.
@@ -0,0 +1,181 @@
{
"viewgroup":{
"textview":{
"name":"apps_tv_title",
"size":32,
"color":"0x32526c",
"focus_color":"0x32526c",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_title",
"x":40,
"y":10,
"w":400,
"h":40,
"textview":true
},
"textview":{
"name":"apps_tv_0",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_0",
"x":40,
"y":90,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_1",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_1",
"x":40,
"y":146,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_2",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_2",
"x":40,
"y":202,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_3",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_3",
"x":40,
"y":258,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_4",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_4",
"x":40,
"y":314,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_5",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_5",
"x":40,
"y":370,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_6",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_6",
"x":40,
"y":426,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_7",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_7",
"x":40,
"y":482,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_hint",
"size":18,
"color":"0x92a4b3",
"focus_color":"0xa7b0bf",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_hint",
"x":40,
"y":520,
"w":400,
"h":36,
"textview":true
},
"textview":{
"name":"apps_tv_note",
"size":16,
"color":"0x9fb8d9",
"focus_color":"0x9fb8d9",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_note",
"x":40,
"y":570,
"w":400,
"h":30,
"textview":true
},
"textview":{
"name":"apps_tv_back",
"size":20,
"color":"0x92a4b3",
"focus_color":"0x1062f2",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_back",
"x":40,
"y":640,
"w":400,
"h":40,
"textview":true
},
"name":"hiby_apps_main",
"type":"lg_view",
"x":0,
"y":0,
"w":480,
"h":720,
"color":"0xe4f5fe",
"viewgroup":true,
"add_layout":true
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+77
View File
@@ -0,0 +1,77 @@
#!/bin/sh
#
# S99ssh - HiBy R3 Pro II: dropbear SSH 服务 + ADB 持久自启
#
# 设计要点:
# * dropbear 为静态 musl 二进制(/usr/sbin/dropbear),仅 pubkey 认证
# (编译时已去掉 password auth,无需 /etc/passwd 写密码)。
# * /root 位于只读 squashfs,authorized_keys 放可写 UBIFS 卷:
# /usr/data/ssh/.ssh/authorized_keys
# 并用 bind-mount 把 /usr/data/ssh 挂到 /root(/root 在固件中是空目录),
# dropbear 按 passwd 的 pw_dir=/root 查找 ~/.ssh/authorized_keys。
# * host key 首次开机用 dropbearkey 生成到 /usr/data/ssh/
# (dropbear -R 的运行时生成路径是只读的 /etc/dropbear,不可用)。
# * 日志:/usr/data/ssh.log(= /data/ssh.log)。
# * 顺带删除 /usr/data/disableadb,使 ADB 在 USB 连接时持久自启
# (etc/init.d/adb/S440adb 检查该文件存在则不起 adb)。
#
case "$1" in
start)
echo "Starting dropbear sshd ..."
# ADB 持久自启:清掉禁用标记
rm -f /usr/data/disableadb
# UBIFS 未挂好/不可写则放弃(避免在只读 squashfs 上瞎写)
[ -d /usr/data ] && [ -w /usr/data ] || {
echo "S99ssh: /usr/data not writable, abort" > /dev/console 2>&1
exit 0
}
mkdir -p /usr/data/ssh/.ssh
chmod 700 /usr/data/ssh
chmod 700 /usr/data/ssh/.ssh
# 首次开机:从只读模板写入 authorized_keys
if [ ! -f /usr/data/ssh/.ssh/authorized_keys ] && [ -f /etc/dropbear/authorized_keys ]; then
cp /etc/dropbear/authorized_keys /usr/data/ssh/.ssh/authorized_keys
fi
chmod 600 /usr/data/ssh/.ssh/authorized_keys 2>/dev/null
# /root 只读 -> bind 可写的 ssh 工作目录过去
mount --bind /usr/data/ssh /root 2>/dev/null
# MeowAPPs 应用加载器: 把看门狗复制到可写卷 (cave exec 路径 /usr/data/meow_watchdog)
if [ -f /usr/bin/meow_watchdog ]; then
cp -f /usr/bin/meow_watchdog /usr/data/meow_watchdog 2>/dev/null
chmod 755 /usr/data/meow_watchdog 2>/dev/null
fi
# 首次开机:生成 ed25519 host key
if [ ! -f /usr/data/ssh/dropbear_ed25519_host_key ]; then
/usr/sbin/dropbearkey -t ed25519 -f /usr/data/ssh/dropbear_ed25519_host_key \
>> /usr/data/ssh.log 2>&1
chmod 600 /usr/data/ssh/dropbear_ed25519_host_key
fi
# 启动 dropbear:仅 pubkey 认证,禁用端口转发
/usr/sbin/dropbear -j -k \
-r /usr/data/ssh/dropbear_ed25519_host_key \
-p 22 \
-P /usr/data/ssh/dropbear.pid \
>> /usr/data/ssh.log 2>&1 &
;;
stop)
killall dropbear 2>/dev/null
sleep 1
umount /root 2>/dev/null
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
exit 0
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
#
# S90meow_bootmenu - R3 Pro II boot selector (Boot Selector phase 0 · M2 接管 HibyOS)
#
# Design (BOOT_SELECTOR_DESIGN.md §2.1 timing / §9 M2):
# * Placed AFTER S80_bt_init, BEFORE S92_03_start_music_player (S80 < S90 < S92),
# so the selector runs before the official player is started.
# * M2: meow_bootmenu is launched with --fork, so:
# 选 HibyOS -> bootmenu forks /usr/bin/hiby_player (最小启动契约),
# parent becomes supervisor/热键守护, blocks rcS.
# 选 MeowOS -> bootmenu exec /usr/bin/meow_launcherd (MeowOS 基础主屏),
# blocks rcS.
# Either way rcS is blocked by S90 until整机重启/热键切换, so S92 (兜底 patch)
# is normally never reached; SSH was moved to S84 so it is already online.
# * rcS runs S??* (non-.sh) as fork-ed subprocesses, so `exec` here replaces
# ONLY this script's process.
# * 红线: 启动前清 /data/recovery_all、禁经 hiby_player.sh、禁 killall、
# 禁 SIGUSR1。回退 = 重刷不含选单固件 (M1 r3proii_m1.upt)。
#
case "$1" in
start)
echo -n "Starting meow_bootmenu: "
# Boot mode marker (tmpfs is cleared each boot; rm once more to be sure).
rm -f /tmp/meow_boot_mode
# 红线:启动前清除 recovery_all,避免进入恢复流程。
rm -f /data/recovery_all
if [ -x /usr/bin/meow_bootmenu ]; then
echo "OK (selector --fork)"
exec /usr/bin/meow_bootmenu --fork
else
echo "MISSING /usr/bin/meow_bootmenu (fall through to S92)"
fi
;;
stop)
echo -n "Stopping meow_bootmenu: "
killall meow_bootmenu 2>/dev/null
echo "OK"
;;
restart|reload)
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
exit 0
@@ -0,0 +1,66 @@
#!/bin/sh
#
# S92_03_start_music_player - Boot Selector phase 0 · M2 兜底 patch
#
# 正常路径:rcS 被 S90meow_bootmenu 阻塞(fork 播放器后父进程监督,或 exec
# meow_launcherd),本脚本**不会执行**。
# 兜底路径:S90 崩溃/选单 fork 失败导致 rcS 推进到本脚本时,读 /tmp/meow_boot_mode
# + pidof 判断,安全拉起播放器或跳过。
#
# 红线:* 仅经 pidof 判断,不 killall hiby_player(触发 sh 看门狗 reboot);
# * 禁 SIGUSR1;* 兜底拉起才用 hiby_player.sh(官方行为,含 sleep 1; reboot
# 看门狗——只在播放器无人接管时兜底,正常 HibyOS 由 bootmenu 直拉二进制)。
#
PL01=/usr/bin/hiby_player.sh
case "$1" in
start)
echo -n "Starting $0 : begin "
# 若 S90 已把模式写成 /tmp/meow_boot_mode,优先按其决策兜底
if [ -f /tmp/meow_boot_mode ]; then
case "$(cat /tmp/meow_boot_mode)" in
meow)
echo "MeowOS mode: skip player (launcher should have taken over)"
exit 0
;;
hiby)
if pidof hiby_player >/dev/null 2>&1; then
echo "player already up (bootmenu fork), skip"
exit 0
fi
[ -f $PL01 ] && ( $PL01 & ) || echo " file <$PL01> don't exist."
;;
*)
# 未知 mode:按官方兜底
if pidof hiby_player >/dev/null 2>&1; then
echo "unknown mode, player already up, skip"
exit 0
fi
[ -f $PL01 ] && ( $PL01 & ) || echo " file <$PL01> don't exist."
;;
esac
else
# 无模式标记:选单崩/未写,官方兜底(先 pidof 防双拉竞态)
if pidof hiby_player >/dev/null 2>&1; then
echo "no mode file, player already up, skip"
exit 0
fi
[ -f $PL01 ] && ( $PL01 & ) || echo " file <$PL01> don't exist."
fi
echo -n "Starting $0 : end "
;;
stop)
echo -n "Stopping $0 : "
echo "OK"
;;
restart|reload)
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
#
# S95shutdown_logo - R3 Pro II 关机 logo(阶段0 · M2.1)
#
# 机制(BusyBox init,见 /etc/inittab):
# ::shutdown:/etc/init.d/rcK
# rcK 按数字**反序**执行 /etc/init.d/S??* stop(S99ssh → S95 → S92 → S80 → …)。
# 本脚本(S95)在 `stop` 分支、大多数服务仍存活、/dev/fb0 仍可用时,
# 先用 cmd_jpeg_display 显示 /etc/shutdown_logo.jpeg 并点亮面板。
#
# ⚠️ 局限(M2.1 记录,待真机验证):poweroff 由**内核长按电源**触发,是否可靠走
# rcK 的 ::shutdown 路径未实测(可能直接断电,rcK 不执行)。若不可靠,关机
# logo 应改由 MeowOS 的 poweroff 处理显示(M3 范畴)。M2.1 先在固件放文件 +
# 写脚本,标注待验证。
#
case "$1" in
start)
;;
stop)
echo -n "S95shutdown_logo: "
if [ -x /usr/bin/cmd_jpeg_display ] && [ -f /etc/shutdown_logo.jpeg ]; then
# 先点亮面板(避免黑屏上画图不可见),再显示关机 logo。
echo 60 > /sys/class/backlight/backlight_pwm0/brightness 2>/dev/null
cmd_jpeg_display /etc/shutdown_logo.jpeg 2>/dev/null
echo "shutdown logo shown"
else
echo "skip (no cmd_jpeg_display or no /etc/shutdown_logo.jpeg)"
fi
;;
restart|reload)
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
exit 0
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# build_bootmenu.sh — build the meow_bootmenu binary: static MIPS32r2 musl (soft-float).
# M2.26: 链入 meow_launcherd/meow_touch.c(统一触摸接口),boot menu 选单/确认框触摸迁到 meow_touch,
# 修「触摸确认点两下」。meow_touch.h 在 ../meow_launcherd/ 下,用 -I 引相对路径。
set -e
cd "$(dirname "$0")"
CC="$HOME/Project/R3ProII/.tools/zig-mipsel-cc"
MEOW_TOUCH_DIR="../meow_launcherd"
"$CC" -Os -static -I "$MEOW_TOUCH_DIR" meow_bootmenu.c "$MEOW_TOUCH_DIR/meow_touch.c" -o meow_bootmenu
ls -la meow_bootmenu
file meow_bootmenu
BIN
View File
Binary file not shown.
File diff suppressed because it is too large. Load diff
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# R3 Pro II — patch v2.2 真机部署脚本(bind-mount 免刷机)
#
# 用法:
# ./deploy_device.sh <设备IP> [ssh 私钥]
#
# 安全规矩(强制):
# - 杀 hiby_player: 杀 hiby_player.sh 父 shell → 杀 hiby_player → 确认零残留 → 手动拉起一个
# - 每步记录: ps 实例数 / brightness / bind 列表
# - busybox 无 pkill/killall; 绝不用 killall(会触发 hiby_player.sh 的 reboot 看门狗)
set -e
IP="${1:?usage: deploy_device.sh <IP> [key]}"
KEY="${2:-$HOME/Project/R3ProII/ssh_keys/<device_key>}"
ROOT="$HOME/Project/R3ProII/patch_v2"
ssh_cmd() {
ssh -i "$KEY" -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@"$IP" "$@"
}
echo "==> 连接 $IP ..."
ssh_cmd "echo OK" || { echo "SSH 失败"; exit 1; }
echo "==> 轮前状态"
ssh_cmd 'echo "instances=$(ps | grep hiby_player | grep -v grep | grep -v sh | wc -l) brightness=$(cat /sys/class/backlight/backlight_pwm0/brightness) binds=$(mount | grep -c overlay)"'
echo "==> 推送文件"
scp -i "$KEY" -o StrictHostKeyChecking=no "$ROOT/hiby_player_patched_v2" root@$IP:/data/hiby_player_patched_v2
scp -i "$KEY" -o StrictHostKeyChecking=no "$ROOT/meow_watchdog/meow_watchdog" root@$IP:/usr/data/meow_watchdog
scp -i "$KEY" -o StrictHostKeyChecking=no "$ROOT/apps_assets/apps/hello/hello" root@$IP:/data/hello.bin
scp -i "$KEY" -o StrictHostKeyChecking=no "$ROOT/apps_assets/apps/hello/app.json" root@$IP:/data/hello_app.json
echo "==> 设备侧组装"
ssh_cmd '
set -e
if ! grep -q "sd_0" /proc/mounts; then
echo "!! TF 卡未挂载"; mount /dev/mmcblk0p1 /usr/data/mnt/sd_0 2>/dev/null || echo "!! 挂载失败"
fi
mkdir -p /usr/data/mnt/sd_0/MeowAPPs/hello /data/overlay
cp -f /data/hello.bin /usr/data/mnt/sd_0/MeowAPPs/hello/hello
cp -f /data/hello_app.json /usr/data/mnt/sd_0/MeowAPPs/hello/app.json
chmod 755 /usr/data/mnt/sd_0/MeowAPPs/hello/hello
chmod 755 /usr/data/meow_watchdog
ls /usr/data/mnt/sd_0/MeowAPPs/hello/
rm -rf /data/overlay/theme1 /data/overlay/str_zh /data/overlay/str_en
cp -a /usr/resource/layout/theme1 /data/overlay/theme1
cp -a /usr/resource/str/simplified_chinese /data/overlay/str_zh
cp -a /usr/resource/str/english /data/overlay/str_en
echo "overlay ready"
'
echo "==> 推送资源文件"
cat > /tmp/deploy_batch.sftp << EOF
put $ROOT/apps_assets/layout/theme1/hiby_apps_main.view /data/overlay/theme1/hiby_apps_main.view
put $ROOT/apps_assets/str/simplified_chinese/apps.ini /data/overlay/str_zh/apps.ini
put $ROOT/apps_assets/str/english/apps.ini /data/overlay/str_en/apps.ini
EOF
sftp -i "$KEY" -o StrictHostKeyChecking=no -b /tmp/deploy_batch.sftp root@$IP
echo "==> 单实例重启 + bind-mount(先杀进程再 umount,顺序关键)"
ssh_cmd '
set -e
# 1) 杀 hiby_player.sh 父 shell(防 reboot 看门狗)
for pid in $(ps | grep "hiby_player.sh" | grep -v grep | awk "{print \$1}"); do kill $pid 2>/dev/null || true; done
sleep 1
# 2) 杀 hiby_player(先杀干净,文件挂载点才能 umount)
for pid in $(ps | grep hiby_player | grep -v grep | grep -v "hiby_player.sh" | awk "{print \$1}"); do kill $pid 2>/dev/null || true; done
sleep 2
echo "remaining=$(ps | grep hiby_player | grep -v grep | wc -l)"
# 3) umount 旧 bind
umount /usr/bin/hiby_player 2>/dev/null || true
umount /usr/bin/meow_watchdog 2>/dev/null || true
umount /usr/resource/layout/theme1 2>/dev/null || true
umount /usr/resource/str/simplified_chinese 2>/dev/null || true
umount /usr/resource/str/english 2>/dev/null || true
# 4) 组装 + bind
cp -f /data/hiby_player_patched_v2 /data/overlay/hiby_player
chmod 755 /data/overlay/hiby_player
mount --bind /data/overlay/hiby_player /usr/bin/hiby_player
# 看门狗: cave 内 exec 路径为 /usr/bin/meow_watchdog(与固件一致), 测试期
# /usr/bin 只读, 把可写卷上的 /usr/data/meow_watchdog bind 过去
mount --bind /usr/data/meow_watchdog /usr/bin/meow_watchdog 2>/dev/null || true
mount --bind /data/overlay/theme1 /usr/resource/layout/theme1
mount --bind /data/overlay/str_zh /usr/resource/str/simplified_chinese
mount --bind /data/overlay/str_en /usr/resource/str/english
echo "md5: $(md5sum /usr/bin/hiby_player | cut -d" " -f1)"
ls /usr/resource/layout/theme1/hiby_apps_main.view >/dev/null && echo "view ok"
ls /usr/resource/str/english/apps.ini >/dev/null && echo "ini ok"
# 5) 拉起一个
nohup /usr/bin/hiby_player > /tmp/hiby_player.log 2>&1 &
sleep 4
echo "instances=$(ps | grep hiby_player | grep -v grep | grep -v sh | wc -l)"
ps | grep hiby_player | grep -v grep | grep -v sh
'
echo "==> 部署完成"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# gdb_cave.sh — 重启设备 gdbserver 并在本机 gdb 里执行给定命令文件
# 用法: ./gdb_cave.sh <gdb_script> [额外gdb参数...]
IP="${GDB_IP:-<alt_device_ip>}"
KEY="$HOME/Project/R3ProII/ssh_keys/<device_key>"
SCRIPT="${1:?usage: gdb_cave.sh <script>}"
shift
# 重启 gdbserver
ssh -i "$KEY" -o StrictHostKeyChecking=no root@$IP "
for pid in \$(ps | grep gdbserver | grep -v grep | awk '{print \$1}'); do kill \$pid 2>/dev/null; done
sleep 1
HP=\$(ps | grep hiby_player | grep -v grep | grep -v sh | awk '{print \$1}' | head -1)
nohup gdbserver --attach :2345 \$HP > /data/gdbserver.log 2>&1 &
sleep 2
cat /data/gdbserver.log | head -2
" 2>&1 | grep -vE "WARNING|vulnerable|upgraded|openssh.com" | grep -E "Attached|Listening"
gdb-multiarch -batch -x "$SCRIPT" "$@" 2>&1 | grep -vE "^The target|set to|^\[|warning:"
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""Generate patch v2 assets:
- layout/theme1/hiby_apps_main.view (app list page)
- str/<13 langs>/apps.ini (title/back/hint/row placeholder text)
"""
import os
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "apps_assets")
VIEW = """{
"viewgroup":{
"textview":{
"name":"apps_tv_title",
"size":32,
"color":"0x32526c",
"focus_color":"0x32526c",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_title",
"x":40,
"y":10,
"w":400,
"h":40,
"textview":true
},
"textview":{
"name":"apps_tv_0",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_0",
"x":40,
"y":90,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_1",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_1",
"x":40,
"y":146,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_2",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_2",
"x":40,
"y":202,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_3",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_3",
"x":40,
"y":258,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_4",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_4",
"x":40,
"y":314,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_5",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_5",
"x":40,
"y":370,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_6",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_6",
"x":40,
"y":426,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_7",
"size":24,
"color":"0x32526c",
"focus_color":"0x1062f2",
"type":"static|left",
"ini":"apps.ini",
"text":"app_row_7",
"x":40,
"y":482,
"w":400,
"h":44,
"textview":true
},
"textview":{
"name":"apps_tv_hint",
"size":18,
"color":"0x92a4b3",
"focus_color":"0xa7b0bf",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_hint",
"x":40,
"y":520,
"w":400,
"h":36,
"textview":true
},
"textview":{
"name":"apps_tv_note",
"size":16,
"color":"0x9fb8d9",
"focus_color":"0x9fb8d9",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_note",
"x":40,
"y":570,
"w":400,
"h":30,
"textview":true
},
"textview":{
"name":"apps_tv_back",
"size":20,
"color":"0x92a4b3",
"focus_color":"0x1062f2",
"type":"static|h_center",
"ini":"apps.ini",
"text":"apps_back",
"x":40,
"y":640,
"w":400,
"h":40,
"textview":true
},
"name":"vg_apps_main_hiby",
"type":"lg_view",
"x":0,
"y":0,
"w":480,
"h":720,
"color":"0xe4f5fe",
"viewgroup":true,
"add_layout":true
}
}
"""
# 13 languages, matching the firmware's str/ dirs
LANGS = ["simplified_chinese", "traditional_chinese", "english", "russian",
"korean", "japanese", "german", "italy", "spain", "poland",
"french", "thai", "ukrainian"]
T = {
"simplified_chinese": ("应用列表", "返回", "点击应用启动,按任意键退出", "---"),
"traditional_chinese": ("應用列表", "返回", "點擊應用啟動,按任意鍵退出", "---"),
"english": ("Apps", "Back", "Tap an app to launch; press any key to exit", "---"),
"russian": ("Приложения", "Назад", "Нажмите приложение; любая клавиша — выход", "---"),
"korean": ("앱", "뒤로", "앱을 눌러 실행; 아무 키나 누르면 종료", "---"),
"japanese": ("アプリ", "戻る", "アプリをタップして起動、任意キーで終了", "---"),
"german": ("Apps", "Zurück", "App antippen; beliebige Taste beendet", "---"),
"italy": ("App", "Indietro", "Tocca un'app; un tasto qualsiasi esce", "---"),
"spain": ("Apps", "Atrás", "Toca una app; cualquier tecla sale", "---"),
"poland": ("Aplikacje", "Wstecz", "Dotknij aplikacji; dowolny klawisz kończy", "---"),
"french": ("Applications", "Retour", "Touchez une app ; toute touche quitte", "---"),
"thai": ("แอป", "กลับ", "แตะแอปเพื่อเปิด กดปุ่มใดก็ได้เพื่อออก", "---"),
"ukrainian": ("Застосунки", "Назад", "Торкніться застосунку; будь-яка клавіша — вихід", "---"),
}
def ini_xml(entries):
body = "".join(f" <{k}>{v}</{k}>\r\n" for k, v in entries)
s = '<?xml version="3.9.7" encoding="utf-16"?>\r\n<resources>\r\n' + body + "</resources>\r\n"
return b"\xff\xfe" + s.encode("utf-16-le")
def main():
# .view
p = os.path.join(ROOT, "layout", "theme1", "hiby_apps_main.view")
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "w") as f:
f.write(VIEW)
# ini per language
for lang in LANGS:
title, back, hint, row = T[lang]
entries = [("apps_title", title), ("apps_back", back), ("apps_hint", hint),
("apps_note", "MeowAPPs/MyApp/app.json")]
for i in range(8):
entries.append((f"app_row_{i}", row))
d = os.path.join(ROOT, "str", lang)
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, "apps.ini"), "wb") as f:
f.write(ini_xml(entries))
print(f"assets written under {ROOT}")
print(" hiby_apps_main.view + apps.ini x", len(LANGS))
if __name__ == "__main__":
main()
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# Build the hello app (SDK B1 版): 链 meow_ui.a(已内含 meow_font.o),
# 额外链系统 libz.a(meow_font 的 inflate 依赖)+ -lpthread。静态 MIPS32r2 (musl sf)。
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
"$CC" -Os -static -I../sdk hello.c ../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread -o hello
ls -la hello
file hello
BIN
View File
Binary file not shown.
+119
View File
@@ -0,0 +1,119 @@
/* hello.c — MeowOS SDK 版示例应用(链 meow_ui.a,B1)。
*
* 契约(MEOW_APP_SDK.md §1/§3/§6):应用只调 meow_ui API —— 屏幕(back_buf +
* flush 上屏)与输入(meow_ui_run 事件循环)由框架接管;不再 open/mmap/ioctl
* /dev/fb0、不再读 /dev/input/event*。本应用验证「清屏 + 画文本 + 画按钮 +
* 响应触摸」这一最小工作闭环。
*
* 交互:
* - 点按钮 → tap 计数 +1(按钮标签实时更新);
* - 左缘左滑(抬手触发)→ 请求退出(回应用列表);滑动过程顶部出现随位移增长的进度条;
* - 物理键 POWER 不再映射 exit(§3.4.1:物理键系统统一管理,电源键=纯息屏/亮屏);
* - 空闲时 on_tick 每秒刷新一行状态(含运行秒数)。
*
* 宿主/qemu 离屏:设 MEOW_UI_DUMP=<ppm 路径> 时,首帧 flush 即把 back_buf 写成
* PPM 截图供检查;设备上无副作用。
*
* Build(见 build_hello.sh):
* zig-mipsel-cc -Os -static hello.c meow_ui.a libz.a -lpthread -o hello
*/
#include "meow_ui.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
static meow_widget *g_btn;
static int g_taps = 0;
static int g_secs = 0;
static char g_btn_label[32] = "TAP ME";
/* M2.51:统一日志改用 SDK meow_log()(按 app 分离 + 级别过滤 + 降级),
* 删除自造轮子的 hellolog()(自读 MEOW_LOG_DIR + snprintf + fopen/fprintf)。 */
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
static void redraw(void)
{
const meow_theme *th = meow_theme_get();
int sb = meow_ui_statusbar_height(); /* A-0 全局状态栏 inset:内容下移避免被盖 */
meow_ui_clear(th->bg);
meow_ui_text_center(240, sb + 8, "MEOWOS HELLO", 28, th->fg);
meow_ui_text_center(240, sb + 38, "SDK: meow_ui.a", 20, th->dim);
meow_widget_draw(g_btn);
char line[64];
snprintf(line, sizeof line, "taps=%d uptime=%ds", g_taps, g_secs);
meow_ui_text_center(240, 500, line, 20, th->dim);
meow_ui_text(20, 660, "left-swipe = exit", 20, th->dim);
meow_ui_flush();
}
static void on_tap(int x, int y)
{
if (meow_widget_hit(g_btn, x, y)) {
g_taps++;
snprintf(g_btn_label, sizeof g_btn_label, "TAPPED x%d", g_taps);
/* 重建按钮标签(release 旧控件,重新 create 保持几何) */
meow_widget_free(g_btn);
g_btn = meow_btn_create(120, 300, 240, 64, g_btn_label);
redraw();
}
}
static void on_key(int keycode, int is_down)
{
/* §3.4.1:物理键语义由系统统一管理,APP 禁映射(电源=息屏/亮屏、音量=调音量、播放=媒体)。
* 这里仅记录日志,不做任何 exit/confirm 行为。 */
(void)keycode; (void)is_down;
meow_log(MEOW_LOG_DEBUG, "key"); /* 不处理:SDK 内部消化系统键语义 */
}
static void on_swipe_progress(int x_start, int dx)
{
/* 左滑过程实时回调:SDK 已自动画顶部进度条;这里演示 app 侧接入(画同长度条,幂等——
* 长度=当前 X=x_start+dx,与 SDK 自动画一致,不会误清条)。
* 抬手达标时 SDK 清条 + 触发 on_swipe(LEFT)→退出。 */
meow_ui_swipe_hint(x_start + dx);
}
static void on_swipe(int dir, int dx, int dy)
{
(void)dx; (void)dy;
if (dir == MEOW_SWIPE_LEFT) { meow_log(MEOW_LOG_INFO, "exit:swipe"); meow_ui_request_exit(); }
}
static void on_tick(void)
{
/* 每秒刷新一次状态行(避免 16ms 高频重绘) */
static uint64_t last = 0;
uint64_t now = meow_ui_time_ms();
if (now - last >= 1000) {
last = now;
g_secs++;
redraw();
}
}
int main(void)
{
meow_log(MEOW_LOG_INFO, "start");
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { meow_log(MEOW_LOG_ERROR, "init failed"); return 1; }
/* M2.46 不再强制 set_mode 深色——主题跟随系统 display.theme/theme_mode(manual/auto+时段),
* 与 launcher s_ui_theme() 判定对齐(meow_theme_get() 已读 settings.conf)。 */
meow_theme_add_listener(NULL); /* noop: 预留主题变化重绘 */
signal(SIGTERM, on_sigterm); /* 看门狗返回键 → 干净退出 */
g_btn = meow_btn_create(120, 300, 240, 64, g_btn_label);
redraw();
meow_ui_events ev = { on_key, on_tap, on_swipe, on_tick, on_swipe_progress };
meow_ui_run(&ev);
meow_widget_free(g_btn);
meow_ui_shutdown();
meow_log(MEOW_LOG_INFO, "exit");
return 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# make_ov.sh — 组装真机 bind 测试用 overlay 包 /data/ov.tgz
#
# 内容:
# overlay/hiby_player <- patch v2.2 稳定版 (7caba121)
# overlay/meow_watchdog <- 看门狗 (bind 到 /usr/bin/meow_watchdog)
# overlay/theme1/ <- theme1 实体副本 + hiby_apps_main.view
# overlay/str/<13语言>/ <- str 实体副本 + apps.ini
#
# 设备侧: 推送 ov.tgz -> setup_ondev.sh (解包+bind+安全重启)
# 产物: /tmp/ov.tar.gz
set -e
ROOT="$HOME/Project/R3ProII"
PATCH="$ROOT/patch_v2"
DST=/tmp/ov
rm -rf "$DST"; mkdir -p "$DST/overlay"
cp -f "$PATCH/hiby_player_patched_v2" "$DST/overlay/hiby_player"
chmod 755 "$DST/overlay/hiby_player"
cp -f "$PATCH/meow_watchdog/meow_watchdog" "$DST/overlay/meow_watchdog"
chmod 755 "$DST/overlay/meow_watchdog"
# theme1 实体副本(避免符号链接自指循环, 见 DEPLOY_ATTEMPT_1.md)
cp -a "$ROOT/unpacked_v14/squashfs-root/usr/resource/layout/theme1" "$DST/overlay/theme1"
cp -f "$PATCH/apps_assets/layout/theme1/hiby_apps_main.view" "$DST/overlay/theme1/hiby_apps_main.view"
# str 实体副本
mkdir -p "$DST/overlay/str"
for lang in simplified_chinese traditional_chinese english russian korean japanese \
german italy spain poland french thai ukrainian; do
cp -a "$ROOT/unpacked_v14/squashfs-root/usr/resource/str/$lang" "$DST/overlay/str/$lang"
cp -f "$PATCH/apps_assets/str/$lang/apps.ini" "$DST/overlay/str/$lang/apps.ini"
done
cd /tmp/ov && tar cf /tmp/ov.tar overlay && gzip -f /tmp/ov.tar
echo "ov.tgz: $(stat -c%s /tmp/ov.tar.gz) B"
md5sum "$DST/overlay/hiby_player" "$DST/overlay/meow_watchdog"
ls -la "$DST/overlay/theme1/hiby_apps_main.view" "$DST/overlay/str/english/apps.ini"
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
#
# S90meow_launcherd - R3 Pro II Plan B app-launcher daemon (zero patch)
#
# Design (ANALYSIS.md §18):
# * PASSIVE daemon: never EVIOCGRABs, never injects events. Reads keys via
# EVIOCGKEY state polling (immune to any grab the player holds), touch via
# normal event reads on event1 (not grabbed). Zero interference with the
# player's own key handling.
# * Hotkey: hold PREVIOUS + VOL_UP together >=300ms -> app menu
# (cross-device: PREVIOUS is a GPIO key on event0, VOL_UP is an ADC key
# on event2. A two-key chord on event2 alone is impossible — its four
# keys share ONE ADC channel, so the old dual-volume hotkey can never
# register physically; see ANALYSIS.md §18.8).
# * Starts BEFORE S92_03_start_music_player (S90 < S92) so it is up early;
# the input devices are created by S11module_driver_default.
# * Stop sends SIGTERM; the daemon SIGCONTs a frozen player before exiting.
#
case "$1" in
start)
echo -n "Starting meow_launcherd: "
if [ -x /usr/bin/meow_launcherd ]; then
/usr/bin/meow_launcherd >/dev/null 2>&1 &
echo "OK"
else
echo "MISSING /usr/bin/meow_launcherd"
fi
;;
stop)
echo -n "Stopping meow_launcherd: "
if [ -f /usr/data/meow_launcherd.pid ]; then
kill "$(cat /usr/data/meow_launcherd.pid)" 2>/dev/null
fi
killall meow_launcherd 2>/dev/null
sleep 1
echo "OK"
;;
restart|reload)
"$0" stop
sleep 1
"$0" start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
exit 0
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# build_launcher.sh — MeowOS launcher (A-1/A-2: launcher 用 SDK 渲染栈).
#
# 变更点(相对 M2.27 / A-0):
# - launcher 不再自建 back_buf/meow_present/refresh_governor —— 改用 SDK 渲染栈:
# fb 指向 SDK 的 back_buf(meow_ui_back_buf),呈现统一 meow_ui_flush()(SDK present,
# 内含 vsync + 帧率门控 + 原子 blit 脏区);刷新率档位经 meow_ui_set_refresh_rate()/
# set_refresh_always_max() 交给 SDK(归口 PERF_SCHEDULER 显示子域)。
# - 播放器 fb 双缓冲交互(fb_backup_player/fb_norm_buf0/fb_restore_player/FBIOPAN/yoffset)
# 保留不动(与 present 上屏不同层)。
# - launcher 与 SDK 共用同一 meow_font.o(已打进 meow_ui.a)——不再单独链 meow_font.a。
# - MEOWOS_VERSION 0.7.21 → 0.7.23(M2.28)。
#
# Build(在 patch_v2/meow_launcherd 下):
# zig-mipsel-cc -I../sdk -Os -static meow_launcherd.c meow_touch.c meow_settings.c meow_notif.c ../sdk/meow_ui.a <sysroot>/lib/libz.a -lpthread -o meow_launcherd
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
[ -f "../sdk/meow_ui.a" ] || { echo "!! 缺 ../sdk/meow_ui.a,先跑 sdk/build_sdk.sh"; exit 1; }
"$CC" -I"../sdk" -Os -static meow_launcherd.c meow_touch.c meow_settings.c meow_notif.c "../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
echo "== meow_launcherd =="
ls -la meow_launcherd
md5sum meow_launcherd
file meow_launcherd
BIN
View File
Binary file not shown.
+268
View File
@@ -0,0 +1,268 @@
/* evtool.c — tiny evdev probe tool for R3 Pro II (static MIPS32r2 musl).
*
* Subcommands:
* evtool codes <dev> dump EV bits (types + key codes)
* evtool dump <dev> [dev...] read + print events (no grab)
* evtool send <dev> <type> <code> <value> inject one event via write()
* evtool grab <dev> [dev...] EVIOCGRAB, then read + print
* evtool fwd <dev> EVIOCGRAB, read, write back every event
* (forward experiment) + print
* evtool keys <dev> EVIOCGKEY poll loop; print state changes
* evtool grabchk <dev> EVIOCGRAB result only (0 / errno)
*
* Event struct is fixed 16 bytes on MIPS32: timeval(8) + type(2) + code(2) + value(4).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdint.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <linux/input.h>
#ifndef EVIOCGRAB
#define EVIOCGRAB _IOW('E', 0x90, int)
#endif
#ifndef EVIOCGKEY
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len)
#endif
#define EV_MAXC 0x1f
#define KEY_MAXC 0x2ff
#define KEY_WORDS ((KEY_MAXC + 1 + 31) / 32)
static unsigned long long now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static int open_dev(const char *dev, int wr)
{
int fd = open(dev, wr ? O_RDWR : O_RDONLY);
if (fd < 0) { fprintf(stderr, "open %s: %s\n", dev, strerror(errno)); return -1; }
return fd;
}
static void print_ev(const char *tag, const unsigned char *ev)
{
uint16_t type = *(uint16_t *)(ev + 8);
uint16_t code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
fprintf(stderr, "%s t=%llu type=%d code=%d val=%d\n",
tag, now_ms(), type, code, val);
}
/* ---------- codes ---------- */
static void cmd_codes(const char *dev)
{
int fd = open_dev(dev, 0);
if (fd < 0) return;
unsigned long evbits[EV_MAXC / 32 + 1];
memset(evbits, 0, sizeof evbits);
if (ioctl(fd, EVIOCGBIT(0, sizeof evbits), evbits) < 0) {
perror("EVIOCGBIT(0)"); return;
}
printf("EV bits:");
for (int i = 0; i <= EV_MAXC; i++)
if (evbits[i / 32] & (1UL << (i % 32))) printf(" %d", i);
printf("\n");
unsigned long kb[KEY_WORDS];
memset(kb, 0, sizeof kb);
if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof kb), kb) >= 0) {
printf("KEY codes:");
for (int i = 0; i <= KEY_MAXC; i++)
if (kb[i / 32] & (1UL << (i % 32))) printf(" %d", i);
printf("\n");
} else {
perror("EVIOCGBIT(EV_KEY)");
}
close(fd);
}
/* ---------- dump ---------- */
static void dump_dev(const char *dev, const char *tag)
{
int fd = open_dev(dev, 0);
if (fd < 0) return;
printf("opened %s as %s\n", dev, tag);
fflush(stdout);
struct pollfd pfd = { fd, POLLIN, 0 };
unsigned char ev[16];
for (;;) {
int r = poll(&pfd, 1, -1);
if (r < 0) { perror("poll"); break; }
if (pfd.revents & POLLIN) {
ssize_t n = read(fd, ev, sizeof ev);
if (n == 16) print_ev(tag, ev);
else if (n < 0) { perror("read"); break; }
}
}
close(fd);
}
static void cmd_dump(int argc, char **argv)
{
for (int i = 0; i < argc; i++) {
pid_t p = fork();
if (p == 0) { dump_dev(argv[i], argv[i]); _exit(0); }
}
for (;;) pause();
}
/* ---------- send ---------- */
static void cmd_send(const char *dev, int type, int code, int value)
{
int fd = open_dev(dev, 1);
if (fd < 0) return;
struct input_event ev;
memset(&ev, 0, sizeof ev);
ev.type = type; ev.code = code; ev.value = value;
ssize_t n = write(fd, &ev, sizeof ev);
printf("write(%s) -> %zd errno=%d (%s)\n", dev, n, errno, strerror(errno));
close(fd);
}
/* ---------- grab / fwd ---------- */
static int do_grab(int fd, int on)
{
int v = on ? 1 : 0;
int r = ioctl(fd, EVIOCGRAB, v);
printf("EVIOCGRAB(%d) -> %d errno=%d (%s)\n", on, r, errno, strerror(errno));
return r;
}
static void loop_forward(const char *dev, int fwd)
{
int fd = open_dev(dev, 1);
if (fd < 0) return;
if (do_grab(fd, 1) < 0) return;
printf("GRABBED %s (fwd=%d), reading...\n", dev, fwd);
fflush(stdout);
struct pollfd pfd = { fd, POLLIN, 0 };
unsigned char ev[16];
unsigned long long nread = 0, nwritten = 0;
for (;;) {
int r = poll(&pfd, 1, -1);
if (r < 0) { perror("poll"); break; }
if (pfd.revents & POLLIN) {
ssize_t n = read(fd, ev, sizeof ev);
if (n != 16) { if (n < 0) perror("read"); continue; }
nread++;
print_ev("G", ev);
if (fwd) {
ssize_t w = write(fd, ev, sizeof ev);
nwritten++;
if (w != 16) fprintf(stderr, " write-back failed: %s\n", strerror(errno));
}
}
}
close(fd);
}
/* ---------- fwdt: grab + self-inject + forward ---------- */
/* Sequence: grab <dev>; after wait_ms inject press(code); after +300ms inject
* release(code); meanwhile forward every read event back to <dev>.
* Optional 5th arg: total duration ms (0 = run forever).
* A second reader on <dev> tells us whether write-back reaches non-grabbers. */
static void loop_fwdt(const char *dev, int code, int wait_ms, int dur_ms)
{
int fd = open_dev(dev, 1);
if (fd < 0) return;
if (do_grab(fd, 1) < 0) return;
printf("FWDT grabbed %s code=%d wait=%d dur=%d\n", dev, code, wait_ms, dur_ms);
fflush(stdout);
struct pollfd pfd = { fd, POLLIN, 0 };
unsigned char ev[16];
unsigned long long t0 = now_ms(), inject_done = 0;
int phase = 0; /* 0=idle, 1=pressed, 2=released */
for (;;) {
unsigned long long t = now_ms();
if (dur_ms > 0 && t - t0 >= (unsigned long long)dur_ms) break;
if (!inject_done && t - t0 >= (unsigned long long)wait_ms) {
struct input_event e; memset(&e, 0, sizeof e);
e.type = EV_KEY; e.code = code; e.value = 1;
if (write(fd, &e, sizeof e) != sizeof e) fprintf(stderr, "inject press fail\n");
printf("== injected PRESS %d\n", code);
phase = 1; inject_done = 1;
} else if (phase == 1 && t - t0 >= (unsigned long long)wait_ms + 300) {
struct input_event e; memset(&e, 0, sizeof e);
e.type = EV_KEY; e.code = code; e.value = 0;
if (write(fd, &e, sizeof e) != sizeof e) fprintf(stderr, "inject release fail\n");
printf("== injected RELEASE %d\n", code);
phase = 2;
}
int r = poll(&pfd, 1, 100);
if (r < 0) { perror("poll"); break; }
if (pfd.revents & POLLIN) {
ssize_t n = read(fd, ev, sizeof ev);
if (n == 16) {
print_ev("RD", ev);
ssize_t w = write(fd, ev, sizeof ev);
if (w != 16) fprintf(stderr, " WB fail: %s\n", strerror(errno));
}
}
}
close(fd);
}
/* ---------- keys (EVIOCGKEY poll) ---------- */
static void cmd_keys(const char *dev, int dur_ms)
{
int fd = open_dev(dev, 0);
if (fd < 0) return;
unsigned long prev[KEY_WORDS], cur[KEY_WORDS];
memset(prev, 0, sizeof prev);
printf("EVIOCGKEY poll on %s (dur=%dms)\n", dev, dur_ms);
fflush(stdout);
unsigned long long t0 = now_ms();
for (;;) {
if (dur_ms > 0 && now_ms() - t0 >= (unsigned long long)dur_ms) break;
memset(cur, 0, sizeof cur);
if (ioctl(fd, EVIOCGKEY(sizeof cur), cur) < 0) { perror("EVIOCGKEY"); break; }
for (int w = 0; w < KEY_WORDS; w++) {
if (cur[w] != prev[w]) {
for (int b = 0; b < 32; b++) {
int cb = (cur[w] >> b) & 1;
int pb = (prev[w] >> b) & 1;
if (cb != pb)
printf("key %d %s\n", w * 32 + b, cb ? "DOWN" : "UP");
}
prev[w] = cur[w];
}
}
fflush(stdout);
usleep(10000); /* 10 ms */
}
close(fd);
}
int main(int argc, char **argv)
{
setvbuf(stderr, NULL, _IONBF, 0);
if (argc < 2) {
fprintf(stderr, "usage: evtool codes|dump|send|grab|fwd|keys|grabchk <args>\n");
return 1;
}
const char *cmd = argv[1];
if (!strcmp(cmd, "codes") && argc >= 3) { cmd_codes(argv[2]); return 0; }
if (!strcmp(cmd, "dump") && argc >= 3) { cmd_dump(argc - 2, argv + 2); return 0; }
if (!strcmp(cmd, "send") && argc >= 6) { cmd_send(argv[2], atoi(argv[3]), atoi(argv[4]), atoi(argv[5])); return 0; }
if (!strcmp(cmd, "grab") && argc >= 3) { loop_forward(argv[2], 0); return 0; }
if (!strcmp(cmd, "fwd") && argc >= 3) { loop_forward(argv[2], 1); return 0; }
if (!strcmp(cmd, "keys") && argc >= 3) { cmd_keys(argv[2], argc >= 4 ? atoi(argv[3]) : 0); return 0; }
if (!strcmp(cmd, "fwdt") && argc >= 4) { loop_fwdt(argv[2], atoi(argv[3]), argc >= 5 ? atoi(argv[4]) : 1000, argc >= 6 ? atoi(argv[5]) : 0); return 0; }
if (!strcmp(cmd, "grabchk") && argc >= 3) {
int fd = open_dev(argv[2], 1);
if (fd >= 0) { do_grab(fd, 1); do_grab(fd, 0); close(fd); }
return 0;
}
fprintf(stderr, "bad args\n");
return 1;
}
+38
View File
@@ -0,0 +1,38 @@
/* fbcopy — copy buffer0 -> buffer1 within /dev/fb0 (mmap both, memcpy).
* Reproduces the player's "screen-off frame lives in buffer1" state so the
* launcher's bug condition (yoffset=1 at menu open) can be tested without
* pressing power (which would also pause the player). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <stdint.h>
#define GETV 0x4600
int main(void) {
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
unsigned char v[160]; memset(v,0,sizeof v);
if (ioctl(fd, GETV, v) < 0) { perror("getvar"); return 1; }
uint32_t yres = *(uint32_t*)(v+4);
uint32_t yv = *(uint32_t*)(v+12);
uint32_t bpp = *(uint32_t*)(v+24);
if (yv <= yres) { printf("not double-buffered (yv=%u yres=%u)\n", yv, yres); return 2; }
/* line_length via fix info */
unsigned char f[68]; memset(f,0,sizeof f);
if (ioctl(fd, 0x4602, f) < 0) { perror("getfix"); return 1; }
uint32_t line = *(uint32_t*)(f+44);
if (!line) line = yres * (bpp/8);
size_t frame = (size_t)line * yres;
unsigned char *p = mmap(NULL, frame*2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) { perror("mmap"); return 1; }
memcpy(p + frame, p, frame); /* buffer1 <- buffer0 */
printf("copied %zu bytes buffer0 -> buffer1 (line=%u yres=%u frame=%zu)\n", frame, line, yres, frame);
munmap(p, frame*2);
close(fd);
return 0;
}
+34
View File
@@ -0,0 +1,34 @@
/* fbpan — set /dev/fb0 yoffset (pan) to argv[1] and print the result. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <stdint.h>
#define GETV 0x4600
#define PAN 0x4606
int main(int argc, char **argv) {
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
unsigned char v[160]; memset(v,0,sizeof v);
if (ioctl(fd, GETV, v) < 0) { perror("getvar"); return 1; }
uint32_t orig = *(uint32_t*)(v+20);
uint32_t yres = *(uint32_t*)(v+4);
uint32_t target = argc > 1 ? (uint32_t)atoi(argv[1]) : 0;
if (yres) target = (target / yres) * yres; /* snap to whole-screen offset */
*(uint32_t*)(v+16) = 0;
*(uint32_t*)(v+20) = target;
errno = 0;
int r = ioctl(fd, PAN, v);
int e = errno;
unsigned char w[160]; memset(w,0,sizeof w);
ioctl(fd, GETV, w);
printf("pan(%u): r=%d errno=%d(%s) yoffset %u -> %u (yres=%u)\n",
target, r, e, strerror(e), orig, *(uint32_t*)(w+20), yres);
close(fd);
return 0;
}
+75
View File
@@ -0,0 +1,75 @@
/* fbpantest.c — empirically find the ioctl that pans /dev/fb0 by yoffset.
* Confirmed already: fb0 is 480x720 RGB565, yres_virtual=1440 (double),
* yoffset selects the displayed buffer, ypanstep=1. We try candidate ioctls
* that set yoffset to the OTHER buffer and read yoffset back; whichever
* changes it is the pan mechanism. We restore yoffset immediately.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <stdint.h>
#define GETV 0x4600
static int get_info(int fd, uint32_t *yoff, uint32_t *yres) {
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fd, GETV, v) < 0) return -1;
*yres = *(uint32_t*)(v + 4);
*yoff = *(uint32_t*)(v + 20);
return 0;
}
static void write_yoffset_to(int fd, uint32_t cmd, uint32_t yoffset) {
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fd, GETV, v) < 0) return;
/* modify only the offset fields, keep everything else */
*(uint32_t*)(v + 16) = 0;
*(uint32_t*)(v + 20) = yoffset;
ioctl(fd, cmd, v);
}
int main(void) {
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0) { perror("open /dev/fb0"); return 1; }
uint32_t orig = 0, yres = 0;
if (get_info(fd, &orig, &yres) < 0) { perror("getvar"); return 1; }
uint32_t target = (orig < yres) ? yres : 0;
printf("orig yoffset=%u yres=%u target(other buf)=%u\n", orig, yres, target);
unsigned tests[] = { 0x4601, 0x4606 }; /* PUT_VSCREENINFO, PAN_DISPLAY(nr 0x06) */
for (unsigned i = 0; i < sizeof(tests)/sizeof(tests[0]); i++) {
uint32_t cmd = tests[i];
write_yoffset_to(fd, cmd, target);
uint32_t now = 0, tmp = 0;
int ok = get_info(fd, &now, &tmp);
printf(" cmd=0x%04X -> yoffset=%u (target %u) %s\n",
cmd, now, target, (ok==0 && now==target) ? "*** PAN WORKS ***" : "");
/* restore */
if (ok == 0 && now == target) {
write_yoffset_to(fd, cmd, orig);
uint32_t back = 0;
get_info(fd, &back, &tmp);
printf(" restored to yoffset=%u\n", back);
break;
}
}
/* also probe FBIOBLANK candidate 0x4611 and 0x4603 just to report which blanks */
errno = 0;
int b1 = ioctl(fd, 0x4603, 5);
int e1 = errno; errno = 0;
int b2 = ioctl(fd, 0x4611, 5);
int e2 = errno;
printf(" blank 0x4603(5) r=%d errno=%d ; 0x4611(5) r=%d errno=%d\n", b1, e1, b2, e2);
/* ensure unblanked back to 0 regardless */
ioctl(fd, 0x4611, 0); /* unblank (works per launcher) */
close(fd);
return 0;
}
+64
View File
@@ -0,0 +1,64 @@
/* fbpantest2.c — does fb0 actually PAN? Print ioctl return + errno.
* Key question: is the display pannable via FBIOPAN_DISPLAY/FBIOPUT_VSCREENINFO,
* or is the double buffer flipped by direct LCDC register writes (desar) that
* the fb var yoffset never reflects?
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <stdint.h>
#define GETV 0x4600
static int get_info(int fd, uint32_t *yoff, uint32_t *yres) {
unsigned char v[160]; memset(v,0,sizeof v);
if (ioctl(fd, GETV, v) < 0) return -1;
*yres = *(uint32_t*)(v + 4);
*yoff = *(uint32_t*)(v + 20);
return 0;
}
int main(void) {
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
uint32_t orig=0,yres=0; get_info(fd,&orig,&yres);
uint32_t target=(orig<yres)?yres:0;
printf("orig yoffset=%u yres=%u target=%u\n", orig,yres,target);
unsigned char v[160];
memset(v,0,sizeof v);
ioctl(fd, GETV, v);
*(uint32_t*)(v+16)=0; *(uint32_t*)(v+20)=target;
v[84]=0; /* activate = 0 (implies activate-on-next?) */
errno=0;
int r6=ioctl(fd, 0x4606, v);
int e6=errno; errno=0;
uint32_t now=0; get_info(fd,&now,&yres);
printf("PAN_DISPLAY(0x4606) r=%d errno=%d(%s) -> yoffset now %u\n", r6,e6,strerror(e6),now);
if (now==target) { errno=0; ioctl(fd,0x4606,v); /* restore */ }
/* now try PUT_VSCREENINFO with activate=FB_ACTIVATE_NOW */
memset(v,0,sizeof v); ioctl(fd, GETV, v);
*(uint32_t*)(v+16)=0; *(uint32_t*)(v+20)=target;
errno=0;
int r1=ioctl(fd, 0x4601, v);
int e1=errno; errno=0;
get_info(fd,&now,&yres);
printf("PUT_VSCREENINFO(0x4601) r=%d errno=%d(%s) -> yoffset now %u\n", r1,e1,strerror(e1),now);
if (now==target) { memset(v,0,sizeof v); ioctl(fd,GETV,v); *(uint32_t*)(v+16)=0; *(uint32_t*)(v+20)=orig; ioctl(fd,0x4601,v); }
/* getid; also read FBIOGET_DISPINFO(0x4618) */
unsigned char d[256]; memset(d,0,sizeof d);
errno=0;
int rd=ioctl(fd, 0x4618, d);
int ed=errno;
printf("GET_DISPINFO(0x4618) r=%d errno=%d(%s)\n", rd, ed, strerror(ed));
for (unsigned i=0;i<64;i+=4){ if (*(uint32_t*)(d+i)) printf(" d[%u]=%08x\n", i, *(uint32_t*)(d+i)); }
close(fd);
return 0;
}
+80
View File
@@ -0,0 +1,80 @@
/* fbprobe.c — inspect /dev/fb0 geometry & current pan position.
* Device-verified ioctl numbers (do NOT swap): FBIOGET_VSCREENINFO=0x4600,
* FBIOGET_FSCREENINFO=0x4602, FBIOBLANK=0x4611.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <stdint.h>
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOGET_FSCREENINFO 0x4602
#define FBIOBLANK 0x4611
#define FBIO_PAN_DISPLAY 0x4606
#define FBIO_WAITFORVSYNC 0x4620
int main(void)
{
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0) { perror("open /dev/fb0"); return 1; }
unsigned char v[160], f[68];
memset(v, 0, sizeof v); memset(f, 0, sizeof f);
if (ioctl(fd, 0x4600, v) < 0) { perror("vscreeninfo(0x4600)"); return 1; }
if (ioctl(fd, 0x4602, f) < 0) { perror("fscreeninfo(0x4602)"); return 1; }
uint32_t xres = *(uint32_t*)(v + 0);
uint32_t yres = *(uint32_t*)(v + 4);
uint32_t xv = *(uint32_t*)(v + 8);
uint32_t yv = *(uint32_t*)(v + 12);
uint32_t xoff = *(uint32_t*)(v + 16);
uint32_t yoff = *(uint32_t*)(v + 20);
uint32_t bpp = *(uint32_t*)(v + 24);
uint32_t vmode = *(uint32_t*)(v + 80);
uint32_t smem_len = *(uint32_t*)(f + 20);
uint32_t visual = *(uint32_t*)(f + 32);
uint16_t xs = *(uint16_t*)(f + 36);
uint16_t ys = *(uint16_t*)(f + 38);
uint32_t line_len = *(uint32_t*)(f + 44);
uint32_t type = *(uint32_t*)(f + 24);
printf("V: xres=%u yres=%u xres_v=%u yres_v=%u xoffset=%u yoffset=%u bpp=%u vmode=%u\n",
xres, yres, xv, yv, xoff, yoff, bpp, vmode);
printf("V: red(offset%u,len%u) green(off%u,len%u) blue(off%u,len%u) transp(off%u,len%u)\n",
*(uint32_t*)(v+28)&0xFF, *(uint32_t*)(v+28)>>8,
*(uint32_t*)(v+32)&0xFF, *(uint32_t*)(v+32)>>8,
*(uint32_t*)(v+36)&0xFF, *(uint32_t*)(v+36)>>8,
*(uint32_t*)(v+40)&0xFF, *(uint32_t*)(v+40)>>8);
printf("F: id='%.16s' smem_len=%u type=%u visual=%u xpanstep=%u ypanstep=%u line_length=%u\n",
(char*)f, smem_len, type, visual, xs, ys, line_len);
unsigned long one = (unsigned long)line_len * yres;
printf("frame size (line*yres) = %lu ; 2*frame = %lu ; smem_len = %u\n",
one, one * 2, smem_len);
printf("DOUBLE-BUFFER? %s\n",
(smem_len >= one * 2) ? "YES (>= 2 frames in smem)" : "single (1 frame)");
printf("YRES_VIRTUAL = %u ; yoffset = %u ; displayed buffer index = %u\n",
yv, yoff, yv > 0 ? (yoff / (yres ? yres : 1)) : 0);
printf("display starts at byte %u (=%u*line_len)\n", yoff * line_len, yoff);
printf("=> visible window x=%u..%u y=%u..%u (of %ux%u virtual)\n",
xoff, xoff + xres, yoff, yoff + yres, xv, yv);
/* try FBIOPAN_DISPLAY to read back set-able + WAITFORVSYNC presence */
unsigned char pan[160];
memcpy(pan, v, sizeof pan);
pan[20] = 0; pan[16] = 0; /* yoffset=0 xoffset=0, only to test */
errno = 0;
/* don't actually pan (avoid disturbing the player); just report errno of a
* no-op probe would need a real pan. Instead check WAITFORVSYNC support. */
uint32_t zero = 0;
errno = 0;
int wfs = ioctl(fd, 0x4620, &zero);
printf("FBIO_WAITFORVSYNC(0x4620) = %d (errno=%d %s)\n", wfs, errno, strerror(errno));
close(fd);
return 0;
}
+64
View File
@@ -0,0 +1,64 @@
/* fbtool — fb0 helper: pan <yoffs> | blank <0|4> | copy | status.
* Used to seed the double-buffer bug condition and clean up /dev/fb0.
* ioctls: GETV 0x4600, GETFIX 0x4602, PAN 0x4606, BLANK 0x4611.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <errno.h>
#include <stdint.h>
#define GETV 0x4600
#define GETFIX 0x4602
#define PAN 0x4606
#define BLANK 0x4611
static int set_blank(int fd, int bl) {
errno = 0;
int r = ioctl(fd, BLANK, bl);
printf("blank(%d) r=%d errno=%d\n", bl, r, errno);
return r;
}
static int set_pan(int fd, int yoff) {
unsigned char v[160]; memset(v,0,sizeof v);
if (ioctl(fd, GETV, v) < 0) { perror("getvar"); return -1; }
*(uint32_t*)(v+16)=0; *(uint32_t*)(v+20)=(uint32_t)yoff;
errno = 0;
int r = ioctl(fd, PAN, v);
unsigned char w[160]; memset(w,0,sizeof w);
ioctl(fd, GETV, w);
printf("pan(%u) r=%d errno=%d yoffset now=%u\n", yoff, r, errno, *(uint32_t*)(w+20));
return r;
}
static int do_copy(int fd) {
unsigned char v[160]; memset(v,0,sizeof v);
if (ioctl(fd, GETV, v) < 0) { perror("getvar"); return -1; }
uint32_t yres=*(uint32_t*)(v+4), yv=*(uint32_t*)(v+12), bpp=*(uint32_t*)(v+24);
unsigned char f[68]; memset(f,0,sizeof f);
if (ioctl(fd, GETFIX, f) < 0) { perror("getfix"); return -1; }
uint32_t line = *(uint32_t*)(f+44); if (!line) line = yres*(bpp/8);
if (yv < yres) { printf("not double-buffered\n"); return -1; }
size_t frame = (size_t)line * yres;
unsigned char *p = mmap(NULL, frame*2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) { perror("mmap"); return -1; }
memcpy(p+frame, p, frame);
munmap(p, frame*2);
printf("copy buffer0 -> buffer1 (%zu bytes)\n", frame);
return 0;
}
int main(int argc, char **argv) {
if (argc < 2) { fprintf(stderr,"usage: fbtool {blank 0|4} | {pan N} | {copy}\n"); return 2; }
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
int r = 0;
if (!strcmp(argv[1],"blank") && argc>2) r = set_blank(fd, atoi(argv[2]));
else if (!strcmp(argv[1],"pan") && argc>2) r = set_pan(fd, atoi(argv[2]));
else if (!strcmp(argv[1],"copy")) r = do_copy(fd);
else fprintf(stderr,"unknown cmd\n"), r=2;
close(fd);
return r<0?1:0;
}
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# gen_ui_font.py — 预渲染 MeowOS UI 位图字库(替代 freetype,零依赖)
#
# 输入默认字体(本脚本用设备自带 default.otf,因为 ~/Fonts 属主为 10195 不可读;
# 如需切换思源黑体/MiSans,改 SRC 路径并把字体拷到可读处,或经 meow.conf font_file= 指定)。
# 输出 ui_font.dat:自定义二进制位图字库(8-bit alpha 灰度,白字用 alpha,blit 时着色)。
# 布局(小端):
# +0 uint32 magic "UIF1"
# +4 uint16 px (设计字号,用于行距/参考)
# +6 uint16 flags (0 = 8-bit alpha 灰度)
# +8 uint32 n (glyph 数)
# +12 uint16 atlas_w (位图集宽)
# +14 uint16 atlas_h
# +16 16*n entry: uint32 cp; uint16 x,y,w,h,adv
# +16+16*n bitmap: atlas_w*atlas_h 字节(8-bit alpha)
#
# 字符集:ASCII 0x20-0x7E + GB2312 一级汉字(0xB0-0xD7) + 常用 CJK 标点 + UI 显式字符串。
import struct, sys
from PIL import Image, ImageDraw, ImageFont
SRC = "fw_ssh/squashfs-root/usr/resource/fonts/default.otf"
PX = 24 # 设计像素高(大字,配合 40px 状态栏)
OUT = sys.argv[1] if len(sys.argv) > 1 else "/tmp/ftg/ui_font.dat"
# ---- 字符集 ----
chars = set()
for c in range(0x20, 0x7F):
chars.add(chr(c)) # ASCII 可打印
# GB2312 一级汉字: 0xB0A1(176,161) .. 0xD7F9(215,249),每行 94
for row in range(0xB0, 0xD8):
for col in range(0xA1, 0xFF):
b = bytes([row, col])
try:
ch = b.decode('gb2312')
except Exception:
continue
if ch and not ch.startswith(' '):
# 去掉 PUA/未定义区
if ord(ch[0]) >= 0x4E00 or (0xFF00 <= ord(ch[0]) <= 0xFFEF):
chars.add(ch[0])
# 常用 CJK 标点 / 全角
for ch in ",。、!?:;()《》【】「」『』—…¥%·×÷=+-·  〇":
chars.add(ch)
# UI 显式中文(电源菜单/状态栏/提示),确保必有
for ch in "关机重启回到取消电源菜单音量蓝牙电量充电返回播放器需网络设置我你的好中英文数分秒年月日应用没有存储卡请插入后开吃到找到放缺失导航打触摸选择无法启动以确认上下":
chars.add(ch)
chars = sorted(chars)
# 宽字符(CJK)用 PX 高,ASCII 用较小一点以保持基线一致;Pillow 自带 advance
font = ImageFont.truetype(SRC, PX)
# ---- 栅格化并打包进 atlas(行优先)----
# 先用足够大的空白画布测量每个字符 bitmap,再打包
tmp_img_cache = {}
maxw = PX * 2
packed = [] # (char, bbox_img, w, h, adv)
scale = 1
# 计算每个字符的 bitmap(grayscale alpha)
metas = []
for ch in chars:
if ch == ' ':
# 空格无 bitmap,只记 advance
adv = font.getlength(' ')
metas.append((ch, None, 0, 0, int(adv)))
continue
try:
bbox = font.getbbox(ch)
except Exception:
bbox = None
if not bbox:
adv = int(font.getlength(ch))
metas.append((ch, None, 0, 0, adv))
continue
x0, y0, x1, y1 = bbox
w = max(1, x1 - x0); h = max(1, y1 - y0)
# 渲染到 grayscale (alpha=灰度)
im = Image.new('L', (w, h), 0)
# 用 getmask 更精确;这里直接画到临时图再裁
mask = font.getmask(ch, mode='L')
im = Image.frombytes('L', mask.size, bytes(mask))
# 裁到 bbox 对应的部分
if 0 <= y0 < im.height and 0 <= x0 < im.width:
im = im.crop((max(0, x0), max(0, y0), min(im.width, x1), min(im.height, y1)))
adv = int(font.getlength(ch))
metas.append((ch, im, im.width, im.height, adv))
# 打包:放入 atlas(简单行打包,一行放满再换行)
atlas_w = 512
atlas_rows = []
x = 0; y = 0; maxY = 0
entry_pos = {} # char -> (x,y,w,h,adv)
# 先按宽高打包
pos_list = []
curx = 0; cury = 0; rowh = 0
entries = []
for ch, im, w, h, adv in metas:
if im is None:
entries.append((ch, 0, 0, 0, 0, adv)); continue
if curx + w > atlas_w:
curx = 0; cury += rowh; rowh = 0
entry_pos[ch] = (curx, cury, w, h)
entries.append((ch, curx, cury, w, h, adv))
curx += w + 1
rowh = max(rowh, h)
if cury + rowh > 4096:
raise RuntimeError("atlas too tall")
atlas_h = cury + rowh
atlas = bytearray(atlas_w * atlas_h)
for ch, im, w, h, adv in metas:
if im is None: continue
x, y, w, h = entry_pos[ch][0], entry_pos[ch][1], entry_pos[ch][2], entry_pos[ch][3]
for yy in range(h):
for xx in range(w):
v = im.getpixel((xx, yy))
atlas[(y + yy) * atlas_w + (x + xx)] = v
# ---- 写文件 ----
with open(OUT, 'wb') as f:
f.write(struct.pack('<I', 0x55494631)) # magic "UIF1"
f.write(struct.pack('<H', PX)) # px
f.write(struct.pack('<H', 0)) # flags 8-bit alpha
f.write(struct.pack('<I', len(entries))) # n
f.write(struct.pack('<H', atlas_w)) # atlas_w
f.write(struct.pack('<H', atlas_h)) # atlas_h
for ch, x, y, w, h, adv in entries:
f.write(struct.pack('<IHHHHH', ord(ch), x, y, w, h, adv))
f.write(bytes(atlas))
print(f"chars={len(chars)} entries={len(entries)} atlas={atlas_w}x{atlas_h} out={OUT}")
import os
print(f"size={os.path.getsize(OUT)} bytes")
# 自检:渲染取样图(含中文 + 英文)
img = Image.new('RGB', (480, 40), (0,0,0))
d = ImageDraw.Draw(img)
f = ImageFont.truetype(SRC, PX)
d.text((4, 4), "关机重启 回到HibyOS 取消 音量蓝牙 12:30:45 100%", font=f, fill=(255,255,255))
img.save('/tmp/ftg/ui_font_sample.png')
print("sample saved /tmp/ftg/ui_font_sample.png")
@@ -0,0 +1,8 @@
#!/bin/sh
# meow_card_path.sh - TF 卡 MeowOS/opt/bin(外围 CLI 迁卡后)在卡在时前插 PATH;无卡跳过不崩。
# 安装到 rootfs /etc/profile.d/(由 /etc/profile 对登录 shell source)。
# 使 SSH 登录会话可直接用卡上工具(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd)。
# MeowOS 启动器把该目录前插进自启子进程的 PATH(见 meow_launcherd.c);本文件覆盖 SSH 会话。
if [ -d /usr/data/mnt/sd_0/MeowOS/opt/bin ]; then
export PATH="/usr/data/mnt/sd_0/MeowOS/opt/bin:$PATH"
fi
+57
View File
@@ -0,0 +1,57 @@
#ifndef MEOW_FONT_H
#define MEOW_FONT_H
/*
* meow_font.h — MeowOS 预渲染位图字体运行时(独立、可复用,不绑定启动器)。
*
* 数据来源:由 gen/gen_font.py 生成的 font.png(灰度 PNG,bit depth 4|8,含各字号面)
* + font.idx(索引:头部 + 每面元数据 + 每字符 16B 记录)。
*
* 设备端加载:模块内建最小 PNG 解码(仅支持灰度非隔行)经 zlib inflate + 反滤波还原像素,
* 无需 libpng;交叉链接 musl 静态 libz.a。像素面按 depth=4|8 打包存储,取像素时按 nibble 解包。
*
* 绘制模型:drawtext(fb, x, y, utf8, color, size);(x,y)=行首「升线(top)」锚点;
* baseline = y + ascent;每字形 blit 于 (x + pen + dx, y + dy);pen 累加 advance。
* color 为 565 位 RGB(R 在 bit15..11,G bit10..5,B bit4..0),与 launcher 一致。
* 无字回退:缺码位画空心方块(tofu)并前进空格宽度。
*
* 字号:字库含若干原生面(如 28、20)。drawtext 请求 size 时先找精确匹配面;
* 若无则就近取一面做最近邻缩放(最优仍用原生字号)。
*/
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 设置 framebuffer 几何:宽/高(像素)与每行字节数(如 480*2)。绘制前调用一次。 */
void meow_font_set_fb(unsigned fb_w, unsigned fb_h, unsigned pitch_bytes);
/* 加载字库(全部字号)。成功返回 0,失败返回负值。可重复调用(先卸载旧的)。 */
int meow_font_load(const char *png_path, const char *idx_path);
/* 释放字库。 */
void meow_font_unload(void);
/* 某字号是否存在(命中原生面返回 1)。 */
int meow_font_has_size(int size);
/* UTF-8 字符串渲染宽度(px)。字面不存在返回 0。 */
int meow_font_width_text(const char *utf8, int size);
/* 行高(px = ascent + descent)。 */
int meow_font_line_height(int size);
int meow_font_ascent(int size);
/* 绘制文本:(x,y)=行首升线顶部;color=RGB565。已按 fb_w/fb_h 裁剪。 */
void meow_font_drawtext(uint16_t *fb, int x, int y, const char *utf8,
uint16_t color, int size);
/* 描边绘制:先以 outline_color 将每字形向 4/8 邻域平移 ol 绘制一圈,再以 color 填充。 */
void meow_font_drawtext_outline(uint16_t *fb, int x, int y, const char *utf8,
uint16_t color, uint16_t outline_color, int ol, int size);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_FONT_H */
BIN
View File
Binary file not shown.
+334
View File
@@ -0,0 +1,334 @@
/* meow_ipc.c — MeowOS IPC 测试钩子(阶段 D · M0+M1;与 evtool 同定位)。
*
* 向 meow_launcherd 的 AF_UNIX IPC(FRAMEWORK_DESIGN §1)发帧、收应答并打印。
* 静态 MIPS32r2 musl(用 zig-mipsel-cc 编译),qemu-mipsel 亦可用。
*
* 子命令:
* meow_ipc hello <app_id> [version] [flags] → MSG_HELLO,期望 MSG_ACK
* meow_ipc ping [app_id] → hello 别名(默认 meow_ipc)
* meow_ipc query <app_id> <activity> → MSG_ACT_QUERY,期望 MSG_ACT_RESULT
* meow_ipc declare <app_id> <route_id=chain> [...] → MSG_ACT_DECLARE,期望 MSG_ACK
* 全局:
* --sock <path> 覆盖 socket 路径(默认可被 MEOW_IPC_SOCK 覆盖,再回退 /usr/data/meow_ipc.sock)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include "meow_ipc.h"
static const char *g_sock = NULL;
static void print_err(const m_err_t *e)
{
fprintf(stderr, "ERR 0x%x: %.*s\n", e->code, e->msg_len < 64 ? e->msg_len : 64, e->msg);
}
static int connect_path(const char *path)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return -1; }
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) {
fprintf(stderr, "connect %s: %s\n", path, strerror(errno));
close(fd);
return -1;
}
return fd;
}
/* 发一帧(头+负载),读回一帧(头+负载)。返回应答类型;-1=失败。 */
static int xchg(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len,
uint8_t *rbuf, uint32_t rcap)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &len, 4);
size_t off = 0;
while (off < sizeof hdr) {
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
if (w <= 0) { perror("write hdr"); return -1; }
off += (size_t)w;
}
if (len && payload) {
size_t p = 0;
while (p < len) {
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
if (w <= 0) { perror("write payload"); return -1; }
p += (size_t)w;
}
}
/* read response header */
uint8_t rhdr[MEOW_IPC_HDR_LEN];
off = 0;
while (off < sizeof rhdr) {
ssize_t r = read(fd, rhdr + off, sizeof rhdr - off);
if (r <= 0) { perror("read hdr"); return -1; }
off += (size_t)r;
}
uint32_t rmagic, rlen;
uint16_t rver, rtype;
memcpy(&rmagic, rhdr, 4);
memcpy(&rver, rhdr + 4, 2);
memcpy(&rtype, rhdr + 6, 2);
memcpy(&rlen, rhdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC) { fprintf(stderr, "bad resp magic %08x\n", rmagic); return -1; }
if (rver != MEOW_IPC_VERSION) { fprintf(stderr, "bad resp version %u\n", rver); return -1; }
if (rlen > rcap) rlen = rcap;
off = 0;
while (off < rlen) {
ssize_t r = read(fd, rbuf + off, rlen - off);
if (r <= 0) { perror("read payload"); return -1; }
off += (size_t)r;
}
return (int)rtype;
}
static int cmd_hello(const char *app_id, uint32_t version, uint32_t flags)
{
const char *sock = g_sock;
int fd = connect_path(sock);
if (fd < 0) return 1;
m_hello_t h;
memset(&h, 0, sizeof h);
strncpy(h.app_id, app_id, MEOW_APP_ID_MAX - 1);
h.version = version;
h.flags = flags;
uint8_t rbuf[256];
int rt = xchg(fd, MSG_HELLO, 1, &h, sizeof h, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("HELLO ok: code=%d status=%u (apps registered)\n", a.code, a.status);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "HELLO: unexpected resp type 0x%04x\n", rt);
return 1;
}
static int cmd_query(const char *app_id, const char *activity)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
m_act_query_t q;
memset(&q, 0, sizeof q);
strncpy(q.app_id, app_id, MEOW_APP_ID_MAX - 1);
strncpy(q.activity, activity, MEOW_ACT_MAX - 1);
uint8_t rbuf[256];
int rt = xchg(fd, MSG_ACT_QUERY, 2, &q, sizeof q, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACT_RESULT) {
m_act_result_t r;
memcpy(&r, rbuf, sizeof r);
size_t cl = r.chain_len < MEOW_CHAIN_MAX ? r.chain_len : MEOW_CHAIN_MAX - 1;
char chain[MEOW_CHAIN_MAX];
memcpy(chain, r.chain, cl);
chain[cl] = 0;
printf("QUERY ok: app=%s act=%s depth=%u chain=%s\n", app_id, activity,
r.depth, cl ? chain : "(empty)");
return r.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "QUERY: unexpected resp type 0x%04x\n", rt);
return 1;
}
static int cmd_declare(const char *app_id, int nent, char **ents)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
m_act_declare_t d;
memset(&d, 0, sizeof d);
strncpy(d.app_id, app_id, MEOW_APP_ID_MAX - 1);
int n = 0;
char *p = d.entries;
int cap = (int)sizeof(d.entries);
for (int i = 0; i < nent && n < 8; i++) {
size_t l = strlen(ents[i]);
if (l + 1 > (size_t)(d.entries + sizeof d.entries - p)) break;
memcpy(p, ents[i], l + 1);
p += l + 1;
n++;
}
d.count = (uint16_t)n;
uint32_t len = (uint32_t)(sizeof d.app_id + 2 + 2 + (p - d.entries));
uint8_t rbuf[256];
int rt = xchg(fd, MSG_ACT_DECLARE, 3, &d, len, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("DECLARE ok: app=%s n=%d code=%d\n", app_id, n, a.code);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "DECLARE: unexpected resp type 0x%04x\n", rt);
return 1;
}
static int cmd_focus(const char *app_id, const char *activity)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
m_act_focus_t f;
memset(&f, 0, sizeof f);
strncpy(f.app_id, app_id, MEOW_APP_ID_MAX - 1);
strncpy(f.activity, activity, MEOW_ACT_MAX - 1);
uint8_t rbuf[256];
int rt = xchg(fd, MSG_ACT_FOCUS, 4, &f, sizeof f, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("FOCUS ok: app=%s act=%s code=%d\n", app_id, activity, a.code);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "FOCUS: unexpected resp type 0x%04x\n", rt);
return 1;
}
static int cmd_back(void)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
uint8_t rbuf[256];
int rt = xchg(fd, MSG_ACT_BACK, 5, NULL, 0, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("BACK ok: code=%d\n", a.code);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "BACK: unexpected resp type 0x%04x\n", rt);
return 1;
}
/* M2a:推送通知 → MSG_NOTIFY,期望 MSG_NOTIFY_ACK。 */
static int cmd_notify(const char *app_id, const char *category, const char *summary,
const char *body, uint32_t replaces_id, int ongoing, int media, int progress)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
m_notify_t n;
memset(&n, 0, sizeof n);
strncpy(n.app_id, app_id, MEOW_APP_ID_MAX - 1);
strncpy(n.category, category, sizeof n.category - 1);
strncpy(n.summary, summary, sizeof n.summary - 1);
if (body) strncpy(n.body, body, sizeof n.body - 1);
n.replaces_id = replaces_id;
n.ongoing = ongoing ? 1 : 0;
n.media = media ? 1 : 0;
n.progress = progress;
n.expire_timeout = ongoing ? 0xFFFFFFFFu : 0; /* ONGOING 常驻 */
uint8_t rbuf[256];
int rt = xchg(fd, MSG_NOTIFY, 6, &n, sizeof n, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_NOTIFY_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("NOTIFY ok: id=%u code=%d\n", a.status, a.code);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "NOTIFY: unexpected resp type 0x%04x\n", rt);
return 1;
}
/* M2a:撤下通知 → MSG_NOTIFY_DISMISS,期望 MSG_ACK。 */
static int cmd_dismiss(uint32_t id)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
uint8_t rbuf[256];
int rt = xchg(fd, MSG_NOTIFY_DISMISS, 7, &id, sizeof id, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("DISMISS ok: id=%u code=%d\n", id, a.code);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "DISMISS: unexpected resp type 0x%04x\n", rt);
return 1;
}
int main(int argc, char **argv)
{
const char *env = getenv("MEOW_IPC_SOCK");
g_sock = (env && env[0]) ? env : MEOW_IPC_PATH;
/* parse leading global options */
int i = 1;
for (; i < argc; i++) {
if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock = argv[++i]; }
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
fprintf(stderr, "usage: meow_ipc [--sock <path>] hello|ping|query|declare ...\n");
return 0;
}
else break;
}
if (i >= argc) { fprintf(stderr, "bad args; try --help\n"); return 1; }
const char *cmd = argv[i];
if (!strcmp(cmd, "hello")) {
if (i + 1 >= argc) { fprintf(stderr, "hello needs <app_id>\n"); return 1; }
uint32_t ver = (i + 2 < argc) ? (uint32_t)strtoul(argv[i + 2], NULL, 0) : 1;
uint32_t flg = (i + 3 < argc) ? (uint32_t)strtoul(argv[i + 3], NULL, 0) : 0;
return cmd_hello(argv[i + 1], ver, flg);
}
if (!strcmp(cmd, "ping")) {
const char *app = (i + 1 < argc) ? argv[i + 1] : "meow_ipc";
return cmd_hello(app, 1, 0);
}
if (!strcmp(cmd, "query")) {
if (i + 2 >= argc) { fprintf(stderr, "query needs <app_id> <activity>\n"); return 1; }
return cmd_query(argv[i + 1], argv[i + 2]);
}
if (!strcmp(cmd, "declare")) {
if (i + 2 >= argc) { fprintf(stderr, "declare needs <app_id> <route_id=chain> [...]\n"); return 1; }
return cmd_declare(argv[i + 1], argc - i - 2, argv + i + 2);
}
if (!strcmp(cmd, "focus")) {
if (i + 2 >= argc) { fprintf(stderr, "focus needs <app_id> <activity>\n"); return 1; }
return cmd_focus(argv[i + 1], argv[i + 2]);
}
if (!strcmp(cmd, "back")) {
return cmd_back();
}
if (!strcmp(cmd, "notify")) {
/* notify <app_id> <category> <summary> [body] [replaces_id] [ongoing] [media] [progress] */
if (i + 2 >= argc) { fprintf(stderr, "notify needs <app_id> <category> <summary>\n"); return 1; }
const char *body = (i + 4 < argc) ? argv[i + 4] : "";
uint32_t rid = (i + 5 < argc) ? (uint32_t)strtoul(argv[i + 5], NULL, 0) : 0;
int ongoing = (i + 6 < argc) ? atoi(argv[i + 6]) : 0;
int media = (i + 7 < argc) ? atoi(argv[i + 7]) : 0;
int progress = (i + 8 < argc) ? atoi(argv[i + 8]) : -1;
return cmd_notify(argv[i + 1], argv[i + 2], argv[i + 3], body, rid, ongoing, media, progress);
}
if (!strcmp(cmd, "dismiss")) {
if (i + 1 >= argc) { fprintf(stderr, "dismiss needs <id>\n"); return 1; }
return cmd_dismiss((uint32_t)strtoul(argv[i + 1], NULL, 0));
}
fprintf(stderr, "unknown cmd %s; try --help\n", cmd);
return 1;
}
+157
View File
@@ -0,0 +1,157 @@
/* meow_ipc.h — MeowOS IPC 协议定义(FRAMEWORK_DESIGN §1 / MEOWOS_API.md §1)。
*
* 客户端(meow_ipc 测试钩子)与守护进程(meow_launcherd)共用一份,杜绝两端漂移。
* 传输:AF_UNIX SOCK_STREAM,路径默认 /usr/data/meow_ipc.sock(可被 --ipc-sock /
* MEOW_IPC_SOCK 覆盖,供 qemu sim)。
* 帧格式:定长头(16B) + 变长负载,**全部小端 LE**(mipsel 原生)。
*
* struct ipc_hdr { LE 字节序
* uint32_t magic; = 0x4D4F5701 == 'MEOW'|1
* uint16_t version; = 1
* uint16_t type; = msg_type
* uint32_t req_id; = 请求id,响应回显
* uint32_t len; = payload 字节数
* };
*
* 本阶段(D · M0+M1)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT。
*/
#ifndef MEOW_IPC_H
#define MEOW_IPC_H
#include <stdint.h>
#include <stddef.h>
#define MEOW_IPC_MAGIC 0x4D4F5701u
#define MEOW_IPC_VERSION 1u
#define MEOW_IPC_PATH "/usr/data/meow_ipc.sock"
#define MEOW_IPC_SOCK_LEN 108 /* sockaddr_un.sun_path */
#define MEOW_IPC_MAX 4096u /* 单帧负载上限 */
#define MEOW_IPC_HDR_LEN 16u
/* 列表编码上限(count 个 NUL 字符串时,distinct 工作缓冲上限) */
#define MEOW_IPC_CARGO 1024u /* 守护进程入队拷贝上限(可小于 MEOW_IPC_MAX) */
/* ---- 消息类型(FRAMEWORK_DESIGN §1 表)---- */
enum {
MSG_NONE = 0x0000,
MSG_HELLO = 0x0001, /* ↑ 声明 app_id/版本/能力 */
MSG_ACK = 0x0002, /* ↓ 握手/应答 */
MSG_ERR = 0x0003, /* ↓ 错误码 */
MSG_REGISTER = 0x0010, /* ↑ 注册后台服务(保持连接) */
MSG_SERVICE_OK = 0x0011, /* ↓ 服务登记成功 */
MSG_SERVICE_STOP = 0x0012, /* ↑ 请求停止服务 */
MSG_NOTIFY = 0x0020, /* ↑ 推送/更新通知 */
MSG_NOTIFY_ACK = 0x0021, /* ↓ 落库结果 */
MSG_NOTIFY_DISMISS = 0x0022, /* ↑ 撤下通知 */
MSG_NOTIFY_LIST = 0x0023, /* ↓ 下发通知清单(重同步) */
MSG_ACT_DECLARE = 0x0030, /* ↑ (重)声明路由表 */
MSG_ACT_QUERY = 0x0031, /* ↑ 解析 activity 链路 */
MSG_ACT_RESULT = 0x0032, /* ↓ 返回链路 */
MSG_ACT_FOCUS = 0x0033, /* ↑ 上报当前顶层 activity(keep_awake 判定,M2.17) */
MSG_ACT_BACK = 0x0034, /* ↑ 上报回到根/无 activity(清 keep_awake,M2.17) */
MSG_NAV = 0x0040, /* ↑ 上报导航/请求退出 */
MSG_SHOW_ACTIVITY= 0x0041, /* ↓ 令前端应用显示某屏 */
MSG_SYS = 0x00F0 /* ↓ 系统事件(守护关闭/服务死亡/播放器状态) */
};
/* ---- 返回码(MEOWOS_API.md §1,与 HTTP 层一致)---- */
enum {
MEOW_OK = 0,
MEOW_ERR_INVAL = -1,
MEOW_ERR_NOENT = -2,
MEOW_ERR_EXISTS = -3,
MEOW_ERR_PERM = -4,
MEOW_ERR_IO = -5,
MEOW_ERR_NOMEM = -6,
MEOW_ERR_NOTINIT = -7,
MEOW_ERR_BUSY = -8,
MEOW_ERR_TIMEOUT = -9,
MEOW_ERR_NODEV = -10,
MEOW_ERR_INTERNAL = -255
};
/* ---- HELLO flags(附录 A 声明式权限)---- */
#define MF_BACKGROUND 0x00000001u /* 可后台常驻 */
#define MF_PLAYBACK 0x00000002u /* 后台音频/播放 */
/* ---- 标识/长度 ---- */
#define MEOW_APP_ID_MAX 32
#define MEOW_ACT_MAX 64
#define MEOW_CHAIN_MAX 160
/* ---- 负载结构(全部 LE;mipsel 原生对齐,字段 4 字节对齐)---- */
/* MSG_HELLO ↑:应用声明 */
typedef struct m_hello {
char app_id[MEOW_APP_ID_MAX];
uint32_t version;
uint32_t flags;
uint32_t caps; /* 预留能力位 */
} m_hello_t; /* 44 B */
/* MSG_ACK ↓:握手/应答 */
typedef struct m_ack {
uint32_t code; /* 0=MEOW_OK */
uint32_t status; /* 附加(如已注册 app 数) */
} m_ack_t; /* 8 B */
/* MSG_ERR ↓:错误码 + 短消息 */
typedef struct m_err {
uint32_t code;
uint16_t msg_len;
char msg[64];
} m_err_t;
/* MSG_ACT_QUERY ↑:解析某 app 的 activity 链路 */
typedef struct m_act_query {
char app_id[MEOW_APP_ID_MAX];
char activity[MEOW_ACT_MAX];
} m_act_query_t; /* 96 B */
/* MSG_ACT_FOCUS ↑:上报当前顶层 activity(keep_awake 判定,M2.17) */
typedef struct m_act_focus {
char app_id[MEOW_APP_ID_MAX];
char activity[MEOW_ACT_MAX];
} m_act_focus_t; /* 96 B */
/* MSG_ACT_RESULT ↓:返回链路(root..target),depth=链路逻辑深度 */
typedef struct m_act_result {
uint32_t depth;
uint32_t code; /* MEOW_OK / MEOW_ERR_NOENT(act 未注册) */
uint16_t chain_len; /* chain 有效字节 */
uint16_t pad;
char chain[MEOW_CHAIN_MAX];
} m_act_result_t; /* 172 B */
/* MSG_ACT_DECLARE ↑:运行时声明路由表。app_id + count 个 NUL 结尾字符串,
* 每串形如 "<route_id>=<chain>"(如 "nowplaying=home;search;nowplaying")。
* 守护进程并集覆盖静态 app.json 路由。 */
typedef struct m_act_declare {
char app_id[MEOW_APP_ID_MAX];
uint16_t count;
uint16_t pad;
char entries[MEOW_IPC_CARGO - MEOW_APP_ID_MAX - 4];
} m_act_declare_t;
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;
* - target_activity/progress 为 MeowOS 专有(存储层映射为 hints 的
* meow.target_activity / meow.progress);
* - actions[]/hints{} v1 走定长空串(M2b D-Bus 桥再填);media/ongoing 标记。
* 宿主:NOTIFICATION_SYSTEM.md §2/§4 + FRAMEWORK_DESIGN §4/附C。 */
typedef struct m_notify {
char app_id[MEOW_APP_ID_MAX]; /* 来源应用;系统事件="system" */
char category[24]; /* 通知类型(媒体通知="media") */
char summary[64];
char body[96];
char app_icon[48];
char target_activity[MEOW_ACT_MAX]; /* MeowOS:点击跳转目标 activity */
uint32_t replaces_id; /* 0=新建;>0=替换已存在通知 */
uint32_t expire_timeout; /* ms;0xFFFFFFFF=ONGOING 常驻 */
int32_t progress; /* MeowOS:0-100;<0=无 */
uint8_t media; /* 1=媒体通知 */
uint8_t ongoing; /* 1=常驻通知 */
uint16_t pad;
} m_notify_t; /* 定长 LE */
#endif /* MEOW_IPC_H */
Binary file not shown.
File diff suppressed because it is too large. Load diff
+502
View File
@@ -0,0 +1,502 @@
/* meow_notif.c — MeowOS 通知存储 + 分发(阶段 D · M2a)
*
* 实现与设计:NOTIFICATION_SYSTEM.md §1-§8 + FRAMEWORK_DESIGN §4/附C。
* - 通知数据结构:对齐 freedesktop.org Desktop Notifications 字段
* (app_name/app_icon/summary/body/actions[]/hints{}/expire_timeout)
* + MeowOS 扩展(target_activity / progress,来自 hints)。
* - 内存存储:固定活跃槽表(ring 语义——每 App×category 配额,满丢该类型最旧条)。
* - 配额(§7,用户拍板):每 App ≤ notify.categories 个类型;每类型活跃
* 通知 ≤ notify.category_quota;媒体通知每 App ≤ notify.media_quota(允许多媒体 App
* 同时)。系统事件=App(app_id="system"),同走配额。配额只管内存活跃态。
* - TTL(§8):通知创建 10s 后自动转储到 TF(无论是否过期/被清)——本批只做
* dump 钩子/日志/回调(notif_dump_cb),实际 TF 落盘 M2c 后置。ONGOING 通知
* 转储后仍保留内存(服务死随清)。
* - 转储钩子:meow_notif_dump_now()(关机前转储所有未上卡);
* meow_notif_before_poweroff()(dump + 清板载)。
*
* 空洞实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg/now_ms),配额经
* meow_settings(B2a)读 notify.categories/category_quota/media_quota(键缺省用默认值),
* 文件 I/O 失败仅打点,绝不崩。注释风格中文 +「M2.a 说明」。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <stdarg.h>
#include <time.h>
#include "meow_notif.h"
#include "meow_settings.h" /* B2a:读 notify.* 配额(MEOW_OK/MEOW_ERR_*) */
/* == 应用/槽位容量(确定静态内存;配额常量由 meow_settings 或默认覆盖)== */
#define MEOW_NOTIF_MAX_APPS 8 /* 同时跟踪的应用数(含 "system") */
#define MEOW_NOTIF_MAX_CATS 4 /* 每 App 类型上限(notify.categories 上限) */
#define MEOW_NOTIF_MAX_SLOTS (MEOW_NOTIF_MAX_APPS * MEOW_NOTIF_MAX_CATS * MEOW_NOTIF_CAT_QUOTA_DEF) /* 128 */
/* 应用条目:已声明类型集 + 活跃媒体通知计数 */
typedef struct notif_app {
char app_id[MEOW_NOTIF_APP_ID_MAX];
char cats[MEOW_NOTIF_MAX_CATS][MEOW_NOTIF_CAT_MAX];
int ncats;
int media_count; /* 当前活跃媒体通知数 */
} notif_app_t;
static notif_app_t g_apps[MEOW_NOTIF_MAX_APPS];
static int g_napps = 0;
static meow_notif_t g_slots[MEOW_NOTIF_MAX_SLOTS];
static uint32_t g_next_id = 1;
static int g_dumped_total = 0; /* 会话内累计转储计数(统计用) */
/* 配额(运行时从 meow_settings notify.* 读;键缺省用默认) */
static int g_max_cats = MEOW_NOTIF_CATS_DEF;
static int g_cat_quota = MEOW_NOTIF_CAT_QUOTA_DEF;
static int g_media_quota = MEOW_NOTIF_MEDIA_QUOTA_DEF;
static meow_notif_dump_cb g_dump_cb = NULL;
static int g_inited = 0;
/* ---- 日志(自包含,不依赖 launcher 的 logmsg;前缀 "notif:")---- */
static void notif_log(const char *fmt, ...)
{
char msg[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof msg, fmt, ap);
va_end(ap);
fprintf(stderr, "notif: %s\n", msg);
}
/* ---- 时钟:墙钟 epoch ms(created_ms,供归档/显示)+ monotonic ms(TTL 判定)---- */
static uint64_t wall_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
static uint64_t mono_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
/* ---- 短串拷贝(保证 NUL 终止 + 截断到 len-1)---- */
static void n_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;
}
/* ---- 配额读取(B2a;键缺省/取失败用默认值)---- */
static void read_quota(void)
{
int v;
if (meow_settings_get_int("notify.categories", &v) == MEOW_OK && v > 0)
g_max_cats = v;
if (meow_settings_get_int("notify.category_quota", &v) == MEOW_OK && v > 0)
g_cat_quota = v;
if (meow_settings_get_int("notify.media_quota", &v) == MEOW_OK && v > 0)
g_media_quota = v;
notif_log("quota: cats=%d cat_quota=%d media_quota=%d", g_max_cats, g_cat_quota, g_media_quota);
}
/* ---- 生命周期 ---- */
int meow_notif_init(void)
{
if (g_inited) return NOTIF_OK;
memset(g_apps, 0, sizeof g_apps);
memset(g_slots, 0, sizeof g_slots);
g_napps = 0;
g_next_id = 1;
g_dumped_total = 0;
read_quota(); /* B2a:notify.* 配额 */
g_inited = 1;
notif_log("init: slots=%d default dump_cb=log-only (M2c -> TF)", MEOW_NOTIF_MAX_SLOTS);
return NOTIF_OK;
}
int meow_notif_shutdown(void)
{
if (!g_inited) return NOTIF_OK;
/* 退出前归档所有未转储项(不清理;before_poweroff 负责清板载) */
meow_notif_dump_now();
return NOTIF_OK;
}
/* ---- App 表查找/创建 ---- */
static notif_app_t *find_app(const char *app_id)
{
for (int i = 0; i < g_napps; i++)
if (!strcmp(g_apps[i].app_id, app_id)) return &g_apps[i];
return NULL;
}
static notif_app_t *get_or_add_app(const char *app_id)
{
notif_app_t *a = find_app(app_id);
if (a) return a;
if (g_napps >= MEOW_NOTIF_MAX_APPS) return NULL; /* 满:丢弃(防御) */
a = &g_apps[g_napps++];
memset(a, 0, sizeof *a);
n_strncpy(a->app_id, app_id, sizeof a->app_id);
return a;
}
/* 某 App 已声明的类型是否包含 cat;无=返回 -1 */
static int app_cat_index(const notif_app_t *a, const char *cat)
{
for (int i = 0; i < a->ncats; i++)
if (!strcmp(a->cats[i], cat)) return i;
return -1;
}
/* 声明类型:已在集合→ok;否则若未超上限则加入;超上限→返回 NOTIF_ERR_QUOTA。 */
static int declare_cat(notif_app_t *a, const char *cat)
{
if (app_cat_index(a, cat) >= 0) return NOTIF_OK;
if (a->ncats >= MEOW_NOTIF_MAX_CATS || a->ncats >= g_max_cats)
return NOTIF_ERR_QUOTA; /* App 类型数超限 */
n_strncpy(a->cats[a->ncats++], cat, sizeof a->cats[0]);
return NOTIF_OK;
}
/* ---- 媒体通知判定:category 命中 MEOW_NOTIF_MEDIA_CAT(前缀)---- */
static int is_media_cat(const char *cat)
{
return (cat && strncasecmp(cat, MEOW_NOTIF_MEDIA_CAT, strlen(MEOW_NOTIF_MEDIA_CAT)) == 0);
}
/* ---- 槽位 ---- */
static int slot_free_index(void)
{
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++)
if (!g_slots[i].active) return i;
return -1;
}
/* 统计某 App × 某类型 的活跃(active 且未撤下)通知数 —— 配额「活跃(未过期)」含义:
* 瞬时通知 10s 后已释放(不占位);ONGOING 常驻(转储后仍占位)——故只看 dismissed。 */
static int count_active_cat(const notif_app_t *a, const char *cat)
{
int n = 0;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (s->active && !s->dismissed &&
!strcmp(s->app_id, a->app_id) && !strcmp(s->category, cat))
n++;
}
return n;
}
/* 统计某 App 的活跃媒体通知数(同样只看 dismissed;ONGOING 媒体仍占配额) */
static int count_active_media(const notif_app_t *a)
{
int n = 0;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (s->active && !s->dismissed && s->media &&
!strcmp(s->app_id, a->app_id)) n++;
}
return n;
}
/* 找某 App × 某类型 最旧(created_ms 最小)活跃通知槽;无=返回 -1 */
static int oldest_slot_cat(const notif_app_t *a, const char *cat)
{
int best = -1;
uint64_t best_ms = UINT64_MAX;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (s->active && !s->dismissed &&
!strcmp(s->app_id, a->app_id) && !strcmp(s->category, cat)) {
if (s->created_ms < best_ms) { best_ms = s->created_ms; best = i; }
}
}
return best;
}
/* 找某 App 最旧媒体通知槽;无=返回 -1 */
static int oldest_media_slot(const notif_app_t *a)
{
int best = -1;
uint64_t best_ms = UINT64_MAX;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (s->active && !s->dismissed && s->media &&
!strcmp(s->app_id, a->app_id)) {
if (s->created_ms < best_ms) { best_ms = s->created_ms; best = i; }
}
}
return best;
}
/* 归档(dump)单条:调 dump_cb + 标 dumped。返回是否首次转储。 */
static int archive_one(meow_notif_t *s)
{
if (!s->active || s->dumped) return 0;
if (g_dump_cb) g_dump_cb(s); /* M2c 替换为 TF 落盘;本批默认日志/空 */
s->dumped = 1;
g_dumped_total++;
notif_log("dump: id=%u app=%s cat=%s ongoing=%d dumped_total=%d",
s->id, s->app_id, s->category, s->ongoing, g_dumped_total);
return 1;
}
/* 释放槽(清 active,槽位可复用) */
static void slot_release(int idx)
{
if (idx < 0 || idx >= MEOW_NOTIF_MAX_SLOTS) return;
memset(&g_slots[idx], 0, sizeof g_slots[idx]);
}
/* 按 id 找活跃槽;无=返回 -1 */
static int slot_by_id(uint32_t id)
{
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++)
if (g_slots[i].active && g_slots[i].id == id) return i;
return -1;
}
/* ---- 核心:post ---- */
int meow_notif_post(const meow_notif_t *n, uint32_t *out_id)
{
int rc = NOTIF_OK;
if (out_id) *out_id = 0;
if (!g_inited) return NOTIF_ERR_NOTINIT;
if (!n || !n->app_id[0]) return NOTIF_ERR_INVAL;
if (!n->category[0]) return NOTIF_ERR_INVAL;
/* 更新(replace)语义:id>0 且已存在 → 就地更新字段 */
if (n->id != 0) {
int idx = slot_by_id(n->id);
if (idx >= 0) {
meow_notif_t *s = &g_slots[idx];
n_strncpy(s->category, n->category, sizeof s->category);
n_strncpy(s->summary, n->summary, sizeof s->summary);
n_strncpy(s->body, n->body, sizeof s->body);
n_strncpy(s->app_icon, n->app_icon, sizeof s->app_icon);
n_strncpy(s->actions, n->actions, sizeof s->actions);
n_strncpy(s->hints, n->hints, sizeof s->hints);
n_strncpy(s->target_activity, n->target_activity, sizeof s->target_activity);
s->expire_timeout = n->expire_timeout;
s->progress = n->progress;
s->ongoing = n->ongoing;
s->media = n->media;
if (out_id) *out_id = s->id;
notif_log("post: UPDATE id=%u app=%s cat=%s", s->id, s->app_id, s->category);
return NOTIF_OK;
}
/* 指定 id 但不存在 → 落为新建(freedesktop 行为:replaces_id 不存在则新建) */
}
notif_app_t *a = get_or_add_app(n->app_id);
if (!a) { notif_log("post: app table full, drop app=%s", n->app_id); return NOTIF_ERR_NOMEM; }
rc = declare_cat(a, n->category);
if (rc != NOTIF_OK) {
notif_log("post: QUOTA app=%s cat=%s (cats=%d max=%d)", n->app_id, n->category,
a->ncats, g_max_cats);
return rc;
}
int media = (n->media != 0) || is_media_cat(n->category);
/* 配额 + 满丢该类型最旧条 */
if (media) {
if (count_active_media(a) >= g_media_quota) {
int old = oldest_media_slot(a);
if (old >= 0) {
notif_log("post: media quota full app=%s -> drop id=%u", n->app_id, g_slots[old].id);
slot_release(old); /* 满丢最旧媒体通知 */
}
}
} else {
if (count_active_cat(a, n->category) >= g_cat_quota) {
int old = oldest_slot_cat(a, n->category);
if (old >= 0) {
notif_log("post: cat quota full app=%s cat=%s -> drop id=%u", n->app_id,
n->category, g_slots[old].id);
slot_release(old); /* 满丢该类型最旧条 */
}
}
}
int idx = slot_free_index();
if (idx < 0) { notif_log("post: NOMEM slots full app=%s", n->app_id); return NOTIF_ERR_NOMEM; }
meow_notif_t *s = &g_slots[idx];
memset(s, 0, sizeof *s);
s->id = g_next_id++;
n_strncpy(s->app_id, n->app_id, sizeof s->app_id);
n_strncpy(s->category, n->category, sizeof s->category);
n_strncpy(s->summary, n->summary, sizeof s->summary);
n_strncpy(s->body, n->body, sizeof s->body);
n_strncpy(s->app_icon, n->app_icon, sizeof s->app_icon);
n_strncpy(s->actions, n->actions, sizeof s->actions);
n_strncpy(s->hints, n->hints, sizeof s->hints);
n_strncpy(s->target_activity, n->target_activity, sizeof s->target_activity);
s->expire_timeout = n->expire_timeout;
s->progress = n->progress;
s->ongoing = n->ongoing;
s->media = media ? 1 : 0;
s->created_ms = wall_ms();
s->dump_at_ms = mono_ms() + MEOW_NOTIF_TTL_MS; /* §8:创建 10s 转储 */
s->active = 1;
if (out_id) *out_id = s->id;
notif_log("post: NEW id=%u app=%s cat=%s media=%d ongoing=%d", s->id, s->app_id,
s->category, s->media, s->ongoing);
return NOTIF_OK;
}
/* ---- dismiss:撤下 + 立即归档 + 释放槽 ---- */
int meow_notif_dismiss(uint32_t id)
{
if (!g_inited) return NOTIF_ERR_NOTINIT;
int idx = slot_by_id(id);
if (idx < 0) return NOTIF_ERR_NOENT;
meow_notif_t *s = &g_slots[idx];
archive_one(s); /* 未被清/过期也已转储(§8) */
s->dismissed = 1;
notif_log("dismiss: id=%u app=%s", id, s->app_id);
slot_release(idx);
return NOTIF_OK;
}
/* ---- count ---- */
int meow_notif_count(const char *app_id)
{
if (!g_inited) return -NOTIF_ERR_NOTINIT;
int n = 0;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (s->active && !s->dismissed &&
(!app_id || !app_id[0] || !strcmp(s->app_id, app_id)))
n++;
}
return n;
}
/* ---- list ---- */
int meow_notif_list(const char *app_id,
int (*cb)(const meow_notif_t *n, void *ctx), void *ctx)
{
if (!g_inited) return -NOTIF_ERR_NOTINIT;
if (!cb) return -NOTIF_ERR_INVAL;
int n = 0;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (!s->active || s->dismissed) continue;
if (app_id && app_id[0] && strcmp(s->app_id, app_id)) continue;
n++;
if (cb(s, ctx) != 0) break;
}
return n;
}
/* ---- dump_now:把当前状态「排空到 TTL 归档态」——归档所有未转储;非 ONGOING 释放槽,
* ONGOING 转储后保留(常驻)。等价「立即触发所有 slot 的 §8 转储」。停机前用。 ---- */
int meow_notif_dump_now(void)
{
if (!g_inited) return -NOTIF_ERR_NOTINIT;
int n = 0;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (!s->active || s->dumped) continue;
int was_ongoing = s->ongoing;
n += archive_one(s);
if (!was_ongoing) slot_release(i); /* 与 tick 一致:非 ONGOING 转储后释放槽 */
else notif_log("dump_now: ongoing id=%u archived (kept for UI)", s->id);
}
return n;
}
/* ---- tick:TTL 转储 + 释放非 ONGOING;ONGOING 转储后保留 ---- */
void meow_notif_tick(void)
{
if (!g_inited) return;
uint64_t now = mono_ms();
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (!s->active || s->dumped) continue;
if (now >= s->dump_at_ms) { /* §8:10s 到点自动转储 */
int was_ongoing = s->ongoing;
archive_one(s);
if (!was_ongoing) slot_release(i); /* 非 ONGOING:转储后释放槽 */
else notif_log("tick: ongoing id=%u archived (kept for UI)", s->id);
}
}
}
/* ---- before_poweroff:转储所有未上卡 + 清板载 ---- */
void meow_notif_before_poweroff(void)
{
if (!g_inited) return;
int n = meow_notif_dump_now();
notif_log("before_poweroff: dumped=%d, clearing board", n);
memset(g_slots, 0, sizeof g_slots);
memset(g_apps, 0, sizeof g_apps);
g_napps = 0;
g_next_id = 1;
}
/* ---- 设置 dump 回调 ---- */
void meow_notif_set_dump_cb(meow_notif_dump_cb cb)
{
g_dump_cb = cb; /* NULL=恢复默认(无回调,仅日志;M2c 接 TF) */
}
/* ---- MSG_NOTIFY_LIST 序列化(守护→App 下发清单;M5 通知中心/重同步用)----
* 负载: [u16 count] + count 条 定长 meow_notif 记录(records 以 app_id/category/
* summary/body/app_icon/target_activity 为主,精简 payload,适配 MEOW_IPC_CARGO)。 */
int meow_notif_serialize_list(const char *app_id, uint8_t *buf, int buf_len)
{
if (!g_inited || !buf || buf_len < (int)(2 + sizeof(uint16_t))) return -NOTIF_ERR_INVAL;
/* 简化 v1:序列化 count(u16)+ 每条固定 128B 之摘要记录(id/category/summary/body/
* app_icon/target_activity/ongoing/progress/media/dismissed)。M5 全套结构后调。 */
uint16_t count = 0;
uint8_t *p = buf + 2;
int cap = buf_len - 2;
const uint16_t rec = 128;
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (!s->active || s->dismissed) continue;
if (app_id && app_id[0] && strcmp(s->app_id, app_id)) continue;
if ((int)(rec) > cap) break;
memset(p, 0, rec);
uint32_t id = s->id;
memcpy(p, &id, 4); /* +0 id */
n_strncpy((char *)(p + 4), s->category, 24); /* +4 category[24] */
n_strncpy((char *)(p + 28), s->summary, 64); /* +28 summary[64] */
n_strncpy((char *)(p + 92), s->body, 24); /* +92 body[24](v1 截断) */
p[116] = s->ongoing;
p[117] = s->media;
p[118] = s->dismissed;
/* +119 target_activity[8] 预留;v1 截断,M5 扩充 */
memcpy(p + 119, s->target_activity, 8);
p += rec;
cap -= rec;
count++;
}
memcpy(buf, &count, 2);
return 2 + (int)count * rec;
}
/* ---- 调试打印 ---- */
void meow_notif_dump(const char *log_prefix)
{
const char *lp = log_prefix ? log_prefix : "";
notif_log("%sactive=%d apps=%d next_id=%u dumped_total=%d",
lp, meow_notif_count(NULL), g_napps, g_next_id, g_dumped_total);
for (int i = 0; i < g_napps; i++) {
notif_log("%s app=%s cats=%d media=%d", lp, g_apps[i].app_id,
g_apps[i].ncats, g_apps[i].media_count);
}
for (int i = 0; i < MEOW_NOTIF_MAX_SLOTS; i++) {
meow_notif_t *s = &g_slots[i];
if (!s->active) continue;
notif_log("%s slot%02d id=%u app=%s cat=%s sum=%s ong=%d media=%d dumped=%d",
lp, i, s->id, s->app_id, s->category, s->summary,
s->ongoing, s->media, s->dumped);
}
}
+132
View File
@@ -0,0 +1,132 @@
/* meow_notif.h — MeowOS 通知存储 + 分发(阶段 D · M2a)
*
* 定位:MeowOS 通知中心后端(存储 + 分发),是阶段 B 通知中心 UI(M5)+ 阶段 D
* (M2b D-Bus 桥 / M2c 转储 TF 卡)的前提。设计规范:NOTIFICATION_SYSTEM.md §1-§8
* + FRAMEWORK_DESIGN §4/附C(freedesktop.org Desktop Notifications)。
*
* 本批(M2a)只做核心存储骨架:
* - 通知数据结构(对齐 freedesktop 字段 + MeowOS 扩展 target_activity/progress);
* - 内存配额(每 App ≤ notify.categories 个类型;每类型活跃 ≤ notify.category_quota;
* 媒体通知每 App ≤ notify.media_quota,允许多媒体 App 同时);
* - TTL 10s 转储钩子(设计 §8,实际 TF 落盘 M2c 后置,本批预留 notif_dump_cb);
* - 原生 IPC 通道接入点(MSG_NOTIFY→ACK / MSG_NOTIFY_DISMISS→ACK;MSG_NOTIFY_LIST
* 为守护→App 下发,本批提供序列化 helper,UI(M5) 接入)。
*
* 空洞实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg/now_ms),
* 配额从 meow_settings 读(B2a;键缺省时用本文件默认值)。注释风格中文 +「M2.a 说明」。
*/
#ifndef MEOW_NOTIF_H
#define MEOW_NOTIF_H
#include <stdint.h> /* uint32_t/uint64_t/int32_t */
#include <stddef.h> /* size_t */
#ifdef __cplusplus
extern "C" {
#endif
/* == 返回码(对齐 meow_ipc.h / MEOWOS_API.md §1,通知专用枚举)== */
enum {
NOTIF_OK = 0,
NOTIF_ERR_INVAL = -1, /* 参数非法 */
NOTIF_ERR_NOMEM = -6, /* 槽满/内存不足 */
NOTIF_ERR_NOTINIT = -7, /* 未 init */
NOTIF_ERR_QUOTA = -12, /* 配额超限(App 类型数 / 类型条数 / 媒体数) */
NOTIF_ERR_NOENT = -2 /* 通知 id 不存在 */
};
/* == 容量/长度(单设备上限;定长,控制静态内存)== */
#define MEOW_NOTIF_APP_ID_MAX 32
#define MEOW_NOTIF_CAT_MAX 24
#define MEOW_NOTIF_SUMMARY_MAX 64
#define MEOW_NOTIF_BODY_MAX 96
#define MEOW_NOTIF_ICON_MAX 48
#define MEOW_NOTIF_ACTIVITY_MAX 48
#define MEOW_NOTIF_ACTIONS_MAX 96 /* NUL 分隔动作键列表 */
#define MEOW_NOTIF_HINTS_MAX 96 /* 原始 "k=v;..." 提示串 */
/* == 配额默认(运行时从 meow_settings notify.* 覆盖;键缺省用此)== */
#define MEOW_NOTIF_CATS_DEF 4 /* 每 App 最多申请的通知类型数(notify.categories) */
#define MEOW_NOTIF_CAT_QUOTA_DEF 4 /* 每类型活跃(未过期)通知上限(notify.category_quota) */
#define MEOW_NOTIF_MEDIA_QUOTA_DEF 1 /* 每 App 媒体通知上限(notify.media_quota) */
#define MEOW_NOTIF_TTL_MS 10000 /* 设计 §8:通知创建 10s 后自动转储到 TF */
/* 媒体通知(category)约定名:命中即按 notify.media_quota 约束(单 App 1 条) */
#define MEOW_NOTIF_MEDIA_CAT "media"
/* 系统事件作为 App 的 app_id(NOTIFICATION_SYSTEM.md §7:系统也抽象为 App,同样走配额) */
#define MEOW_NOTIF_SYSTEM_APP "system"
/* 通知结构(对齐 freedesktop + MeowOS 扩展)。
* fields 注释:NOTIFICATION_SYSTEM.md §4。 */
typedef struct meow_notif {
uint32_t id; /* 全局递增通知 id(0=无效/未分配) */
char app_id[MEOW_NOTIF_APP_ID_MAX]; /* 来源应用;系统事件="system" */
char category[MEOW_NOTIF_CAT_MAX]; /* 通知类型(媒体通知="media") */
char summary[MEOW_NOTIF_SUMMARY_MAX]; /* 标题(单行) */
char body[MEOW_NOTIF_BODY_MAX]; /* 正文(可多行) */
char app_icon[MEOW_NOTIF_ICON_MAX]; /* 图标 icon name 或 path */
char actions[MEOW_NOTIF_ACTIONS_MAX]; /* actions[](NUL 分隔;v1 空,M2b 填) */
char hints[MEOW_NOTIF_HINTS_MAX]; /* hints{} 原始 "k=v;..." 串(v1 空,M2b 填) */
uint32_t expire_timeout; /* 超时 ms;0xFFFFFFFF=ONGOING 常驻 */
uint64_t created_ms; /* 创建时刻(墙钟 epoch ms,供归档/显示) */
uint64_t dump_at_ms; /* 转储时刻(monotonic ms,TTL=创建+10s;ONGOING 亦到此转储) */
int32_t progress; /* MeowOS:0-100 进度;<0=无 */
char target_activity[MEOW_NOTIF_ACTIVITY_MAX]; /* MeowOS:点击跳转目标 activity */
uint8_t ongoing; /* 1=常驻通知(服务死随清) */
uint8_t media; /* 1=媒体通知(走 notify.media_quota) */
uint8_t dismissed; /* 1=已撤下 */
uint8_t dumped; /* 1=已转储(归档) */
uint8_t active; /* 1=槽位占用 */
} meow_notif_t;
/* 转储回调(M2c 落盘 / 本批默认仅日志):通知归档时调用。 */
typedef void (*meow_notif_dump_cb)(const meow_notif_t *n);
/* == 生命周期 == */
int meow_notif_init(void); /* 幂等;从 meow_settings 读 notify.* 配额(B2a) */
int meow_notif_shutdown(void); /* 保存/清理(进程退出前调;本批=归档未转储项) */
/* == 核心 API == */
/* 推送/更新通知。n->id 为 0(新建);若 n->id>0 或 replace 语义,
* 按 n 中同 id 更新已存在通知(未找到则新建)。返回 NOTIF_OK/NOTIF_ERR_*;
* *out_id 返回分配/更新的通知 id(可为 NULL)。 */
int meow_notif_post(const meow_notif_t *n, uint32_t *out_id);
/* 撤下通知(dismiss)。随之为该通知归档(未转储则立即调 dump_cb + 标记),并释放槽。 */
int meow_notif_dismiss(uint32_t id);
/* 活跃(未转储)通知数。app_id 为 NULL/空=全部。 */
int meow_notif_count(const char *app_id);
/* 遍历(回调)当前通知。app_id 为 NULL/空=全部。返回遍历到的条数;
* 回调返回非 0 则中止遍历。 */
int meow_notif_list(const char *app_id,
int (*cb)(const meow_notif_t *n, void *ctx), void *ctx);
/* 立即转储所有未上卡(未 dumped)通知:逐条调 dump_cb + 标 dumped(不释放;
* 停机前转储用)。返回转储条数。 */
int meow_notif_dump_now(void);
/* 主循环周期调(每 10ms 排空 TODO):TTL 到期 → 归档 + (非 ONGOING)释放槽;
* dismissed 且未转储 → 归档 + 释放;ONGOING 转储后仍保留内存(服务死随清)。 */
void meow_notif_tick(void);
/* 正常电源事件(关机/重启)前调用:先转储所有未上卡通知,再清板载。 */
void meow_notif_before_poweroff(void);
/* 覆盖转储回调(NULL=恢复默认日志;M2c 接 TF 落盘)。 */
void meow_notif_set_dump_cb(meow_notif_dump_cb cb);
/* 调试:打印当前 Active 表/App 表(前缀)。 */
void meow_notif_dump(const char *log_prefix);
/* MSG_NOTIFY_LIST(守护→App 下发清单,M5 通知中心/重同步用):
* 把 app_id 的当前通知序列化为 MSG_NOTIFY_LIST 负载写入 buf(≤MEOW_IPC_CARGO),
* 返回负载字节数;<0=无(app 无通知)。 */
int meow_notif_serialize_list(const char *app_id, uint8_t *buf, int buf_len);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_NOTIF_H */
+709
View File
@@ -0,0 +1,709 @@
/* meow_settings.c — MeowOS 系统设置后端(阶段 B2a)
*
* 实现与设计:SYSTEM_SETTINGS.md §1/§2/§3 + MEOWOS_API.md §2。
* - 统一存储:板载 /usr/data/meow/settings.conf(UBIFS,KB 级小文件,扁平 key=value)。
* 导入/导出(备份)走同一后端;默认备份路径 TF 卡 MeowOS/backups/settings/setting.bak。
* - 单点数据源:全部设置键存于本模块内存表(名称/类型/默认/取值域/当前值),
* 实体 UI / Web 面板 / CLI 全走 meow_settings_* API,不留旁路。
* - clamp/校验集中 set(亮度 auto clamp >= display.brightness_min;枚举越界拒绝;
* str 截断 MEOW_SET_MAX_LEN);set 成功后标记脏 + 向 listener 广播(key 字符串)。
* - 落盘:meow_settings_save()(写板载 settings.conf,保留 "# meow-settings 1" 头)。
* 为避免 UBIFS 高频写,调用方在低频点(如音量渐变结束)才落盘一次。
*
* 空实现风格:本文件自包含(不依赖 meow_launcherd.c 的 logmsg),文件 I/O 失败仅打点,
* 绝不崩(内存继续用,符合 SYSTEM_SETTINGS.md §8「无卡/无板载用内存缓存」)。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <stdarg.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include "meow_settings.h"
/* 存储路径(板载 UBIFS 小文件)/ 默认备份路径(TF 卡)。
* 允许编译期覆盖(供宿主机单元测试指向临时文件;设备上默认不变)。 */
#ifndef MEOW_SETTINGS_FILE
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf"
#endif
#ifndef MEOW_SETTINGS_BAK
#define MEOW_SETTINGS_BAK "/usr/data/mnt/sd_0/MeowOS/backups/settings/setting.bak"
#endif
/* 表/监听器/临时缓冲上限 */
#define MEOW_SET_MAX_KEYS 64
#define MEOW_SET_MAX_LISTENERS 16
#define MEOW_SET_NAME_LEN 48
#define MEOW_SET_ENUM_LEN 96
#define MEOW_SET_LABELS_LEN 160
#define MEOW_SET_ENUM_MAX_OPT 8
/* 一个设置项:名称/类型/默认/当前值(统一字符串)+ clamp 域 + 枚举名/标签 */
typedef struct meow_setting {
char key[MEOW_SET_NAME_LEN];
meow_set_type type;
char def[MEOW_SET_MAX_LEN];
char cur[MEOW_SET_MAX_LEN];
int min, max; /* SET_INT 取值域(max<=0 表示仅保底、无上界) */
char enum_names[MEOW_SET_ENUM_LEN];
char enum_labels[MEOW_SET_LABELS_LEN];
int dirty;
} meow_setting;
static meow_setting g_sets[MEOW_SET_MAX_KEYS];
static int g_nsets = 0;
static meow_settings_listener g_listeners[MEOW_SET_MAX_LISTENERS];
static int g_nlisteners = 0;
static int g_inited = 0;
static int g_broadcasting = 0; /* 防 listener 递归 set */
/* ---- 默认值表:与 SYSTEM_SETTINGS.md §1 逐键一致(新增设置照附录扩展)---- */
static const meow_setting_def g_defs[] = {
/* 显示 */
{ "display.brightness", SET_INT, "101", 0, 255, NULL, NULL },
{ "display.brightness_min", SET_INT, "1", 0, 255, NULL, NULL },
{ "display.screen_off_mode", SET_ENUM, "panel",0, 0, "panel,black", "面板下电,背光全黑" },
{ "display.screen_off_timeout", SET_INT, "30", 0, 86400, NULL, NULL },
{ "display.refresh_rate", SET_ENUM, "60", 0, 0, "60,30,15", "60Hz,30Hz,15Hz" },
{ "display.refresh_always_max", SET_BOOL, "0", 0, 0, NULL, NULL },
{ "display.theme", SET_ENUM, "dark", 0, 0, "dark,light", "深色,浅色" },
/* B2b 主题三方式:display.theme_mode 决定判定方式(manual=读 display.theme;
* auto=按深色时段)。时段两键为分钟数 0-1439,start==end 视为未配置(回退 manual)。
* 注释:SYSTEM_SETTINGS.md §1 同步新增三行。 */
{ "display.theme_mode", SET_ENUM, "manual", 0, 0, "manual,auto", "手动,时段自动" },
{ "display.theme_dark_start", SET_INT, "0", 0, 1439, NULL, NULL },
{ "display.theme_dark_end", SET_INT, "0", 0, 1439, NULL, NULL },
{ "display.background", SET_STR, "black",0, 0, NULL, NULL },
/* 电源 */
{ "power.longpress_menu_ms", SET_INT, "3000", 0, 0, NULL, NULL },
{ "power.forceoff_ms", SET_INT, "8000", 0, 0, NULL, NULL },
{ "power.defeat_hotkey", SET_STR, "power_voldown", 0,0, NULL, NULL },
/* 开机 */
{ "boot.default", SET_ENUM, "meow", 0, 0, "meow,hiby", "MeowOS,HiByOS" },
{ "boot.menu_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
{ "boot.timeout_ms", SET_INT, "5000", 0, 0, NULL, NULL },
/* 输入 */
{ "input.hotkey_seq", SET_STR, "volup,volup,volup", 0,0, NULL, NULL },
{ "input.hotkey_window_ms", SET_INT, "2500", 0, 0, NULL, NULL },
{ "input.touch_arm_delay_ms", SET_INT, "700", 0, 0, NULL, NULL },
/* M2.38:设置页滑动方向切换(0=标准:手指上滑→列表往下翻;1=反向) */
{ "input.swipe_invert", SET_BOOL, "0", 0, 0, NULL, NULL },
/* 字体/主题 */
{ "font.file", SET_STR, "MeowAPPs/fonts/font.png", 0,0, NULL, NULL },
{ "font.size.title", SET_INT, "28", 0, 0, NULL, NULL },
{ "font.size.body", SET_INT, "20", 0, 0, NULL, NULL },
{ "ui.style", SET_ENUM, "md3", 0, 0, "md3,miui", "Material You,MIUI" },
/* 通知 */
{ "notify.center_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
{ "notify.app_quota", SET_INT, "16", 0, 0, NULL, NULL },
/* M2a:通知配额(NOTIFICATION_SYSTEM.md §7,用户拍板 2026-08-29)。
* notify.categories = 每 App 最多申请的通知类型数(≤4)
* notify.category_quota = 每类型活跃(未过期)通知上限(默认 4)
* notify.media_quota = 每 App 媒体通知上限(默认 1;允许多媒体 App 同时) */
{ "notify.categories", SET_INT, "4", 1, 4, NULL, NULL },
{ "notify.category_quota", SET_INT, "4", 1, 16, NULL, NULL },
{ "notify.media_quota", SET_INT, "1", 1, 1, NULL, NULL },
/* 日志 */
{ "log.level", SET_ENUM, "info", 0, 0, "error,warn,info,debug", "错误,警告,信息,调试" },
{ "log.to_card", SET_BOOL, "1", 0, 0, NULL, NULL },
/* 音频 */
{ "audio.volume", SET_INT, "60", 0, 100, NULL, NULL },
{ "audio.volume_max", SET_INT, "100", 0, 100, NULL, NULL },
{ "audio.volume_step", SET_INT, "1", 1, 5, NULL, NULL },
{ "audio.output", SET_STR, "hw:0,0", 0,0, NULL, NULL },
/* 网络(M2.39 设置页 WiFi/蓝牙开关;wifi 默认开=允许 ensure_wifi 自动连网,
* 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */
{ "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
{ "network.bluetooth_enabled", SET_BOOL, "0", 0, 0, NULL, NULL },
};
/* ---- 日志(自包含,不依赖 launcher 的 logmsg;前缀 "settings:")---- */
static void sset_log(const char *fmt, ...)
{
char msg[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof msg, fmt, ap);
va_end(ap);
fprintf(stderr, "settings: %s\n", msg);
}
/* ---- 内部查找 ---- */
static meow_setting *find_set(const char *key)
{
if (!key) return NULL;
for (int i = 0; i < g_nsets; i++)
if (!strcmp(g_sets[i].key, key)) return &g_sets[i];
return NULL;
}
/* ---- 枚举名/标签的逗号串切分辅助 ---- */
static int enum_count_str(const char *names)
{
if (!names || !names[0]) return 0;
int n = 1;
for (const char *p = names; *p; p++)
if (*p == ',') n++;
return n;
}
/* 取第 idx 个名字(返回指向 names 内部,静态覆盖;用完即弃) */
static const char *enum_at_str(const char *names, int idx)
{
static char buf[MEOW_SET_ENUM_LEN * 2];
if (!names || idx < 0) return NULL;
strncpy(buf, names, sizeof buf - 1);
buf[sizeof buf - 1] = 0;
char *p = buf;
for (int i = 0; i < idx; i++) {
while (*p && *p != ',') p++;
if (*p == ',') p++; else return NULL;
}
while (*p == ' ' || *p == '\t') p++;
/* 截到逗号/结尾 */
char *e = p;
while (*e && *e != ',') e++;
*e = 0;
return p;
}
static int enum_index_of_name_str(const char *names, const char *name)
{
if (!names || !name) return -1;
int n = enum_count_str(names);
for (int i = 0; i < n; i++) {
const char *s = enum_at_str(names, i);
if (s && !strcmp(s, name)) return i;
}
return -1;
}
/* ---- clamp/校验集中:int 取值域(max<=0 → 仅保底)---- */
static int clamp_int(meow_setting *s, int v)
{
if (s->max > 0) {
if (v < s->min) v = s->min;
if (v > s->max) v = s->max;
} else {
if (v < s->min) v = s->min;
}
return v;
}
/* display.brightness 额外 clamp >= display.brightness_min(防黑屏,SYSTEM_SETTINGS §3)
* 且 M2.39 上限 100(max_brightness=101,但 101 会闪屏,用户确认上限 100)。 */
static int clamp_brightness(int v)
{
int mn = 1; /* def 默认(brightness_min 未 init/空时用默认 1=极暗但非全黑,屏仍可见) */
meow_setting *bm = find_set("display.brightness_min");
if (bm && bm->cur[0]) mn = atoi(bm->cur);
if (v < mn) v = mn;
if (v > 100) v = 100;
return v;
}
/* ---- 按类型把字符串值 apply 到 cur(读文件/导入时用;含 clamp/校验)---- */
static void apply_value(meow_setting *s, const char *v)
{
if (!s || !v) return;
switch (s->type) {
case SET_INT: {
int iv = atoi(v);
iv = clamp_int(s, iv);
if (!strcmp(s->key, "display.brightness")) iv = clamp_brightness(iv);
snprintf(s->cur, sizeof s->cur, "%d", iv);
break;
}
case SET_BOOL: {
int b = 0;
char p[16], *q = p;
strncpy(p, v, sizeof p - 1); p[sizeof p - 1] = 0;
while (*q == ' ' || *q == '\t') q++;
long n = strtol(q, NULL, 10);
if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1;
else if (q[0] && n != 0) b = 1; /* 数值非 0(含旧风格 "2")视为 true;向后兼容 */
snprintf(s->cur, sizeof s->cur, "%d", b);
break;
}
case SET_ENUM: {
int idx = enum_index_of_name_str(s->enum_names, v);
if (idx < 0) idx = atoi(v); /* 向后兼容:数值索引 */
if (idx >= 0 && idx < enum_count_str(s->enum_names)) {
const char *name = enum_at_str(s->enum_names, idx);
if (name) strncpy(s->cur, name, sizeof s->cur - 1);
}
/* 未知值忽略(保留默认) */
break;
}
case SET_STR:
strncpy(s->cur, v, sizeof s->cur - 1);
s->cur[sizeof s->cur - 1] = 0;
break;
}
s->dirty = 0;
}
/* ---- 广播(防 listener 递归 set)---- */
static void notify_key(const char *key)
{
if (g_broadcasting) return;
g_broadcasting = 1;
for (int i = 0; i < g_nlisteners; i++)
if (g_listeners[i]) g_listeners[i](key);
g_broadcasting = 0;
}
/* import 内部:apply + 脏 + 广播(定义在下方 import 之前声明) */
static void apply_value_nosave(meow_setting *s, const char *v, int *applied);
/* ---- 注册 ---- */
int meow_settings_register(const meow_setting_def *def)
{
if (!def || !def->key) return MEOW_ERR_INVAL;
if (g_nsets >= MEOW_SET_MAX_KEYS) return MEOW_ERR_NOMEM;
if (find_set(def->key)) return MEOW_ERR_EXISTS;
meow_setting *s = &g_sets[g_nsets++];
memset(s, 0, sizeof *s);
strncpy(s->key, def->key, sizeof s->key - 1);
s->type = def->type;
strncpy(s->def, def->def ? def->def : "", sizeof s->def - 1);
strncpy(s->cur, s->def, sizeof s->cur - 1);
s->min = def->min;
s->max = def->max;
if (def->enum_names) strncpy(s->enum_names, def->enum_names, sizeof s->enum_names - 1);
if (def->enum_labels) strncpy(s->enum_labels, def->enum_labels, sizeof s->enum_labels - 1);
s->dirty = 0;
return MEOW_OK;
}
/* ---- 初始化(幂等;重载=重置默认 + 重读 settings.conf)---- */
int meow_settings_init(void)
{
if (g_nsets == 0) {
for (unsigned i = 0; i < sizeof g_defs / sizeof g_defs[0]; i++) {
int rc = meow_settings_register(&g_defs[i]);
/* 表满/重复为内部异常;防御性继续(注册失败仅该键缺失) */
(void)rc;
}
}
/* 重置全部到默认,再读文件(与旧 settings_load 语义一致:默认 + 覆盖) */
for (int i = 0; i < g_nsets; i++) {
strncpy(g_sets[i].cur, g_sets[i].def, sizeof g_sets[i].cur - 1);
g_sets[i].dirty = 0;
}
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
if (!f) {
sset_log("no %s, defaults active", MEOW_SETTINGS_FILE);
g_inited = 1;
return MEOW_OK;
}
char buf[768];
while (fgets(buf, sizeof buf, f)) {
char *eq = strchr(buf, '=');
if (!eq) continue;
*eq = 0;
/* 左去空 */
char *k = buf;
while (*k == ' ' || *k == '\t') k++;
int kl = (int)strlen(k);
while (kl > 0 && (k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0;
if (!k[0] || k[0] == '#') continue; /* 空行/注释 */
/* 右值去空 */
char *v = eq + 1;
while (*v == ' ' || *v == '\t') v++;
int vl = (int)strlen(v);
while (vl > 0 && (v[vl-1] == '\n' || v[vl-1] == '\r')) v[--vl] = 0;
while (vl > 0 && (v[vl-1] == ' ' || v[vl-1] == '\t')) v[--vl] = 0;
meow_setting *s = find_set(k);
if (s) apply_value(s, v);
/* 未知键忽略(SYSTEM_SETTINGS §3 存储格式) */
}
fclose(f);
/* 读取后统一补一次亮度下限 clamp(brightness_min 可能在 brightness 之后读入) */
meow_setting *b = find_set("display.brightness");
if (b) {
int iv = atoi(b->cur);
int cv = clamp_brightness(iv);
snprintf(b->cur, sizeof b->cur, "%d", cv);
}
g_inited = 1;
/* M2.39:迁移旧持久亮度下限 25 → 1(SYSTEM_SETTINGS §1 下限默认已改 1,但设备
* settings.conf 里还残留旧版写入的 display.brightness_min=25,覆盖默认表,导致亮度
* 滑条被 clamp 在 25 调不下去;且 SMENU 设置页并不暴露 brightness_min 这一项,故 25
* 只能是旧默认残留→强制视为待迁移,改写为 1 并落盘,让用户能调到「极暗但可见」的 1)。
* 此处只针对恰为旧默认 25 的值;用户其它自定义值(非 25)不动。 */
meow_setting *bmin = find_set("display.brightness_min");
if (bmin && !strcmp(bmin->cur, "25")) {
snprintf(bmin->cur, sizeof bmin->cur, "%d", 1);
bmin->dirty = 1;
sset_log("brightness_min stale default 25 -> 1 (migrated)");
meow_settings_save();
}
return MEOW_OK;
}
/* ---- 生命周期 ---- */
int meow_settings_shutdown(void)
{
if (g_inited) meow_settings_save();
return MEOW_OK;
}
void meow_settings_dump(const char *log_prefix)
{
const char *lp = log_prefix ? log_prefix : "";
for (int i = 0; i < g_nsets; i++)
sset_log("%s%s = %s", lp, g_sets[i].key, g_sets[i].cur);
}
/* ---- get ---- */
int meow_settings_get_int(const char *key, int *out)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_INT) return MEOW_ERR_INVAL;
if (!out) return MEOW_ERR_INVAL;
*out = atoi(s->cur);
return MEOW_OK;
}
int meow_settings_get_str(const char *key, char *buf, int buf_len)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_STR) return MEOW_ERR_INVAL;
if (!buf || buf_len <= 0) return MEOW_ERR_INVAL;
strncpy(buf, s->cur, buf_len);
buf[buf_len - 1] = 0;
return MEOW_OK;
}
int meow_settings_get_enum(const char *key, int *out)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_ENUM) return MEOW_ERR_INVAL;
if (!out) return MEOW_ERR_INVAL;
*out = enum_index_of_name_str(s->enum_names, s->cur);
if (*out < 0) *out = 0; /* 非法当前值兜底:首项 */
return MEOW_OK;
}
int meow_settings_get_bool(const char *key, int *out)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_BOOL) return MEOW_ERR_INVAL;
if (!out) return MEOW_ERR_INVAL;
*out = (s->cur[0] == '1');
return MEOW_OK;
}
/* ---- 标注脏 + 广播的公共尾部 ---- */
static void mark_changed(meow_setting *s, const char *newval)
{
if (strcmp(newval, s->cur) == 0) return; /* 未变:不广播 */
strncpy(s->cur, newval, sizeof s->cur - 1);
s->cur[sizeof s->cur - 1] = 0;
s->dirty = 1;
notify_key(s->key);
}
/* ---- set ---- */
int meow_settings_set_int(const char *key, int v)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_INT) return MEOW_ERR_INVAL;
v = clamp_int(s, v);
if (!strcmp(s->key, "display.brightness")) v = clamp_brightness(v);
char tmp[24];
snprintf(tmp, sizeof tmp, "%d", v);
mark_changed(s, tmp);
return MEOW_OK;
}
int meow_settings_set_str(const char *key, const char *v)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_STR) return MEOW_ERR_INVAL;
char tmp[MEOW_SET_MAX_LEN];
strncpy(tmp, v ? v : "", sizeof tmp - 1);
tmp[sizeof tmp - 1] = 0;
mark_changed(s, tmp);
return MEOW_OK;
}
int meow_settings_set_enum(const char *key, int idx)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_ENUM) return MEOW_ERR_INVAL;
int n = enum_count_str(s->enum_names);
if (idx < 0 || idx >= n) return MEOW_ERR_INVAL; /* 越界拒绝 */
const char *name = enum_at_str(s->enum_names, idx);
if (!name) return MEOW_ERR_INVAL;
mark_changed(s, name);
return MEOW_OK;
}
int meow_settings_set_bool(const char *key, int v)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!g_inited) return MEOW_ERR_NOTINIT;
if (s->type != SET_BOOL) return MEOW_ERR_INVAL;
char tmp[8];
snprintf(tmp, sizeof tmp, "%d", v ? 1 : 0);
mark_changed(s, tmp);
return MEOW_OK;
}
/* ---- listener ---- */
int meow_settings_add_listener(meow_settings_listener cb)
{
if (!cb) return MEOW_ERR_INVAL;
for (int i = 0; i < g_nlisteners; i++)
if (g_listeners[i] == cb) return MEOW_ERR_EXISTS;
if (g_nlisteners >= MEOW_SET_MAX_LISTENERS) return MEOW_ERR_NOMEM;
g_listeners[g_nlisteners++] = cb;
return MEOW_OK;
}
int meow_settings_remove_listener(meow_settings_listener cb)
{
for (int i = 0; i < g_nlisteners; i++) {
if (g_listeners[i] == cb) {
memmove(&g_listeners[i], &g_listeners[i+1],
(g_nlisteners - i - 1) * sizeof *g_listeners);
g_nlisteners--;
return MEOW_OK;
}
}
return MEOW_ERR_NOENT;
}
/* ---- enum 辅助 ---- */
int meow_settings_enum_label(const char *key, int idx, char *buf, int buf_len)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (s->type != SET_ENUM) return MEOW_ERR_INVAL;
if (!buf || buf_len <= 0) return MEOW_ERR_INVAL;
if (idx < 0 || idx >= enum_count_str(s->enum_names)) return MEOW_ERR_INVAL;
const char *lab = enum_at_str(s->enum_labels, idx);
if (!lab || !lab[0]) lab = enum_at_str(s->enum_names, idx); /* 无标签回退名字 */
if (!lab) return MEOW_ERR_INVAL;
strncpy(buf, lab, buf_len);
buf[buf_len - 1] = 0;
return MEOW_OK;
}
int meow_settings_enum_count(const char *key)
{
meow_setting *s = find_set(key);
if (!s) return -MEOW_ERR_NOENT;
if (s->type != SET_ENUM) return -MEOW_ERR_INVAL;
return enum_count_str(s->enum_names);
}
int meow_settings_enum_index_of_name(const char *key, const char *name)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (s->type != SET_ENUM) return MEOW_ERR_INVAL;
return enum_index_of_name_str(s->enum_names, name);
}
void meow_settings_enum_names(const char *key, const char **out_names, int *n)
{
static char scratch[MEOW_SET_ENUM_LEN];
static const char *ptrs[MEOW_SET_ENUM_MAX_OPT];
if (n) *n = 0;
meow_setting *s = find_set(key);
if (!s || s->type != SET_ENUM || !out_names) return;
strncpy(scratch, s->enum_names, sizeof scratch - 1);
scratch[sizeof scratch - 1] = 0;
char *p = scratch;
int k = 0;
while (*p && k < MEOW_SET_ENUM_MAX_OPT) {
while (*p == ' ' || *p == '\t') p++;
if (!*p) break;
ptrs[k++] = p;
while (*p && *p != ',') p++;
if (*p == ',') { *p = 0; p++; }
}
for (int i = 0; i < k; i++) out_names[i] = ptrs[i];
if (n) *n = k;
}
/* ---- list ---- */
int meow_settings_list(int (*cb)(const char *key, meow_set_type t, void *ctx), void *ctx)
{
if (!cb) return MEOW_ERR_INVAL;
if (!g_inited) return MEOW_ERR_NOTINIT;
for (int i = 0; i < g_nsets; i++)
cb(g_sets[i].key, g_sets[i].type, ctx);
return MEOW_OK;
}
/* ---- 默认值 / 复位 ---- */
int meow_settings_get_default(const char *key, char *buf, int len)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (!buf || len <= 0) return MEOW_ERR_INVAL;
strncpy(buf, s->def, len);
buf[len - 1] = 0;
return MEOW_OK;
}
int meow_settings_reset(const char *key)
{
meow_setting *s = find_set(key);
if (!s) return MEOW_ERR_NOENT;
if (strcmp(s->cur, s->def) != 0) {
strncpy(s->cur, s->def, sizeof s->cur - 1);
s->cur[sizeof s->cur - 1] = 0;
s->dirty = 1;
notify_key(s->key);
}
return MEOW_OK;
}
int meow_settings_reset_all(void)
{
for (int i = 0; i < g_nsets; i++) {
meow_setting *s = &g_sets[i];
if (strcmp(s->cur, s->def) != 0) {
strncpy(s->cur, s->def, sizeof s->cur - 1);
s->cur[sizeof s->cur - 1] = 0;
s->dirty = 1;
notify_key(s->key);
}
}
return MEOW_OK;
}
/* ---- 落盘(写板载 settings.conf;保留 "# meow-settings 1" 头)---- */
static void write_full_conf(int fd)
{
char buf[576];
int l = snprintf(buf, sizeof buf, "# meow-settings 1\n");
if (l > 0) write(fd, buf, l);
for (int i = 0; i < g_nsets; i++) {
meow_setting *s = &g_sets[i];
const char *val = s->cur;
char num[24];
/* 非字符串类型把 cur 规范为可读形式(enum=名字,bool/int=数字) */
if (s->type == SET_INT) { snprintf(num, sizeof num, "%d", atoi(s->cur)); val = num; }
else if (s->type == SET_BOOL) { snprintf(num, sizeof num, "%d", s->cur[0] == '1'); val = num; }
else if (s->type == SET_ENUM) { val = s->cur; }
l = snprintf(buf, sizeof buf, "%s = %s\n", s->key, val);
if (l > 0) write(fd, buf, l);
}
}
int meow_settings_save(void)
{
if (!g_inited) return MEOW_ERR_NOTINIT;
if (system("mkdir -p /usr/data/meow 2>/dev/null") != 0) {
/* 目录已存在等返回非 0 也属正常,不视为失败 */
}
int fd = open(MEOW_SETTINGS_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
sset_log("save: open %s fail: %s", MEOW_SETTINGS_FILE, strerror(errno));
return MEOW_ERR_IO;
}
write_full_conf(fd);
close(fd);
for (int i = 0; i < g_nsets; i++) g_sets[i].dirty = 0;
return MEOW_OK;
}
/* ---- export:写一份 .conf 到 path(默认 TF 卡备份)---- */
int meow_settings_export(const char *path)
{
const char *p = path ? path : MEOW_SETTINGS_BAK;
/* 自动建目录(取 p 的 dirname),失败静默(导出尽力而为) */
{
char dir[320];
char cmd[344];
snprintf(dir, sizeof dir, "%s", p);
char *slash = strrchr(dir, '/');
if (slash) {
if (slash == dir) slash[1] = 0; else *slash = 0;
snprintf(cmd, sizeof cmd, "mkdir -p '%s' 2>/dev/null", dir);
system(cmd);
}
}
int fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
sset_log("export: open %s fail: %s", p, strerror(errno));
return MEOW_ERR_IO;
}
write_full_conf(fd);
close(fd);
sset_log("exported -> %s", p);
return MEOW_OK;
}
/* ---- import:从 .conf 读入覆盖;逐键校验,损坏键不中断 ---- */
int meow_settings_import(const char *path)
{
if (!g_inited) return MEOW_ERR_NOTINIT;
const char *p = path ? path : MEOW_SETTINGS_BAK;
FILE *f = fopen(p, "r");
if (!f) { sset_log("import: open %s fail: %s", p, strerror(errno)); return MEOW_ERR_NOENT; }
int bad = 0, applied = 0;
char buf[768];
while (fgets(buf, sizeof buf, f)) {
char *eq = strchr(buf, '=');
if (!eq) continue;
*eq = 0;
char *k = buf;
while (*k == ' ' || *k == '\t') k++;
int kl = (int)strlen(k);
while (kl > 0 && (k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0;
if (!k[0] || k[0] == '#') continue;
char *v = eq + 1;
while (*v == ' ' || *v == '\t') v++;
int vl = (int)strlen(v);
while (vl > 0 && (v[vl-1] == '\n' || v[vl-1] == '\r')) v[--vl] = 0;
while (vl > 0 && (v[vl-1] == ' ' || v[vl-1] == '\t')) v[--vl] = 0;
meow_setting *s = find_set(k);
if (!s) { bad++; continue; } /* 未知键:计损坏,不中断 */
/* 逐键校验:走 set 的 clamp/校验(apply_value 已 clamp);值合法才算成功 */
apply_value_nosave(s, v, &applied);
}
fclose(f);
meow_settings_save(); /* 导入后落盘(应用覆盖) */
sset_log("imported %s: applied=%d invalid=%d", p, applied, bad);
return applied > 0 ? MEOW_OK : MEOW_ERR_INVAL;
}
/* ---- import 内部:apply + 脏 + 广播(不重置整表)---- */
static void apply_value_nosave(meow_setting *s, const char *v, int *applied)
{
if (!s || !v) return;
char before[MEOW_SET_MAX_LEN];
strncpy(before, s->cur, sizeof before - 1); before[sizeof before - 1] = 0;
apply_value(s, v);
if (strcmp(before, s->cur) != 0) {
s->dirty = 1;
notify_key(s->key);
if (applied) (*applied)++;
}
}
+104
View File
@@ -0,0 +1,104 @@
/* meow_settings.h — MeowOS 系统设置后端(阶段 B2a)
*
* 统一设置子系统:全部系统设置抽象为「名称/类型/默认/取值域」的扁平表,
* 单点存板载 /usr/data/meow/settings.conf(UBIFS,KB 级小文件,key=value)。
* 实体 UI / Web 面板 / CLI 均走本 API,不留旁路。
*
* 设计规范:SYSTEM_SETTINGS.md §1/§2/§3 + MEOWOS_API.md §2。
* 返回码:MEOWOS_API.md §1(MEOW_OK=0,负值错误码)。
* 注释风格:与 meow_launcherd.c 一致(中文注释 + 「M2.x 说明」格式)。
*
* 阶段 B2a 只建后端 + 核心 API;实体设置页(B2b)/Web 面板(C)后续复用。
*/
#ifndef MEOW_SETTINGS_H
#define MEOW_SETTINGS_H
#ifdef __cplusplus
extern "C" {
#endif
/* 统一错误码(MEOWOS_API.md §1;C 函数返回 int,0=成功,负=错误码) */
#ifndef MEOW_OK
#define MEOW_OK 0 /* 成功 */
#define MEOW_ERR_INVAL -1 /* 参数/取值非法 */
#define MEOW_ERR_NOENT -2 /* 键/资源不存在 */
#define MEOW_ERR_EXISTS -3 /* 已存在(如重复注册) */
#define MEOW_ERR_PERM -4 /* 无权限(本机信任模型下少用,保留) */
#define MEOW_ERR_IO -5 /* 读写(存储/文件)失败 */
#define MEOW_ERR_NOMEM -6 /* 内存不足 */
#define MEOW_ERR_NOTINIT -7 /* 未初始化(如 settings 未 init) */
#define MEOW_ERR_BUSY -8 /* 忙(如服务已运行) */
#define MEOW_ERR_TIMEOUT -9 /* 超时 */
#define MEOW_ERR_NODEV -10 /* 设备不存在(如无卡) */
#define MEOW_ERR_INTERNAL -255 /* 内部错误 */
#endif /* MEOW_OK */
/* 字符串类型最大长度(set_str 截断;str 存取缓冲区建议为此值) */
#define MEOW_SET_MAX_LEN 256
/* 类型 schema:每个设置键注册“名称/类型/默认/取值域”,存取统一走表 */
typedef enum { SET_INT, SET_STR, SET_ENUM, SET_BOOL } meow_set_type;
/* 设置项注册描述(MEOWOS_API §2.7:新增设置照此;实体 UI/web/CLI 自动生成控件) */
typedef struct meow_setting_def {
const char *key; /* 如 "display.brightness" */
meow_set_type type;
const char *def; /* 默认值字符串(int/str/enum/bool 统一字符串) */
int min, max; /* SET_INT 取值域(max<=0 表示仅保底、无上界);其他 0 */
const char *enum_names; /* SET_ENUM:逗号分隔选项名,如 "panel,black" */
const char *enum_labels; /* 对应中文标签,如 "面板下电,背光全黑双击" */
} meow_setting_def;
/* 变更通知回调:key 变化时调用(实体 UI 据此重绘 / web 经 MeowPanel 推送) */
typedef void (*meow_settings_listener)(const char *key);
/* ---- 初始化/生命周期 ---- */
/* 加载 storage + 注册默认;幂等,可重载(重载=重置默认再读 settings.conf)。
* 返回 MEOW_OK / MEOW_ERR_IO(文件可读性)/ MEOW_ERR_NOMEM(表满)。 */
int meow_settings_init(void);
/* 保存 + 清理(进程退出前调;不自动调用) */
int meow_settings_shutdown(void);
/* 调试:打印当前全部键(前缀 + key = value) */
void meow_settings_dump(const char *log_prefix);
/* ---- 设置项注册(启动时批量注册;重复注册返回 MEOW_ERR_EXISTS)---- */
int meow_settings_register(const meow_setting_def *def);
/* ---- 类型化存取(get 返回 0=成功,负=错误码;set 含 clamp/校验 + 广播)---- */
int meow_settings_get_int (const char *key, int *out);
int meow_settings_get_str (const char *key, char *buf, int buf_len);
int meow_settings_get_enum(const char *key, int *out); /* 返回索引(名字->索引) */
int meow_settings_get_bool(const char *key, int *out); /* 0/1 */
int meow_settings_set_int (const char *key, int v); /* clamp 到取值域 */
int meow_settings_set_str (const char *key, const char *v);/* 长度截断 MEOW_SET_MAX_LEN */
int meow_settings_set_enum(const char *key, int idx); /* 校验枚举合法,越界拒绝 */
int meow_settings_set_bool(const char *key, int v);
/* ---- 变更通知(set 成功后广播 key 字符串)---- */
int meow_settings_add_listener(meow_settings_listener cb);
int meow_settings_remove_listener(meow_settings_listener cb);
/* ---- 枚举辅助(供 UI/web 显示中文标签)---- */
int meow_settings_enum_label(const char *key, int idx, char *buf, int buf_len);
int meow_settings_enum_count(const char *key); /* 选项数;错误负值 */
int meow_settings_enum_index_of_name(const char *key, const char *name);
void meow_settings_enum_names(const char *key, const char **out_names, int *n);
/* ---- 列表:枚举当前全部键,供 web 面板 / 设置页生成控件 ---- */
int meow_settings_list(int (*cb)(const char *key, meow_set_type t, void *ctx), void *ctx);
/* ---- 默认值 / 复位 ---- */
int meow_settings_get_default(const char *key, char *buf, int len);
int meow_settings_reset(const char *key); /* 单键复位默认 */
int meow_settings_reset_all(void); /* 全量复位 */
/* ---- 落盘 / 导入导出(备份)---- */
int meow_settings_save(void); /* 写板载 settings.conf,保留 "# meow-settings 1" 头 */
int meow_settings_export(const char *path); /* 写一份 .conf 到 path;NULL=默认 TF 卡备份路径 */
int meow_settings_import(const char *path); /* 从 .conf 读入覆盖;逐键校验,记录损坏键不中断 */
#ifdef __cplusplus
}
#endif
#endif /* MEOW_SETTINGS_H */
+69
View File
@@ -0,0 +1,69 @@
/* meow_touch.c — 实现 Launcher 统一触摸接口(见 meow_touch.h)。 */
#include "meow_touch.h"
void mt_reset(meow_touch *mt)
{
mt->down = 0;
mt->valid = 0;
mt->down_x = -1;
mt->down_y = -1;
mt->last_x = -1;
mt->last_y = -1;
}
void mt_abs(meow_touch *mt, int x, int y)
{
if (x >= 0) mt->last_x = x;
if (y >= 0) mt->last_y = y;
/* 已按下且原点未锁,且两轴终于都有效 → 锁原点(goodix 时序免疫:只认 down 之后
* 的首个有效坐标,绝不锁残留 -1/上一手势旧坐标)。 */
if (mt->down && !mt->valid && mt->last_x >= 0 && mt->last_y >= 0) {
mt->down_x = mt->last_x;
mt->down_y = mt->last_y;
mt->valid = 1;
}
}
int mt_down(meow_touch *mt)
{
mt->down = 1;
mt->valid = 0;
/* 本次以「无坐标」起步:BTN_TOUCH 先于 ABS(本设备实测),down 前的 last 是上一
* 手势残留(-1/旧坐标),必须丢弃,只认 down 后到来的首个有效 ABS。 */
mt->last_x = -1;
mt->last_y = -1;
return 1;
}
int mt_up(meow_touch *mt, int *dx, int *dy)
{
int tx = mt->down_x, ty = mt->down_y;
int was_valid = mt->valid;
int ddx = mt->last_x - tx, ddy = mt->last_y - ty;
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
if (dx) *dx = ddx;
if (dy) *dy = ddy;
/* 复位手势态(down/valid);保留 down_x/y 供 mt_in 判定按钮命中,调用方下一手势
* 再由 mt_down 重置。 */
mt->down = 0;
mt->valid = 0;
if (!was_valid || tx < 0 || ty < 0) return 0; /* 从未锁到有效坐标:ghost/残留,丢本次 */
if (adx >= MEOW_TOUCH_SWIPE_THRESH || ady >= MEOW_TOUCH_SWIPE_THRESH) return 0; /* 滑动 */
return 1; /* 有效 tap */
}
int mt_in(meow_touch *mt, int x, int y, int w, int h)
{
return mt->down_x >= x && mt->down_x < x + w &&
mt->down_y >= y && mt->down_y < y + h;
}
/* 全局左缘返回判定(A-0):原点在左缘 + 向右位移≥MEOW_EDGE_SWIPE 且横向主导。
* 与 SDK meow_ui_input.c 的 EDGE_W/SWIPE_THRESH 同一语义,供设置页/应用列表复用。 */
int meow_edge_back(int down_x, int dx, int dy)
{
int ady = dy < 0 ? -dy : dy;
if (down_x >= 0 && down_x < MEOW_EDGE_W && dx >= MEOW_EDGE_SWIPE && ady < dx)
return 1;
return 0;
}
+63
View File
@@ -0,0 +1,63 @@
/* meow_touch.h — Launcher 统一触摸接口(轻量,不依赖 SDK)。
*
* 背景:Launcher 的低性能 UI(应用列表 / 电源菜单 / 确认框)各自有一套独立、重复的
* 触摸解析,除应用列表(M2.23 已修原点锁定)外其余都踩同一个 goodix 依赖 bug:
* BTN_TOUCH=1 按下边沿到来时 ABS 坐标可能尚未更新(goodix 时序不保证 ABS 与
* BTN_TOUCH 先后),旧代码在按下边沿用「当时 t_last_x/y」立即锁原点 → 锁到上一手势
* 残留(-1/旧坐标)→ 抬手命中按钮失败 → 「点两下」。本接口把「坐标时序免疫」的
* 手势解析收敛成一份,供各 UI 复用,语义与 M2.23 应用列表修法一致。
*
* 关键语义:
* - mt_down() 不立即锁原点:只标「已按下」,并把 last 坐标归 -1(本次以无坐标起步),
* 原点由 mt_abs() 在 down 之后出现的首个有效坐标锁定(BOTH >=0 才锁)。杜绝 GOODIX
* 残留,与 M2.23 应用列表 t_down_valid 同源。
* - mt_up() 算抬手位移:abs(dx)>=MEOW_TOUCH_SWIPE_THRESH || abs(dy)>=… → 判滑动
* (返回 0,供将来切屏),否则有效 tap(返回 1)。抬手后复位手势态(down/valid),
* 但保留 down_x/y 供 mt_in() 判定按钮命中 —— 调用方处理完再开下一手势。
* - mt_reset() 彻底清空,杜绝跨手势污染。
*
* 与应用列表(run_menu 内联,M2.23 直接内联实现)语义一致;列表保留内联(含 cooldown
* 丢弃/滑动日志/行带判定等额外逻辑),其余 UI 迁到本接口。
*/
#ifndef MEOW_TOUCH_H
#define MEOW_TOUCH_H
typedef struct meow_touch {
int down, valid; /* down=已按下;valid=原点已锁(down 后首个有效 ABS) */
int down_x, down_y; /* 按下原点(goodix 时序免疫:等 down 后首个有效坐标才锁) */
int last_x, last_y; /* 最新坐标(-1=本次尚未见到有效坐标) */
} meow_touch;
/* 抬手位移 >= 阈值判为滑动(不命中);与 launcher MENU_SWIPE_THRESH(65) 一致。 */
#define MEOW_TOUCH_SWIPE_THRESH 65
/* 全局左缘返回判定(A-0 方案,M2.39 抽出共享,供设置页/应用列表等 system 界面复用,
* 与 SDK meow_ui_input.c 的 EDGE_W(24)/SWIPE_THRESH(65) 对齐——同一套"左缘起点+右滑≥阈值"
* 语义,避免各界面自造重复判定。 */
#define MEOW_EDGE_W 24 /* 左缘起点宽度(down_x < 此值才认返回手势) */
#define MEOW_EDGE_SWIPE 65 /* 向右位移阈值(dx >= 此值且横向主导 → 左缘返回) */
/* 左缘返回判定:down_x=按下原点X,dx/dy=抬手位移。
* 返回 1=命中(left-edge + 向右≥MEOW_EDGE_SWIPE 且横向主导);0=否。
* 供设置页/应用列表等系统界面复用;SDK 手势引擎对 APP 用 on_swipe(LEFT) 同语义。 */
int meow_edge_back(int down_x, int dx, int dy);
/* 手势结束彻底清空(下一手势零污染;也用于起始初始化)。 */
void mt_reset(meow_touch *mt);
/* 喂 ABS_X/ABS_Y(调用方按事件逐个喂:AX 事件传 (val, mt->last_y),AY 事件传
* (mt->last_x, val)):更新 last;若已按下且未锁原点且两轴均有效 → 锁原点。 */
void mt_abs(meow_touch *mt, int x, int y);
/* BTN_TOUCH=1 按下边沿:标 down + last 归 -1(本次以无坐标起步,只认 down 后首个有效
* ABS 作原点)。返回 1=接受此按下(是否进一步处理由调用方决定,如 cooldown/日志)。 */
int mt_down(meow_touch *mt);
/* BTN_TOUCH=0 抬手边沿:计算位移写入 dx/dy 输出参数;复位 down/valid(保留 down_x/y 供 mt_in)。
* 返回 1=有效 tap,0=滑动(位移>=阈值)或从未锁到有效坐标(ghost/残留)。 */
int mt_up(meow_touch *mt, int *dx, int *dy);
/* 按下原点是否落在矩形 [x, x+w) × [y, y+h)(tap 命中;须在 mt_up() 返回 1 后调用)。 */
int mt_in(meow_touch *mt, int x, int y, int w, int h);
#endif /* MEOW_TOUCH_H */
+80
View File
@@ -0,0 +1,80 @@
/* pm_icons.h — 电源菜单圆钮图标(Material Symbols Rounded, MD3),单通道 alpha 位图 56x56。
* 生成自 material-symbols@0.47.0 rounded 字体光栅化;每像素 0..255(亮度/alpha)。
* ★ 统一 ink 高度 36px + 几何居中:5 图标视觉大小一致、圆心对齐。
* ★ 为日/夜间预留:位图只存亮度,绘制颜色由 pm_theme_* 提供,换主题仅改配色。 */
#ifndef PM_ICONS_H
#define PM_ICONS_H
#define PM_ICON_SIZE 56
typedef struct { const unsigned char *data; } pm_icon_t;
static const unsigned char PM_ICON_POWER[3136]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,74,238,238,74,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,193,255,255,193,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,201,254,254,201,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,1,0,199,255,255,199,0,1,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,29,0,0,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,0,0,29,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,1,2,8,186,247,148,1,2,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,2,1,148,247,186,8,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,153,255,254,255,46,0,2,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,2,0,46,255,254,255,153,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,71,255,249,253,238,26,1,1,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,1,1,26,238,253,249,255,71,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,5,213,254,250,255,89,0,3,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,3,0,89,255,250,254,213,5,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,87,255,248,255,176,0,3,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,3,0,176,255,248,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,193,254,252,249,40,1,3,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,3,1,40,249,252,254,193,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,44,250,252,255,174,0,3,0,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,0,3,0,174,255,252,250,44,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,118,255,249,255,79,1,4,0,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,0,4,1,79,255,249,255,118,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,180,255,254,240,19,0,1,0,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,0,1,0,19,240,254,255,180,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,221,255,255,201,0,1,0,0,0,0,0,0,0,0,1,0,200,255,255,200,0,1,0,0,0,0,0,0,0,0,1,0,202,255,255,221,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,243,254,255,171,0,3,0,0,0,0,0,0,0,0,1,0,199,255,255,199,0,1,0,0,0,0,0,0,0,0,3,0,171,255,254,243,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,253,251,255,155,0,4,0,0,0,0,0,0,0,0,1,0,202,252,252,202,0,1,0,0,0,0,0,0,0,0,4,0,155,255,251,253,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,253,251,255,156,0,4,0,0,0,0,0,0,0,0,3,0,183,255,255,183,0,3,0,0,0,0,0,0,0,0,4,0,156,255,251,253,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,244,254,255,171,0,3,0,0,0,0,0,0,0,0,2,1,40,181,181,40,1,2,0,0,0,0,0,0,0,0,3,0,171,255,254,244,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,222,255,255,199,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,199,255,255,222,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,183,255,255,237,16,0,1,0,0,0,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,0,0,1,0,16,237,255,255,183,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,122,255,250,255,70,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,1,70,255,250,255,122,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,251,252,255,161,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,161,255,252,251,48,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,198,254,252,245,30,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,30,245,252,254,198,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,92,255,248,255,165,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,165,255,248,255,92,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,7,217,254,250,255,84,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,84,255,250,254,217,6,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,76,255,249,253,243,50,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,50,243,253,249,255,76,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,159,255,248,255,233,51,0,4,2,0,0,0,0,0,0,0,0,0,0,0,0,2,4,0,51,233,255,248,255,159,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,10,205,255,248,255,241,86,0,0,3,3,1,0,0,0,0,0,0,1,3,3,0,0,86,241,255,248,255,205,10,3,1,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,2,0,26,217,255,248,255,255,160,36,0,0,0,1,3,4,4,3,1,0,0,0,36,160,255,255,248,255,217,26,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,27,202,255,251,251,255,244,158,72,19,0,0,0,0,0,0,19,72,158,244,255,251,251,255,202,27,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,10,156,255,255,248,255,255,255,236,199,171,155,155,171,200,236,255,255,255,248,255,255,156,10,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,79,210,255,255,253,250,255,255,255,255,255,255,255,255,250,253,255,255,210,79,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,6,94,193,249,255,255,255,254,251,251,254,255,255,255,249,193,94,6,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,50,120,180,220,243,253,253,243,220,180,120,50,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const unsigned char PM_ICON_RESTART[3136]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,42,209,240,129,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,38,223,255,255,255,40,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,38,227,255,251,255,255,45,4,6,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,36,225,255,252,253,253,130,0,0,0,0,0,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,35,224,255,252,255,255,251,186,176,149,104,49,1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,189,255,250,255,255,255,255,255,255,255,255,255,208,116,18,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,225,252,253,255,255,255,255,252,248,247,248,254,255,255,226,99,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,4,1,3,0,116,255,250,255,255,255,254,255,255,255,255,255,249,250,255,255,169,13,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,132,255,251,254,254,249,118,89,133,190,249,255,255,252,251,255,203,22,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,98,100,7,0,1,3,0,134,255,251,252,255,209,0,0,0,40,144,250,255,252,250,255,207,15,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,156,255,255,171,2,2,1,4,0,135,255,251,250,255,58,4,5,0,0,52,210,255,252,250,255,180,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,87,255,248,251,245,14,0,1,1,4,0,137,255,255,217,15,1,1,2,5,0,15,190,255,252,250,255,113,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,14,229,254,250,255,188,2,2,0,0,1,2,0,77,108,26,0,1,0,0,0,4,0,11,206,255,251,253,243,27,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,115,255,250,253,245,41,0,2,0,0,0,0,1,0,0,0,1,0,0,0,0,0,3,0,35,242,253,250,255,140,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,7,217,254,250,255,128,0,4,0,0,0,0,0,0,3,4,2,0,0,0,0,0,0,0,4,0,118,255,250,254,231,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,71,255,253,255,237,18,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,13,232,255,252,255,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,147,255,248,255,158,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,150,255,248,254,158,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,203,255,250,255,85,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,1,81,255,250,255,208,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,235,255,254,254,42,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,40,253,254,255,237,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,250,255,255,242,22,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,22,242,255,255,251,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,254,255,238,18,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,18,238,255,254,254,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,255,255,245,25,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,26,245,255,255,248,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,231,255,253,255,49,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,49,255,253,255,230,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,196,255,249,255,97,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,1,98,255,249,255,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,138,255,248,255,175,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,176,255,248,255,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,62,254,253,254,246,32,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,33,247,254,254,254,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,209,254,249,255,159,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,162,255,249,254,206,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,100,255,251,252,255,72,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,77,255,252,251,255,96,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,7,216,255,251,254,236,39,0,5,0,0,0,0,0,0,0,0,0,0,0,0,5,0,44,238,254,251,255,212,6,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,63,255,251,252,255,226,47,0,2,3,1,0,0,0,0,0,0,1,4,1,0,53,230,255,252,251,255,59,0,3,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,125,255,250,252,255,241,101,0,0,0,2,0,0,0,0,2,0,0,2,107,245,255,252,250,255,120,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,154,255,251,252,255,255,197,90,17,0,1,0,0,1,0,17,93,201,255,255,252,251,255,150,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,146,255,255,250,252,255,255,230,82,0,3,3,0,82,230,255,255,252,251,255,255,144,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,103,242,255,255,250,249,255,230,9,1,1,9,231,255,249,250,255,255,241,101,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,36,159,245,255,255,255,230,10,0,0,9,227,255,255,255,245,159,35,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,46,142,224,241,97,0,3,3,0,92,240,224,142,46,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const unsigned char PM_ICON_BACK[3136]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,31,186,248,242,150,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,29,215,255,254,255,255,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,29,219,255,251,255,254,253,249,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,28,216,255,251,255,255,255,253,253,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,27,215,255,251,255,255,255,251,255,195,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,26,214,255,252,255,255,255,252,255,223,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,25,213,255,251,255,255,255,252,255,227,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,25,212,255,251,255,255,255,252,255,227,39,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,24,211,255,251,255,255,255,252,255,228,40,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,22,210,255,251,255,255,255,252,255,229,42,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,22,209,255,251,255,255,255,252,255,230,42,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,21,208,255,251,255,255,255,252,255,231,43,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,20,206,255,251,255,255,255,252,255,232,45,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,19,205,255,251,255,255,255,252,255,233,46,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,205,255,251,255,255,255,252,255,234,47,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,200,255,251,255,255,255,253,255,235,48,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,176,255,251,255,255,255,253,255,237,49,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,251,253,255,255,255,253,254,229,43,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,251,253,255,255,255,253,254,229,43,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,176,255,251,255,255,255,253,255,237,50,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,200,255,251,255,255,255,253,255,235,48,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,205,255,251,255,255,255,252,255,234,47,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,19,205,255,251,255,255,255,252,255,233,46,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,20,206,255,251,255,255,255,252,255,232,45,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,21,208,255,251,255,255,255,252,255,231,44,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,22,209,255,251,255,255,255,252,255,230,43,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,23,210,255,251,255,255,255,252,255,230,42,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,24,211,255,251,255,255,255,252,255,228,40,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,25,212,255,251,255,255,255,252,255,227,40,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,25,214,255,251,255,255,255,252,255,227,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,26,214,255,252,255,255,255,252,255,223,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,27,215,255,251,255,255,255,251,255,195,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,28,216,255,251,255,255,255,253,253,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,29,219,255,251,255,254,253,249,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,30,215,255,254,255,255,149,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,31,186,248,243,150,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const unsigned char PM_ICON_UPGRADE[3136]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,31,201,249,201,31,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,28,214,255,252,255,214,28,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,28,218,255,252,255,252,255,218,28,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,27,216,255,251,255,255,255,251,255,216,27,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,27,215,255,251,255,255,255,255,255,251,255,215,27,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,26,214,255,251,255,255,255,255,255,255,255,251,255,214,26,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,25,213,255,251,255,253,255,255,255,255,255,253,255,251,255,213,25,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,25,212,255,251,255,253,255,252,255,255,255,252,255,253,255,251,255,212,25,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,25,212,255,251,255,251,255,134,194,255,250,255,194,134,255,251,255,251,255,212,25,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,184,255,250,255,251,255,143,0,199,255,250,255,200,0,142,255,251,255,250,255,184,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20,241,250,252,250,255,143,0,1,201,255,250,255,201,1,0,140,255,250,251,250,242,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,204,255,255,255,144,0,4,0,200,255,250,255,200,0,4,0,140,255,255,255,196,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,186,213,123,0,3,2,0,200,255,250,255,200,0,2,3,0,115,201,174,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,200,255,250,255,200,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,3,0,0,1,0,200,255,250,255,200,0,1,0,0,3,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,200,255,250,255,200,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,199,255,250,255,199,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,203,252,247,252,203,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,137,255,255,255,137,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,7,132,188,132,7,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,2,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,115,156,156,157,157,157,157,157,157,157,157,157,157,157,157,157,157,157,156,156,115,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,175,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,175,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,250,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,251,250,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,224,255,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,255,224,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,220,254,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,254,220,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const unsigned char PM_ICON_CANCEL[3136]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,194,250,227,90,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,90,227,250,194,36,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,197,255,253,255,252,91,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,91,252,255,253,255,197,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,253,255,253,254,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,254,253,255,253,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,235,255,253,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,253,255,235,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,88,254,253,254,255,254,253,255,93,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,92,255,253,254,255,254,253,254,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91,255,253,254,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,254,253,255,91,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,4,0,93,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,93,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,92,255,253,254,255,254,253,255,92,0,4,0,0,0,0,0,0,0,0,4,0,92,255,253,254,255,254,253,255,92,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,5,0,92,255,253,254,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,93,0,5,0,0,0,0,5,0,93,255,253,254,255,254,253,255,92,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,93,0,4,0,0,4,0,92,255,253,254,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,92,0,5,5,0,92,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,93,255,253,254,255,254,253,255,93,0,0,93,255,253,254,255,254,253,255,92,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,83,83,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,254,255,255,255,255,254,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,93,255,253,254,255,254,255,255,254,255,254,253,255,93,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,255,254,255,254,254,255,254,255,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,83,247,253,255,255,255,255,253,247,83,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,83,247,253,255,255,255,255,253,247,83,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,255,254,255,254,254,255,254,255,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,93,255,253,254,255,254,255,255,254,255,254,253,255,93,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,254,255,255,255,255,254,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,83,83,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,92,255,253,254,255,254,253,255,93,0,0,93,255,253,254,255,254,253,255,93,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,92,0,5,5,0,92,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,254,253,255,92,0,4,0,0,4,0,93,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,92,255,253,254,255,254,253,255,93,0,5,0,0,0,0,5,0,93,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,254,253,255,92,0,5,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,92,255,253,254,255,254,253,255,92,0,4,0,0,0,0,0,0,0,0,4,0,92,255,253,254,255,254,253,255,92,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,93,255,253,254,255,254,253,255,93,0,5,0,0,0,0,0,0,0,0,0,0,5,0,93,255,253,254,255,254,253,255,93,0,4,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,91,255,253,254,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,254,253,255,91,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,88,254,253,254,255,254,253,255,92,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,93,255,253,254,255,254,253,254,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,235,255,253,255,254,253,255,92,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,92,255,253,254,255,253,255,235,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,253,255,253,254,255,93,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,93,255,254,253,255,253,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,197,255,253,255,252,91,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,91,252,255,253,255,197,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,194,250,227,90,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,90,227,250,194,36,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const pm_icon_t pm_icons[5] = {
{ PM_ICON_POWER },
{ PM_ICON_RESTART },
{ PM_ICON_BACK },
{ PM_ICON_UPGRADE },
{ PM_ICON_CANCEL },
};
/* 图标绘制颜色(RGB565,16 位)。为日/夜间预留:选中/未选中 + 圆钮背景。
* MD3 无边框高亮:选中=未选中底色稍亮(深灰微冷调,无描边圈);icon_sel 白 / icon_nor 浅灰蓝。
* 换主题仅改此表,不重生成图标。 */
typedef struct { unsigned short icon_sel, icon_nor, btn_sel_bg, btn_nor_bg; } pm_theme_t;
static const pm_theme_t pm_theme_light = {
0xFFFF, 0xBC63, /* icon_sel 亮白, icon_nor 浅灰蓝(#B8C0D8) */
0x4A6B, 0x39E9 }; /* btn_sel_bg(#4A4F5A 底稍亮), btn_nor_bg(#3A3E48 底) */
static const pm_theme_t pm_theme_dark = {
0xFFFF, 0x9D39, /* icon_sel 亮白, icon_nor 浅灰蓝(#9AA4C8) */
0x2967, 0x1904 }; /* btn_sel_bg(#2A2E38 底稍亮), btn_nor_bg(#1D2027 底) */
#endif /* PM_ICONS_H */
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
# run_ab.sh — A/B launcher v6.5 (bug) vs v6.6 (fix) with the display panned to
# buffer1 (yoffset=720). Reproduces the reported bug: launcher draws the menu
# to buffer0 while the panel shows buffer1 -> menu invisible.
HPID=976
st(){ awk '{print $3}' "/proc/$HPID/stat" 2>/dev/null; }
yoff(){ /data/fbprobe 2>/dev/null | grep -oE "yoffset = [0-9]+"; }
seed(){
/data/fbtool blank 0 >/dev/null 2>&1 # unblank so pan is not EBUSY
/data/fbtool pan 720 >/dev/null 2>&1 # display -> buffer1 (yoffset=720)
echo " seeded yoffset=$(yoff)"
}
run_one(){
BIN=$1; TAG=$2
seed
echo "=== $TAG ($BIN) ==="
rm -f /usr/data/meow_launcherd.pid
"$BIN" --sim-hotkey 1200 --sim-exit 9000 >/tmp/m_$TAG.log 2>&1 &
LPID=$!
sleep 4
echo " DURING t=4s: player_stat=$(st) yoffset=$(yoff)"
dd if=/dev/fb0 of=/tmp/${TAG}_menu_b0.raw bs=960 count=720 status=none 2>/dev/null
echo " menu_b0=$(md5sum /tmp/${TAG}_menu_b0.raw 2>/dev/null | awk '{print $1}')"
echo " log: $(grep -E 'fb:|fb backup|fb pan|menu open' /tmp/m_$TAG.log | tr '\n' ' | ')"
sleep 6
echo " AFTER t=10s: player_stat=$(st) yoffset=$(yoff)"
echo " log2: $(grep -E 'fb restore|menu close|restore:|SIGCONT' /tmp/m_$TAG.log | tr '\n' ' | ')"
kill "$LPID" 2>/dev/null; sleep 1; killall meow_launcherd 2>/dev/null
echo " killed; player_stat=$(st)"
}
echo "player pid=$HPID"
run_one /data/meow_launcherd.v65 OLD
run_one /data/meow_launcherd.v66 NEW
echo "=== cleanup: restore player's natural screen-off (brightness 0 + blank) ==="
/data/fbtool blank 0 >/dev/null 2>&1
/data/fbtool pan 0 >/dev/null 2>&1
echo "brightness now: $(cat /sys/class/backlight/backlight_pwm0/brightness)"
echo "final yoffset=$(yoff) player_stat=$(st)"
echo DONE
+48
View File
@@ -0,0 +1,48 @@
#!/bin/sh
# run_bugcond.sh — A/B test of launcher v6.5 (bug) vs v6.6 (fix) under the
# bug condition: the display is on buffer1 (yoffset=720) with the player's
# frame in buffer1 (the post-power screen-off state). Reproduces the reported
# "menu invisible / screen frozen on last frame".
HPID=976
st(){ awk '{print $3}' "/proc/$HPID/stat" 2>/dev/null; }
yoff(){ /data/fbprobe 2>/dev/null | grep -oE "yoffset = [0-9]+"; }
setup_bugcond(){
# player frame into buffer1 (simulates the player's screen-off frame), then
# put the display on buffer1 so yoffset=720 -> buffer1 displayed.
/data/fbcopy >/dev/null 2>&1
/data/fbpan 720 >/dev/null 2>&1
echo " setup: buffer1<-buffer0, yoffset=$(yoff)"
}
run_one(){
BIN=$1; TAG=$2
echo "=== $TAG ($BIN) ==="
rm -f /usr/data/meow_launcherd.pid
"$BIN" --sim-hotkey 1200 --sim-exit 9000 >/tmp/m_$TAG.log 2>&1 &
LPID=$!
sleep 4
echo " DURING t=4s: player_stat=$(st) yoffset=$(yoff)"
dd if=/dev/fb0 of=/tmp/${TAG}_menu_b0.raw bs=960 count=720 status=none
dd if=/dev/fb0 of=/tmp/${TAG}_menu_b1.raw bs=960 skip=720 count=720 status=none
echo " menu b0=$(md5sum /tmp/${TAG}_menu_b0.raw 2>/dev/null | awk '{print $1}') b1=$(md5sum /tmp/${TAG}_menu_b1.raw 2>/dev/null | awk '{print $1}')"
echo " player_stat_after_capture=$(st)"
sleep 6
echo " AFTER t=10s: player_stat=$(st) yoffset=$(yoff)"
dd if=/dev/fb0 of=/tmp/${TAG}_close_b1.raw bs=960 skip=720 count=720 status=none
echo " close b1=$(md5sum /tmp/${TAG}_close_b1.raw 2>/dev/null | awk '{print $1}')"
echo " log: $(grep -E 'fb:|fb backup|fb pan|fb restore|menu open|menu close' /tmp/m_$TAG.log | tr '\n' ' | ')"
kill "$LPID" 2>/dev/null; sleep 1; killall meow_launcherd 2>/dev/null
echo " killed; player_stat=$(st)"
}
echo "launcher stopped, player pid=$HPID"
setup_bugcond
run_one /data/meow_launcherd.v65 OLD
setup_bugcond # re-setup for the NEW run (player frame still in buffer1)
run_one /data/meow_launcherd.v66 NEW
echo "=== cleanup ==="
/data/fbpan 720 >/dev/null 2>&1 # leave display on buffer1 (player's frame)
echo "final yoffset=$(yoff) player_stat=$(st)"
echo "BACKLIGHT: $(cat /sys/class/backlight/backlight_pwm0/brightness 2>/dev/null)"
echo DONE
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff. Show more