commit 245bee29461075de77d7022816635ed8506c22ca Author: MeowOS Dev Date: Sun Aug 30 01:12:34 2026 +0800 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 软件仓库 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c055e55 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/APPS.md b/APPS.md new file mode 100644 index 0000000..f827e80 --- /dev/null +++ b/APPS.md @@ -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.json # 应用描述(必填) + │ ├── # 静态 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//app.json`(state=2) | +| MeowAPPs 为空目录 | 同上(扫描计数 0 → state=2) | +| app.json 损坏/缺 name 或 exec 字段 | 跳过该应用,列表其余项正常 | +| 二进制不存在/不可执行 | 行尾标 `[MISSING]`;点击/确认该行提示 toast「CANNOT START: 」(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 + · 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 `(固件 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 --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 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_.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 写 `/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 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: 」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/ root@`(登录口令见私密记录,勿入库)。 +> 所有设备操作遵守纪律:动播放器前数实例;重启前 `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//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 /Packages` 缓存到 `MeowOS/opt/nyai/cache/`;无网正确提示、不硬失败 | +| `nyai install <包或文件.mpk>` | ① 参数是 `.mpk` 路径→直接装;② 是包名→按缓存索引取 `.mpk`(本地文件或 http 拉取)装;自动解析 Depends(递归)、校验 SHA256+app.json、同名先备份 `.bak_`、数据 init(建 MeowData//{data,cache}) | +| `nyai remove [--purge]` | 卸载 MeowAPPs/(数据默认保留);`--purge` 连 MeowData/ 删;清理 `.bak_` 残留 | +| `nyai list` | 列出 MeowAPPs/* 已装应用(名称/版本/占用),跳过 fonts/logs/.bak | +| `nyai info ` | 读 app.json 显示 name/version/exec/size/routes/services/depends/description | +| `nyai search [pattern]` | 扫索引列出可装包(含版本/描述) | +| `nyai list-upgradable` | 索引版本 > 已装版本的包 | +| `nyai index ` | 为 `` 生成 `Packages` 索引(读每个 .mpk 的 app.json 提取 version/description,算 SHA256/Size/Filename) | +| `nyai export [out.mpk]` | 打包 MeowAPPs//(不含 data/cache)→ `.mpk` | +| `nyai scan [dir]` / `nyai upgrade [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 ` 生成,行式、空行分隔记录): + +``` +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 只建目录不建链接。 diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md new file mode 100644 index 0000000..41c35e5 --- /dev/null +++ b/DEVELOPMENT_PLAN.md @@ -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.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 与网易云随后。 diff --git a/FRAMEWORK_DESIGN.md b/FRAMEWORK_DESIGN.md new file mode 100644 index 0000000..e8a6d1d --- /dev/null +++ b/FRAMEWORK_DESIGN.md @@ -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//` 目录名(app.json 可覆盖)。 +- `activity_id` 全局唯一为 `app_id/activity_id`。 +- **启动链路**:根屏→目标屏的 activity_id 序列(`;` 分隔),链长=逻辑深度。 +- 静态路由用 **app.json 扁平行**(不引入 JSON 解析器): + `route.=path`(如 `route.nowplaying=home;search;nowplaying`);`framework=true`(看门狗返回发 SIGUSR1);`service.=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=` + `MEOW_CHAIN=;;...;`。 +- 向下兼容:应用若不发 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 ]`。验证:`meow_ipc hello`→ACK、播放器不动(MeowOS 模式下无播放器即满足)。 +- ✅ **M1 路由表 + 解析**(0.7.10,M2.15):`app.json` 扁平行 `route.=path` / `framework=true` / `service.=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 ` 返回链路(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 解析结果)、`--ipc-only N`(免 fb/输入跑 IPC)、`--ipc-sock `(覆盖 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=/data>`、`XDG_CACHE_HOME=/cache>`、`XDG_CONFIG_HOME=/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 → 状态/音频子系统(已有,确认保留)。 diff --git a/GESTURE_BACK_DESIGN.md b/GESTURE_BACK_DESIGN.md new file mode 100644 index 0000000..c0072ec --- /dev/null +++ b/GESTURE_BACK_DESIGN.md @@ -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` | diff --git a/KEY_CONTROL_DESIGN.md b/KEY_CONTROL_DESIGN.md new file mode 100644 index 0000000..7a5b53e --- /dev/null +++ b/KEY_CONTROL_DESIGN.md @@ -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 菜单同样一致、电源菜单任何界面可弹(含应用内)、息屏后任意键唤醒。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a95568b --- /dev/null +++ b/LICENSE @@ -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. diff --git a/MEOWOS_API.md b/MEOWOS_API.md new file mode 100644 index 0000000..79a493a --- /dev/null +++ b/MEOWOS_API.md @@ -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_<域>_` | `MEOW_BOOT_MEOW` | +| 常量/宏 | `MEOW__` | `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": , "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 [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 `、`--ipc-only N --ipc-sock `(配合 `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) diff --git a/MEOW_APP_SDK.md b/MEOW_APP_SDK.md new file mode 100644 index 0000000..3202226 --- /dev/null +++ b/MEOW_APP_SDK.md @@ -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//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=` 可把 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` 为准。* diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md new file mode 100644 index 0000000..4400910 --- /dev/null +++ b/NOTIFICATION_SYSTEM.md @@ -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/.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)。 diff --git a/PERF_SCHEDULER.md b/PERF_SCHEDULER.md new file mode 100644 index 0000000..7a91bea --- /dev/null +++ b/PERF_SCHEDULER.md @@ -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 负担。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2932a0 --- /dev/null +++ b/README.md @@ -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/.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` 纪律章节。 diff --git a/SYSTEM_SETTINGS.md b/SYSTEM_SETTINGS.md new file mode 100644 index 0000000..0b5d8e1 --- /dev/null +++ b/SYSTEM_SETTINGS.md @@ -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 轻刷。 diff --git a/TF_CARD_LAYOUT.md b/TF_CARD_LAYOUT.md new file mode 100644 index 0000000..6ad794b --- /dev/null +++ b/TF_CARD_LAYOUT.md @@ -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.json + 二进制 + 资源 +│ ├── fonts/ # 预渲染字库 font.png + font.idx(可由 MeowOS 加载) +│ ├── logs/ # 统一日志(meow_.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 文件管理页直接映射这些目录。 diff --git a/bootlogos/boot_anim_0.png b/bootlogos/boot_anim_0.png new file mode 100644 index 0000000..c43133e Binary files /dev/null and b/bootlogos/boot_anim_0.png differ diff --git a/bootlogos/logo.jpeg b/bootlogos/logo.jpeg new file mode 100755 index 0000000..7fdb45f Binary files /dev/null and b/bootlogos/logo.jpeg differ diff --git a/bootlogos/logo.png b/bootlogos/logo.png new file mode 100644 index 0000000..1087eb1 Binary files /dev/null and b/bootlogos/logo.png differ diff --git a/bootlogos/logo1.jpeg b/bootlogos/logo1.jpeg new file mode 100755 index 0000000..a93ee61 Binary files /dev/null and b/bootlogos/logo1.jpeg differ diff --git a/bootlogos/logo1_new.jpeg b/bootlogos/logo1_new.jpeg new file mode 100644 index 0000000..07237e2 Binary files /dev/null and b/bootlogos/logo1_new.jpeg differ diff --git a/bootlogos/logo2.jpeg b/bootlogos/logo2.jpeg new file mode 100755 index 0000000..7fdb45f Binary files /dev/null and b/bootlogos/logo2.jpeg differ diff --git a/bootlogos/logo2_new.jpeg b/bootlogos/logo2_new.jpeg new file mode 100644 index 0000000..07237e2 Binary files /dev/null and b/bootlogos/logo2_new.jpeg differ diff --git a/bootlogos/logo_new.jpeg b/bootlogos/logo_new.jpeg new file mode 100644 index 0000000..07237e2 Binary files /dev/null and b/bootlogos/logo_new.jpeg differ diff --git a/bootlogos/shutdown_logo.jpeg b/bootlogos/shutdown_logo.jpeg new file mode 100644 index 0000000..a706f4c Binary files /dev/null and b/bootlogos/shutdown_logo.jpeg differ diff --git a/docs/MEOWOS_SPEC_REF.md b/docs/MEOWOS_SPEC_REF.md new file mode 100644 index 0000000..7947062 --- /dev/null +++ b/docs/MEOWOS_SPEC_REF.md @@ -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//{data,cache} # 应用专属数据(exFAT 无软链,用 env 注入) +├── MeowAPPs/ # 应用(/app.json+二进制+资源, fonts/, logs/, backgrounds/) +├── MeowOpt/ # 板载外部工具/二进制(如 mdnsd/,卡上加载,拔卡降级不崩)`[DEV_NOTES][meow_svcd.md]` +└── MeowOS/ # MeowOS 专属(一切非必要文件) + ├── backups/settings/setting.bak # 设置导出默认路径 + ├── logs/ # 系统日志 meow_.log + apps/...(见 §3 日志) + ├── notifications/(转储区,保留最新 10MB,不被电源事件清) + ├── opt/{bin,nyai,sources.list}, lib/, tools/ + └── themes/charging// # 充电屏主题(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//{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`/`MEOW_CHAIN`;;...; `[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 次(间隔 权威签名 = `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/.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 /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 / 数据 +- `/` = 程序主体+app.json+资源;`MeowData//{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.`/`service.`/`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=`(默认 default);`MeowOS/themes/charging//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/.log`(单文件);meow_ui.h §9 / DEV_NOTES #9 为 **`apps/<包名>/logs/<启动时间>_.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)并存,命名仅大小写之差、语义可能重叠。→ **两者是否为同一用途/是否需归并待确认**;当前按各自文档记录,勿混用。 +`[各源标注见上]` diff --git a/font/demo/glyph_contact_sheet.png b/font/demo/glyph_contact_sheet.png new file mode 100644 index 0000000..e4bf6fc Binary files /dev/null and b/font/demo/glyph_contact_sheet.png differ diff --git a/font/demo/meow_demo.device.png b/font/demo/meow_demo.device.png new file mode 100644 index 0000000..2ae1bf6 Binary files /dev/null and b/font/demo/meow_demo.device.png differ diff --git a/font/demo/meow_demo.mips.png b/font/demo/meow_demo.mips.png new file mode 100644 index 0000000..2ae1bf6 Binary files /dev/null and b/font/demo/meow_demo.mips.png differ diff --git a/font/demo/meow_demo.png b/font/demo/meow_demo.png new file mode 100644 index 0000000..2ae1bf6 Binary files /dev/null and b/font/demo/meow_demo.png differ diff --git a/font/demo/meow_font_demo.c b/font/demo/meow_font_demo.c new file mode 100644 index 0000000..0f44ed8 --- /dev/null +++ b/font/demo/meow_font_demo.c @@ -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 +#include +#include + +#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; +} diff --git a/font/demo/meow_font_demo.mips b/font/demo/meow_font_demo.mips new file mode 100755 index 0000000..688211e Binary files /dev/null and b/font/demo/meow_font_demo.mips differ diff --git a/font/demo/raw2png.py b/font/demo/raw2png.py new file mode 100644 index 0000000..e3dbe24 --- /dev/null +++ b/font/demo/raw2png.py @@ -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 +""" +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() diff --git a/font/gen/gen_font.py b/font/gen/gen_font.py new file mode 100644 index 0000000..7058597 --- /dev/null +++ b/font/gen/gen_font.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +gen_font.py — MeowOS 预渲染字库生成器(主机侧)· v2:像素进 PNG、索引进 idx。 + +产出(单份存储,设备与主机共用同一套字节): + /font.png 灰度 PNG(bit depth 4|8),含全部字号的面,每面一个子矩形 + /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(" """ +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(" +#include +#include + +/* ---------------- 最小 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); +} diff --git a/font/meow_font.h b/font/meow_font.h new file mode 100644 index 0000000..bb0f9c0 --- /dev/null +++ b/font/meow_font.h @@ -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 +#include + +#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 */ diff --git a/font/src/MiSans-Regular.otf b/font/src/MiSans-Regular.otf new file mode 100644 index 0000000..149f52b Binary files /dev/null and b/font/src/MiSans-Regular.otf differ diff --git a/mips-repo/README.md b/mips-repo/README.md new file mode 100644 index 0000000..f2d570d --- /dev/null +++ b/mips-repo/README.md @@ -0,0 +1,38 @@ +# mips-repo — MeowOS 板载 mipsel 软件仓库(宿主侧) + +目标设备:R3 Pro II / MeowOS 固件(**mipsel-linux-muslsf**,静态,无 glibc 依赖)。 +用途:存放针对该设备的交叉编译二进制与构建信息,便于**复用**到后续固件 / TF 卡 +(MeowLib / MeowOpt 目录)。 + +## 仓库结构(每工具一个目录) +``` +mips-repo/ + README.md # 本文件 + / + build.sh # 交叉编译脚本(可重复执行) + manifest.md # 元信息:来源/版本/arch/校验/产物路径/安装目标 + bin/ # 交叉编译产物(静态 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 /bin`,写 `/build.sh`(用 zig-mipsel-cc,`-Os -static`)。 +2. 跑 `build.sh`,产出放 `/bin/`。 +3. 写 `/manifest.md`:来源(如 Apple-OSS/mDNSResponder 版本)、ARCH、`file` + 输出摘要、`ls -l` 大小、构建命令、安装路径(固件 or TF MeowOpt)。 + +## 部署到哪里 +- 体积小(约 <200KB)→ 可直接打进固件(/usr/bin 或由 supervisor 启动)。 +- 体积大 → 放 TF 卡 `MeowOpt//`,由 MeowOS 启动时从卡加载(拔卡降级不崩)。 +- **按需取用**:从 `MeowOpt//` 按需部署,不一定要全部入库;若某工具体积仍偏重(如 mdnsd 的 885~994KB),可评估更小方案(OpenWrt linkd / libmdns,或自研极简响应器——仅回 A 记录 + `_ssh` 服务)以塞进固件 <200KB。 diff --git a/mips-repo/mdnsd/bin/mdnsd b/mips-repo/mdnsd/bin/mdnsd new file mode 100755 index 0000000..c1a1853 Binary files /dev/null and b/mips-repo/mdnsd/bin/mdnsd differ diff --git a/mips-repo/mdnsd/bin/mdnsd.bin b/mips-repo/mdnsd/bin/mdnsd.bin new file mode 100755 index 0000000..e8c0ee6 Binary files /dev/null and b/mips-repo/mdnsd/bin/mdnsd.bin differ diff --git a/mips-repo/mdnsd/build.sh b/mips-repo/mdnsd/build.sh new file mode 100755 index 0000000..81306f6 --- /dev/null +++ b/mips-repo/mdnsd/build.sh @@ -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\"" diff --git a/mips-repo/mdnsd/manifest.md b/mips-repo/mdnsd/manifest.md new file mode 100644 index 0000000..abb9525 --- /dev/null +++ b/mips-repo/mdnsd/manifest.md @@ -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/ +- 源码 : 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 -t _xxxx._tcp. -p `;`-b` 后台,`-P` pidfile, + `-v 0..2` 调试,`-r` 同时绑 53,`-f ` 读服务清单。 +- 权限:需 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。 diff --git a/patch_v2/apply_patch_v2.py b/patch_v2/apply_patch_v2.py new file mode 100644 index 0000000..59fcf32 --- /dev/null +++ b/patch_v2/apply_patch_v2.py @@ -0,0 +1,1109 @@ +#!/usr/bin/env python3 +"""Apply the App Launcher patch (v2.1) to the stock hiby_player (v1.4). + +Replaces the Qobuz streaming entry with a "MeowAPPs" app list page: + 1. checks the TF card (/usr/data/mnt/sd_0 via /proc/mounts); if missing + shows "未检测到存储卡" (no hang) + 2. scans /usr/data/mnt/sd_0/MeowAPPs/*/app.json; if absent/empty shows + "无可用应用" + directory example (no hang); malformed app.json is + skipped (no crash) + 3. shows app names (UTF-8 -> UTF-16 runtime conversion) with per-row + click callbacks + 4. on row click: access(X_OK) check -> "无法启动该应用" hint on failure + (no hang); else fork(4002)+execve(4011), parent blocks in + waitpid(4007) = "让位"; on app exit the UI resumes + +Keeps the v2.0 fixes: "viewgroup" type @ 0x7ee7a0 (not "imageview"), +0x46c800(parent, view), text setter vtable[0x198], child enumeration. + +Usage: python3 apply_patch_v2.py [stock] [output] +""" +import sys, struct + +STOCK = sys.argv[1] if len(sys.argv) > 1 else "hiby_player_stock" +OUT = sys.argv[2] if len(sys.argv) > 2 else "hiby_player_patched_v2" + +# ---------------------------------------------------------------- encoders +def enc_lw(rt, off, rs): return (0x23 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) +def enc_sw(rt, off, rs): return (0x2B << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) +def enc_sb(rt, off, rs): return (0x28 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) +def enc_sh(rt, off, rs): return (0x29 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) +def enc_lbu(rt, off, rs): return (0x24 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) +def enc_slt(rd, rs, rt): return 0x0000002A | (rs << 21) | (rt << 16) | (rd << 11) +def enc_slti(rt, rs, imm): return (0x0A << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) +def enc_sltiu(rt, rs, imm): return (0x0B << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) +def enc_addiu(rt, rs, imm): return (0x09 << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) +def enc_andi(rt, rs, imm): return (0x0C << 26) | (rs << 21) | (rt << 16) | (imm & 0xFFFF) +def enc_lui(rt, imm): return (0x0F << 26) | (rt << 16) | (imm & 0xFFFF) +def enc_beq(rs, rt, off): return (0x04 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) +def enc_bne(rs, rt, off): return (0x05 << 26) | (rs << 21) | (rt << 16) | (off & 0xFFFF) +def enc_blez(rs, off): return (0x06 << 26) | (rs << 21) | (off & 0xFFFF) +def enc_j(target): return (0x02 << 26) | ((target >> 2) & 0x03FFFFFF) +def enc_jal(target): return (0x03 << 26) | ((target >> 2) & 0x03FFFFFF) +def enc_jr(rs): return 0x00000008 | (rs << 21) +def enc_jalr(rd, rs): return 0x00000009 | (rs << 21) | (rd << 11) +def enc_move(rd, rs): return 0x00000021 | (rs << 21) | (rd << 11) +def enc_addu(rd, rs, rt): return 0x00000021 | (rs << 21) | (rt << 16) | (rd << 11) +def enc_or(rd, rs, rt): return 0x00000025 | (rs << 21) | (rt << 16) | (rd << 11) +def enc_sll(rd, rt, sa): return 0x00000000 | (rt << 16) | (rd << 11) | (sa << 6) +NOP = 0x00000000 +SYSCALL = 0x0000000C + +# ---------------------------------------------------------------- globals +# .data zero region 0x9636bc (verified zeros in stock; see ANALYSIS.md §17) +# 全局区迁移(2026-08-28): 0x9636bc 被 hiby_player 运行时复用(实测 g_page 被写 1) +# -> 0x9060e4 (.data 大零区, 真机 gdb 实测启动后 60s 全零未被写) +G_BASE_HI = 0x90 # lui 高 16 位 +G_BASE_LO = 0x60E4 # addiu 低 16 位(正偏移) +G_STATE = 0x0000 # 0=ok 1=no card 2=no apps +G_PAGE = 0x0004 +G_QOBUZ = 0x0008 +G_COUNT = 0x000C +G_ROWS = 0x0010 # 8x u32 +G_NAMES = 0x0030 # 8x16B +G_EXECS = 0x00B0 # 8x48B +G_SCAN = 0x0230 # 256B +G_UTF16 = 0x0330 # 8x48B +G_HINT = 0x04B0 # 48B utf16 +G_NOTE = 0x04E0 # 48B utf16 +G_PENDING = 0x0510 # autotest pending flag +G_TRACE = 0x0514 # loop_hook 执行计数器(调试) +G_SIGREG = 0x0518 # SIGUSR1 handler 已注册标志 +G_PARENT = 0x051C # entry 回调原始 parent(0x46c800/0x46c600 需要) + +PLT = dict(fread=0x8E6180, access=0x8E6240, strstr=0x8E6280, fopen=0x8E62E0, + open=0x8E6120, read=0x8E6F00, + signal=0x8E6970, + fgets=0x8E6440, strlen=0x8E6510, readdir=0x8E65B0, strcmp=0x8E6650, + fclose=0x8E6760, opendir=0x8E6C00, closedir=0x8E6C40, strcpy=0x8E70B0) + +CREATE_VIEW = 0x49BE00 +ACTIVATE_VIEW = 0x46C800 +SET_CALLBACK = 0x46C2C0 + +CAVE = 0x41B8B0 +STR_BASE = CAVE + 0x1000 +STR_VIEWGROUP = 0x7EE7A0 + +# ---------------------------------------------------------------- assembler +class Asm: + def __init__(self, base): + self.base = base + self.words = [] + self.patches = [] + self.syms = {} + + def here(self): + return self.base + 4 * len(self.words) + + def label(self, name): + self.syms[name] = self.here() + + def emit(self, word): + self.words.append((self.here(), word)) + return self.here() - 4 + + def raw(self, words): + for w in words: + self.emit(w) + + def b(self, rs, rt, label, eq=True): + a = self.emit(None) + self.patches.append((a, "beq" if eq else "bne", rs, rt, label)) + + def blez(self, rs, label): + a = self.emit(None) + self.patches.append((a, "blez", rs, 0, label)) + + def j(self, label): + a = self.emit(None) + self.patches.append((a, "j", 0, 0, label)) + + def jal(self, label_or_abs): + if isinstance(label_or_abs, int): + self.emit(enc_jal(label_or_abs)) + else: + a = self.emit(None) + self.patches.append((a, "jal", 0, 0, label_or_abs)) + + def load_cstr(self, rt, label): + a1 = self.emit(None) + a2 = self.emit(None) + self.patches.append((a1, "lui", rt, rt, label)) + self.patches.append((a2, "addiu", rt, rt, label)) + + def load_abs(self, rt, addr): + hi, lo = (addr >> 16) & 0xFFFF, addr & 0xFFFF + if lo & 0x8000: + self.raw([enc_lui(rt, (hi + 1) & 0xFFFF), enc_addiu(rt, rt, lo - 0x10000)]) + else: + self.raw([enc_lui(rt, hi), enc_addiu(rt, rt, lo)]) + + def gp(self): + self.raw([enc_lui(28, 0x97), enc_addiu(28, 28, -0x3BC0)]) + + def load_g(self, rt, off): + self.raw([enc_lui(rt, G_BASE_HI), enc_lw(rt, G_BASE_LO + off, rt)]) + + def addr_g(self, rt, off): + self.raw([enc_lui(rt, G_BASE_HI), enc_addiu(rt, rt, G_BASE_LO + off)]) + + def store_g(self, rs, off): + self.raw([enc_lui(8, G_BASE_HI), enc_sw(rs, G_BASE_LO + off, 8)]) + + def li(self, rt, imm): + if 0 <= imm <= 0x7FFF: + self.raw([enc_addiu(rt, 0, imm)]) + else: + self.raw([enc_lui(rt, (imm >> 16) & 0xFFFF), enc_addiu(rt, rt, imm & 0xFFFF)]) + + def resolve(self): + for a, kind, rs, rt, arg in self.patches: + t = self.syms[arg] if isinstance(arg, str) else arg + if kind in ("beq", "bne"): + off = (t - (a + 4)) // 4 + assert -0x8000 <= off < 0x8000, f"{kind}@{a:#x}->{t:#x} off {off}" + w = enc_beq(rs, rt, off) if kind == "beq" else enc_bne(rs, rt, off) + elif kind == "blez": + off = (t - (a + 4)) // 4 + assert -0x8000 <= off < 0x8000 + w = enc_blez(rs, off) + elif kind == "j": + w = enc_j(t) + elif kind == "jal": + w = enc_jal(t) + elif kind == "lui": + w = enc_lui(rt, (t >> 16) & 0xFFFF) + elif kind == "addiu": + lo = t & 0xFFFF + if lo & 0x8000: + lo -= 0x10000 + for j in range(len(self.words)): + va, ww = self.words[j] + if va == a - 4 and ww is not None and (ww >> 26) == 0x0F \ + and ((ww >> 16) & 0x1F) == rt: + self.words[j] = (va, enc_lui(rt, ((t >> 16) + 1) & 0xFFFF)) + break + else: + for j in range(len(self.words)): + va, ww = self.words[j] + if va == a - 4 and ww is not None and (ww >> 26) == 0x0F \ + and ((ww >> 16) & 0x1F) == rt: + self.words[j] = (va, enc_lui(rt, (t >> 16) & 0xFFFF)) + break + w = enc_addiu(rt, rt, lo & 0xFFFF) + self.words[self.words.index((a, None))] = (a, w) + for a, w in self.words: + assert w is not None, f"unresolved @{a:#x}" + + def blob(self): + self.resolve() + out = bytearray() + for a, w in self.words: + out.extend(struct.pack("=7(扩展事件)才返回; type<7 继续 +A.raw([NOP]) +A.raw([enc_addiu(3, 0, 4)]) +A.b(2, 3, "entry_ret", eq=False) +A.raw([NOP]) +# parent fallback: if $a0==0 use layout ctx (*(0x96d124)) +A.b(4, 0, "entry_have_parent", eq=False) +A.raw([NOP]) +A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) +A.label("entry_have_parent") +A.store_g(4, G_PARENT) # g_parent = effective parent +# view guard: if $a1==0 skip mark/g_qobuz +A.b(5, 0, "entry_noview", eq=True) +A.raw([NOP]) +A.raw([enc_addiu(2, 0, 1), enc_sb(2, 0xC1, 5)]) # mark selected +A.addr_g(8, G_QOBUZ) +A.raw([enc_sw(5, 0, 8)]) # g_qobuz = view +A.label("entry_noview") +A.load_g(8, G_PAGE) +A.b(8, 0, "entry_activate", eq=False) +A.raw([NOP]) +# ---- build page ---- +A.raw([enc_addiu(29, 29, -0x20)]) +A.raw([enc_sw(31, 0x1C, 29), enc_sw(4, 0x18, 29), enc_sw(16, 0x14, 29), enc_sw(17, 0x10, 29)]) +# card check +A.jal("check_card") +A.raw([NOP]) +A.store_g(2, G_STATE) # state = 0/1 +# scan MeowAPPs +A.jal("scan") +A.raw([NOP]) +# if count==0 and card present -> state=2 +A.load_g(8, G_COUNT) +A.b(8, 0, "entry_have", eq=False) +A.raw([NOP]) +A.load_g(8, G_STATE) +A.b(8, 0, "entry_have", eq=False) # card missing -> keep state=1 +A.raw([NOP]) +A.li(8, 2) +A.store_g(8, G_STATE) +A.label("entry_have") +# create page: 0x49be00(parent, "hiby_apps_main", "viewgroup") +A.load_abs(6, STR_VIEWGROUP) +A.load_cstr(5, "s_apps_main") +A.raw([enc_lw(4, 0x18, 29)]) +A.jal(CREATE_VIEW) +A.raw([NOP]) +A.gp() +A.b(2, 0, "entry_skip_setup", eq=True) +A.raw([NOP]) +A.addr_g(8, G_PAGE) +A.raw([enc_sw(2, 0, 8)]) # g_page = view +A.raw([enc_move(4, 2)]) +A.jal("setup") +A.raw([NOP]) +# 两条路径(建页成功/失败)在此汇聚恢复栈帧, 避免 v0==0 时 $ra 指向 +# CREATE_VIEW 返回地址跳回 entry 中间(潜在崩溃) +A.label("entry_skip_setup") +A.raw([enc_lw(17, 0x10, 29), enc_lw(16, 0x14, 29), enc_lw(31, 0x1C, 29)]) +A.raw([enc_addiu(29, 29, 0x20)]) +A.label("entry_activate") +A.load_g(4, G_PAGE) +A.b(4, 0, "entry_ret", eq=True) +A.raw([NOP]) +# 0x46c800(a0=parent, a1=view): 沿 a1 的 0xf8 链找 layout_main 并显示 a1 +# (0x46c800 反汇编实证: s0=s2=a1 起链; 原版回调 0x4f0120 参数原样传递) +A.load_g(5, G_PAGE) # a1 = page (激活应用页) +A.load_g(4, G_PARENT) # a0 = parent +A.jal(ACTIVATE_VIEW) +A.raw([NOP]) +A.label("entry_ret") +A.gp() +A.raw([enc_jr(31), NOP]) + +# ---------------- check_card() -> $v0=1 if sd_0 mounted ---------------- +A.label("check_card") +A.gp() +A.raw([enc_addiu(29, 29, -0x20)]) +A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) +A.load_cstr(4, "s_mounts") +A.load_cstr(5, "s_r") +A.jal(PLT["fopen"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "cc_nofile", eq=True) +A.raw([NOP]) +A.raw([enc_move(16, 2)]) # s0 = FILE* +A.raw([enc_addiu(17, 0, 0)]) # s1 = found = 0 +A.label("cc_loop") +A.addr_g(4, G_SCAN) # a0 = buf (fgets(buf, size, FILE)) +A.raw([enc_addiu(5, 0, 128)]) # a1 = size +A.raw([enc_move(6, 16)]) # a2 = FILE* (原顺序错误: a0=FILE,a1=buf,a2=size) +A.jal(PLT["fgets"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "cc_done", eq=True) +A.raw([NOP]) +A.addr_g(4, G_SCAN) +A.load_cstr(5, "s_sd0") +A.jal(PLT["strstr"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "cc_found", eq=False) +A.raw([NOP]) +A.j("cc_loop") +A.raw([NOP]) +A.label("cc_found") +A.raw([enc_addiu(17, 0, 1)]) +A.label("cc_done") +A.raw([enc_move(4, 16)]) +A.jal(PLT["fclose"]) +A.raw([NOP]) +A.gp() +A.raw([enc_move(2, 17)]) +A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) +A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) +A.label("cc_nofile") +A.raw([enc_addiu(2, 0, 0), enc_jr(31), NOP]) + +# ---------------- setup($a0=page): rows + hints + back ---------------- +A.label("setup") +A.gp() +A.raw([enc_addiu(29, 29, -0x20)]) +A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29), enc_sw(18, 0x10, 29)]) +A.raw([enc_move(16, 4)]) +A.raw([enc_addiu(17, 0, 0)]) # i +A.label("setup_loop") +A.load_g(2, G_COUNT) +A.raw([enc_slt(3, 17, 2)]) +A.b(3, 0, "setup_hints", eq=True) +A.raw([NOP]) +A.load_cstr(4, "s_tv_prefix") +A.addr_g(5, G_SCAN) +A.jal(PLT["strcpy"]) +A.raw([NOP]) +A.gp() +A.addr_g(4, G_SCAN) +A.jal(PLT["strlen"]) +A.raw([NOP]) +A.addr_g(8, G_SCAN) +A.raw([enc_addu(8, 8, 2)]) +A.raw([enc_addiu(2, 17, 0x30), enc_sb(2, 0, 8), enc_sb(0, 1, 8)]) +A.raw([enc_move(5, 8)]) +A.raw([enc_move(4, 16)]) +A.jal("find") +A.raw([NOP]) +A.b(2, 0, "setup_next", eq=True) +A.raw([NOP]) +A.addr_g(8, G_ROWS) +A.raw([enc_sll(9, 17, 2), enc_addu(8, 8, 9), enc_sw(2, 0, 8)]) +A.raw([enc_move(18, 2)]) +A.addr_g(5, G_NAMES) +A.raw([enc_sll(9, 17, 4), enc_addu(5, 5, 9)]) +A.addr_g(6, G_UTF16) +A.raw([enc_sll(9, 17, 5), enc_sll(10, 17, 4), enc_addu(6, 6, 9)]) +A.raw([enc_move(4, 18)]) +A.jal("settext") +A.raw([NOP]) +A.raw([enc_move(4, 18)]) +A.load_cstr(5, "rowcb") +A.jal(SET_CALLBACK) +A.raw([NOP]) +A.label("setup_next") +A.raw([enc_addiu(17, 17, 1)]) +A.j("setup_loop") +A.raw([NOP]) +# hints by state +A.label("setup_hints") +A.load_g(8, G_STATE) +A.raw([enc_addiu(9, 0, 1)]) +A.b(8, 9, "setup_nocard", eq=True) +A.raw([NOP]) +A.raw([enc_addiu(9, 0, 2)]) +A.b(8, 9, "setup_noapp", eq=True) +A.raw([NOP]) +# state 0 +A.load_cstr(4, "s_t_hint_ok") +A.jal("set_hint") +A.raw([NOP]) +A.j("setup_backrow") +A.raw([NOP]) +A.label("setup_nocard") +A.load_cstr(4, "s_t_no_card") +A.jal("set_hint") +A.raw([NOP]) +A.j("setup_backrow") +A.raw([NOP]) +A.label("setup_noapp") +A.load_cstr(4, "s_t_no_app") +A.jal("set_hint") +A.raw([NOP]) +A.load_cstr(4, "s_t_example") +A.jal("set_note") +A.raw([NOP]) +# back row +A.label("setup_backrow") +A.raw([enc_move(4, 16)]) +A.load_cstr(5, "s_tv_back") +A.jal("find") +A.raw([NOP]) +A.b(2, 0, "setup_skipback", eq=True) +A.raw([NOP]) +A.raw([enc_move(4, 2)]) +A.load_cstr(5, "backcb") +A.jal(SET_CALLBACK) +A.raw([NOP]) +A.label("setup_skipback") +A.raw([enc_lw(18, 0x10, 29), enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29)]) +A.raw([enc_lw(31, 0x1C, 29), enc_addiu(29, 29, 0x20)]) +A.raw([enc_jr(31), NOP]) + +# ---------------- set_hint($a0=utf8) ---------------- +A.label("set_hint") +A.raw([enc_addiu(29, 29, -0x20)]) +A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) +A.raw([enc_move(17, 4)]) +A.load_g(4, G_PAGE) +A.b(4, 0, "hint_out", eq=True) +A.raw([NOP]) +A.load_cstr(5, "s_tv_hint") +A.jal("find") +A.raw([NOP]) +A.b(2, 0, "hint_out", eq=True) +A.raw([NOP]) +A.raw([enc_move(4, 2), enc_move(5, 17)]) +A.addr_g(6, G_HINT) +A.jal("settext") +A.raw([NOP]) +A.label("hint_out") +A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) +A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) + +# ---------------- set_note($a0=utf8) ---------------- +A.label("set_note") +A.raw([enc_addiu(29, 29, -0x20)]) +A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) +A.raw([enc_move(17, 4)]) +A.load_g(4, G_PAGE) +A.b(4, 0, "note_out", eq=True) +A.raw([NOP]) +A.load_cstr(5, "s_tv_note") +A.jal("find") +A.raw([NOP]) +A.b(2, 0, "note_out", eq=True) +A.raw([NOP]) +A.raw([enc_move(4, 2), enc_move(5, 17)]) +A.addr_g(6, G_NOTE) +A.jal("settext") +A.raw([NOP]) +A.label("note_out") +A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) +A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) + +# ---------------- settext($a0=row, $a1=utf8, $a2=buf) ---------------- +A.label("settext") +A.raw([enc_addiu(29, 29, -0x20)]) +A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29), enc_sw(18, 0x10, 29)]) +A.raw([enc_move(16, 4), enc_move(17, 5), enc_move(18, 6)]) +A.raw([enc_move(4, 5), enc_move(5, 6)]) +A.jal("u2u") +A.raw([NOP]) +A.raw([enc_lw(4, 0x48, 16)]) +A.b(4, 0, "settext_out", eq=True) +A.raw([NOP]) +A.raw([enc_lw(9, 0x198, 4)]) +A.raw([enc_move(5, 18)]) +A.raw([enc_jalr(31, 9), NOP]) +A.label("settext_out") +A.raw([enc_lw(18, 0x10, 29), enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29)]) +A.raw([enc_lw(31, 0x1C, 29), enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) + +# ---------------- u2u($a0=utf8, $a1=utf16buf): utf8->utf16le ---------------- +A.label("u2u") +A.label("u2u_loop") +A.raw([enc_lbu(9, 0, 4)]) +A.b(9, 0, "u2u_end", eq=True) +A.raw([NOP]) +A.raw([enc_andi(10, 9, 0x80)]) +A.b(10, 0, "u2u_ascii", eq=True) +A.raw([NOP]) +A.raw([enc_andi(10, 9, 0xE0)]) +A.raw([enc_addiu(11, 0, 0xC0)]) +A.b(10, 11, "u2u_two", eq=True) +A.raw([NOP]) +# 3-byte +A.raw([enc_lbu(11, 1, 4), enc_lbu(12, 2, 4)]) +A.raw([enc_andi(10, 9, 0x0F), enc_sll(10, 10, 12)]) +A.raw([enc_andi(13, 11, 0x3F), enc_sll(13, 13, 6)]) +A.raw([enc_or(10, 10, 13)]) +A.raw([enc_andi(13, 12, 0x3F), enc_or(10, 10, 13)]) +A.raw([enc_sh(10, 0, 5)]) +A.raw([enc_addiu(4, 4, 3), enc_addiu(5, 5, 2)]) +A.j("u2u_loop") +A.raw([NOP]) +A.label("u2u_two") +A.raw([enc_lbu(11, 1, 4)]) +A.raw([enc_andi(10, 9, 0x1F), enc_sll(10, 10, 6)]) +A.raw([enc_andi(13, 11, 0x3F), enc_or(10, 10, 13)]) +A.raw([enc_sh(10, 0, 5)]) +A.raw([enc_addiu(4, 4, 2), enc_addiu(5, 5, 2)]) +A.j("u2u_loop") +A.raw([NOP]) +A.label("u2u_ascii") +A.raw([enc_sh(9, 0, 5)]) +A.raw([enc_addiu(4, 4, 1), enc_addiu(5, 5, 2)]) +A.j("u2u_loop") +A.raw([NOP]) +A.label("u2u_end") +A.raw([enc_sh(0, 0, 5), enc_jr(31), NOP]) + +# ---------------- find($a0=parent, $a1=name) -> $v0 ---------------- +A.label("find") +A.raw([enc_lw(8, 0xFC, 4)]) +A.b(8, 0, "find_nf", eq=True) +A.raw([NOP]) +A.raw([enc_move(13, 5)]) +A.raw([enc_lw(9, 0x30, 8)]) +A.raw([enc_jalr(31, 9), enc_move(4, 8)]) +A.blez(2, "find_nf") +A.raw([enc_move(12, 2)]) +A.raw([enc_addiu(10, 0, 0)]) +A.label("find_loop") +A.raw([enc_lw(9, 0x2C, 8)]) +A.raw([enc_move(5, 10)]) +A.raw([enc_jalr(31, 9), enc_move(4, 8)]) +A.b(2, 0, "find_next", eq=True) +A.raw([NOP]) +A.raw([enc_move(11, 2)]) +A.raw([enc_addiu(4, 11, 0x20)]) +A.raw([enc_move(5, 13)]) +A.jal(PLT["strcmp"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "find_found", eq=True) +A.raw([NOP]) +A.label("find_next") +A.raw([enc_addiu(10, 10, 1)]) +A.raw([enc_slt(3, 10, 12)]) +A.b(3, 0, "find_loop", eq=False) +A.raw([NOP]) +A.label("find_nf") +A.raw([enc_addiu(2, 0, 0), enc_jr(31), NOP]) +A.label("find_found") +A.raw([enc_move(2, 11), enc_jr(31), NOP]) + +# ---------------- rowcb(parent, view, &type) ---------------- +A.label("rowcb") +A.gp() +A.raw([enc_lw(2, 0, 6), enc_slti(3, 2, 7)]) +A.b(3, 0, "rowcb_ret", eq=True) # type>=7 返回 +A.raw([NOP]) +A.raw([enc_addiu(3, 0, 4)]) +A.b(2, 3, "rowcb_ret", eq=False) +A.raw([NOP]) +A.addr_g(8, G_ROWS) +A.raw([enc_addiu(9, 0, 0)]) +A.label("rowcb_loop") +A.raw([enc_sltiu(3, 9, 8)]) +A.b(3, 0, "rowcb_ret", eq=True) +A.raw([NOP]) +A.raw([enc_lw(2, 0, 8)]) +A.b(2, 5, "rowcb_launch", eq=True) +A.raw([NOP]) +A.raw([enc_addiu(8, 8, 4), enc_addiu(9, 9, 1)]) +A.j("rowcb_loop") +A.raw([NOP]) +A.label("rowcb_launch") +A.raw([enc_move(4, 9)]) +A.jal("launch") +A.raw([NOP]) +A.label("rowcb_ret") +A.gp() +A.raw([enc_jr(31), NOP]) + +# ---------------- backcb(parent, view, &type) ---------------- +# 返回流媒体页: 0x46c800(parent, qobuz 入口 view) — 沿 qobuz 链显示流媒体页 +A.label("backcb") +A.gp() +A.raw([enc_lw(2, 0, 6), enc_slti(3, 2, 7)]) +A.b(3, 0, "backcb_ret", eq=True) # type>=7 返回 +A.raw([NOP]) +A.raw([enc_addiu(3, 0, 4)]) +A.b(2, 3, "backcb_ret", eq=False) +A.raw([NOP]) +A.load_g(5, G_QOBUZ) +A.b(5, 0, "backcb_ret", eq=True) +A.raw([NOP]) +A.load_g(4, G_PARENT) +A.jal(ACTIVATE_VIEW) +A.raw([NOP]) +A.label("backcb_ret") +A.gp() +A.raw([enc_jr(31), NOP]) + +# ---------------- launch($a0=index) ---------------- +# frame: ra@0x24 s0@app_pid@0x20 s1@wd_pid@0x1c appargv@0x08,0x0c +# wdargv@0x10,0x14,0x18 status@0x04 +A.label("launch") +A.gp() +A.raw([enc_addiu(29, 29, -0x28)]) +A.raw([enc_sw(31, 0x24, 29), enc_sw(16, 0x20, 29), enc_sw(17, 0x1C, 29)]) +A.addr_g(8, G_EXECS) +A.raw([enc_sll(9, 4, 5), enc_sll(10, 4, 4), enc_addu(9, 9, 10)]) +A.raw([enc_addu(8, 8, 9)]) +# access(path, X_OK=1) +A.raw([enc_move(4, 8)]) +A.raw([enc_addiu(5, 0, 1)]) +A.jal(PLT["access"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "launch_fail", eq=False) +A.raw([NOP]) +# fork app +A.raw([enc_addiu(2, 0, 4002), SYSCALL]) +A.b(2, 0, "launch_app_parent", eq=False) +A.raw([NOP]) +# app child: execve(path, {path,0}, 0) +A.raw([enc_sw(8, 0x08, 29), enc_sw(0, 0x0C, 29)]) +A.raw([enc_move(4, 8)]) +A.raw([enc_addiu(5, 29, 0x08)]) +A.raw([enc_addiu(6, 0, 0)]) +A.raw([enc_addiu(2, 0, 4011), SYSCALL]) +A.raw([enc_addiu(2, 0, 4001), enc_addiu(4, 0, 1), SYSCALL]) +A.label("launch_app_parent") +A.raw([enc_move(16, 2)]) # s0 = app_pid +# itoa(app_pid) -> g_scan +A.addr_g(5, G_SCAN) +A.raw([enc_move(4, 16)]) +A.jal("itoa") +A.raw([NOP]) +# fork watchdog +A.raw([enc_addiu(2, 0, 4002), SYSCALL]) +A.b(2, 0, "launch_wd_parent", eq=False) +A.raw([NOP]) +# wd child: execve("/usr/data/meow_watchdog", {wd, g_scan, 0}, 0) +A.load_cstr(4, "s_wd") +A.raw([enc_sw(4, 0x10, 29)]) +A.addr_g(8, G_SCAN) +A.raw([enc_sw(8, 0x14, 29), enc_sw(0, 0x18, 29)]) +A.raw([enc_addiu(5, 29, 0x10)]) +A.raw([enc_addiu(6, 0, 0)]) +A.raw([enc_addiu(2, 0, 4011), SYSCALL]) +A.raw([enc_addiu(2, 0, 4001), enc_addiu(4, 0, 1), SYSCALL]) +A.label("launch_wd_parent") +A.raw([enc_move(17, 2)]) # s1 = wd_pid +# waitpid(app_pid, &status, 0) — blocks = 让位 +A.raw([enc_move(4, 16)]) +A.raw([enc_addiu(5, 29, 0x04)]) +A.raw([enc_addiu(6, 0, 0)]) +A.raw([enc_addiu(2, 0, 4007), SYSCALL]) +# kill(wd_pid, SIGTERM=15) +A.raw([enc_move(4, 17)]) +A.raw([enc_addiu(5, 0, 15)]) +A.raw([enc_addiu(2, 0, 4037), SYSCALL]) +A.raw([enc_addiu(2, 0, 0)]) # v0 = 0 (ok) +A.raw([enc_lw(17, 0x1C, 29), enc_lw(16, 0x20, 29), enc_lw(31, 0x24, 29)]) +A.raw([enc_addiu(29, 29, 0x28), enc_jr(31), NOP]) +A.label("launch_fail") +# hint "无法启动该应用" +A.raw([enc_sw(31, 0x24, 29)]) +A.load_cstr(4, "s_t_fail") +A.jal("set_hint") +A.raw([NOP]) +A.raw([enc_addiu(2, 0, 1)]) # v0 = 1 (failed) +A.raw([enc_lw(31, 0x24, 29), enc_addiu(29, 29, 0x28), enc_jr(31), NOP]) + +# ---------------- itoa($a0=value, $a1=buf) ---------------- +# t0=value t1=end t2=tmp t3=quot t4=rem; writes digits reversed then copies +A.label("itoa") +A.raw([enc_move(8, 4)]) # t0 = value +A.raw([enc_addiu(9, 5, 12)]) # t1 = buf+12 (temp tail) +A.raw([enc_sb(0, 1, 9)]) # temp[13] = 0 terminator +A.label("itoa_loop") +A.raw([enc_addiu(10, 0, 10)]) +A.raw([0x0000001B | (8 << 21) | (10 << 16)]) # divu $t0, $t2 +A.raw([NOP]) +A.raw([0x00000012 | (11 << 11)]) # mflo $t3 (商) +A.raw([0x00000010 | (12 << 11)]) # mfhi $t4 (余) +A.raw([enc_addiu(12, 12, 0x30)]) # t4 = rem + '0' +A.raw([enc_sb(12, 0, 9)]) +A.raw([enc_addiu(9, 9, -1)]) +A.raw([enc_move(8, 11)]) # t0 = quotient +A.b(11, 0, "itoa_loop", eq=False) # quotient!=0 -> loop +A.raw([NOP]) +A.label("itoa_copy") +A.raw([enc_addiu(10, 9, 1)]) # t2 = end+1 +A.label("itoa_cp") +A.raw([enc_lbu(12, 0, 10)]) +A.raw([enc_sb(12, 0, 5)]) +A.raw([enc_addiu(10, 10, 1)]) +A.raw([enc_addiu(5, 5, 1)]) +A.b(12, 0, "itoa_done", eq=False) +A.raw([NOP]) +A.j("itoa_cp") +A.raw([NOP]) +A.label("itoa_done") +A.raw([enc_jr(31), NOP]) + +# ---------------- scan() ---------------- +A.label("scan") +A.store_g(0, G_COUNT) +A.load_cstr(4, "s_appsdir") +A.jal(PLT["opendir"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "scan_done", eq=True) +A.raw([NOP]) +A.raw([enc_addiu(29, 29, -0x20)]) +A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29), enc_sw(18, 0x10, 29)]) +A.raw([enc_move(16, 2)]) +A.raw([enc_addiu(17, 0, 0)]) +A.label("scan_loop") +A.raw([enc_move(4, 16)]) +A.jal(PLT["readdir"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "scan_close", eq=True) +A.raw([NOP]) +A.raw([enc_move(18, 2)]) +A.raw([enc_lbu(2, 0x0B, 18)]) +A.raw([enc_addiu(3, 0, 0x2E)]) +A.b(2, 3, "scan_proc", eq=False) +A.raw([NOP]) +A.raw([enc_lbu(2, 0x0C, 18)]) +A.b(2, 0, "scan_loop", eq=True) +A.raw([NOP]) +A.raw([enc_addiu(3, 0, 0x2E)]) +A.b(2, 3, "scan_proc", eq=False) +A.raw([NOP]) +A.raw([enc_lbu(2, 0x0D, 18)]) +A.b(2, 0, "scan_proc", eq=False) +A.raw([NOP]) +A.j("scan_loop") +A.raw([NOP]) +A.label("scan_proc") +A.raw([enc_sltiu(2, 17, 8)]) +A.b(2, 0, "scan_close", eq=True) +A.raw([NOP]) +A.addr_g(4, G_SCAN) +A.load_cstr(5, "s_appsdir") +A.jal(PLT["strcpy"]) +A.raw([NOP]) +A.gp() +A.addr_g(4, G_SCAN) +A.jal(PLT["strlen"]) +A.raw([NOP]) +A.addr_g(8, G_SCAN) +A.raw([enc_addu(8, 8, 2)]) +A.raw([enc_move(4, 8)]) +A.raw([enc_addiu(5, 18, 0x0B)]) +A.jal(PLT["strcpy"]) +A.raw([NOP]) +A.gp() +A.addr_g(4, G_SCAN) +A.jal(PLT["strlen"]) +A.raw([NOP]) +A.addr_g(8, G_SCAN) +A.raw([enc_addu(4, 8, 2)]) +A.load_cstr(5, "s_appjson") +A.jal(PLT["strcpy"]) +A.raw([NOP]) +A.gp() +A.addr_g(4, G_SCAN) +A.load_cstr(5, "s_r") +A.jal(PLT["fopen"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "scan_next", eq=True) +A.raw([NOP]) +A.raw([enc_move(18, 2)]) +A.addr_g(4, G_SCAN) # a0 = buf (fgets(buf, size, FILE)) +A.raw([enc_addiu(4, 4, 0x60)]) +A.raw([enc_addiu(5, 0, 96)]) # a1 = size +A.raw([enc_move(6, 18)]) # a2 = FILE* +A.jal(PLT["fgets"]) +A.raw([NOP]) +A.gp() +A.raw([enc_move(4, 18)]) +A.jal(PLT["fclose"]) +A.raw([NOP]) +A.gp() +# parse name +A.addr_g(4, G_SCAN) +A.raw([enc_addiu(4, 4, 0x60)]) +A.load_cstr(5, "s_name_k") +A.jal(PLT["strstr"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "scan_next", eq=True) +A.raw([NOP]) +A.raw([enc_move(18, 2)]) +A.label("scan_skip1") +A.raw([enc_lbu(2, 0, 18)]) +A.raw([enc_addiu(3, 0, 0x22)]) +A.b(2, 3, "scan_got1", eq=True) +A.raw([NOP]) +A.raw([enc_addiu(18, 18, 1)]) +A.j("scan_skip1") +A.raw([NOP]) +A.label("scan_got1") +A.raw([enc_addiu(18, 18, 1)]) +A.addr_g(8, G_NAMES) +A.raw([enc_sll(9, 17, 4), enc_addu(8, 8, 9)]) +A.raw([enc_move(9, 18)]) +A.label("scan_copy1") +A.raw([enc_lbu(2, 0, 9)]) +A.raw([enc_addiu(3, 0, 0x22)]) +A.b(2, 3, "scan_end1", eq=True) +A.raw([NOP]) +A.b(2, 0, "scan_end1", eq=True) +A.raw([NOP]) +A.raw([enc_sb(2, 0, 8), enc_addiu(8, 8, 1), enc_addiu(9, 9, 1)]) +A.j("scan_copy1") +A.raw([NOP]) +A.label("scan_end1") +A.raw([enc_sb(0, 0, 8)]) +# parse exec (missing -> skip app: no count++) +A.addr_g(4, G_SCAN) +A.raw([enc_addiu(4, 4, 0x60)]) +A.load_cstr(5, "s_exec_k") +A.jal(PLT["strstr"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "scan_next", eq=True) +A.raw([NOP]) +A.raw([enc_move(18, 2)]) +A.label("scan_skip2") +A.raw([enc_lbu(2, 0, 18)]) +A.raw([enc_addiu(3, 0, 0x22)]) +A.b(2, 3, "scan_got2", eq=True) +A.raw([NOP]) +A.raw([enc_addiu(18, 18, 1)]) +A.j("scan_skip2") +A.raw([NOP]) +A.label("scan_got2") +A.raw([enc_addiu(18, 18, 1)]) +A.addr_g(8, G_EXECS) +A.raw([enc_sll(9, 17, 5), enc_sll(10, 17, 4), enc_addu(9, 9, 10)]) +A.raw([enc_addu(8, 8, 9)]) +A.raw([enc_move(9, 18)]) +A.label("scan_copy2") +A.raw([enc_lbu(2, 0, 9)]) +A.raw([enc_addiu(3, 0, 0x22)]) +A.b(2, 3, "scan_end2", eq=True) +A.raw([NOP]) +A.b(2, 0, "scan_end2", eq=True) +A.raw([NOP]) +A.raw([enc_sb(2, 0, 8), enc_addiu(8, 8, 1), enc_addiu(9, 9, 1)]) +A.j("scan_copy2") +A.raw([NOP]) +A.label("scan_end2") +A.raw([enc_sb(0, 0, 8)]) +A.raw([enc_addiu(17, 17, 1)]) +A.label("scan_next") +A.j("scan_loop") +A.raw([NOP]) +A.label("scan_close") +A.raw([enc_move(4, 16)]) +A.jal(PLT["closedir"]) +A.raw([NOP]) +A.gp() +A.store_g(17, G_COUNT) +A.raw([enc_lw(18, 0x10, 29), enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29)]) +A.raw([enc_lw(31, 0x1C, 29), enc_addiu(29, 29, 0x20)]) +A.label("scan_done") +A.raw([enc_jr(31), NOP]) + +# ---------------- touch_hook / autotest_check ---------------- +# 0x47b820 (hgl_ts 触摸读取, lg_ial 线程, 息屏时也在 poll 触摸) 入口被 +# patch 为 "j touch_hook; nop" —— nop 消除 delay slot, 避免原 prologue +# 第 2 条 (sw ra) 以未调整的 sp 先执行(乱写调用者栈, 上一版 SIGTRAP 主嫌)。 +# hook 内补执行原 prologue 两条后跳回 0x47b828。 +A.label("touch_hook") +A.raw([enc_addiu(29, 29, -0x80)]) # 原 0x47b820 prologue 指令1 +A.raw([enc_sw(31, 0x7C, 29)]) # 原 prologue 指令2 +A.raw([enc_sw(28, 0x08, 29)]) # 保存 gp (0x08(sp) 原函数未用) +# 无信号方案: 早期 lg_ial 线程与主线程并发 signal() 疑触发 glibc 竞态 SIGSEGV。 +# touch_hook 只置 pending, 由主循环 loop_hook 轮询消费(主循环带超时, 无需唤醒信号)。 +A.jal("autotest_check") +A.raw([NOP]) +A.raw([enc_lw(28, 0x08, 29)]) # 恢复 gp +A.raw([enc_j(0x47B828)]) # 回原函数继续 +A.raw([NOP]) + +# loop_hook: 0x436fc0 (system_if 主循环, 主线程) -> "j loop_hook; nop" +# pending 置位时: 解析 qobuz 入口视图(0x49be00), 调它的回调 (view+0x140) +# cb(parent, view, &type=4) —— 与真实点击完全同路径, 主线程执行无 UI 竞态。 +A.label("loop_hook") +A.raw([enc_addiu(29, 29, -0x20)]) # 原 0x436fc0 prologue 指令1 +A.raw([enc_sw(31, 0x1C, 29)]) # 原 prologue 指令2 +A.raw([enc_sw(28, 0x08, 29)]) # 保存 gp (0x08(sp) 原函数未用) +# trace 计数器(调试用, gdb 读 0x906614) +A.raw([enc_lui(8, G_BASE_HI), enc_lw(9, G_BASE_LO + G_TRACE, 8)]) +A.raw([enc_addiu(9, 9, 1), enc_sw(9, G_TRACE, 8)]) +A.load_g(8, G_PENDING) +A.b(8, 0, "lh_done", eq=True) +A.raw([NOP]) +A.store_g(0, G_PENDING) # 清 pending +# type=4 @ sp+0x0c +A.raw([enc_addiu(2, 0, 4), enc_sw(2, 0x0C, 29)]) +# a0 = ctx = *(0x96d124) +A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) +A.b(4, 0, "lh_done", eq=True) # ctx 未就绪 -> 放弃 +A.raw([NOP]) +# 0x49be00(ctx, "stream_media_vg_qobuz", "viewgroup") -> qobuz 入口视图 +A.load_abs(6, STR_VIEWGROUP) +A.load_cstr(5, "s_qobuz_name") +A.jal(CREATE_VIEW) +A.raw([NOP]) +A.gp() +A.b(2, 0, "lh_entry_fallback", eq=True) # 视图未找到(未进过流媒体页) -> 直接 entry +A.raw([NOP]) +# 调视图回调: lw t9, 0x140(view); t9(ctx, view, &type) +A.raw([enc_move(10, 2)]) # t2 = view (t 寄存器, 不污染 callee-saved) +A.raw([enc_lw(9, 0x140, 10)]) +A.b(9, 0, "lh_entry_fallback", eq=True) +A.raw([NOP]) +A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) # a0 = ctx +A.raw([enc_move(5, 10)]) # a1 = view +A.raw([enc_addiu(6, 29, 0x0C)]) # a2 = &type +A.raw([enc_jalr(31, 9), NOP]) +A.j("lh_done") +A.raw([NOP]) +# fallback: entry(ctx, 0, &type) —— 从任意页面直接创建+激活应用列表页。 +# view=0 跳过 g_qobuz/选中标记; g_parent 由 entry 内取 ctx 兜底。 +A.label("lh_entry_fallback") +A.raw([enc_lui(8, 0x97), enc_lw(4, -0x2EDC, 8)]) # a0 = ctx +A.raw([enc_addiu(5, 0, 0)]) # a1 = 0 +A.raw([enc_addiu(6, 29, 0x0C)]) # a2 = &type +A.jal("entry") +A.raw([NOP]) +A.gp() +A.label("lh_done") +A.raw([enc_lw(28, 0x08, 29)]) # 恢复 gp +A.raw([enc_j(0x436FC8)]) # 回原函数继续 +A.raw([NOP]) + +# ---------------- pending_hook (v3, 2026-08-28) ---------------- +# 触发机制修正: 0x436fc0 不是主循环(init 一次性调用, 已实证 G_TRACE 恒 0)。 +# 真正的主线程周期循环 = LiteGUI 主循环 0x44a820/0x44a900(同构双副本), 每轮 +# 调用 0x488020(触摸轮询, 已实证 X/Y 全局更新)。把两处 "jal 0x488020" +# (0x44a898/0x44a950) 改为 "j pending_hook_{a,b}; nop", hook 内: +# 执行原 delay slot → 检查 G_PENDING → 有则清 + entry(ctx, 0, &type=4) +# → jal 0x488020(原调用) → jr t0 回各自循环继续。主线程执行, 无 UI 竞态。 +A.label("pending_hook_a") +A.load_abs(8, 0x44A8A0) # t0 = 0x44a8a0 (循环 A 续点) +A.b(0, 0, "pending_common", eq=True) # 无条件跳公共段 +A.raw([NOP]) +A.label("pending_hook_b") +A.load_abs(8, 0x44A958) # t0 = 0x44a958 (循环 B 续点) +A.label("pending_common") +A.raw([enc_sw(8, 0x14, 29)]) # 续点存 sp+0x14 (entry 会践踏 t0) +A.raw([enc_addiu(4, 29, 0x18)]) # a0 = sp+0x18 (原 delay slot) +A.raw([enc_lui(9, 0x90), enc_lw(9, 0x65F4, 9)]) # t9 = G_PENDING (0x9065f4) +A.b(9, 0, "ph_call", eq=True) # pending == 0 → 直接原调用 +A.raw([NOP]) +A.raw([enc_lui(9, 0x90), enc_sw(0, 0x65F4, 9)]) # 清 pending +A.raw([enc_addiu(2, 0, 4), enc_sw(2, 0x0C, 29)]) # type=4 @ sp+0x0c +A.raw([enc_lui(9, 0x97), enc_lw(4, -0x2EDC, 9)]) # a0 = ctx = *(0x96d124) +A.b(4, 0, "ph_call", eq=True) # ctx 未就绪 → 跳过 +A.raw([NOP]) +A.raw([enc_addiu(5, 0, 0)]) # a1 = 0 (view: 跳过 g_qobuz/选中) +A.raw([enc_addiu(6, 29, 0x0C)]) # a2 = &type +A.raw([enc_lui(8, 0x90), enc_lw(9, 0x65F8, 8)]) # 诊断: G_TRACE++ +A.raw([enc_addiu(9, 9, 1), enc_sw(9, 0x65F8, 8)]) +A.jal("entry") +A.raw([NOP]) +A.gp() +A.label("ph_call") +A.jal(0x488020) # 原调用 (触摸轮询, 保持 UI 状态) +A.raw([enc_addiu(4, 29, 0x18)]) # 原 delay slot +A.raw([enc_lw(8, 0x14, 29)]) # jal 返回后恢复续点 (0x488020 践踏 t0) +A.raw([enc_jr(8)]) # 回各自循环续点 +A.raw([NOP]) + +A.label("autotest_check") +# 注意: 此函数被插在 0x47b820 prologue 中间执行, 原函数依赖 $a0 (touch obj) +# 必须保存/恢复所有 caller-saved: a0-a3 +A.raw([enc_addiu(29, 29, -0x20)]) # 独立临时帧 +A.raw([enc_sw(31, 0x1C, 29), enc_sw(16, 0x18, 29), enc_sw(17, 0x14, 29)]) +A.raw([enc_sw(4, 0x10, 29), enc_sw(5, 0x0C, 29), enc_sw(6, 0x08, 29), enc_sw(7, 0x04, 29)]) +# 防抖: 布局上下文(0x96d124)必须已就绪(UI 初始化完成)才触发 +A.raw([enc_lui(8, 0x97), enc_lw(8, -0x2EDC, 8)]) +A.b(8, 0, "at_done", eq=True) +A.raw([NOP]) +A.load_cstr(4, "s_autotest") +A.raw([enc_addiu(5, 0, 0)]) # F_OK +A.jal(PLT["access"]) +A.raw([NOP]) +A.gp() +A.b(2, 0, "at_done", eq=False) # 文件不存在 -> done +A.raw([NOP]) +# 存在: 删除(一次性) + pending=1 (主循环 loop_hook 轮询消费, 无信号) +A.load_cstr(4, "s_autotest") +A.jal(0x8E6E60) # remove +A.raw([NOP]) +A.gp() +A.raw([enc_addiu(2, 0, 1)]) +A.store_g(2, G_PENDING) +A.label("at_done") +A.raw([enc_lw(7, 0x04, 29), enc_lw(6, 0x08, 29), enc_lw(5, 0x0C, 29), enc_lw(4, 0x10, 29)]) +A.raw([enc_lw(17, 0x14, 29), enc_lw(16, 0x18, 29), enc_lw(31, 0x1C, 29)]) +A.raw([enc_addiu(29, 29, 0x20), enc_jr(31), NOP]) + +# ---------------- assemble ---------------- +cave_code = A.blob() +assert len(cave_code) <= 0x1000, f"cave code overflow: {len(cave_code):#x}" +cave_code += b"\x00" * (STR_BASE - (CAVE + len(cave_code))) +assert len(cave_code) == STR_BASE - CAVE +for name, bs in STRINGS: + cave_code += bs +while len(cave_code) % 4: + cave_code += b"\x00" + +print(f"cave total: {len(cave_code)} bytes") +for k in ["entry", "check_card", "setup", "set_hint", "set_note", "settext", + "u2u", "find", "rowcb", "backcb", "launch", "scan"]: + print(f" {k}: {A.syms[k]:#x}") + +PATCH_LUI_OFF = 0x4EFE28 - 0x400000 +PATCH_ADD_OFF = 0x4EFE2C - 0x400000 +PATCH_TOUCH_OFF = 0x47B820 - 0x400000 +PATCH_LOOP_OFF = 0x436FC0 - 0x400000 +CAVE_OFF = CAVE - 0x400000 + +def main(): + data = bytearray(open(STOCK, "rb").read()) + orig_lui = struct.unpack_from(" {OUT} ({len(data)} bytes)") + +if __name__ == "__main__": + main() diff --git a/patch_v2/apps_assets/apps/hello/app.json b/patch_v2/apps_assets/apps/hello/app.json new file mode 100644 index 0000000..dbb7c3e --- /dev/null +++ b/patch_v2/apps_assets/apps/hello/app.json @@ -0,0 +1 @@ +{"name":"Hello App","exec":"/usr/data/mnt/sd_0/MeowAPPs/hello/hello","version":"0.1","immersive":1} \ No newline at end of file diff --git a/patch_v2/apps_assets/apps/hello/hello b/patch_v2/apps_assets/apps/hello/hello new file mode 100755 index 0000000..9168934 Binary files /dev/null and b/patch_v2/apps_assets/apps/hello/hello differ diff --git a/patch_v2/apps_assets/layout/theme1/hiby_apps_main.view b/patch_v2/apps_assets/layout/theme1/hiby_apps_main.view new file mode 100644 index 0000000..fdf66eb --- /dev/null +++ b/patch_v2/apps_assets/layout/theme1/hiby_apps_main.view @@ -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 + } +} diff --git a/patch_v2/apps_assets/str/english/apps.ini b/patch_v2/apps_assets/str/english/apps.ini new file mode 100644 index 0000000..37820ef Binary files /dev/null and b/patch_v2/apps_assets/str/english/apps.ini differ diff --git a/patch_v2/apps_assets/str/french/apps.ini b/patch_v2/apps_assets/str/french/apps.ini new file mode 100644 index 0000000..a55cbfd Binary files /dev/null and b/patch_v2/apps_assets/str/french/apps.ini differ diff --git a/patch_v2/apps_assets/str/german/apps.ini b/patch_v2/apps_assets/str/german/apps.ini new file mode 100644 index 0000000..00486d5 Binary files /dev/null and b/patch_v2/apps_assets/str/german/apps.ini differ diff --git a/patch_v2/apps_assets/str/italy/apps.ini b/patch_v2/apps_assets/str/italy/apps.ini new file mode 100644 index 0000000..ad181b3 Binary files /dev/null and b/patch_v2/apps_assets/str/italy/apps.ini differ diff --git a/patch_v2/apps_assets/str/japanese/apps.ini b/patch_v2/apps_assets/str/japanese/apps.ini new file mode 100644 index 0000000..20cc116 Binary files /dev/null and b/patch_v2/apps_assets/str/japanese/apps.ini differ diff --git a/patch_v2/apps_assets/str/korean/apps.ini b/patch_v2/apps_assets/str/korean/apps.ini new file mode 100644 index 0000000..d2dafe9 Binary files /dev/null and b/patch_v2/apps_assets/str/korean/apps.ini differ diff --git a/patch_v2/apps_assets/str/poland/apps.ini b/patch_v2/apps_assets/str/poland/apps.ini new file mode 100644 index 0000000..71c613c Binary files /dev/null and b/patch_v2/apps_assets/str/poland/apps.ini differ diff --git a/patch_v2/apps_assets/str/russian/apps.ini b/patch_v2/apps_assets/str/russian/apps.ini new file mode 100644 index 0000000..39f65d8 Binary files /dev/null and b/patch_v2/apps_assets/str/russian/apps.ini differ diff --git a/patch_v2/apps_assets/str/simplified_chinese/apps.ini b/patch_v2/apps_assets/str/simplified_chinese/apps.ini new file mode 100644 index 0000000..9ad071e Binary files /dev/null and b/patch_v2/apps_assets/str/simplified_chinese/apps.ini differ diff --git a/patch_v2/apps_assets/str/spain/apps.ini b/patch_v2/apps_assets/str/spain/apps.ini new file mode 100644 index 0000000..e4cdb77 Binary files /dev/null and b/patch_v2/apps_assets/str/spain/apps.ini differ diff --git a/patch_v2/apps_assets/str/thai/apps.ini b/patch_v2/apps_assets/str/thai/apps.ini new file mode 100644 index 0000000..547bdea Binary files /dev/null and b/patch_v2/apps_assets/str/thai/apps.ini differ diff --git a/patch_v2/apps_assets/str/traditional_chinese/apps.ini b/patch_v2/apps_assets/str/traditional_chinese/apps.ini new file mode 100644 index 0000000..0464e03 Binary files /dev/null and b/patch_v2/apps_assets/str/traditional_chinese/apps.ini differ diff --git a/patch_v2/apps_assets/str/ukrainian/apps.ini b/patch_v2/apps_assets/str/ukrainian/apps.ini new file mode 100644 index 0000000..c3700a5 Binary files /dev/null and b/patch_v2/apps_assets/str/ukrainian/apps.ini differ diff --git a/patch_v2/bootmenu/S84ssh b/patch_v2/bootmenu/S84ssh new file mode 100755 index 0000000..df16291 --- /dev/null +++ b/patch_v2/bootmenu/S84ssh @@ -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 diff --git a/patch_v2/bootmenu/S90meow_bootmenu b/patch_v2/bootmenu/S90meow_bootmenu new file mode 100755 index 0000000..f26ae7f --- /dev/null +++ b/patch_v2/bootmenu/S90meow_bootmenu @@ -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 diff --git a/patch_v2/bootmenu/S92_03_start_music_player b/patch_v2/bootmenu/S92_03_start_music_player new file mode 100644 index 0000000..4a59cf6 --- /dev/null +++ b/patch_v2/bootmenu/S92_03_start_music_player @@ -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 $? diff --git a/patch_v2/bootmenu/S95shutdown_logo b/patch_v2/bootmenu/S95shutdown_logo new file mode 100644 index 0000000..78d02b2 --- /dev/null +++ b/patch_v2/bootmenu/S95shutdown_logo @@ -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 diff --git a/patch_v2/bootmenu/build_bootmenu.sh b/patch_v2/bootmenu/build_bootmenu.sh new file mode 100755 index 0000000..b4c9f4b --- /dev/null +++ b/patch_v2/bootmenu/build_bootmenu.sh @@ -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 diff --git a/patch_v2/bootmenu/meow_bootmenu b/patch_v2/bootmenu/meow_bootmenu new file mode 100755 index 0000000..bc00c12 Binary files /dev/null and b/patch_v2/bootmenu/meow_bootmenu differ diff --git a/patch_v2/bootmenu/meow_bootmenu.c b/patch_v2/bootmenu/meow_bootmenu.c new file mode 100644 index 0000000..0113dde --- /dev/null +++ b/patch_v2/bootmenu/meow_bootmenu.c @@ -0,0 +1,1595 @@ +/* meow_bootmenu.c — R3 Pro II MeowOS/HibyOS 开机引导选单(阶段0 · M0 二进制自检版) + * + * M2.14 热键一次触发修复(2026-08-29,MEOW_BOOTMENU_VER 0.1.0-m2.14): + * 开机武装延迟(ARM_DELAY_MS) + 播放器忙判门(proc_is_busy) 不再「静默丢弃」热键—— + * 命中即置 hotkey_pending,待条件满足(延迟结束/播放器不忙)后自动弹确认框 + * (用户只按一次,不需第二下)。新增 --sim-hotkey-strict 让 sim 走该门(测 pending)。 + * + * M2.26 触摸确认迁移(2026-08-29,MEOW_BOOTMENU_VER 0.1.0-m2.26):选单触摸 + 热键 + * 确认框触摸从各自 t_last_x/y + t_down_x/y 解析迁到 meow_touch 统一接口(mt_reset/ + * mt_abs/mt_down/mt_up;goodix 时序免疫:BTN_TOUCH=1 不锁原点,等按下后首个有效 ABS + * 才锁,位移>=65 判滑动不命中)——修「触摸确认要点两下」(电源键一下即可确认)。 + * 绘制/选单逻辑/超时倒计时/fork 播放器/supervisor/热键触发核心路径不动。 + * + * M2 诊断 · 信号记录 hook(仅记录,不改行为;排查「HibyOS 播放器 UI 触发关机/升级 → + * 卡最后一帧但系统没真关机/SSH 能连」第一步):supervisor 加 bm_log() 写 TF 卡 + * MeowAPPs/logs/meow_boot_.log(卡不可用降级 stderr),记录 supervisor + * **收到**的信号(SIGTERM/SIGINT/SIGQUIT/SIGHUP——handler 只置标志,主循环 log 后 + * signal(s,SIG_DFL)+raise(s) 恢复默认重抛,与未加 hook 一致终止)、**发出**给播放器的 + * 信号(SIGSTOP/SIGCONT/SIGTERM/SIGKILL)、以及**播放器退出原因**(WTERMSIG/WEXITSTATUS)。 + * 不转发信号、不善后、不 SHOW 关机画面、不截断。 + * + * M2.29 supervisor 接管关机/升级(2026-08-29,MEOW_BOOTMENU_VER 0.1.0-m2.29-takeover): + * 基于 M2 诊断 hook 抓到的两条真机信号,让 supervisor 把「播放器 UI 触发关机/升级 → + * 卡最后一帧但系统没真关机(SSH 能连)」收束掉: + * - 播放器**自然退出态 0**(UI 关机,非热键 SIGTERM 杀):waitpid 识别 WIFEXITED&&0 + * → supervisor 执行真关机(sync + reboot(2) POWER_OFF),不卡最后一帧。 + * 区分:热键切 MeowOS 的 SIGTERM 杀仍在 confirm 分支内 waitpid,不进自然退出检测。 + * - mtd5(ota 标记分区) 变 ota:kernel2(播放器 UI 升级 → nandwrite 写 mtd5):supervisor + * 主循环每 500ms 读一次 /dev/mtd5 头字节(nandwrite 在 offset 0 写 256B 定长标记, + * 只读 ~15B,IO 极低),命中即判升级 → reboot(2) RESTART → uboot 读 mtd5=ota:kernel2 + * 引导 kernel2(升级模式,自动刷 TF 卡 r3proii.upt)。不卡最后一帧。 + * 说明:/proc/mtd 只反映分区表不变读,无法观察内容变化,故用轻量读 /dev/mtd5 轮询。 + * 两根都写 bm_log(supervisor 接管 shutdown/upgrade)。 + * + * ============================================================================ + * WHY (设计见 BOOT_SELECTOR_DESIGN.md §3) + * ============================================================================ + * - 开机最先显示选单(HibyOS / MeowOS),选择后 fork 播放器(HibyOS)或 + * exec MeowOS 主屏(MeowOS);HibyOS 模式热键(电源+音量减)杀播放器接管。 + * - 本二进制 = 选单本体(M0)。M0 只做二进制自检:SSH 推 /data 独立跑, + * **不进启动链**(不碰 S90/S92/播放器/SSH 顺序)。 + * + * M0 自检目标(BOOT_SELECTOR_DESIGN.md §9 M0): + * --dumpkeys N / --sim-select H|M / --boot-only / --version 验证 + * fb0 初始化、绘制、选择/超时决策、CLI 钩子。**默认不 fork 播放器**—— + * M0 严禁触碰到正在运行的 hiby_player(设备上它正由 init 拉起)。 + * + * ⚠️ M0 记号(BOOT_SELECTOR_DESIGN.md §2.3 播放器 fork 契约已按 §3.6 写好, + * 但本阶段默认关闭):fork_hiby_player() 实现了「env -i 复刻 11 变量 + + * chdir("/") + stdio 重定向 + execve /usr/bin/hiby_player」的完整契约, + * 仅在传 --fork 时才会真正 exec。M2 接管 HibyOS 时由 S90 传 --fork 打开。 + * + * ⚠️ 字体限制:本设备的 5x7 点阵字体(与 meow_launcherd/hello 相同)只含 + * ASCII(0-9 A-Z a-z 空格 . ! - _),**无 CJK 字形**。设计稿中的中文标签 + * (如「[HIBYOS 官方播放器]」)无法用该字体渲染,故本版用 ASCII 标签 + * ([HIBYOS PLAYER] / [MEOWOS HOME]),CJK 渲染留待后续引入 TTF 渲染器。 + * + * fb0 要点(ANALYSIS.md §18.18,勿写反): + * FBIOGET_VSCREENINFO=0x4600 / FBIOGET_FSCREENINFO=0x4602 / FBIOPAN_DISPLAY=0x4606 + * 本机 xres=480 yres=720 bpp=16 line=960 yres_virtual=1440(双缓冲)ypanstep=1 + * mmap 整个虚拟帧缓冲(含两帧);画前 FBIOPAN_DISPLAY pan 到 yoffset=0(buffer0)。 + * 显示哪个缓冲由 fb_var.yoffset 决定——播放器电源/息屏重绘可能把 yoffset 留在 + * buffer1,选单画在 buffer0 而面板显示 buffer1 会「选单不可见/屏幕冻结」(v6.5 bug)。 + * + * 静态 MIPS32r2 musl(soft-float)编译: + * ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -static meow_bootmenu.c -o meow_bootmenu + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* M2.29:supervisor 接管关机/升级,直连 reboot(2) syscall */ +#include +#include "meow_touch.h" + +/* ---- 本设备 fb ioctl 号(实测,勿写反)---- */ +#define FBIOGET_VSCREENINFO 0x4600 +#define FBIOGET_FSCREENINFO 0x4602 +#define FBIOPAN_DISPLAY 0x4606 + +/* ---- 路径 ---- */ +#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER(116) */ +#define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL(114/115) PLAY(164) */ +#define DEV_TOUCH "/dev/input/event1" /* goodix-ts(未 grab,可共享读) */ +#define FB_DEV "/dev/fb0" +#define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs" +#define CONF_FILE MEOWAPPS "/meow.conf" /* TF 卡配置:boot_default/boot_timeout_ms/boot_menu */ +#define MODE_FILE "/tmp/meow_boot_mode" /* 开机模式标记(tmpfs,开机清空) */ +#define PLAYER_BIN "/usr/bin/hiby_player" +#define PLAYER_LOG "/usr/data/meow_player.log" +#define LAUNCHER_BIN "/usr/bin/meow_launcherd" /* MeowOS 基础主屏(M2 接管) */ + +/* ---- 面板/背光(与 meow_launcherd 相同路径,§18.21/§红线)---- */ +#ifndef FBIOBLANK +#define FBIOBLANK 0x4611 +#endif +#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness" +#define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power" + +/* ---- 热键切换(HibyOS→MeowOS):按住电源 + 窗口内单击音量减 ---- */ +#define HK_PWRWIN_MIN 60 /* 电源按下后 60ms 起 */ +#define HK_PWRWIN_MAX 2500 /* 至 2500ms 止 */ +#define KILL_GRACE_MS 3000 /* SIGTERM 后等待退出,超时 SIGKILL */ +#define ARM_DELAY_MS 20000 /* 开机武装延迟(§18.23):播放器拉起后 N ms 内热键不触发,避媒体扫描期冻结 */ +#define BUSY_STATE_CHAR 'D' /* 播放器忙判门:/proc//stat 态为 D(不可中断 IO=扫描)则暂不热键 */ +#define SCAN_KEEP_MS 90000 /* Bug 4:HibyOS 开机媒体扫描窗(90s)内亮度保底,防黑屏几分钟(只写亮度不碰 blank) */ + +/* ---- key codes ---- */ +#ifndef KEY_POWER +#define KEY_POWER 116 +#endif +#ifndef KEY_VOLUMEDOWN +#define KEY_VOLUMEDOWN 114 +#endif +#ifndef KEY_VOLUMEUP +#define KEY_VOLUMEUP 115 +#endif +#ifndef KEY_PLAYPAUSE +#define KEY_PLAYPAUSE 164 +#endif + +/* ---- 事件号 / ioctl ---- */ +#define BTN_TOUCH 0x14a +#define ABS_X 0x00 +#define ABS_Y 0x01 +#define ABS_MT_POSITION_X 0x35 /* goodix 走 MT 协议 B,仅 ABS_X/Y 收不到坐标(M2.1 bug) */ +#define ABS_MT_POSITION_Y 0x36 +#ifndef EV_KEY +#define EV_KEY 1 +#endif +#ifndef EV_ABS +#define EV_ABS 3 +#endif +#ifndef EVIOCGKEY +#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len) +#endif +#define KEYBUF_WORDS 32 /* 1024 bits, 覆盖 KEY_MAX 767 */ + +/* ---- 编译默认值(可被 /usr/data/mnt/sd_0/MeowAPPs/meow.conf 覆写)---- */ +#define DEF_BOOT_DEFAULT 1 /* 1=HibyOS 0=MeowOS */ +#define DEF_BOOT_TIMEOUT_MS 5000 +#define DEF_BOOT_MENU 1 +#define DEF_BG 0x000000 + +#define POLL_MS 10 +#define SIM_SELECT_DELAY_MS 2000 /* --sim-select 延迟后模拟选 */ +#define GHOST_COOLDOWN_MS 800 /* 入口 drain + 短暂冷却防幽灵触摸(M2.1:缩短,不吞首次有效输入) */ +#define MEOW_BOOTMENU_VER "0.1.0-m2.29-takeover" /* M2.29:supervisor 接管关机(poweroff)/升级(reboot) */ + +/* ---- 版式 ---- */ +#define STATUS_H 40 +#define ROW_H 52 +#define ENTRY0_Y 176 +#define ENTRY1_Y 228 +#define FOOTER_Y 700 +#define BOOTMENU_OS_VER "0.7.9" /* 与 meow_launcherd MEOWOS_VERSION 对齐;右下角开发版本 */ + +/* ================================================================== */ +/* fb0 状态 */ +/* ================================================================== */ +static unsigned char *fb = NULL; +static int fb_fd = -1; +static int fb_xres = 480, fb_yres = 720, fb_line = 960, fb_bpp = 16; +static unsigned int fb_yres_virtual = 0; +static unsigned int fb_active_yoffset = 0; +static unsigned char *fb_snapshot = NULL; /* 进入选单前快照(恢复播放器帧用) */ +static size_t fb_snapshot_len = 0; +static int fb_orig_yoffset = -1; + +/* 触摸状态(选单/确认框/--dumpkeys 共用;M2.26 迁到 meow_touch 统一接口: + * goodix 时序免疫——BTN_TOUCH=1 不锁原点,等按下后首个有效 ABS 才锁,位移>=65 判滑动)。 */ +static meow_touch mt_touch; +static int touch_fd = -1; + +static inline void put_px(int x, int y, uint32_t color) +{ + if (x < 0 || x >= fb_xres || y < 0 || y >= fb_yres) return; + unsigned char *p = fb + (size_t)y * fb_line + x * (fb_bpp / 8); + if (fb_bpp == 16) { + uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) | + ((color >> 3) & 0x001F); + *(uint16_t *)p = c; + } else { + *(uint32_t *)p = (color << 8) | 0xFF; + } +} + +static void fill_rect(int x0, int y0, int w, int h, uint32_t color) +{ + for (int y = y0; y < y0 + h; y++) + for (int x = x0; x < x0 + w; x++) + put_px(x, y, color); +} + +/* 5x7 点阵(与 meow_launcherd/hello 相同;仅 ASCII) */ +static const unsigned char font[][8] = { + {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00}, + {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00}, + {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00}, + {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00}, + {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00}, + {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00}, + {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00}, + {0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00}, + {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00}, + {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00}, + {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, + {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00}, + {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00}, + {0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00}, + {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00}, + {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00}, + {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00}, + {0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, + {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00}, + {0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00}, + {0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00}, + {0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00}, + {0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00}, + {0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00}, + {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, + {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00}, + {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00}, + {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00}, + {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00}, + {0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00}, + {0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, + {0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00}, + {0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00}, + {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00}, + {0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00}, + {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00}, + {0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00}, + {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, +}; + +static int glyph_index(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'Z') return 10 + (c - 'A'); + if (c >= 'a' && c <= 'z') return 10 + (c - 'a'); + if (c == ' ') return 36; + if (c == '.') return 37; + if (c == '!') return 38; + if (c == '-' || c == '_') return 39; + return 36; +} + +static void draw_glyph(int x, int y, char c, int scale, uint32_t color) +{ + const unsigned char *g = font[glyph_index(c)]; + for (int row = 0; row < 7; row++) { + unsigned char bits = g[row]; + for (int col = 0; col < 5; col++) { + if (bits & (0x10 >> col)) { + for (int dy = 0; dy < scale; dy++) + for (int dx = 0; dx < scale; dx++) + put_px(x + col * scale + dx, y + row * scale + dy, color); + } + } + } +} + +static void draw_text(int x, int y, const char *s, int scale, uint32_t color) +{ + while (*s) { + draw_glyph(x, y, *s, scale, color); + x += 6 * scale; + s++; + } +} + +/* ================================================================== */ +/* fb0 初始化 / 双缓冲 pan(§18.18) */ +/* ================================================================== */ +static int fb_open(void) +{ + int fd = open(FB_DEV, O_RDWR); + if (fd < 0) { fprintf(stderr, "fb open failed: %s\n", strerror(errno)); return -1; } + unsigned char vinfo[160], finfo[68]; + memset(vinfo, 0, sizeof vinfo); + memset(finfo, 0, sizeof finfo); + if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 || + ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) { + fprintf(stderr, "fb ioctl failed\n"); + close(fd); return -1; + } + fb_xres = *(uint32_t *)(vinfo + 0); + fb_yres = *(uint32_t *)(vinfo + 4); + fb_yres_virtual = *(uint32_t *)(vinfo + 12); + fb_active_yoffset = *(uint32_t *)(vinfo + 20); + fb_bpp = *(uint32_t *)(vinfo + 24); + fb_line = *(uint32_t *)(finfo + 44); + if (!fb_line) fb_line = fb_xres * (fb_bpp / 8); + if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* 单缓冲兜底 */ + fb = mmap(NULL, (size_t)fb_line * fb_yres_virtual, + PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (fb == MAP_FAILED) { + fprintf(stderr, "fb mmap failed: %s\n", strerror(errno)); + close(fd); return -1; + } + fb_fd = fd; + fprintf(stderr, "fb: %dx%d bpp=%d line=%d vres=%u yoffset=%u\n", + fb_xres, fb_yres, fb_bpp, fb_line, fb_yres_virtual, fb_active_yoffset); + return 0; +} + +/* 选单进入前快照「当前显示缓冲」一帧(播放器事件驱动不主动重绘,须恢复) */ +static void fb_snapshot_player(void) +{ + if (fb_yres_virtual <= fb_yres) { fb_orig_yoffset = -1; return; } + if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0; + fb_orig_yoffset = (int)fb_active_yoffset; + fb_snapshot_len = (size_t)fb_line * fb_yres; + fb_snapshot = malloc(fb_snapshot_len); + if (!fb_snapshot) { fb_orig_yoffset = -1; fb_snapshot_len = 0; return; } + memcpy(fb_snapshot, fb + (size_t)fb_active_yoffset * fb_line, fb_snapshot_len); +} + +/* 画前 FBIOPAN_DISPLAY pan 到 yoffset=0(buffer0,选单/app 共同绘制目标) */ +static void fb_pan_buf0(void) +{ + if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; } + if (fb_active_yoffset == 0) return; /* 已显示 buffer0 */ + unsigned char v[160]; + memset(v, 0, sizeof v); + if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return; + *(uint32_t *)(v + 16) = 0; /* xoffset */ + *(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */ + ioctl(fb_fd, FBIOPAN_DISPLAY, v); + fb_active_yoffset = 0; +} + +/* 选单退出:把快照拷回原缓冲并 pan 回原 yoffset(播放器帧保持不动) */ +static void fb_restore_player_frame(void) +{ + int restore_y = fb_orig_yoffset; + if (fb_fd >= 0 && restore_y >= 0 && fb_snapshot) { + memcpy(fb + (size_t)restore_y * fb_line, fb_snapshot, fb_snapshot_len); + unsigned char v[160]; + memset(v, 0, sizeof v); + if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) == 0) { + *(uint32_t *)(v + 16) = 0; + *(uint32_t *)(v + 20) = (uint32_t)restore_y; + ioctl(fb_fd, FBIOPAN_DISPLAY, v); + } + fprintf(stderr, "bootmenu: fb restore -> yoffset=%d\n", restore_y); + } + if (fb_snapshot) { free(fb_snapshot); fb_snapshot = NULL; } + fb_orig_yoffset = -1; + fb_snapshot_len = 0; +} + +static void fb_close(void) +{ + if (fb && fb != MAP_FAILED) + munmap(fb, (size_t)fb_line * fb_yres_virtual); + if (fb_fd >= 0) close(fb_fd); + fb = NULL; fb_fd = -1; +} + +/* ================================================================== */ +/* 状态栏数据源(与 meow_launcherd 相同:电量/WIFI/BT/音量) */ +/* ================================================================== */ +static int read_file_int(const char *path) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + char buf[16]; + int n = read(fd, buf, sizeof buf - 1); + close(fd); + if (n <= 0) return -1; + buf[n] = 0; + return atoi(buf); +} + +static int bluetooth_present(void) +{ + return access("/sys/class/bluetooth/hci0", F_OK) == 0; +} + +/* 音量:CS43198 ALSA control(实测死镜像,可能恒 0%,见 §18.13) */ +static int amixer_vol(int numid) +{ + char cmd[96]; + snprintf(cmd, sizeof cmd, "amixer -c 0 cget numid=%d 2>/dev/null", numid); + FILE *p = popen(cmd, "r"); + if (!p) return -1; + char buf[128]; + int v = -1; + while (fgets(buf, sizeof buf, p)) { + char *s = strstr(buf, ": values="); + if (s) { v = atoi(s + 9); break; } + } + pclose(p); + return v; +} + +/* 40px 状态栏:行1=时钟(左) 电量% WIFI BT(右),行2=VOL nn%(左),y=40 分隔线 */ +static void draw_statusbar(uint32_t bg) +{ + char timebuf[16], right[48]; + time_t now = time(NULL); + struct tm tm; + localtime_r(&now, &tm); + snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", + tm.tm_hour, tm.tm_min, tm.tm_sec); + right[0] = 0; + + char batt[8] = ""; + int b = read_file_int("/sys/class/power_supply/battery/capacity"); + if (b >= 0) snprintf(batt, sizeof batt, "%d%%", b); + + char wifi[8] = ""; + int wf = open("/sys/class/net/wlan0/operstate", O_RDONLY); + if (wf >= 0) { + char x[8]; + int n = read(wf, x, sizeof x - 1); + close(wf); + if (n > 0) { x[n] = 0; if (!strncmp(x, "up", 2)) snprintf(wifi, sizeof wifi, "WIFI"); } + } + + char vol[16] = ""; + int v = amixer_vol(1); + if (v >= 0) snprintf(vol, sizeof vol, "VOL %d%%", v * 100 / 255); + + char bt[8] = ""; + if (bluetooth_present()) snprintf(bt, sizeof bt, "BT"); + + draw_text(8, 4, timebuf, 1, 0x9FB8D9); + if (batt[0]) strcat(right, batt); + if (wifi[0]) { if (right[0]) strcat(right, " "); strcat(right, wifi); } + if (bt[0]) { if (right[0]) strcat(right, " "); strcat(right, bt); } + if (right[0]) { + int rx = fb_xres - 8 - (int)strlen(right) * 6; + draw_text(rx, 4, right, 1, 0x9FB8D9); + } + if (vol[0]) draw_text(8, 22, vol, 1, 0x9FB8D9); + fill_rect(0, 40, fb_xres, 1, 0x333355); /* 分隔线(移至 y=40) */ +} + +/* 只刷时钟数字(每秒一次,不弹 amixer/不碰菜单体) */ +static void draw_clock(uint32_t bg) +{ + char timebuf[16]; + time_t now = time(NULL); + struct tm tm; + localtime_r(&now, &tm); + snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", + tm.tm_hour, tm.tm_min, tm.tm_sec); + int w = (int)strlen(timebuf) * 6; + fill_rect(4, 2, 8 + w, 12, bg); + draw_text(8, 4, timebuf, 1, 0x9FB8D9); +} + +/* ================================================================== */ +/* 配置:boot_default / boot_timeout_ms / boot_menu(从 TF 卡尽力读) */ +/* ================================================================== */ +static int cfg_boot_default = DEF_BOOT_DEFAULT; +static long cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS; +static int cfg_boot_menu = DEF_BOOT_MENU; +static uint32_t cfg_bg = DEF_BG; + +static void load_config(void) +{ + cfg_boot_default = DEF_BOOT_DEFAULT; + cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS; + cfg_boot_menu = DEF_BOOT_MENU; + cfg_bg = DEF_BG; + FILE *f = fopen(CONF_FILE, "r"); + if (!f) { fprintf(stderr, "bootmenu: no %s, using defaults\n", CONF_FILE); return; } + char buf[128]; + while (fgets(buf, sizeof buf, f)) { + if (!strncmp(buf, "boot_default=", 13)) { + cfg_boot_default = atoi(buf + 13); + if (cfg_boot_default != 0 && cfg_boot_default != 1) cfg_boot_default = DEF_BOOT_DEFAULT; + } else if (!strncmp(buf, "boot_timeout_ms=", 16)) { + long t = atol(buf + 16); + if (t >= 500 && t <= 60000) cfg_boot_timeout_ms = t; + } else if (!strncmp(buf, "boot_menu=", 10)) { + int m = atoi(buf + 10); + if (m == 0 || m == 1) cfg_boot_menu = m; + } else if (!strncmp(buf, "bg_color=", 9)) { + unsigned int v; + if (sscanf(buf + 9, "%x", &v) == 1) cfg_bg = v & 0xFFFFFF; + } + } + fclose(f); + fprintf(stderr, "bootmenu: cfg boot_default=%d timeout_ms=%ld boot_menu=%d bg=%06x\n", + cfg_boot_default, cfg_boot_timeout_ms, cfg_boot_menu, cfg_bg); +} + +/* ================================================================== */ +/* 时钟(单调,强收敛) */ +/* ================================================================== */ +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; +} + +/* ================================================================== */ +/* TF 卡日志(bm_log)——诊断留存:supervisor 收到/发出信号、播放器退出原因。 + * - 借鉴 launcher log_open_card/logmsg 思路,但**不共享 /tmp/meow_bootts** + * (launcher 缓存的是完整基名 meow_,直接复读会拼成 meow_boot_meow_…); + * 每次自行按 /proc/uptime 推开机时间戳,同一 boot 内文件名恒定。 + * - 写 /usr/data/mnt/sd_0/MeowAPPs/logs/meow_boot_.log(O_APPEND|O_CREAT 0644)。 + * - 卡不可用(未挂载/不可写)→ 降级 stderr。**只记录,不改任何行为。** + * ================================================================== */ +#define BM_LOG_DIR "/usr/data/mnt/sd_0/MeowAPPs/logs" +static int bm_log_fd = -1; +static char bm_log_file[256] = ""; + +static void bm_boot_ts(char *out, size_t n) +{ + time_t now = time(NULL), up = 0; + int fd = open("/proc/uptime", O_RDONLY); + if (fd >= 0) { + char buf[64]; + int r = read(fd, buf, sizeof buf - 1); + close(fd); + if (r > 0) { + buf[r] = 0; + char *p = strchr(buf, ' '); + if (p) *p = 0; + up = (time_t)atof(buf); + } + } + time_t boot = (time_t)((double)now - (double)up + 0.5); + struct tm tm; + localtime_r(&boot, &tm); + strftime(out, n, "%Y%m%d_%H%M%S", &tm); +} + +static void bm_log_open(void) +{ + if (bm_log_fd >= 0) return; + mkdir(BM_LOG_DIR, 0755); + char ts[64]; + bm_boot_ts(ts, sizeof ts); + snprintf(bm_log_file, sizeof bm_log_file, "%s/meow_boot_%s.log", BM_LOG_DIR, ts); + bm_log_fd = open(bm_log_file, O_WRONLY | O_APPEND | O_CREAT, 0644); + if (bm_log_fd < 0) bm_log_file[0] = 0; /* 卡不可用 → 降级 stderr */ +} + +/* 带时间戳写 TF 卡日志;卡不可用则降级 stderr。仅记录,不改行为。 */ +static void bm_log(const char *fmt, ...) +{ + char buf[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof buf, fmt, ap); + va_end(ap); + char line[576]; + int l = snprintf(line, sizeof line, "%llu %s\n", now_ms(), buf); + if (bm_log_fd < 0) bm_log_open(); + if (bm_log_fd >= 0) { + if (write(bm_log_fd, line, l) < 0) bm_log_fd = -1; /* 卡拔出 → 回退 stderr */ + } + fprintf(stderr, "%llu %s\n", now_ms(), buf); +} + +/* ---- supervisor 信号记录(只记录,不改行为)---- + * 注册 SIGTERM/SIGINT/SIGQUIT/SIGHUP,handler 只置标志;主循环顶部先 bm_log + * 「收到信号 N」,再 signal(s, SIG_DFL) + raise(s) 重抛——**恢复默认动作**, + * 使 supervisor 仍按今日方式终止(不转发、不善后、不截断,与未加 hook 一致)。 */ +static volatile sig_atomic_t bm_sig_flag[64]; + +static const char *bm_sig_name(int sig) +{ + switch (sig) { + case SIGTERM: return "SIGTERM"; + case SIGKILL: return "SIGKILL"; + case SIGSTOP: return "SIGSTOP"; + case SIGCONT: return "SIGCONT"; + case SIGINT: return "SIGINT"; + case SIGQUIT: return "SIGQUIT"; + case SIGHUP: return "SIGHUP"; + default: return "?"; + } +} + +static void bm_sig_catch(int sig) +{ + if (sig > 0 && sig < 64) bm_sig_flag[sig] = 1; +} + +static void bm_sig_install(void) +{ + signal(SIGTERM, bm_sig_catch); + signal(SIGINT, bm_sig_catch); + signal(SIGQUIT, bm_sig_catch); + signal(SIGHUP, bm_sig_catch); +} + +/* 主循环顶部调用:记录已收到的信号,并恢复默认动作重抛(保行为不变)。 */ +static void bm_sig_poll(void) +{ + for (int s = 1; s < 64; s++) { + if (bm_sig_flag[s]) { + bm_sig_flag[s] = 0; + bm_log("supervisor: received signal %d (%s)", s, bm_sig_name(s)); + signal(s, SIG_DFL); /* 恢复默认动作(== 未加 hook 时) */ + raise(s); /* 重抛:已登记即按原方式终止 */ + break; + } + } +} + +/* ---- M2.29 supervisor 接管:直连 reboot(2) syscall + mtd5(ota) 侦测 ---- + * 借鉴 meow_launcherd sys_do_reboot(BusyBox reboot 非 PID1 不完成握手,故直接 syscall)。 + * sync() 落盘后调用 reboot(2),内核立即重启/关机;正常不返回,防御性死循环。 */ +#define LINUX_REBOOT_MAGIC1 0xfee1dead +#define LINUX_REBOOT_MAGIC2 672274793 /* 0x28121969 */ +#define LINUX_REBOOT_CMD_RESTART 0x01234567 +#define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc +static void bm_do_reboot(int cmd) +{ + sync(); + bm_log("supervisor: bm_do_reboot cmd=0x%x", cmd); + syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, NULL); + bm_log("supervisor: bm_do_reboot returned: %s", strerror(errno)); + for (;;) usleep(1000); /* 防御:永不返回 */ +} + +/* mtd5 是 ota 标记分区(8M)。bootmode.sh Recovery 用 nandwrite 在 offset 0 写 256B + * 定长标记。这里每 500ms 读一次 mtd5 头 ~15B(单页一小读,IO 极低),命中 + * "ota:kernel2" 即判「播放器触发升级」。open/close 每次都关,避免长期占 mtd 设备。 */ +#define MTD_OTA "/dev/mtd5" +static int mtd_ota_is_upgrade(void) +{ + int fd = open(MTD_OTA, O_RDONLY); + if (fd < 0) return 0; /* 读不到 → 无升级迹象 */ + char buf[16]; + memset(buf, 0, sizeof buf); + ssize_t n = read(fd, buf, sizeof buf - 1); + close(fd); + if (n <= 0) return 0; + return strstr(buf, "ota:kernel2") != NULL; +} + +/* ================================================================== */ +/* 按键状态轮询(EVIOCGKEY) */ +/* ================================================================== */ +struct keypoll { + int fd; + unsigned long cur[KEYBUF_WORDS]; + unsigned long prev[KEYBUF_WORDS]; + int ok; +}; + +static int kp_open(struct keypoll *kp, const char *dev) +{ + memset(kp, 0, sizeof *kp); + kp->fd = open(dev, O_RDONLY | O_NONBLOCK); + if (kp->fd < 0) { fprintf(stderr, "open %s: %s\n", dev, strerror(errno)); return -1; } + if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) { + fprintf(stderr, "EVIOCGKEY %s: %s\n", dev, strerror(errno)); + close(kp->fd); return -1; + } + memcpy(kp->prev, kp->cur, sizeof kp->cur); + kp->ok = 1; + return 0; +} + +/* 轮询设备键状态;对每个「按下边沿/抬起边沿」调 cb(code, down, ctx)。 + * 返回是否有边沿。 */ +static int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx), + void *ctx) +{ + if (!kp->ok) return 0; + if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) return 0; + int changed = 0; + for (int w = 0; w < KEYBUF_WORDS; w++) { + if (kp->cur[w] == kp->prev[w]) continue; + for (int b = 0; b < 32; b++) { + int c = (kp->cur[w] >> b) & 1; + int p = (kp->prev[w] >> b) & 1; + if (c != p) { + int code = w * 32 + b; + changed = 1; + if (cb) cb(code, c, ctx); + } + } + kp->prev[w] = kp->cur[w]; + } + return changed; +} + +static int kp_is_down(const struct keypoll *k, int code) +{ + return (k->cur[code / 32] >> (code % 32)) & 1; +} + +/* ================================================================== */ +/* 选单绘制 */ +/* ================================================================== */ +static void draw_countdown(uint32_t bg, long remaining_ms) +{ + /* 右侧倒计时 'AUTO N S'(scale2) */ + int sec = (int)((remaining_ms + 999) / 1000); + char s[16]; + snprintf(s, sizeof s, "AUTO %dS", sec); + int w = (int)strlen(s) * 12; /* scale2 = 12px/字 */ + int x = fb_xres - 8 - w; + /* 清一下原区域(含右边距)再画 */ + fill_rect(x - 4, 92, w + 12, 16, bg); + draw_text(x, 94, s, 2, 0xFFE066); +} + +static void draw_entry(int y, const char *label, int selected) +{ + if (selected) { + fill_rect(8, y - 4, fb_xres - 16, ROW_H - 8, 0x2A3A6A); + draw_text(24, y, label, 2, 0xFFFFFF); + } else { + draw_text(24, y, label, 2, 0xC8D0E8); + } +} + +static void draw_menu(int cursor, long remaining_ms) +{ + fill_rect(0, 0, fb_xres, fb_yres, cfg_bg); + + /* 状态栏 + 标题 + 分隔线 */ + draw_statusbar(cfg_bg); + draw_text(20, 64, "MEOWOS", 3, 0xFFE066); + draw_text(22, 96, "BOOT SELECTOR", 2, 0x9FB8D9); + fill_rect(0, 122, fb_xres, 2, 0x3A3A5C); + + /* 两个入口 */ + draw_entry(ENTRY0_Y, "[HIBYOS PLAYER]", cursor == 0); + draw_entry(ENTRY1_Y, "[MEOWOS HOME]", cursor == 1); + + /* 右侧倒计时 */ + draw_countdown(cfg_bg, remaining_ms); + + /* 底部按键提示 */ + draw_text(20, FOOTER_Y, + "VOLUP UP VOLDN DOWN PLAY OK POWER OK", 1, 0x666688); + /* 右下角开发版本 */ + { + char ver[40]; + snprintf(ver, sizeof ver, "MeowOS v%s boot", BOOTMENU_OS_VER); + draw_text(fb_xres - 8 - (int)strlen(ver) * 6, FOOTER_Y, ver, 1, 0x666688); + } +} + +/* ================================================================== */ +/* 面板/背光 helper(复用 meow_launcherd 语义,§18.21.5 / §红线) */ +/* ================================================================== */ +static int read_brightness(void) +{ + int fd = open(BACKLIGHT, O_RDONLY); + if (fd < 0) return -1; + char buf[16]; + int n = read(fd, buf, sizeof buf - 1); + close(fd); + if (n <= 0) return -1; + buf[n] = 0; + return atoi(buf); +} + +static void write_file_str(const char *path, const char *val) +{ + int fd = open(path, O_WRONLY); + if (fd < 0) return; + if (write(fd, val, strlen(val)) < 0) + fprintf(stderr, "bootmenu: write %s <- %s: %s\n", path, val, strerror(errno)); + close(fd); +} + +static void write_brightness(int val) +{ + char buf[8]; + int l = snprintf(buf, sizeof buf, "%d\n", val); + int fd = open(BACKLIGHT, O_WRONLY); + if (fd < 0) { fprintf(stderr, "bootmenu: backlight open fail: %s\n", strerror(errno)); return; } + if (write(fd, buf, l) < 0) fprintf(stderr, "bootmenu: backlight write %d: %s\n", val, strerror(errno)); + close(fd); +} + +/* 面板 unblank(FBIOBLANK 0)。仅用于杀播放器后把屏点亮,以便 MeowOS 接管; + * 播放器已死,故不涉及「播放器 state machine」一致性(§18.11 关注点)。 + * bootmenu 选单/自检阶段绝不调用本函数(§18.24 纪律:不碰 panel blank)。 */ +static void unblank_panel(void) +{ + int fbfd = open(FB_DEV, O_RDWR); + if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } + fprintf(stderr, "bootmenu: panel unblanked\n"); +} + +/* 读取 /proc//stat 状态字符('S'/'T'/'D'...),失败返回 '\0'。 */ +static char proc_state(int pid) +{ + char path[64], line[512]; + snprintf(path, sizeof path, "/proc/%d/stat", pid); + int fd = open(path, O_RDONLY); + if (fd < 0) return '\0'; + ssize_t n = read(fd, line, sizeof line - 1); + close(fd); + if (n <= 0) return '\0'; + line[n] = 0; + char *p = strrchr(line, ')'); + if (!p || p + 2 >= line + n) return '\0'; + return p[2]; +} + +/* 读取 /proc//stat 状态是否为 T(stopped)。T 态 SIGTERM 不投递, + * 须先 SIGCONT(§18.21.3)。 */ +static int proc_is_stopped(int pid) +{ + return proc_state(pid) == 'T'; +} + +/* 播放器忙判门(§18.23):D(不可中断 IO = 开机媒体库扫描)视为忙。 + * 忙期热键暂不触发,避免扫描期 SIGSTOP 冻结拉长卡顿。 */ +static int proc_is_busy(int pid) +{ + return proc_state(pid) == BUSY_STATE_CHAR; +} + +/* ================================================================== */ +/* 播放器 fork 契约(顺序:先 munmap+close fb0,再 fork) */ +/* ⚠️ M0 记号:完整契约已按 BOOT_SELECTOR_DESIGN.md §3.6 实现,但仅在 */ +/* 显式 --fork 时执行。M0 自检不传 --fork,故绝不 fork 正在运行的 */ +/* hiby_player。M2 接管 HibyOS 时由 S90meow_bootmenu 传 --fork 开启。 */ +/* ================================================================== */ +extern char **environ; + +static void write_mode(int choice); /* 定义在下方;supervisor 使用 */ +static void bootmenu_supervisor(int player_pid); /* 定义在下方;fork 函数调用 */ + +/* ---- 真机 verif sim 钩子(M2):--sim-hotkey N / --sim-confirm Y|N ---- */ +static long sim_hotkey_ms = 0; /* supervisor 启动后 N ms 模拟热键(电源按住+音量减) */ +static int sim_confirm = 0; /* confirm 框打开后 2s 自动回答:'Y'=是 'N'=否 */ +static int sim_confirm_done = 0; +static int sim_hk_done = 0; +static int sim_hotkey_strict = 0; /* M2.14:--sim-hotkey-strict = 让 sim 热键走武装延迟/忙判门(测 pending) */ + +static void fork_hiby_player(void) +{ + pid_t pid = fork(); + if (pid < 0) { fprintf(stderr, "fork player failed: %s\n", strerror(errno)); return; } + if (pid == 0) { + /* child:完全复刻 init 拉起时的环境(§18.21.5 最小启动契约) */ + chdir("/"); /* 必须:播放器有相对路径读(充电态等) */ + clearenv(); + setenv("USER", "root", 1); + setenv("SHLVL", "3", 1); + setenv("HOME", "/", 1); + setenv("PS1", "#", 1); + setenv("TERM", "vt102", 1); + setenv("PATH", "/bin:/sbin:/usr/bin:/usr/sbin", 1); + setenv("env_adb_device_name_prefix", "ingenic", 1); + setenv("env_adb_device_use_diffrent_name", "y", 1); + setenv("SHELL", "/bin/sh", 1); + setenv("PWD", "/", 1); + setenv("EDITOR", "/bin/vi", 1); + /* 无 LD_LIBRARY_PATH / 无 TZ(与开机一致) */ + int dn = open("/dev/null", O_RDONLY); + if (dn >= 0) { dup2(dn, 0); close(dn); } + int lf = open(PLAYER_LOG, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (lf >= 0) { dup2(lf, 1); dup2(lf, 2); close(lf); } + execve(PLAYER_BIN, (char *[]){ "hiby_player", NULL }, environ); + _exit(127); + } + /* parent(M2):退居监督者/热键守护——poll(10ms) 检测热键(按住电源 + + * 窗口 60-2500ms 内单击音量减)→ SIGTERM 播放器 → 3s 未退 SIGKILL + * (T 态先 SIGCONT)→ waitpid 收割 → unblank+亮度恢复 → 写 + * /tmp/meow_boot_mode=meow → exec meow_launcherd(MeowOS 基础主屏)。 + * 播放器异常退出(waitpid WNOHANG)→ 记日志,阶段 0 不自动重启(HGL)。 + * 本函数在父进程永不返回(阻塞 rcS 直到整机重启或热键切换)。 */ + bootmenu_supervisor(pid); + /* fork 失败已在上面 return;此处不可达(supervisor 永不返回), + * 仅防御:若 supervisor 异常返回则让 rcS 推进(S92 兜底)。 */ + return; +} + +/* ---- 热键确认框(M2 用户定案:热键后先确认,是才杀播放器)---- */ +#define CONF_DEFAULT_CURSOR 1 /* 默认 NO(更安全:不误杀) */ +#define CONF_COOLDOWN_MS 800 /* 防 goodix 幽灵触摸(§18.16);M2.1 缩短,不吞首次有效输入 */ +#define CONF_ROW0_Y 280 +#define CONF_ROW1_Y 330 +#define CONF_ROW_H 44 +#define CONF_ROW_LEFT 60 +#define CONF_ROW_RIGHT (fb_xres - 60) + +/* 简单绘制:压暗底 + 外框 + 标题 + [YES][NO] + 按键提示。 + * 字体仅 ASCII(无 CJK),故用 ENTER MEOWOS? / STOP HIBYOS PLAYER。 */ +static void draw_confirm_box(int cursor) +{ + fill_rect(0, 0, fb_xres, fb_yres, 0x0D0D14); /* 压暗背景 */ + fill_rect(36, 180, fb_xres - 72, 230, 0x3A3A5C); /* 外框 */ + fill_rect(40, 184, fb_xres - 80, 222, 0x1A1A2A); /* 内底 */ + draw_text(56, 200, "STOP HIBYOS PLAYER", 2, 0xFFE066); + draw_text(56, 232, "AND ENTER MEOWOS?", 2, 0x9FB8D9); + /* YES / NO 两行 */ + if (cursor == 0) { + fill_rect(CONF_ROW_LEFT, CONF_ROW0_Y, CONF_ROW_RIGHT - CONF_ROW_LEFT, CONF_ROW_H, 0x2A3A6A); + draw_text(CONF_ROW_LEFT + 30, CONF_ROW0_Y + 12, "YES", 2, 0xFFFFFF); + } else { + draw_text(CONF_ROW_LEFT + 30, CONF_ROW0_Y + 12, "YES", 2, 0x667088); + } + if (cursor == 1) { + fill_rect(CONF_ROW_LEFT, CONF_ROW1_Y, CONF_ROW_RIGHT - CONF_ROW_LEFT, CONF_ROW_H, 0x2A3A6A); + draw_text(CONF_ROW_LEFT + 30, CONF_ROW1_Y + 12, "NO", 2, 0xFFFFFF); + } else { + draw_text(CONF_ROW_LEFT + 30, CONF_ROW1_Y + 12, "NO", 2, 0x667088); + } + draw_text(56, CONF_ROW1_Y + CONF_ROW_H + 20, + "VOL^V NAV PLAY OK POWER EXIT", 1, 0x666688); +} + +/* 弹确认框并等待选择。返回 0=否(留下) 1=是(杀播放器) -1=无法绘制/异常。 + * 调用方负责:先 SIGSTOP 播放器(防其重绘覆盖)+ unblank/亮度点亮面板; + * 返回后调用方按结果处理面板/播放器。本函数只负责 fb0 叠框 + 输入。 */ +static int confirm_switch(void) +{ + if (fb_open() != 0) { fprintf(stderr, "confirm: fb open failed -> default NO\n"); return -1; } + fb_snapshot_player(); + fb_pan_buf0(); + + /* touch drain + 短暂冷却(防 goodix 幽灵触摸;M2.1 缩短到 800ms 不吞首次有效输入) */ + int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (tfd >= 0) { + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } + } + unsigned long long cooldown_until = now_ms() + CONF_COOLDOWN_MS; + mt_reset(&mt_touch); /* 清触摸手势态(down/valid/origin/last),避免上次残留误派发 */ + + struct keypoll kp_keys, kp_power; + if (kp_open(&kp_keys, DEV_KEYS) != 0) fprintf(stderr, "confirm: WARN no keys dev\n"); + if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "confirm: WARN no power dev\n"); + + int cursor = CONF_DEFAULT_CURSOR; + int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0; + + /* 键基线:把「当前已按住的键」写进 prev,避免热键残留的电源按住被误判为 + * 新的下沿而立即取消(电源仍按住,kp.poll 的 prev 已同步,但本地 prev_pwr + * 未同步——launcher run_menu 同款处理,§18.17)。 */ + if (kp_keys.ok) { + kp_poll(&kp_keys, NULL, NULL); + if (kp_is_down(&kp_keys, KEY_VOLUMEDOWN)) prev_vold = 1; + if (kp_is_down(&kp_keys, KEY_VOLUMEUP)) prev_volu = 1; + if (kp_is_down(&kp_keys, KEY_PLAYPAUSE)) prev_play = 1; + } + if (kp_power.ok) { + kp_poll(&kp_power, NULL, NULL); + if (kp_is_down(&kp_power, KEY_POWER)) prev_pwr = 1; + } + + draw_confirm_box(cursor); + fprintf(stderr, "confirm: box shown (default cursor %d)\n", cursor); + + int result = -1; + unsigned long long box_t0 = now_ms(); + for (;;) { + /* --sim-confirm Y|N:框打开 2s 后自动回答 */ + if (sim_confirm && !sim_confirm_done && now_ms() - box_t0 >= 2000) { + sim_confirm_done = 1; + result = (sim_confirm == 'Y') ? 1 : 0; + fprintf(stderr, "confirm: SIM answer %c -> %s\n", sim_confirm, + result == 1 ? "YES" : "NO"); + break; + } + + int vold = 0, volu = 0, play = 0, pwr = 0; + if (kp_keys.ok) { + kp_poll(&kp_keys, NULL, NULL); + vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + } + if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); } + + if (vold && !prev_vold) { cursor = (cursor + 1) % 2; draw_confirm_box(cursor); } + if (volu && !prev_volu) { cursor = (cursor - 1 + 2) % 2; draw_confirm_box(cursor); } + if (play && !prev_play) { result = (cursor == 0) ? 1 : 0; break; } + if (pwr && !prev_pwr) { result = 0; break; } /* 电源=取消=否 */ + prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr; + + if (tfd >= 0) { + struct pollfd pfd = { tfd, POLLIN, 0 }; + if (poll(&pfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) { + uint16_t type = *(uint16_t *)(ev + 8); + uint16_t code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_ABS) { + if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt_touch, val, mt_touch.last_y); + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt_touch, mt_touch.last_x, val); + } else if (type == EV_KEY && code == BTN_TOUCH) { + if (val) { /* 按压边沿:接受但**不锁原点**——等按下后首个有效 ABS 才锁(goodix 时序免疫) */ + if (now_ms() < cooldown_until) mt_reset(&mt_touch); /* 幽灵/冷却:丢弃本次 */ + else mt_down(&mt_touch); + } else { /* 抬起边沿:按按下原点派发;滑动/未锁到坐标 → mt_up 返回 0,不命中、不崩溃 */ + int dx = 0, dy = 0; + if (mt_up(&mt_touch, &dx, &dy)) { + int ty = mt_touch.down_y; + if (ty >= CONF_ROW0_Y && ty < CONF_ROW0_Y + CONF_ROW_H) { result = 1; break; } + else if (ty >= CONF_ROW1_Y && ty < CONF_ROW1_Y + CONF_ROW_H) { result = 0; break; } + } + } + } + } + if (result >= 0) break; + } + } + usleep(POLL_MS * 1000); + } + + if (tfd >= 0) close(tfd); + fb_restore_player_frame(); + fb_close(); + fprintf(stderr, "confirm: result %s\n", result == 1 ? "YES (kill player)" : + result == 0 ? "NO (stay HibyOS)" : "unknown"); + return result; +} + +/* ---- HibyOS 分支:父进程监督者/热键守护(M2)---- */ +static void bootmenu_supervisor(int player_pid) +{ + /* 记录开机后的正常亮度,供「杀播放器后亮度==0 则恢复 saved/60」。 + * 此刻播放器刚拉起,可能已写入其背光值;过早读到 0 则回退默认 60。 */ + int saved_brightness = read_brightness(); + /* M2.9 Bug 3:开机初期播放器启动画面会把背光设为瞬时高值(本机 max=101), + * 若直接以其为「保底亮度」,会在扫描窗首次写回时跳一次最高亮度(开机瞬间闪一下)。 + * 只信任稳定读数:读到 0 或异常高(>稳定上限 60,适中非最大)一律回退稳定值 60, + * 确保扫描期写回平滑(只写目标一次、不跳 max)。 */ + if (saved_brightness <= 0 || saved_brightness > 60) saved_brightness = 60; + fprintf(stderr, "supervisor: player pid=%d, saved brightness=%d, monitoring...\n", + player_pid, saved_brightness); + /* 信号记录 hook(M2 诊断):先装 handler、start 一行写入 TF 卡日志。 + * 只记录,不改行为(handler 仅置标志,主循环 log 后恢复默认重抛)。 */ + bm_sig_install(); + bm_log("supervisor: start pid=%d saved_brightness=%d mode=hiby ver=%s", + player_pid, saved_brightness, MEOW_BOOTMENU_VER); + /* Bug 4:HibyOS 开机媒体扫描窗内做亮度保底。播放器忙(媒体扫描)时其背光 + * 待机会把亮度降到 0、面板 blank → 黑屏几分钟。supervisor 在扫描窗内若读到 + * 亮度 0,写回保底亮度(saved 或 60)。**只写 brightness、绝不碰 FBIOBLANK** + * (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。 */ + unsigned long long scan_keep_until = now_ms() + SCAN_KEEP_MS; + unsigned long long last_bri_floor_at = 0; /* 亮度保底节流:≥400ms 才再写一次 */ + + struct keypoll kp_power, kp_keys; + if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "supervisor: WARN no power dev\n"); + if (kp_open(&kp_keys, DEV_KEYS) != 0) fprintf(stderr, "supervisor: WARN no keys dev\n"); + + int prev_pwr = 0, prev_vold = 0; + int pwr_hold_armed = 0; + unsigned long long pwr_down_at = 0; + int player_alive = 1; + int hotkey_pending = 0; /* M2.14:热键在武装延迟/播放器忙时置 pending,条件满足后自动弹框 */ + unsigned long long sim_t0 = now_ms(); /* --sim-hotkey: supervisor 启动计时 */ + unsigned long long last_mtd5_check = 0; /* T2 升级接管:mtd5 轮询节流 */ + + for (;;) { + unsigned long long t = now_ms(); + + /* 0) 记录 supervisor 收到的信号(SIGTERM/SIGINT/SIGQUIT/SIGHUP)。 + * 只记录后恢复默认重抛——不改行为(仍按今日方式终止)。 */ + bm_sig_poll(); + + /* 0b) T2 升级接管:每 500ms 读一次 mtd5 头字节,命中 ota:kernel2 即判 + * 「播放器 UI 触发升级」(nandwrite 已写标记但 system 呆在最后一帧没 reboot) + * → supervisor 执行 reboot → uboot 引导 kernel2(升级模式,自动刷固件)。 + * 只读 ~15B/500ms,IO 极低;/proc/mtd 只反映分区表不变读,观察不了内容。 */ + if (now_ms() - last_mtd5_check >= 500) { + last_mtd5_check = now_ms(); + if (mtd_ota_is_upgrade()) { + bm_log("supervisor 接管 upgrade: mtd5=ota:kernel2 -> reboot"); + bm_do_reboot(LINUX_REBOOT_CMD_RESTART); + } + } + + /* 1) waitpid(WNOHANG):播放器退出 → 记日志;自然退出态 0(UI 关机)→ 真关机 + * (M2.29 接管,避免「卡最后一帧但系统活着/SSH 能连」);其余不自动重启(HGL)。 */ + if (player_alive) { + int st = 0; + pid_t r = waitpid(player_pid, &st, WNOHANG); + if (r == player_pid) { + player_alive = 0; + if (WIFSIGNALED(st)) { + bm_log("player exited by signal %d (%s)", WTERMSIG(st), bm_sig_name(WTERMSIG(st))); + } else if (WIFEXITED(st) && WEXITSTATUS(st) == 0) { + /* T2 关机接管:播放器 **自然** exit(0)(非热键 SIGTERM 杀)—— + * 热键杀仍在 confirm 分支内 waitpid,不会走到这里。 */ + bm_log("supervisor 接管 shutdown: player exit 0 -> poweroff"); + bm_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); + } else { + bm_log("player exited status=%d", WEXITSTATUS(st)); + /* 阶段 0:不自动重启(HGL 不可二次获取,§18.21.4)。 */ + } + } else if (r < 0) { + player_alive = 0; /* 已被收割/错误 */ + } + } + + /* 2a) Bug 4 亮度保底:扫描窗内、播放器存活且亮度被降到 0 → 写回保底亮度。 + * (播放器扫描忙时无法响应唤醒,我们只把背光拉回,画面仍为播放器最后帧, + * 不再黑屏数分钟;不碰 blank。) */ + if (player_alive && now_ms() < scan_keep_until) { + int bri = read_brightness(); + if (bri <= 0 && now_ms() - last_bri_floor_at >= 400) { + int floor = saved_brightness > 0 ? saved_brightness : 60; + write_brightness(floor); + last_bri_floor_at = now_ms(); + fprintf(stderr, "supervisor: scan-window brightness floor -> %d\n", floor); + } + } + + /* 2) EVIOCGKEY 采样(event0 电源 / event2 音量) */ + int pwr = 0, vold = 0; + if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); } + if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL); vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); } + + /* --sim-hotkey N:supervisor 启动后 N ms 模拟「电源按住 3s + N+800 音量减点按」, + * 驱动同一套热键判定(与 launcher --sim-hotkey 同构,§18.17 注入不可投递)。 */ + if (sim_hotkey_ms > 0 && !sim_hk_done) { + long se = (long)(now_ms() - sim_t0); + if (se >= sim_hotkey_ms && se < sim_hotkey_ms + 3000) pwr = 1; + if (se >= sim_hotkey_ms + 800 && se < sim_hotkey_ms + 950) vold = 1; + } + + /* 3) 热键:电源按下边沿 arming,窗口内音量减按下边沿触发 */ + if (pwr && !prev_pwr) { pwr_hold_armed = 1; pwr_down_at = t; } + if (!pwr && prev_pwr) pwr_hold_armed = 0; /* 松开:disarm */ + if (pwr_hold_armed && pwr && (t - pwr_down_at) > HK_PWRWIN_MAX) + pwr_hold_armed = 0; /* 超窗:disarm */ + + int hotkey_fired = 0; + if (pwr_hold_armed && pwr && vold && !prev_vold) { + long since = (long)(t - pwr_down_at); + if (since >= HK_PWRWIN_MIN && since <= HK_PWRWIN_MAX) { + fprintf(stderr, "supervisor: HOTKEY power held + VOL_DOWN tap (%ldms) -> switch to MeowOS\n", + since); + hotkey_fired = 1; + } else { + fprintf(stderr, "supervisor: POWER+VOL_DOWN ignored (%ldms outside window)\n", since); + } + } + prev_pwr = pwr; prev_vold = vold; + + /* 4) 热键触发 → 弹确认框 → 是:杀播放器进 MeowOS;否:留在 HibyOS。 + * 「是/否」分支严格按用户定案:否不移交给 MeowOS, + * 恢复播放器息屏副作用并 SIGCONT;是才 kill。 + * M2.14:开机武装延迟 + 播放器忙判门不再「静默丢弃」——热键一旦命中即置 + * hotkey_pending,待条件满足(延迟结束 / 播放器不忙)后**自动**弹确认框,用户只按一次。 */ + int in_armed_delay = (sim_hotkey_ms <= 0 || sim_hotkey_strict) && (now_ms() - sim_t0) < ARM_DELAY_MS; + int player_busy = (sim_hotkey_ms <= 0 || sim_hotkey_strict) && player_alive && proc_is_busy(player_pid); + if (hotkey_pending && !in_armed_delay && !player_busy) { + hotkey_fired = 1; hotkey_pending = 0; + fprintf(stderr, "supervisor: HOTKEY pending cleared -> fire confirm box\n"); + } + if (hotkey_fired) { + sim_hk_done = 1; /* --sim-hotkey 触发一次后不再模拟 */ + /* §18.23:开机武装延迟 / 播放器忙判门——不静默丢弃,置 pending 待条件满足后自动弹框 + * (仅约束真实热键;--sim-hotkey 为显式测试钩子,直通)。 */ + if (in_armed_delay || player_busy) { + hotkey_pending = 1; + fprintf(stderr, "supervisor: hotkey queued (armed delay=%d busy=%c) -> pending\n", + (int)in_armed_delay, player_busy ? proc_state(player_pid) : '-'); + pwr_hold_armed = 0; + kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); + prev_pwr = kp_is_down(&kp_power, KEY_POWER); + prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + continue; + } + /* 刷新 saved 亮度:若播放器当前亮度仍为正(如「背光常亮」设置), + * 以它为准;若电源按下已触发灭屏(亮度 0),保留此前 saved(≥60)。 */ + int now_bri = read_brightness(); + if (now_bri > 0) saved_brightness = now_bri; + /* 先冻结播放器(SIGSTOP→T),防止其在确认框上重绘覆盖; + * 同时捕获面板是否已因电源按下而灭屏(was_blank),供较后恢复。 */ + if (player_alive && !proc_is_stopped(player_pid)) { + bm_log("supervisor -> player %d SIGSTOP (freeze for confirm)", player_pid); + kill(player_pid, SIGSTOP); + for (int i = 0; i < 100 && !proc_is_stopped(player_pid); i++) usleep(10000); + } + int raw_brightness = read_brightness(); + int was_blank = (raw_brightness <= 0); + + /* 点亮面板以便看清确认框(播放器已冻结,不会 re-blank)。 */ + unblank_panel(); + if (was_blank) write_brightness(saved_brightness); + if (read_file_int(BACKLIGHT_POWER) > 0) write_file_str(BACKLIGHT_POWER, "0\n"); + + int ans = confirm_switch(); + + if (ans == 1) { + /* ===== 是:杀播放器 → 进 MeowOS ===== */ + if (player_alive) { + /* 帧冻结是 T 态:先 SIGCONT 让 SIGTERM 可投递(§18.21.3) */ + if (proc_is_stopped(player_pid)) { + fprintf(stderr, "supervisor: player is T, SIGCONT first\n"); + bm_log("supervisor -> player %d SIGCONT (resume from T)", player_pid); + kill(player_pid, SIGCONT); + } + fprintf(stderr, "supervisor: SIGTERM %d\n", player_pid); + bm_log("supervisor -> player %d SIGTERM (hotkey -> MeowOS)", player_pid); + kill(player_pid, SIGTERM); + unsigned long long kstart = now_ms(); + int exited = 0; + while (now_ms() - kstart < KILL_GRACE_MS) { + int st = 0; + if (waitpid(player_pid, &st, WNOHANG) == player_pid) { exited = 1; break; } + usleep(POLL_MS * 1000); + } + if (!exited) { + fprintf(stderr, "supervisor: player not exiting in %dms, SIGKILL\n", KILL_GRACE_MS); + bm_log("supervisor -> player %d SIGKILL (grace %dms expired)", player_pid, KILL_GRACE_MS); + kill(player_pid, SIGKILL); + int st = 0; + waitpid(player_pid, &st, 0); + } + player_alive = 0; + } + /* 播放器已死:面板已在确认框点亮(brightness=saved)。写 mode=meow, + * 然后 exec MeowOS 基础主屏(接管 fb0 画 buffer0+pan0)。 */ + write_mode(0); + fprintf(stderr, "supervisor: exec /usr/bin/meow_launcherd\n"); + fflush(NULL); + execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); + fprintf(stderr, "supervisor: exec launcher failed: %s\n", strerror(errno)); + _exit(127); + } else { + /* ===== 否 / -1:留在 HibyOS ===== + * 恢复播放器息屏副作用:进框前已灭屏则 re-blank(0+FBIOBLANK 4) + * 使物理面板匹配播放器 state machine;否则仅当读回 0 才写回 saved。 */ + if (was_blank) { + write_brightness(0); + int f = open(FB_DEV, O_RDWR); + if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); } + } else if (read_brightness() <= 0) { + write_brightness(saved_brightness); + } + fprintf(stderr, "supervisor: hotkey cancelled (stay HibyOS)\n"); + if (player_alive && proc_is_stopped(player_pid)) { + bm_log("supervisor -> player %d SIGCONT (hotkey cancelled)", player_pid); + kill(player_pid, SIGCONT); + } + /* 消化键边沿基线:把 prev 对齐到当前实际按下态(含电源仍按住), + * 并 disarm 热键,避免残留边沿立即重触发新热键(误弹确认框)。 */ + pwr_hold_armed = 0; + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + prev_pwr = kp_is_down(&kp_power, KEY_POWER); + prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + continue; + } + } + + usleep(POLL_MS * 1000); + } +} + +/* ================================================================== */ +/* 模式标记 + 决策 */ +/* ================================================================== */ +static void write_mode(int choice) +{ + /* choice: 1=HibyOS 0=MeowOS */ + FILE *f = fopen(MODE_FILE, "w"); + if (f) { fprintf(f, "%s\n", choice == 1 ? "hiby" : "meow"); fclose(f); } + fprintf(stdout, "bootmenu: DECISION -> %s (mode=%s)\n", + choice == 1 ? "HIBYOS 官方播放器" : "MEOWOS MeowOS 主屏", + choice == 1 ? "hiby" : "meow"); +} + +/* Bug 1:选 MeowOS → 把它持久为开机默认(写 spec 标记 + meow.conf boot_default=0),与 launcher + * «回到HibyOS»(set_boot_default_hiby 写 boot_default=1)对称。效果:本次选中即进 MeowOS 主屏,且 + * 下一次开机选单默认 MeowOS(不再「选 MeowOS 仍落回 HibyOS」)。追加一行,bootmenu 逐行读、最后一行生效。 */ +static void set_boot_default_meow(void) +{ + int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) { write(fd, "0\n", 2); close(fd); fprintf(stderr, "bootmenu: /data/meow_boot_default=0\n"); } + char conf[256]; + snprintf(conf, sizeof conf, "%s/meow.conf", MEOWAPPS); + fd = open(conf, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (fd >= 0) { + if (write(fd, "boot_default=0\n", 15) < 0) fprintf(stderr, "bootmenu: write boot_default failed\n"); + close(fd); + fprintf(stderr, "bootmenu: boot_default=0 appended to meow.conf\n"); + } else { + fprintf(stderr, "bootmenu: meow.conf open failed: %s\n", strerror(errno)); + } +} + +/* ================================================================== */ +/* --dumpkeys N:轮询 N ms,打印按键边沿(真机抓 key code 用) */ +/* ================================================================== */ +static int dump_cb(int code, int down, void *ctx) +{ + unsigned long long start = *(unsigned long long *)ctx; + fprintf(stdout, "[%llums] key %d %s\n", now_ms() - start, code, + down ? "DOWN" : "UP"); + return 0; +} + +static int run_dumpkeys(long ms) +{ + struct keypoll kp_power, kp_keys; + int p_ok = kp_open(&kp_power, DEV_POWER) == 0; + int k_ok = kp_open(&kp_keys, DEV_KEYS) == 0; + touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (touch_fd < 0) fprintf(stderr, "dumpkeys: open %s: %s\n", DEV_TOUCH, strerror(errno)); + fprintf(stderr, "dumpkeys: polling %ld ms (power=%d keys=%d touch=%d)\n", + ms, p_ok, k_ok, touch_fd >= 0); + unsigned long long start = now_ms(); + while ((long)(now_ms() - start) < ms) { + if (p_ok) kp_poll(&kp_power, dump_cb, &start); + if (k_ok) kp_poll(&kp_keys, dump_cb, &start); + if (touch_fd >= 0) { + struct pollfd pfd = { touch_fd, POLLIN, 0 }; + if (poll(&pfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { + uint16_t type = *(uint16_t *)(ev + 8); + uint16_t code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_ABS) { + if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt_touch, val, mt_touch.last_y); + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt_touch, mt_touch.last_x, val); + } else if (type == EV_KEY && code == BTN_TOUCH) { + fprintf(stdout, "[%llums] touch BTN_TOUCH %d @ (%d,%d)\n", + now_ms() - start, val, mt_touch.last_x, mt_touch.last_y); + } + } + } + } + usleep(POLL_MS * 1000); + } + return 0; +} + +/* ================================================================== */ +/* 选单守护模式主循环 */ +/* ================================================================== */ +static int run_menu(int sim_sel, int do_fork) +{ + struct keypoll kp_power, kp_keys; + if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "bootmenu: WARN no power dev\n"); + if (kp_open(&kp_keys, DEV_KEYS) != 0) fprintf(stderr, "bootmenu: WARN no keys dev\n"); + touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); + if (touch_fd < 0) fprintf(stderr, "bootmenu: WARN no touch dev\n"); + + /* 入口 drain + 短暂冷却(防 goodix 幽灵触摸,§18.16;M2.1 缩短到 800ms 不吞首次有效输入) */ + if (touch_fd >= 0) { + unsigned char ev[16]; + while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } + } + unsigned long long cooldown_until = now_ms() + GHOST_COOLDOWN_MS; + mt_reset(&mt_touch); /* 清触摸手势态,避免上次残留误派发(goodix 时序免疫:last 归 -1 起步) */ + + int cursor = (cfg_boot_default == 1) ? 0 : 1; /* 高亮随默认 */ + unsigned long long start = now_ms(); + unsigned long long deadline = start + (unsigned long long)cfg_boot_timeout_ms; /* 用户操作→重计(倒计时交互) */ + int dirty = 1, last_cursor = -1; + long last_clock_tick = -10000, last_cd_tick = -10000; + int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0; + int sim_done = 0; + int choice = -1; + + for (;;) { + unsigned long long t = now_ms(); + long el = (long)(t - start); + long remaining = (long)(deadline - t); /* 距 deadline:空闲才倒数,有输入则重计 */ + + if (remaining <= 0) { /* 空闲超时 → 单调强收敛到默认 */ + choice = cfg_boot_default; + fprintf(stderr, "bootmenu: TIMEOUT -> default %s\n", + choice == 1 ? "hiby" : "meow"); + break; + } + + /* --sim-select H|M:在 SIM_SELECT_DELAY_MS 后模拟确认 */ + if (sim_sel && !sim_done && el >= SIM_SELECT_DELAY_MS) { + sim_done = 1; + if (sim_sel == 'H') { cursor = 0; choice = 1; } + else { cursor = 1; choice = 0; } + fprintf(stderr, "bootmenu: SIM SELECT %c -> %s\n", + sim_sel, choice == 1 ? "hiby" : "meow"); + break; + } + + /* 按键边沿 */ + int vold = 0, volu = 0, play = 0, pwr = 0; + if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); } + if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL); + vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); } + + if (vold && !prev_vold) { /* 音量下 = 下移 */ + cursor = (cursor + 1) % 2; + deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; /* 用户操作 → 倒计时重计 */ + fprintf(stderr, "bootmenu: key VOL_DOWN -> cursor %d (cd reset)\n", cursor); + } + if (volu && !prev_volu) { /* 音量上 = 上移(2 项循环:-1 取模) */ + cursor = (cursor - 1 + 2) % 2; + deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; /* 用户操作 → 倒计时重计 */ + fprintf(stderr, "bootmenu: key VOL_UP -> cursor %d (cd reset)\n", cursor); + } + if (play && !prev_play) { /* 播放 = 确认 */ + choice = (cursor == 0) ? 1 : 0; + fprintf(stderr, "bootmenu: key PLAY -> select row %d (%s)\n", + cursor, choice == 1 ? "hiby" : "meow"); + break; + } + if (pwr && !prev_pwr) { /* 电源 = 确认当前光标项(M2.9 Bug 1:不再固定取默认) */ + choice = (cursor == 0) ? 1 : 0; + fprintf(stderr, "bootmenu: key POWER -> confirm row %d (%s)\n", + cursor, choice == 1 ? "hiby" : "meow"); + break; + } + prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr; + + /* 触摸(event1):按压→抬起 tap(同 launcher,读 ABS_X/Y 与 ABS_MT_*,§18.20) */ + if (touch_fd >= 0) { + struct pollfd pfd = { touch_fd, POLLIN, 0 }; + if (poll(&pfd, 1, 0) > 0) { + unsigned char ev[16]; + while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { + uint16_t type = *(uint16_t *)(ev + 8); + uint16_t code = *(uint16_t *)(ev + 10); + int32_t val = *(int32_t *)(ev + 12); + if (type == EV_ABS) { + if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt_touch, val, mt_touch.last_y); + else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt_touch, mt_touch.last_x, val); + } else if (type == EV_KEY && code == BTN_TOUCH) { + if (val) { /* 按压边沿:接受但**不锁原点**——等按下后首个有效 ABS 才锁(goodix 时序免疫,修“点两下”) */ + if (now_ms() < cooldown_until) mt_reset(&mt_touch); /* 幽灵/冷却:丢弃本次 */ + else mt_down(&mt_touch); + } else { /* 抬起边沿:按按下原点派发;滑动/未锁到坐标 → mt_up 返回 0,不命中、不崩溃 */ + int dx = 0, dy = 0; + if (mt_up(&mt_touch, &dx, &dy)) { + int ty = mt_touch.down_y; + if (ty >= ENTRY0_Y - 4 && ty < ENTRY0_Y + ROW_H - 8) { + cursor = 0; choice = 1; deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; + fprintf(stderr, "bootmenu: touch row 0 -> hiby\n"); + } else if (ty >= ENTRY1_Y - 4 && ty < ENTRY1_Y + ROW_H - 8) { + cursor = 1; choice = 0; deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; + fprintf(stderr, "bootmenu: touch row 1 -> meow\n"); + } + } + } + } + } + if (choice >= 0) break; + } + } + + /* 光标变化 → 整屏重绘 */ + if (cursor != last_cursor) dirty = 1; + if (dirty) { + draw_menu(cursor, remaining); + last_cursor = cursor; + dirty = 0; + } + + /* 每秒刷时钟 */ + if (el - last_clock_tick >= 1000) { last_clock_tick = el; draw_clock(cfg_bg); } + /* 每秒刷倒计时(秒值变化才刷) */ + long cd = (remaining + 999) / 1000; + if (el - last_cd_tick >= 1000) { last_cd_tick = el; draw_countdown(cfg_bg, remaining); } + + usleep(POLL_MS * 1000); + } + + write_mode(choice >= 0 ? choice : cfg_boot_default); + if (touch_fd >= 0) close(touch_fd); + + /* 顺序(§2.3):先恢复 fb(选单不再占屏),再 munmap+close fb0,才 fork */ + fb_restore_player_frame(); + fb_close(); + /* M2(--fork):HibyOS→fork 播放器(父进程转监督者,阻塞 rcS); + * MeowOS→exec meow_launcherd(MeowOS 基础主屏,接管 fb0)。 */ + if (do_fork) { + if (choice == 1) { + fork_hiby_player(); /* 父进程陷入 supervisor,永不返回 */ + } else { + set_boot_default_meow(); /* Bug 1:选 MeowOS → 持久为开机默认 */ + execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); + fprintf(stderr, "bootmenu: exec launcher failed: %s\n", strerror(errno)); + _exit(127); + } + } + return 0; /* M0/M1(无 --fork):选单退出,rcS 推进 */ +} + +/* ================================================================== */ +/* main */ +/* ================================================================== */ +static void usage(void) +{ + fprintf(stderr, + "usage: meow_bootmenu [--dumpkeys N] [--sim-select H|M] [--boot-only]\n" + " [--timeout-ms N] [--fork] [--version]\n" + " (no args) 守护选单模式(显示菜单,等待选择/超时)\n" + " --dumpkeys N 轮询 N ms 打印按键边沿(抓 key code)\n" + " --sim-select H|M 显示菜单,%d ms 后模拟选 H=HibyOS / M=MeowOS\n" + " --boot-only 跳过选单,直接取默认(boot_default)\n" + " --timeout-ms N 覆写超时毫秒(测试钩子)\n" + " --fork M2 开启:选 HibyOS 时 fork 播放器并转监督者;\n" + " 选 MeowOS 时 exec /usr/bin/meow_launcherd(默认关)\n" + " --sim-hotkey N 监督者启动 N ms 后模拟热键(M2 真机 verif)\n" + " --sim-confirm Y|N 确认框打开 2s 后自动答 Y=是 / N=否\n" + " --version 打印版本并退出\n", + SIM_SELECT_DELAY_MS); +} + +int main(int argc, char **argv) +{ + long dumpkeys = 0, timeout_override = 0; + int sim_sel = 0; + int boot_only = 0; + int do_fork = 0; + int do_version = 0; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]); + else if (!strcmp(argv[i], "--sim-select") && i + 1 < argc) { + char c = argv[++i][0]; + if (c == 'h' || c == 'H') sim_sel = 'H'; + else if (c == 'm' || c == 'M') sim_sel = 'M'; + else { usage(); return 1; } + } + else if (!strcmp(argv[i], "--boot-only")) boot_only = 1; + else if (!strcmp(argv[i], "--timeout-ms") && i + 1 < argc) timeout_override = atol(argv[++i]); + else if (!strcmp(argv[i], "--fork")) do_fork = 1; + else if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey_ms = atol(argv[++i]); + else if (!strcmp(argv[i], "--sim-hotkey-strict") && i + 1 < argc) { sim_hotkey_ms = atol(argv[++i]); sim_hotkey_strict = 1; } + else if (!strcmp(argv[i], "--sim-confirm") && i + 1 < argc) { + char c = argv[++i][0]; + if (c == 'y' || c == 'Y') sim_confirm = 'Y'; + else if (c == 'n' || c == 'N') sim_confirm = 'N'; + else { usage(); return 1; } + } + else if (!strcmp(argv[i], "--version")) do_version = 1; + else { usage(); return 1; } + } + + if (do_version) { + printf("meow_bootmenu %s (mipsel static musl)\n", MEOW_BOOTMENU_VER); + return 0; + } + + load_config(); + if (timeout_override > 0) cfg_boot_timeout_ms = timeout_override; + + if (dumpkeys > 0) return run_dumpkeys(dumpkeys); + + /* 无菜单直进(boot_menu=0 或 --boot-only):§3「直进默认锁 HGL」 */ + if (boot_only || !cfg_boot_menu) { + int choice = cfg_boot_default; + fprintf(stderr, "bootmenu: boot-only -> default %s\n", + choice == 1 ? "hiby" : "meow"); + write_mode(choice); + if (do_fork) { + if (choice == 1) fork_hiby_player(); + else { + set_boot_default_meow(); /* Bug 1:选 MeowOS → 持久为开机默认 */ + execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); + fprintf(stderr, "bootmenu: exec launcher failed: %s\n", strerror(errno)); + _exit(127); + } + } + return 0; + } + + if (fb_open() != 0) return 1; + fb_snapshot_player(); /* 快照当前显示缓冲(恢复播放器帧用) */ + fb_pan_buf0(); /* 画前 pan 到 yoffset=0(buffer0) */ + draw_menu(cfg_boot_default == 1 ? 0 : 1, cfg_boot_timeout_ms); /* 预画一帧 */ + run_menu(sim_sel, do_fork); + return 0; +} diff --git a/patch_v2/deploy_device.sh b/patch_v2/deploy_device.sh new file mode 100755 index 0000000..fd24ee5 --- /dev/null +++ b/patch_v2/deploy_device.sh @@ -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 [key]}" +KEY="${2:-$HOME/Project/R3ProII/ssh_keys/}" +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 "==> 部署完成" diff --git a/patch_v2/gdb_cave.sh b/patch_v2/gdb_cave.sh new file mode 100755 index 0000000..7a346a2 --- /dev/null +++ b/patch_v2/gdb_cave.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# gdb_cave.sh — 重启设备 gdbserver 并在本机 gdb 里执行给定命令文件 +# 用法: ./gdb_cave.sh [额外gdb参数...] +IP="${GDB_IP:-}" +KEY="$HOME/Project/R3ProII/ssh_keys/" +SCRIPT="${1:?usage: gdb_cave.sh