Files
MeowOS-For-R3P2/APPS.md
T

447 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/
├── <package>/ # 目录名 = app.json 的 package(权威 app_id)
│ ├── app.json # 应用描述(必填;package 字段为权威 app_id)
│ ├── <binary> # 静态 MIPS32r2 ELF(exec 指向)
│ └── icon.png # 预留:列表图标(v2 未用)
└── ...
```
`<package>` 即 `app.json` 的 `package` 字段(反域名权威标识,如 `os.meow.hello`);**目录名必须等于 package**,launcher 以 package 为 app_id,缺失时回退目录名。
### 2.1 app.json 字段
```json
{
"name": "Hello App",
"exec": "/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello",
"package": "os.meow.hello",
"version": "0.1",
"author": "whoever"
}
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `name` | ✓ | 列表显示名。**v2 仅支持 ASCII**(UTF-8 中文留待 v3,需完整 UTF-8→UTF-16 转换) |
| `exec` | ✓ | 应用可执行文件绝对路径(必须是**静态** MIPS32r2 ELF,soft/hard-float 均可) |
| `package` | — | 反域名权威标识(如 `os.meow.hello`),launcher 取之为 app_id;**目录名=package**。缺省回退目录名 `de->d_name`;两者存在且不同 → WARN |
| `version` / `author` | — | 保留字段,v2 不解析 |
| `fullscreen` | — | app 级全屏:`1`=进入全屏(状态栏**收合至提示线** + **保留左缘返回/顶部提示线/下滑通知中心**,见 MEOWOS_SPEC_REF §5.1);activity 级用它 `route.<id>.fullscreen`。**app.json `fullscreen` 字段此前未解析/已定未做 → 本次实现**(launcher 启动/切 activity 据声明预设 SDK 全屏,env `MEOW_FULLSCREEN`)[源:用户 2026-08-30] |
| `fonts` | — | 字库档位声明:`"full"` / `"gbk"` / `"common"`(app 需大字库时可声明 `"full"`,默认小字库 `common`≈4k;launcher 据此为该 app 加载大字库 `gbk`≈20k,从已扫描的 `MeowOS/fonts/<family>/<weight>/` 里按同 family(其次同 weight)选大字库,M2.6)。**注意**:与 `"fonts":["<handle>"]`(应用自带字库句柄数组)**同名二义**,见 MEOWOS_SPEC_REF §7 A-14 [源:用户 2026-08-30] |
- 目录扫描深度 1 级:只枚举 `MeowAPPs/` 下的直接子目录
- 应用计数上限 **8**(超出忽略;单屏 8 行)
- app.json 解析器极简(launcher 内实现):仅匹配 `"name":`、`"exec":`、`"package":` 后的引号字符串(及扩展 icon/framework/immersive/route./`awake.<id>`(keep_awake)/service.),**`fullscreen`(本次实现)与 `fonts`(档位串) 已解析**,不做 JSON 校验(多行/单行均可)[源:审计 2026-08-30][源:用户 2026-08-30]
- 列表刷新时机:每次菜单打开时重新扫描(TF 卡热插拔后立即可见)
### 2.2 边界情况行为(launcher 实现,全部真机验证)
| 场景 | 行为 |
|---|---|
| TF 卡未挂载/未插入 | 菜单提示「NO STORAGE CARD / INSERT TF CARD」(state=1,/proc/mounts 无 sd_0) |
| 卡挂载但无 MeowAPPs 目录 | 提示「NO APPS FOUND」+ 底部示例行 `MeowAPPs/<id>/app.json`(state=2) |
| MeowAPPs 为空目录 | 同上(扫描计数 0 → state=2) |
| app.json 损坏/缺 name 或 exec 字段 | 跳过该应用,列表其余项正常 |
| 二进制不存在/不可执行 | 行尾标 `[MISSING]`;点击/确认该行提示 toast「CANNOT START: <name>」(access X_OK 检查,不 fork;`launch_app` 返回 -1) |
| 二进制存在但 execve 失败 | v6.8 起 `launch_app` 用 CLOEXEC 管道检测 execve 失败(子进程写 errno),返回 -1 → toast;**不再把 127 误当「应用跑完」**。toast「CANNOT START」**仅在返回 -1(真启动失败)时显示**,应用干净退出(返回 0)不显示(Bug 3,旧版无条件显示) |
| 扫描读取出错(opendir/fopen/fgets 失败) | 各自跳过;最终无应用时按 state=2 提示 |
状态机:state 0=正常 / 1=未检测到存储卡 / 2=无可用应用。
---
## 3. 应用生命周期契约
### 3.1 启动流程(meow_launcherd 侧)
```
热键(按住电源 + 单击音量减;电源按下后 60ms~2.5s 内收到音量减按下边沿触发;v6.7 起窗口下限由 300ms 改为 60ms,窗口 60–2500ms)
→ meow_launcherd 检测(EVIOCGKEY 状态轮询,10ms;电源按下即 arm,窗口外或松开即 disarm)
→ SIGSTOP hiby_player(/proc/N/exe == /usr/bin/hiby_player;等待状态 T)
→ 背光仅在面板灭时拉回(brightness==0 才写 saved/60,绝不无脑拉亮度)+ unblank 面板(先 SIGSTOP 再 unblank)→ fb0 直绘应用列表
→ 菜单循环(10ms):
· 触摸(event1 事件读)点击行 → 启动;点击「BACK TO PLAYER」行 → 退出菜单
· 按键(EVIOCGKEY 状态轮询):音量减/加=移动光标,播放键=启动
· **电源 = 纯息屏/亮屏(launcher 不干预亮度/面板,不退出菜单)**;退出菜单仅「BACK TO PLAYER」触摸行(或 launcher SIGTERM)
· 应用启动:fork → execve(exec[i])(失败 exit 127);同时 fork meow_watchdog <pid>
· waitpid(app) 阻塞;应用内强杀 = 音量减×3,退出回菜单
→ 应用退出 → 菜单退出 → SIGCONT hiby_player → 播放器恢复重绘
```
### 3.2 让位机制(守护进程方案,2026-08-28 真机验证)
**结论:SIGSTOP 整进程冻结 hiby_player(UI+音频+全部线程),菜单期间播放器完全不动作;SIGCONT 后原样恢复。hiby_player.sh 看门狗只盯进程退出,SIGSTOP 不触发 reboot。**
依据(真机实证,见 ANALYSIS.md §18):
1. **SIGSTOP 不触发看门狗**:进程存活(状态 T),`hiby_player.sh` 的 `sleep 1; reboot` 不执行。
2. **恢复确定可靠**:菜单每条退出路径(含 SIGTERM 处理、错误路径)都会 SIGCONT;`meow_launcherd` 自身被 SIGTERM 时也先 SIGCONT 再退出。
3. **崩溃安全**:应用崩溃/被杀 → waitpid 返回 → 自动 SIGCONT 恢复。若守护进程本身被 SIGKILL,播放器会冻结——手动 `kill -CONT $(pidof hiby_player)` 恢复(详见 §1.3)。
4. **菜单期间播放器完全不刷新**(SIGSTOP),fb0 归守护进程独占绘制,无抢绘。
5. **音频**:SIGSTOP 连音频线程一起冻结(应用运行期无声音;如需出声见 §5 未来方案——v2 的 SIGSTOP 方案下播放器无法代播,需改 waitpid-in-callback 让位或音频线程解冻,留待 v3)。
6. **应用退出 → SIGCONT → 播放器首帧整页重绘**,无需应用清屏。
### 3.3 应用该做什么 / 不该做什么
**应用必须:**
| # | 规则 | 说明 |
|---|---|---|
| 1 | 干净退出 | 任意退出码均可;守护进程 waitpid 立即返回,SIGCONT 恢复。`exit(0/1)` 即可,不要 `abort()` 留下僵尸 |
| 2 | 独占屏幕自绘 | 自行 `open("/dev/fb0")` + `mmap` 后绘制;设备帧缓冲 480×720(分辨率/色深运行时用 FBIOGET_VSCREENINFO/FSCREENINFO 探测,RGB565 或 RGBA8888 自适应)。**注意:本设备 ioctl 号 VSCREENINFO=0x4600、FSCREENINFO=0x4602(勿颠倒,见 §4.3)** |
| 3 | 自己读输入 | `open("/dev/input/event*")` 逐个打开,读到任意 key/touch 事件即处理。按任意键退出是推荐约定(物理事件对所有读者多播,播放器已冻结,无需担心抢事件) |
| 4 | 及时退出 | 建议空闲超时(如 30s 无操作)自动退出,避免长期占屏(hello 60s 看门狗) |
**应用不该做:**
| # | 规则 | 原因 |
|---|---|---|
| 1 | 不要尝试"恢复" fb | SIGCONT 后播放器首帧即整页重绘,无需你清屏。你清屏反而闪一下 |
| 2 | 不要 kill hiby_player | 触发 hiby_player.sh 看门狗 → 整机重启 |
| 3 | 不要继承占用声卡 | SIGSTOP 期间声卡仍被冻结的播放器持有,直接打开会失败/挂起(v2 无音频) |
| 4 | 不要 fork 出长期后台进程 | 你的退出应"真正退出",否则 SIGCONT 后后台进程仍占屏/抢输入 |
| 5 | 不要碰 NAND 分区/固件 | 只写 TF 卡与 /usr/data |
### 3.4 退出与恢复
- 应用退出 → waitpid 返回 → 菜单关闭 → SIGCONT 播放器 → 播放器恢复原界面
- 菜单内「BACK TO PLAYER」行 → 直接 SIGCONT(不启动任何应用);**电源键在菜单内不再退出菜单(v6.5 起电源=纯息屏/亮屏,仅 sys_server/内核处理背光)**
- 整机重启 → 一切回退原版(守护进程由 S90 随开机自启)
### 3.4.1 物理按键语义 —— 强制规范(系统统一管理,APP 禁映射)
> ⚠️ **红线**:物理按键是**系统资源**,语义由 MeowOS(launcher/watchdog/内核)**统一管理**。**任何非系统组件(MeowAPP)不得将物理按键映射成任何行为**,包括但不限于:电源键→OK/确认/exit/翻页,音量键→确认/选择,播放键→确认/返回。
**为什么**:用户在任何界面都必须能靠物理键完成系统级操作——**电源键=息屏/亮屏**(MeowOS 界面也必须能息屏)。若 APP 抢走电源键当"OK/exit",用户一按电源键,屏没息反而退出了 APP,**无法息屏**;音量键/播放键同理。物理键必须留给系统。
**约定(MeowOS 界面 = launcher 主菜单/选单/电源菜单,是唯一允许映射物理键的地方)**:
> ⚠️ **适用范围**:**除了电源键菜单(`show_power_menu`,允许"电源=确认、音量=导航")以外,所有按键信号由 MeowOS 全局接管**——**任何界面(含 MeowOS 自身的设置页 / 应用列表 / 选单、以及 MeowAPP)都不得私自定义按键行为**。物理键的语义只有一份:电源=息屏/亮屏/长按菜单/8s强关,音量=调系统音量,播放/上下曲=媒体控制(未落地前「抓住」)。任何界面(如系统设置页)**不得**把电源键/播放键映射成"返回/确认"这类自定义行为;界面导航一律走触摸(返回按钮/左缘左滑),按键语义统一由系统判。
| 键 | 系统语义(APP 收到的是语义事件,不是裸 keycode) |
|---|---|
| 电源 | 息屏/亮屏、长按弹电源菜单;**APP 内=纯息屏/亮屏**(绝不 exit/OK) |
| 音量 +/− | 调系统音量;**APP 内=调音量**(绝不选中/确认/翻页) |
| 播放/暂停 | 全局媒体控制(播放/暂停当前媒体会话);**APP 内=媒体控制**(绝不确认/打开) |
| 返回 | 触屏左滑/虚拟返回键 → SDK 语义 `on_swipe(LEFT)` / watchdog 信号,**非物理键** |
**MeowAPP 侧**:
- APP **不读 `/dev/input/event*`**、**不 open/mmap fb0**、**不处理任何物理 keycode**——一切走 SDK 的语义回调(`on_tap`/`on_swipe`/`on_tick`;`on_key` **仅用于 SDk 内部状态,不含电源/音量/播放的确认/exit 语义**)。
- APP 想"返回/退出"→ 用 `meow_ui_request_exit()` 或左滑,**绝不监听物理键映射 exit**。
- SDK 的 `on_key` 回调:**禁止 APP 用它实现"电源=exit"这类映射**。SDK 内部消化物理键的系统语义(息屏/音量/媒体),**只把触摸返回/手势等语义事件给 APP**。
**落地**:hello.c 当前 `on_key(KEY_POWER)→request_exit()` 违反此规范,需删除(改由 SDK/系统处理息屏);APP 退出走左滑/`request_exit`/系统返回。**MeowOS 系统设置页(run_settings_page)曾私自定义"电源/播放键=返回",M2.45 已按本规范改为全局语义(电源=息屏/亮屏/长按菜单/8s强关,播放=抓住),退出仅靠触摸(底部返回/左缘左滑)**——任何 MeowOS 界面新增时须遵守本规范,不得私自定义按键。
### 3.5 虚拟返回键 / 沉浸模式 / 看门狗
应用启动时,launcher **同时 fork 看门狗进程** `/usr/data/meow_watchdog <app_pid>`(固件 S99ssh 首启从 `/usr/bin/meow_watchdog` 拷到可写卷;launcher 先试 /usr/data 再试 /usr/bin):
| 特性 | 行为 |
|---|---|
| 虚拟返回键 | 看门狗独立 mmap `/dev/fb0`(RGB565 自适应),每 100ms 重画左上角 48×48 返回按钮(深色底+白箭头);读 `/dev/input/event1` 触摸,按下落在按钮区 → `SIGTERM` 应用 → 1s 后 `SIGKILL`(应用卡死也能退出)。**v6.4 防抖**(针对"启动误杀"):`last_x/y=-1` 起始(无坐标的按下丢弃)、BTN_TOUCH 按实际值匹配边沿(消除失同步翻转)、仅短按 ≤600ms 有效、启动后先 drain 一次、**冷却期 2.5s**(实测幽灵触摸窗口 1.2–2.25s;此窗内返回键不响应,改 `COOLDOWN_MS` 可调) |
| 沉浸模式 | 应用写 `/tmp/meowapp_state` 内容为 `1` → 看门狗不画按钮、只识别「左缘 24px 内起点向右滑 >50px」手势 → 发 `SIGUSR1`;写 `0` 或删除 → 恢复虚拟键 |
| 看门狗自退出 | 应用进程消失(`kill(pid,0)` ESRCH)或 launcher SIGTERM → 退出 |
| 强制退出兜底 | 应用运行期(launcher 处于 waitpid):**音量减连按 3 次**(间隔 < `hotkey_seq_gap`,与主序列同阈值)→ SIGTERM → 0.5s → SIGKILL → 恢复。⚠️ 响应音量键的应用(如示例 hello)会先自行退出(`exit:key`),强杀序列是**不响应音量键/卡死应用**的兜底 |
| 长按电源 | 硬件级强制关机(设备固有),最终兜底 |
| 测试模式 | `meow_watchdog <pid> --sim-tap N` / `--sim-swipe N`,无触摸设备也可验证 |
**应用协议约定**:
- 默认(非沉浸)由看门狗画返回键,应用**不必**处理返回手势
- **⚠️ 返回键信号 = `SIGTERM`**(看门狗 `handle_touch` 按钮命中 → `kill(app_pid, SIGTERM)`→1s→`SIGKILL`);`SIGUSR1` **仅**是沉浸左缘滑动手势,不是返回键。com 与 SIGTERM 默认终止会**不画 BYE** 直接死,故示例 hello 安装 `SIGTERM` 处理器:画 BYE + `exit(0)`,使返回键呈现确定性的「bye → 立即退出」(v6.8,Bug 2)。
- **⚠️ 应用启动后须防 goodix 幽灵触摸**(v6.8,Bug 2 根因):goodix 在启动触摸的尾部发出一段幽灵触摸(实测启动后 1.2-2.25s,§18.16),无防护时应用会**误把幽灵当下按而立即退出画 BYE**,且 launcher 菜单会把同一幽灵重放成行点击而**重开应用**(用户看到「bye 后又回 hello 主界面、需再点一次」)。hello v6.8 增加 `COOLDOWN_MS=2500`:启动后前 2.5s 忽略一切按下边沿,只有用户**真正**的下按才触发退出。launcher 侧同步在应用退出后 drain + 冷却(见 §4.7)。
- 沉浸应用写 `/tmp/meowapp_state`,处理 `SIGUSR1` 作为「返回事件」
- 应用异常卡死 → 虚拟返回键(若可见)→ SIGKILL;沉浸态卡死 → 音量减连按 3 次(强杀序列)
- **应用日志**:launcher 通过环境变量 `MEOW_LOG_DIR` 告知 TF 卡日志目录(无卡时不设),应用自行决定文件名(示例 hello 写 `apps/<app_id>/logs/<ts>_<pid>.log`;[源:审计 2026-08-30]);启动时先 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 定案;v6.7 起窗口下限 300ms→60ms)**:**按住电源 → 电源按下后 60ms~2.5s 内单击音量减**(在窗口内收到音量减的**按下边沿**且电源仍被按住)⇒ 开菜单。电源按下会触发播放器息屏流程(**已接受的副作用**,用 v6.2/v6.3 已验证时序保证菜单可见:SIGSTOP 播放器 → unblank 面板 → 仅灭屏时拉背光);音量减单击副作用 = **音量 -1**(可接受)。电源松开或窗口超时(>2.5s)即 disarm;音量减在窗口外(<60ms 或 >2.5s)按下视为普通音量键,不触发。**纯单键序列**(v6 volup×3)与组合键(上一曲+音量加,GPIO 断,§18.12)均已退役,仅保留在 `meow.conf hotkey=seq`(历史备选,不推荐)。
热键 `meow.conf` 配置:
```
hotkey=power_voldown # 默认:电源按住 + 单击音量减(60-2500ms 窗口;v6.7 起下限 300ms→60ms)
hotkey=seq # 历史序列热键(单键连按 N 次,不推荐)
```
(`hotkey_seq=`/`hotkey_seq_gap=` 仅在 `hotkey=seq` 时生效。)
⚠️ **v6.1 修复**:早期版本 hotkey_seq 解析在行尾 `\n` 处死循环(外层 while 不消费 EOL),配了该行 daemon 即卡死——已修复并在真机复测。该解析器同样用于 `hotkey=` 行(无此问题,逐行 `strncmp`)。
**音量副作用与恢复**:v6.5 主热键(电源+音量减)的副作用是**音量 -1**(单击音量减,播放器收到,直接作为音量调节——**无恢复需求,接受**)。序列热键(`hotkey=seq`,默认已不使用)的每次按下仍会音量+1×N,launcher 在第 1 次按下快照 CS43198 音量、序列完成触发时写回(**⚠️ 实测该 control 写无效,恢复为尽力而为**)——因已退役,不再展开。
**热键演变记录**:v1 电源+音量减 → v2 电源+上一曲 → v3 双音量同按(物理不可能:event2 单 ADC 通道)→ v4 上一曲+音量加(上一曲 GPIO PC28 硬件链断,§18.12)→ v6 单键序列(volup×3,单键物理必可注册)→ **v6.5 电源+单击音量减(定案)**:回到"跨设备组合键"能力(电源在 event0、音量减在 event2,两键不同 ADC/GPIO,物理可同按),利用电源按下必然触发播放器息屏的既有行为,以"按住电源 + 窗口内单击音量减"作热键,同时**移除菜单/应用内电源=逐级返回**(电源在菜单/应用内改为纯息屏/亮屏,见 §3.1/§3.4)。
### 4.3 fb0 绘图
- 480×720 RGB565(运行时 ioctl 探测,RGB565/RGBA8888 自适应)
- **⚠️ 本设备 fb ioctl 号(实测)**:`FBIOGET_VSCREENINFO=0x4600`、`FBIOGET_FSCREENINFO=0x4602`(旧式无 size 编码号)。hello.c/meow_watchdog.c 曾沿用标准头文件常量写反(0x4602/0x4600),导致 ioctl 返回垃圾、mmap EINVAL——已全部修正并用探针实测通过。
- **⚠️ 双缓冲(v6.6)**:本设备 fb0 为**双缓冲**(实测 `yres_virtual=1440=2×720`、`smem_len=2 帧`、`ypanstep=1`),**面板显示哪个缓冲由 fb_var.yoffset 决定,`FBIOPAN_DISPLAY=0x4606` 可翻页**。launcher 与 hello 等 app 都只画 mmap 偏移 0(buffer0),若 yoffset 停在 buffer1(播放器电源/息屏重绘后),菜单画在 buffer0 而面板显示 buffer1 → 菜单不可见、屏幕冻结(v6.5 bug)。v6.6 起 `fb_open` **mmap 整个虚拟帧缓冲**(含两帧),菜单打开时 `fb_backup_player` 快照当前 yoffset 处缓冲 → `fb_norm_buf0`(FBIOPAN_DISPLAY 把 yoffset 归 0,菜单/app 共同绘制目标)→ 回退时 `fb_restore_player` 把备份拷回原 yoffset 缓冲并 pan 回原 yoffset(播放器事件驱动不主动重绘,必须恢复 yoffset)。**面板 blank/亮度状态仍归播放器**(§18.11 教训),launcher 只在 fb 缓冲/翻页层面介入。
- **重绘纪律(v6.9,修复列表 ~3fps 频闪)**:菜单只在**内容真正变化**时整屏重绘——光标移动(输入)、应用退出/进入(重写了 buffer0)、toast 起止、未来通知徽标变化时置 `dirty`;**绝无周期整屏重绘**。v6.8 的「500ms 安全重绘」会在 500ms 定时把**整个菜单**重写进正在显示的 buffer0(就地改写产生撕裂/脉冲,且 `draw_statusbar` 每次调用都 `amixer` popen 拖慢主循环),正是频闪根因。若真需周期刷新(时钟每秒),用**只刷时钟数字**的轻量路径 `draw_clock()`(不弹 amixer、不碰应用列表),其余状态(电量/WIFI/BT/音量)仅在事件(dirty)时随整屏刷新。
- 布局:标题 MEOWAPPS / 高亮行(光标)/ 状态提示 / `BACK TO PLAYER` 行 / 底部按键提示 / toast 区(y≈584-617);**右下角开发版本号**(v6.9)——`MeowOS v<MEOWOS_VERSION> dev`,右对齐画在底部行 y=700,与 `BACK TO PLAYER`(y≤688)和左侧按键提示(至 x≈254)均不重叠。
### 4.4 命令行(含测试钩子)
```
meow_launcherd # 守护模式
meow_launcherd --scan # 打印应用列表与状态,退出
meow_launcherd --dumpkeys N # 轮询 N 毫秒,记录所有按键状态边沿(真机抓 key code 用)
meow_launcherd --sim-hotkey N # N 毫秒后模拟热键(默认=电源按住→N+800ms 单击音量减;hotkey=seq 时=音量加 ×3 快速连按,每次 300ms 间隔)
meow_launcherd --sim-tap N # 菜单打开 N 毫秒后模拟点击第 0 行
meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
```
测试钩子用于无物理输入时的全链路自动化验证(注入事件不投递、无法远程按物理键)。
### 4.5 统一日志系统(MeowAPPs 组件共用)
- **默认位置**:TF 卡 `/usr/data/mnt/sd_0/MeowOS/logs/meow_<bootts>.log`(如 `meow_20260828_020123.log`;`bootts` = 当前墙钟 − /proc/uptime,同一 boot 会话内多次启动共用同一文件)。**系统日志**(launcher/watchdog 等 MeowOS 组件)写此文件;**应用日志**另按 `apps/<app_id>/logs/<ts>_<pid>.log` 分文件(见本 §4.5「组件环境变量」)。⚠️ `meow_bootmenu` 仍用 `MeowAPPs/logs`(代码侧不一致;bootmenu 与 launcher 日志目录**待统一为 `MeowOS/logs`**,[源:审计 2026-08-30])
- **无 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` → 应用:日志目录(每 App `MeowOS/logs/apps/<app_id>/logs/`,应用写 `<ts>_<pid>.log`;[源:审计 2026-08-30])
- 内容:时间戳 + 全部按键边沿/菜单/应用事件(真机按键证据)
- PID 文件:`/usr/data/meow_launcherd.pid`;播放器发现:扫描 `/proc/N/exe` == `/usr/bin/hiby_player`
### 4.6 UI 规范(应用列表与 MeowAPPs 界面)
- **纯黑背景为默认**(`menu_bg = 0x000000`)
- **可配置**:TF 卡 `MeowAPPs/meow.conf` 支持 `bg_color=RRGGBB`(如 `bg_color=10284f`),菜单打开时重新加载
- **40px 状态栏**(v6.8 由 20px 翻倍;v6.9 时钟加秒):第 1 行 = 左侧时钟(**HH:MM:SS,每秒刷新**,v6.9;用 `draw_clock()` 只刷时钟数字,不弹 amixer、不碰应用列表——见 §4.3 重绘纪律)、右侧 `电量% WIFI BT`(电量读 `/sys/class/power_supply/battery/capacity`、WIFI 读 `/sys/class/net/wlan0/operstate`、蓝牙读 `/sys/class/bluetooth/hci0` 存在性);第 2 行 = 左侧 `VOL nn%`(音量读 CS43198 / amixer numid=1,值 0-255);下方 1px 分隔线(移至 y=40)。⚠️ 音量 amixer control 实测为**死镜像**(§18.13:播放器走 sa_hgl_dma 硬件直通,ALSA control 读回恒 0),故展示值可能恒为 0%,为已知设备限制。
- **开发版本号**(v6.9):右下角绘制 `MeowOS v<ver> dev`(字体无小写,实际渲染为大写 `MEOWOS V0.6.9 DEV`)。单一来源 = `meow_launcherd.c` 的 `MEOWOS_VERSION` 宏;`build_fw_apps.sh` 打包时用 `sed` 从该宏抓取写入 rootfs 的 `etc/meowos_version`,屏显与固件文件**同源,不会两处不一致**(版本号改一处即可)。
- 示例应用 hello 同样黑底 + 40px 状态栏(应用内左上角 48×48 返回键由 watchdog 绘制,允许盖住状态栏左侧)
### 4.7 应用退出后的菜单触摸守卫 + 启动失败检测(v6.8,Bug 2/3/4)
**Bug 2/4 根因(应用退出后菜单重放残留触摸)**:应用运行期间 launcher 的菜单触摸 fd(`touch_fd`)**不轮询**(被 `launch_app` 的 waitpid 阻塞),期间收集的触摸事件(goodix 幽灵触摸 + 真正退出应用的那次点击/按键)**全部排队**。应用退出回到菜单后,菜单下一轮 poll 会把这些残留事件**重放成新的行点击**——落在应用行则**重开应用**(症状:bye 后回 hello 主界面、需再点一次,Bug 2);落在「BACK TO PLAYER」行则**关菜单 → SIGCONT 播放器**(整屏闪回播放器,Bug 4)。
**修复(`run_menu` + `menu_after_app_exit()`)**:
1. **drain**:应用退出后立即读完并丢弃 `touch_fd` 的全部残留事件。
2. **冷却**:应用**真正跑过**(`launch_app` 返回 0)时置 `menu_touch_cooldown_until = now + 2500ms`,冷却窗内忽略一切触摸按下边沿(与 watchdog 的同值 COOLDOWN_MS 对齐,覆盖实测幽灵窗 1.2-2.25s);启动失败(返回 -1)不置冷却,避免无谓锁菜单。
3. **菜单重绘**:v6.8 曾把 `last_redraw` 从绝对时间戳 `t` 改为相对量 `el`(使 500ms 安全重绘生效)以覆盖应用退出后的残帧(如 BYE)。**v6.9 起摒弃周期整屏重绘**(它在 500ms 定时把整个菜单重写进正在显示的 buffer0,造成列表 ~3fps 频闪):改为**仅 dirty 时整屏重绘**,应用退出/进入路径显式置 `dirty=1` 主动覆盖应用帧;时钟每秒用轻量 `draw_clock()` 只刷数字。
**Bug 3 根因与修复**:旧代码在所有 `launch_app` 调用点**无条件**置「CANNOT START: <name>」toast,即使应用**正常跑完**。修复:仅当 `launch_app` 返回 **-1(真启动失败)** 才置 toast;应用干净退出(返回 0)不显示。`launch_app` 用 **CLOEXEC 管道**检测 execve 失败(子进程写 errno → 父进程读 >0 判失败并返回 -1),不再把子进程 `_exit(127)` 误当「应用跑完」。
**副作用(已知取舍)**:应用退出后 2.5s 内菜单触摸不响应(返回键误触/退出后紧急再操作需等待;与 watchdog 相同值,实测幽灵窗 1.2-2.25s)。物理触摸的幽灵无法注入复现(§18.2 注入不投递),以上防抖逻辑在注入/sim 链路已验证,物理最终确认需用户。
---
## 5. 音频通路(未来应用的出声方案)
参考 ANALYSIS.md 第 11 节结论:
- 声卡 = 内部唯一 card0(CS43198 双 DAC),**无混音**,谁打开谁独占;hiby_player 常驻持有 `hw:0,0`。
- **v2(SIGSTOP 让位)下播放器完全冻结,无法代播**。应用直接开 ALSA 会失败。v3 候选:
1. **改让位机制为"仅 UI 冻结"**(如让播放器走 waitpid-in-callback 或独立音频线程),应用经 unix socket 汇入通道(0x73d320 区,`AIRPLAY:CONNECT` 类消息 + 16-bit PCM)出声——采样率定 44.1k/16bit(shairport 路径)。
2. 应用退出后由播放器继续(现状 v2)。
- 音量/通路切换由 hiby_player 管理(硬件音量寄存器),应用只管喂 PCM。
---
## 6. 测试流程
> 设备已刷 SSH 固件:`ssh -i ssh_keys/<device_key> root@<IP>`(登录口令见私密记录,勿入库)。
> 所有设备操作遵守纪律:动播放器前数实例;重启前 `rm -f /data/recovery_all`;息屏测试后恢复背光;结束恢复设备干净。
0. **轮前检查**:`ls /data/recovery_all`(存在即删+告警);`ps | grep hiby_player | wc -l` 应=1。
1. **推送**:`meow_launcherd/meow_launcherd` → `/data/meow_launcherd`(测试期直接跑 `/data/...`);`meow_watchdog` → `/usr/data/meow_watchdog`;`hello` → `/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello`。
2. **验证输入路径**(无物理按键时的替代证据):
- `meow_launcherd --scan` → 列出应用
- `meow_launcherd --dumpkeys 6000` + 物理按键 → 日志出现 `key VOL_DOWN/VOL_UP DOWN/UP`
- 或注入状态模拟:`evtool send /dev/input/event2 1 114 1`(仅更新键状态,无投递,供 EVIOCGKEY 观察)
3. **全链路**(自动化):`meow_launcherd --sim-hotkey 2000 --sim-tap 4000`;期间 `cat /proc/<pid>/stat` 应 T(冻结);fb0 截图验证菜单/应用画面(黑底+状态栏);注入上一曲(165)+音量加(115)按住 2s 强杀应用 → 日志 SIGCONT → 播放器回 R、实例 1、背光 101。
4. **边界 ×5**:无卡(umount sd_0 → state=1 → 重挂)、无目录(mv MeowAPPs → state=2)、空目录(state=2)、坏 app.json(跳过)、二进制缺失(`[MISSING]` + toast),各拍 fb0 截图存 `device_test/`。
5. **稳定性**:守护进程跑 ≥3 分钟,播放器 R、实例 1、无 recovery_all;SIGTERM 干净退出。
6. **固件**:`build_fw_apps.sh` → `fw_apps/r3proii_ssh_apps.upt`,回验 0 diff、rootfs ≤ 45,088,768 B。
7. **回退**:重启即回退原版(守护进程不装则无热键);设备侧删除 `/data/meow_launcherd` 等测试文件。
---
## 7. 已知风险与未决项
0. **⚠️ `/data/recovery_all` 恢复触发(设备会被出厂重置,最高优先红线)**:hiby_player 内部存在写该标志的代码路径(`0x4823d0` 无限循环 / `0x56d1c0` 消息处理器)。调试期间设备已被出厂重置两次。防护规矩:**每次重启播放器前 `rm -f /data/recovery_all`;部署后确认稳定才允许设备重启;发现该文件立即删除并告警**。Plan B 全程未触发(SIGSTOP 不崩溃播放器)。
1. **守护进程被 SIGKILL/崩溃 → 播放器冻结**:SIGTERM 路径会自动 SIGCONT,但 SIGKILL 无法拦截。恢复:`kill -CONT $(pidof hiby_player)`。v3 可加双重守护(两个进程互保)。
2. **热键副作用**:v6.5 主热键(电源+单击音量减)会触发播放器息屏流程(菜单打开用后置时序 unblank+仅灭屏拉背光,见 §3.1)、音量 **-1**(可接受);**不再碰电源键"逐级返回"**。仅当 `hotkey=seq` 时才有音量 +N(amixer 音量 control 写无效无法恢复,见 §4.2;可配 `hotkey_seq=play,play,play` 换取播放停顿)。
3. **菜单触摸依赖 event1 物理多播**:播放器不抓触摸(已实测),若未来固件抓取,菜单仍有按键导航兜底。
4. **UTF-8 应用名**:v2 仅 ASCII;中文名需实现 UTF-8→UTF-16 转换。
5. **应用挂死且虚拟返回键不可见(沉浸态)**:音量减连按 3 次强杀兜底;再不行长按电源硬件关机。
6. **SIGSTOP 冻结音频线程**:应用运行期无声(v3 方向见 §5)。
7. **开机时序**:S90 在 S92 播放器之前启动;输入设备由 S11 加载完成,launcher 启动时重试打开(30 次 × 1s)。
---
## 附录:MeowAPPs 开发规范 v2(包结构与数据目录/软件包)
### 1. 应用目录结构
```
MeowAPPs/<package>/ # 目录名 = app.json 的 package(权威 app_id)
├── app.json # 应用配置(name/exec/package/路由/图标/框架声明等)
├── meow # 程序主体(静态 MIPS 二进制;也可多文件/脚本)
└── <资源文件/图标…> # 只读程序资源
MeowData/<package>/
├── data/ # 持久应用数据(配置、播放列表、登录态…)
└── cache/ # 缓存(可清)
```
> `<package>` = `app.json` 的 `package`(反域名,如 `os.meow.hello`),与目录名一致;launcher/app 数据/日志/`.mpk` 均以它为包名。
> 注:TF 卡为 exFAT,**不支持符号链接**,故数据目录不用链接,改由 MeowOS 通过**环境变量**注入(见 §2)。
### 2. 数据目录(MeowOS 负责,用环境变量注入)
- 本文档 `<包名>` = `app.json` 的 `package`(反域名权威标识,如 `os.meow.hello`),与目录名一致。
- **程序主体 + 必要配置文件** 放 `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`
- **包名 = 文件名茎 = `app.json` 的 `package`**(如 `os.meow.hello` → `os.meow.hello.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 一致)。**包名 `<pkg>` = `app.json` 的 `package`(= `.mpk` 文件名茎 = 目录名)**。**部署**:`MeowOS/opt/bin/nyai`(卡,随 TF_CARD_LAYOUT 外围工具目录)。
**调用**:M2.12+ 固件 `meow_card_path.sh` 已将 `MeowOS/opt/bin` 前插 PATH,`nyai` 可直接调用;更早固件(M2.11 及以前,无 `/etc/profile.d/meow_card_path.sh`)SSH 需绝对路径[源:审计 2026-08-30]
`/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai` 或 `export PATH=/usr/data/mnt/sd_0/MeowOS/opt/bin:$PATH`;
M2.12+ 固件该目录已由 `meow_card_path.sh` 前插,`nyai` 可直接调用。
**命令**:
| 命令 | 说明 |
|---|---|
| `nyai update` | 读 sources.list 各源:本地读 `<源>/Packages`;http 用卡上 curl `-fsSL <url>/Packages` 缓存到 `MeowOS/opt/nyai/cache/`;无网正确提示、不硬失败 |
| `nyai install <包或文件.mpk>` | ① 参数是 `.mpk` 路径→直接装;② 是包名→按缓存索引取 `.mpk`(本地文件或 http 拉取)装;自动解析 Depends(递归)、校验 SHA256+app.json、同名先备份 `.bak_<pid>`、数据 init(建 MeowData/<pkg>/{data,cache}) |
| `nyai remove <pkg> [--purge]` | 卸载 MeowAPPs/<pkg>(数据默认保留);`--purge` 连 MeowData/<pkg> 删;清理 `.bak_` 残留 |
| `nyai list` | 列出 MeowAPPs/* 已装应用(名称/版本/占用),跳过 fonts/logs/.bak |
| `nyai info <pkg>` | 读 app.json 显示 name/version/exec/size/routes/services/depends/description |
| `nyai search [pattern]` | 扫索引列出可装包(含版本/描述) |
| `nyai list-upgradable` | 索引版本 > 已装版本的包 |
| `nyai index <dir>` | 为 `<dir>` 生成 `Packages` 索引(读每个 .mpk 的 app.json 提取 version/description,算 SHA256/Size/Filename) |
| `nyai export <pkg> [out.mpk]` | 打包 MeowAPPs/<pkg>/(不含 data/cache)→ `<pkg>.mpk` |
| `nyai scan [dir]` / `nyai upgrade <pkg> [file.mpk]` | 列出 .mpk / 升级(保留数据) |
| `nyai --help` | 帮助;退出码 0=成功 / 非0=错误 |
**源配置** `MeowOS/opt/nyai/sources.list`(卡;无此文件则默认本地源 `MeowOS/opt/repo`):
```
# 每行一个源,支持多源与 # 注释
local /usr/data/mnt/sd_0/MeowOS/opt/repo # 本地目录
http http://10.0.0.1/nyai # http(s) 源(url 下挂 Packages)
deb [trusted=yes] /usr/data/mnt/sd_0/MeowOS/opt/repo # apt 风格别名
file:///usr/data/mnt/sd_0/MeowOS/opt/repo # file:// 等价本地
```
**索引 `Packages`**(`nyai index <dir>` 生成,行式、空行分隔记录):
```
Package: simtest # 包名 = .mpk 文件名茎 = app.json.package
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 只建目录不建链接。