WIP checkpoint (M2.53→M2.54 工作区): 字体归MeowOS/fonts+SDK跟随+多字体(进行中)+boot板载+状态栏28+亮度/音量修复+文档刷新

This commit is contained in:
MeowOS Dev committed 2026-08-30 03:27:24 +08:00
1 parent 245bee2946
commit 064635b3c4
44 files changed
+2834 -308

No files matched your search

+1
View File
@@ -64,3 +64,4 @@ ssh_keys/
# 本地开发文档(Analysis/Notes/Test/device_test/docs-plan,仅本地保留,不入库) # 本地开发文档(Analysis/Notes/Test/device_test/docs-plan,仅本地保留,不入库)
devdocs/ devdocs/
scripts/build/ scripts/build/
fonts_lib/
+23 -17
View File
@@ -42,19 +42,21 @@
``` ```
/usr/data/mnt/sd_0/ # TF 卡挂载点 /usr/data/mnt/sd_0/ # TF 卡挂载点
└── MeowAPPs/ └── MeowAPPs/
├── <app_id>/ ├── <package>/ # 目录名 = app.json 的 package(权威 app_id)
│ ├── app.json # 应用描述(必填) │ ├── app.json # 应用描述(必填;package 字段为权威 app_id)
│ ├── <binary> # 静态 MIPS32r2 ELF(exec 指向) │ ├── <binary> # 静态 MIPS32r2 ELF(exec 指向)
│ └── icon.png # 预留:列表图标(v2 未用) │ └── icon.png # 预留:列表图标(v2 未用)
└── ... └── ...
``` ```
`<package>` 即 `app.json` 的 `package` 字段(反域名权威标识,如 `os.meow.hello`);**目录名必须等于 package**,launcher 以 package 为 app_id,缺失时回退目录名。
### 2.1 app.json 字段 ### 2.1 app.json 字段
```json ```json
{ {
"name": "Hello App", "name": "Hello App",
"exec": "/usr/data/mnt/sd_0/MeowAPPs/hello/hello", "exec": "/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello",
"package": "os.meow.hello",
"version": "0.1", "version": "0.1",
"author": "whoever" "author": "whoever"
} }
@@ -64,11 +66,12 @@
|---|---|---| |---|---|---|
| `name` | ✓ | 列表显示名。**v2 仅支持 ASCII**(UTF-8 中文留待 v3,需完整 UTF-8→UTF-16 转换) | | `name` | ✓ | 列表显示名。**v2 仅支持 ASCII**(UTF-8 中文留待 v3,需完整 UTF-8→UTF-16 转换) |
| `exec` | ✓ | 应用可执行文件绝对路径(必须是**静态** MIPS32r2 ELF,soft/hard-float 均可) | | `exec` | ✓ | 应用可执行文件绝对路径(必须是**静态** MIPS32r2 ELF,soft/hard-float 均可) |
| `package` | — | 反域名权威标识(如 `os.meow.hello`),launcher 取之为 app_id;**目录名=package**。缺省回退目录名 `de->d_name`;两者存在且不同 → WARN |
| `version` / `author` | — | 保留字段,v2 不解析 | | `version` / `author` | — | 保留字段,v2 不解析 |
- 目录扫描深度 1 级:只枚举 `MeowAPPs/` 下的直接子目录 - 目录扫描深度 1 级:只枚举 `MeowAPPs/` 下的直接子目录
- 应用计数上限 **8**(超出忽略;单屏 8 行) - 应用计数上限 **8**(超出忽略;单屏 8 行)
- app.json 解析器极简(launcher 内实现):仅匹配 `"name":` 与 `"exec":` 后的引号字符串,不做 JSON 校验(多行/单行均可) - app.json 解析器极简(launcher 内实现):仅匹配 `"name":`、`"exec":`、`"package":` 后的引号字符串(及扩展 icon/framework/immersive/route./`awake.<id>`(keep_awake)/service.),不做 JSON 校验(多行/单行均可)[源:审计 2026-08-30]
- 列表刷新时机:每次菜单打开时重新扫描(TF 卡热插拔后立即可见) - 列表刷新时机:每次菜单打开时重新扫描(TF 卡热插拔后立即可见)
### 2.2 边界情况行为(launcher 实现,全部真机验证) ### 2.2 边界情况行为(launcher 实现,全部真机验证)
@@ -92,7 +95,7 @@
### 3.1 启动流程(meow_launcherd 侧) ### 3.1 启动流程(meow_launcherd 侧)
``` ```
热键(按住电源 + 单击音量减;电源按下后 300ms~2.5s 内收到音量减按下边沿触发) 热键(按住电源 + 单击音量减;电源按下后 60ms~2.5s 内收到音量减按下边沿触发;v6.7 起窗口下限由 300ms 改为 60ms,窗口 60–2500ms)
→ meow_launcherd 检测(EVIOCGKEY 状态轮询,10ms;电源按下即 arm,窗口外或松开即 disarm) → meow_launcherd 检测(EVIOCGKEY 状态轮询,10ms;电源按下即 arm,窗口外或松开即 disarm)
→ SIGSTOP hiby_player(/proc/N/exe == /usr/bin/hiby_player;等待状态 T) → SIGSTOP hiby_player(/proc/N/exe == /usr/bin/hiby_player;等待状态 T)
→ 背光仅在面板灭时拉回(brightness==0 才写 saved/60,绝不无脑拉亮度)+ unblank 面板(先 SIGSTOP 再 unblank)→ fb0 直绘应用列表 → 背光仅在面板灭时拉回(brightness==0 才写 saved/60,绝不无脑拉亮度)+ unblank 面板(先 SIGSTOP 再 unblank)→ fb0 直绘应用列表
@@ -188,7 +191,7 @@
- **⚠️ 应用启动后须防 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)。 - **⚠️ 应用启动后须防 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` 作为「返回事件」 - 沉浸应用写 `/tmp/meowapp_state`,处理 `SIGUSR1` 作为「返回事件」
- 应用异常卡死 → 虚拟返回键(若可见)→ SIGKILL;沉浸态卡死 → 音量减连按 3 次(强杀序列) - 应用异常卡死 → 虚拟返回键(若可见)→ SIGKILL;沉浸态卡死 → 音量减连按 3 次(强杀序列)
- **应用日志**:launcher 通过环境变量 `MEOW_LOG_DIR` 告知 TF 卡日志目录(无卡时不设),应用自行决定文件名(示例 hello 写 `hello.log`);启动时先 drain 输入队列、只响应"按下边沿"退出(释放事件/轨迹事件忽略) - **应用日志**:launcher 通过环境变量 `MEOW_LOG_DIR` 告知 TF 卡日志目录(无卡时不设),应用自行决定文件名(示例 hello 写 `apps/<app_id>/logs/<ts>_<pid>.log`;[源:审计 2026-08-30]);启动时先 drain 输入队列、只响应"按下边沿"退出(释放事件/轨迹事件忽略)
### 3.6 应用运行期卡移除(M2.11 最佳努力) ### 3.6 应用运行期卡移除(M2.11 最佳努力)
@@ -232,11 +235,11 @@
| 下一曲 | event2 | 163 | (未用) | | 下一曲 | event2 | 163 | (未用) |
| 上一曲 | event0 | 165 | (弃用——GPIO PC28 硬件链断,§18.12) | | 上一曲 | 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`(历史备选,不推荐)。 **热键判定(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` 配置: 热键 `meow.conf` 配置:
``` ```
hotkey=power_voldown # 默认:电源按住 + 单击音量减(300-2500ms 窗口) hotkey=power_voldown # 默认:电源按住 + 单击音量减(60-2500ms 窗口;v6.7 起下限 300ms→60ms)
hotkey=seq # 历史序列热键(单键连按 N 次,不推荐) hotkey=seq # 历史序列热键(单键连按 N 次,不推荐)
``` ```
(`hotkey_seq=`/`hotkey_seq_gap=` 仅在 `hotkey=seq` 时生效。) (`hotkey_seq=`/`hotkey_seq_gap=` 仅在 `hotkey=seq` 时生效。)
@@ -269,12 +272,12 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
### 4.5 统一日志系统(MeowAPPs 组件共用) ### 4.5 统一日志系统(MeowAPPs 组件共用)
- **默认位置**:TF 卡 `/usr/data/mnt/sd_0/MeowAPPs/logs/meow_<boot时间戳>.log`(如 `meow_20260828_020123.log`;boot 时间戳 = 当前墙钟 − /proc/uptime,同一 boot 会话内多次启动共用同一文件) - **默认位置**: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 文件 - **无 TF 卡 / 卡晚挂载(S90 早于卡挂载)**:**开不了卡日志**——日志落 **/tmp 启动缓冲**(`/tmp/meow_boot.log`,上限 256KB 滚动),**绝不写板载存储**(/usr/data 只剩 ~10MB);**卡出现后自动迁移**(主循环 ~1s 周期重试 + 输入设备重试循环内也检查;无需开菜单)——`log_poll_card()` 打开卡日志并把 /tmp 缓冲整段搬入后删除 /tmp 文件
- **容量上限**:logs/ 目录总量 1GB,开机时超限按 mtime 删最旧 - **容量上限**:logs/ 目录总量 1GB,开机时超限按 mtime 删最旧
- **组件环境变量**: - **组件环境变量**:
- `MEOW_LOG_FILE` → meow_watchdog:追加写同一 boot 日志文件(无该变量则走 stderr) - `MEOW_LOG_FILE` → meow_watchdog:追加写同一 boot 日志文件(无该变量则走 stderr)
- `MEOW_LOG_DIR` → 应用:日志目录(示例 hello 写 `<dir>/hello.log`) - `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` - PID 文件:`/usr/data/meow_launcherd.pid`;播放器发现:扫描 `/proc/N/exe` == `/usr/bin/hiby_player`
@@ -319,7 +322,7 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
> 所有设备操作遵守纪律:动播放器前数实例;重启前 `rm -f /data/recovery_all`;息屏测试后恢复背光;结束恢复设备干净。 > 所有设备操作遵守纪律:动播放器前数实例;重启前 `rm -f /data/recovery_all`;息屏测试后恢复背光;结束恢复设备干净。
0. **轮前检查**:`ls /data/recovery_all`(存在即删+告警);`ps | grep hiby_player | wc -l` 应=1。 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`。 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. **验证输入路径**(无物理按键时的替代证据): 2. **验证输入路径**(无物理按键时的替代证据):
- `meow_launcherd --scan` → 列出应用 - `meow_launcherd --scan` → 列出应用
- `meow_launcherd --dumpkeys 6000` + 物理按键 → 日志出现 `key VOL_DOWN/VOL_UP DOWN/UP` - `meow_launcherd --dumpkeys 6000` + 物理按键 → 日志出现 `key VOL_DOWN/VOL_UP DOWN/UP`
@@ -350,20 +353,22 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
### 1. 应用目录结构 ### 1. 应用目录结构
``` ```
MeowAPPs/<包名>/ MeowAPPs/<package>/ # 目录名 = app.json 的 package(权威 app_id)
├── app.json # 应用配置(名称/exec/路由/图标/框架声明等) ├── app.json # 应用配置(name/exec/package/路由/图标/框架声明等)
├── meow # 程序主体(静态 MIPS 二进制;也可多文件/脚本) ├── meow # 程序主体(静态 MIPS 二进制;也可多文件/脚本)
└── <资源文件/图标…> # 只读程序资源 └── <资源文件/图标…> # 只读程序资源
MeowData/<包名>/ MeowData/<package>/
├── data/ # 持久应用数据(配置、播放列表、登录态…) ├── data/ # 持久应用数据(配置、播放列表、登录态…)
└── cache/ # 缓存(可清) └── cache/ # 缓存(可清)
``` ```
> `<package>` = `app.json` 的 `package`(反域名,如 `os.meow.hello`),与目录名一致;launcher/app 数据/日志/`.mpk` 均以它为包名。
> 注:TF 卡为 exFAT,**不支持符号链接**,故数据目录不用链接,改由 MeowOS 通过**环境变量**注入(见 §2)。 > 注:TF 卡为 exFAT,**不支持符号链接**,故数据目录不用链接,改由 MeowOS 通过**环境变量**注入(见 §2)。
### 2. 数据目录(MeowOS 负责,用环境变量注入) ### 2. 数据目录(MeowOS 负责,用环境变量注入)
- 本文档 `<包名>` = `app.json` 的 `package`(反域名权威标识,如 `os.meow.hello`),与目录名一致。
- **程序主体 + 必要配置文件** 放 `MeowAPPs/<包名>/`(可替换/可升级)。 - **程序主体 + 必要配置文件** 放 `MeowAPPs/<包名>/`(可替换/可升级)。
- **软件数据** 放 `MeowData/<包名>/{data,cache}`(独立于程序,重装/升级保留数据)。 - **软件数据** 放 `MeowData/<包名>/{data,cache}`(独立于程序,重装/升级保留数据)。
- **MeowOS 每次启动应用前**:验证 `MeowData/<包名>/{data,cache}` 存在(否则建)。 - **MeowOS 每次启动应用前**:验证 `MeowData/<包名>/{data,cache}` 存在(否则建)。
@@ -376,6 +381,7 @@ MeowData/<包名>/
### 3. 软件包 `.mpk` ### 3. 软件包 `.mpk`
- **包名 = 文件名茎 = `app.json` 的 `package`**(如 `os.meow.hello` → `os.meow.hello.mpk`)。
- **格式**:`.mpk` = tar.gz(含 `app.json` + `meow` 二进制 + 资源),**不含 data/cache**(运行时按设备生成)。 - **格式**:`.mpk` = tar.gz(含 `app.json` + `meow` 二进制 + 资源),**不含 data/cache**(运行时按设备生成)。
- **导出**:打包 `MeowAPPs/<包名>/`(**不含 data/cache 目录**)→ `<包名>.mpk`。 - **导出**:打包 `MeowAPPs/<包名>/`(**不含 data/cache 目录**)→ `<包名>.mpk`。
- **导入/安装**(MeowOS 软件包安装程序 `meow_pkg_install()`): - **导入/安装**(MeowOS 软件包安装程序 `meow_pkg_install()`):
@@ -392,8 +398,8 @@ MeowData/<包名>/
### 5. 软件包管理器 CLI `nyai`(静态 MIPS,apt/yum 风格,含镜像源) ### 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 外围工具目录)。 `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.11,无 `/etc/profile.d/meow_card_path.sh`)SSH 需绝对路径 **调用**: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`; `/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` 可直接调用。 M2.12+ 固件该目录已由 `meow_card_path.sh` 前插,`nyai` 可直接调用。
@@ -426,7 +432,7 @@ file:///usr/data/mnt/sd_0/MeowOS/opt/repo # file:// 等价本
**索引 `Packages`**(`nyai index <dir>` 生成,行式、空行分隔记录): **索引 `Packages`**(`nyai index <dir>` 生成,行式、空行分隔记录):
``` ```
Package: simtest # 包名 = .mpk 文件名茎 Package: simtest # 包名 = .mpk 文件名茎 = app.json.package
Version: 1.2.3 # 来自该 .mpk 内 app.json Version: 1.2.3 # 来自该 .mpk 内 app.json
Filename: simtest.mpk Filename: simtest.mpk
Size: 388 # 字节 Size: 388 # 字节
+2 -2
View File
@@ -131,7 +131,7 @@
- **返回语义**:默认回应用列表;接阶段 D 后按导航栈回上一层 - **返回语义**:默认回应用列表;接阶段 D 后按导航栈回上一层
- **虚拟返回键**:启动器看门狗在左上角绘制 48×48;**沉浸模式**(应用声明)隐藏之,改左缘滑动返回(SIGUSR1);物理强杀组合保留 - **虚拟返回键**:启动器看门狗在左上角绘制 48×48;**沉浸模式**(应用声明)隐藏之,改左缘滑动返回(SIGUSR1);物理强杀组合保留
- **亮度安全**:任何亮度写入 clamp ≥25;不干扰播放器自动息屏 - **亮度安全**:任何亮度写入 clamp ≥25;不干扰播放器自动息屏
- **日志**:TF 卡 `MeowAPPs/logs/`,开机新文件,1GB 滚动;无卡不写板载 - **日志**:TF 卡 `MeowOS/logs/`(系统日志 `meow_<bootts>.log` + 应用日志 `apps/<app_id>/logs/<ts>_<pid>.log`),开机新文件,1GB 滚动;无卡不写板载([源:审计 2026-08-30])
- **应用目录**:TF 卡 `MeowApps/<app>/{app.json, 二进制}`;app.json 含名称/图标/执行路径/路由表(阶段 D) - **应用目录**:TF 卡 `MeowApps/<app>/{app.json, 二进制}`;app.json 含名称/图标/执行路径/路由表(阶段 D)
--- ---
@@ -141,7 +141,7 @@
- 迭代回路:改代码 → 交叉编译(zig + musl.cc)→ SSH 推 `/data` 或 bind-mount 部署 → 真机验证(注入 + 用户物理确认)→ repack 固件刷入 - 迭代回路:改代码 → 交叉编译(zig + musl.cc)→ SSH 推 `/data` 或 bind-mount 部署 → 真机验证(注入 + 用户物理确认)→ repack 固件刷入
- 注入仅能测逻辑路径(触摸注入不可投递)→ **物理项一律需用户确认** - 注入仅能测逻辑路径(触摸注入不可投递)→ **物理项一律需用户确认**
- 每次部署前后核对:播放器实例数、背光、bind 列表、recovery_all 标志 - 每次部署前后核对:播放器实例数、背光、bind 列表、recovery_all 标志
- 崩溃/异常优先看 `MeowAPPs/logs/` 与 `/proc/interrupts`(区分软件/硬件) - 崩溃/异常优先看 `MeowOS/logs/` 与 `/proc/interrupts`(区分软件/硬件)[源:审计 2026-08-30]
- 刷机走官方 SD 卡升级通道;变砖用官方固件刷回 + TF 卡全量备份兜底 - 刷机走官方 SD 卡升级通道;变砖用官方固件刷回 + TF 卡全量备份兜底
--- ---
+1 -1
View File
@@ -111,5 +111,5 @@
| `patch_v2/meow_watchdog/meow_watchdog.c` | 删除 `paint_button()` 返回键绘制/按钮命中;保留兜底 | | `patch_v2/meow_watchdog/meow_watchdog.c` | 删除 `paint_button()` 返回键绘制/按钮命中;保留兜底 |
| `patch_v2/meow_launcherd/meow_launcherd.c` | 解析 app.json `immersive` → 写 `/tmp/meowapp_state` | | `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/hello_app/hello.c` | 删 `on_key(KEY_POWER)→exit`;声明 immersive;处理 `on_swipe_progress` 画条 |
| `patch_v2/apps_assets/apps/hello/app.json` | 加 `"immersive":1` | | `patch_v2/apps_assets/apps/os.meow.hello/app.json` | 加 `"immersive":1` |
| `APPS.md` / `MEOW_APP_SDK.md` | 已写 §3.4.1 规范;SDK 文档补 `on_swipe_progress`/`meow_ui_swipe_hint` | | `APPS.md` / `MEOW_APP_SDK.md` | 已写 §3.4.1 规范;SDK 文档补 `on_swipe_progress`/`meow_ui_swipe_hint` |
+2 -2
View File
@@ -64,7 +64,7 @@ int meow_settings_set_str (const char *key, const char *v);/* 长度截断 MEOW
int meow_settings_set_enum(const char *key, int v); /* 校验枚举合法 */ int meow_settings_set_enum(const char *key, int v); /* 校验枚举合法 */
int meow_settings_set_bool(const char *key, bool v); int meow_settings_set_bool(const char *key, bool v);
``` ```
- **所有 clamp/校验集中在 set**(亮度 clamp ≥ `display.brightness_min` 默认 25、枚举越界拒绝、str 截断)。 - **所有 clamp/校验集中在 set**(亮度 clamp ≥ `display.brightness_min` 默认 **1**(M2.39 由 25 改 1,旧持久 25 自动迁移;[源:审计 2026-08-30])、枚举越界拒绝、str 截断)。
- set 成功后**自动 `settings_save()` 落盘**(板载 /usr/data/meow/settings.conf)并**通知 listener**。 - set 成功后**自动 `settings_save()` 落盘**(板载 /usr/data/meow/settings.conf)并**通知 listener**。
### 2.3 枚举辅助(供 UI/web 显示中文标签) ### 2.3 枚举辅助(供 UI/web 显示中文标签)
@@ -122,7 +122,7 @@ int meow_settings_register(const meow_setting_def *def); /* 注册一个设置
``` ```
# meow-settings 1 # meow-settings 1
display.brightness = 101 display.brightness = 101
display.brightness_min = 25 display.brightness_min = 1
display.screen_off_mode = panel display.screen_off_mode = panel
boot.default = meow boot.default = meow
input.hotkey_seq = volup,volup,volup input.hotkey_seq = volup,volup,volup
+14 -6
View File
@@ -84,14 +84,14 @@ void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alp
### 3.1 全局状态栏(系统级组件,A+C 方案并入) ### 3.1 全局状态栏(系统级组件,A+C 方案并入)
**定位**:状态栏是**系统级全局组件**,由 SDK 统一绘制——所有界面(launcher 菜单 + MeowAPP 画面)默认都显示,**仅当 APP 声明全屏(fullscreen)才隐藏**。数据由 SDK 内置状态读取器抓系统快照(与 launcher 同源单点一致),非各画各的。 **定位**:状态栏是**系统级全局组件**,由 SDK 统一绘制——所有界面(launcher 菜单 + MeowAPP 画面)默认都显示,**仅由 SDK 运行时 `meow_ui_set_fullscreen(1)` 控制隐藏**(`fullscreen` 字段**未在 app.json 解析**,已定未做,见 §3.1 注 [源:审计 2026-08-30])。数据由 SDK 内置状态读取器抓系统快照(与 launcher 同源单点一致),非各画各的。
```c ```c
/* 状态栏显隐(默认显示;APP 声明全屏则隐藏——沉浸) */ /* 状态栏显隐(默认显示;APP 声明全屏则隐藏——沉浸) */
void meow_ui_set_statusbar(int visible); /* 1=显示(默认) 0=隐藏(全屏) */ void meow_ui_set_statusbar(int visible); /* 1=显示(默认) 0=隐藏(全屏) */
int meow_ui_statusbar_visible(void); int meow_ui_statusbar_visible(void);
/* 全屏声明:等价 app.json `fullscreen:1`(外观上隐藏状态栏 + 隐藏返回键)。 */ /* 全屏:由 SDK 运行时 meow_ui_set_fullscreen(1) 控制(app.json `fullscreen` 未解析,已定未做;外观=隐藏状态栏+隐藏返回键)。[源:审计 2026-08-30] */
void meow_ui_set_fullscreen(int on); void meow_ui_set_fullscreen(int on);
/* 状态栏高度(px,供 APP 布局上移内容,避免被盖——status bar inset) */ /* 状态栏高度(px,供 APP 布局上移内容,避免被盖——status bar inset) */
@@ -99,10 +99,10 @@ int meow_ui_statusbar_height(void);
``` ```
**绘制规则**: **绘制规则**:
- `meow_ui_flush()` 上屏前,SDK 自动在顶部画状态栏(slim ~20-40px),内容:时钟 / 电量 / WiFi / 蓝牙 / 音量(与 launcher 一致)。 - `meow_ui_flush()` 上屏前,SDK 自动在顶部画状态栏(**28px**,`meow_ui_statusbar_height()` 即此值;用户定 40→28,栏内字号 20),内容:时钟 / 电量 / WiFi / 蓝牙 / 音量(与 launcher 一致)。
- 状态栏数据由 SDK 内置读取器周期刷新(~1s 轻刷,读 `/sys/class/power_supply/*`、WiFi 状态、时钟、音量寄存器——与 launcher 同一批 source)。 - 状态栏数据由 SDK 内置读取器周期刷新(~1s 轻刷,读 `/sys/class/power_supply/*`、WiFi 状态、时钟、音量寄存器——与 launcher 同一批 source)。
- **z 顺序**:状态栏是最顶层。APP 内容需 `meow_ui_statusbar_height()` 下移(inset),否则被状态栏盖住(有状态栏 OS 的通用做法)。 - **z 顺序**:状态栏是最顶层。APP 内容需 `meow_ui_statusbar_height()` 下移(inset),否则被状态栏盖住(有状态栏 OS 的通用做法)。
- **全屏(fullscreen)**:`meow_ui_set_fullscreen(1)` 或 app.json `fullscreen:1` → 隐藏状态栏 + 隐藏返回键(沉浸);退出全屏恢复。 - **全屏**:运行时 `meow_ui_set_fullscreen(1)` 控制 → 隐藏状态栏 + 隐藏返回键(沉浸);退出恢复。**注**:app.json `fullscreen` 字段未在解析器读取(已定未做 [源:审计 2026-08-30]),故不能经 app.json 声明全屏。
**与 launcher 统一(A+C)**:launcher 迁移到 SDK 后,其状态栏(现私有 `draw_statusbar()`)改走 SDK 这一套,全局统一、单点一致。 **与 launcher 统一(A+C)**:launcher 迁移到 SDK 后,其状态栏(现私有 `draw_statusbar()`)改走 SDK 这一套,全局统一、单点一致。
@@ -307,17 +307,25 @@ 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`。 构建出 `patch_v2/sdk/meow_ui.a`;应用编译链(zig-mipsel-cc)额外链 `meow_ui.a meow_font.a libz.a -lpthread`。
> **构建耦合(已知设计,非缺陷)**:`build_sdk.sh` 用 `-I../meow_launcherd` 引 `meow_ipc.h`(`meow_notify_ipc.c` 复用共享协议头,同一份、杜绝两端漂移),故 SDK 编译期依赖 launcher 头、**非独立库**;**运行时无碍** —— App 只链 `meow_ui.a`(含 meow_font.o),不链入 launcher 符号,故 app 侧无 launcher 依赖。
**app.json 扩展字段**(向后兼容,缺省即旧行为): **app.json 扩展字段**(向后兼容,缺省即旧行为):
```json ```json
{ {
"name": "Hello App", "name": "Hello App",
"exec": "...", "exec": "/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello",
"package": "os.meow.hello", // 反域名权威 app_id;目录名=package,launcher 取之为 app_id
"version": "0.1", "version": "0.1",
"framework": 1, // 1=走 meow_ui(框架承载);缺省/0=裸自绘(旧) "framework": 1, // 1=走 meow_ui(框架承载);缺省/0=裸自绘(旧)
"immersive": 0, // 1=沉浸模式(隐藏虚拟返回键,左缘滑动返回) "immersive": 0, // 1=沉浸模式(隐藏虚拟返回键,左缘滑动返回)
"icon": "apps/hello/icon.png" // 可选:列表图标(供图片层) "icon": "apps/os.meow.hello/icon.png" // 可选:列表图标(供图片层)
} }
``` ```
上例 `<pkg>`=`os.meow.hello`(= `apps/<pkg>/` 目录名 = `package` 字段值);exec 即 `<挂载点>/MeowAPPs/<pkg>/<binary>`。
> **app 字体来源(M2.x 已落地)**:app 若**自带字库**(`load_handle`,见 §5.7.3,**未实现**)则用它;否则 SDK `meow_ui_gfx.c load_font()` **优先读 launcher 注入的环境变量** `MEOW_FONT_PNG`/`MEOW_FONT_IDX`(=系统 `font.file` 当前生效字体,绝对路径)/`MEOW_FONT_APP`(family),无 env 则回退卡默认 `MeowOS/fonts/default/font.png` → 固件。因此**app 未自带字库时跟随系统 `font.file`**,不再硬编码 `MeowAPPs/fonts`。
>
> **app 多字体(M2.5x,`display.multi_font`)**:launcher 注入 `MEOW_MULTI_FONT=0/1` 与 6 类 `MEOW_FONT_CLS_<类>_PNG/IDX`(命中则覆盖全局;类值为空=跟随全局不注入)。SDK `meow_ui_gfx` 的 `meow_gfx_load_multifont()` 读 env → 调 `meow_font_config_multifont()`(同 png 去重、≤4 套上限、缺类回退全局)构建**逐字符分类路由**:`meow_font_drawtext/width_text/line_height` 按 `meow_font_class_of(cp)`(`MEOW_FCLS_CJK/_CJK_SYM/_NUM/_LATIN/_LATIN_SYM/_OTHER`)用对应字库取字形与度量;`meow_font_has_glyph(cp)` 可预判缺字。`display.multi_font=0`/无 env → 单字体(现状,零额外开销)。底层接口见 meow_font.h:`meow_font_load_slot`/`meow_font_unload_slot`/`meow_font_enable_multifont`/`meow_font_set_class_slot`/`meow_font_config_multifont`;`MEOW_MAX_SLOTS=5`。
--- ---
+24 -15
View File
@@ -9,40 +9,49 @@
| 文档 | 内容 | | 文档 | 内容 |
|---|---| |---|---|
| `docs/MEOWOS_SPEC_REF.md` | 权威速查总索引(首选入口;常量/路径照抄原文 + 出处 + 矛盾清单) | | `docs/MEOWOS_SPEC_REF.md` | 权威速查总索引(首选入口;常量/路径照抄原文 + 出处 + 矛盾清单) |
| `ANALYSIS.md` | 主技术档案:固件逆向、启动链、fb 双缓冲、HGL、一切踩坑与纪律(§ 章节持续累积) | | `devdocs/ANALYSIS.md` | 主技术档案(**本地开发档,gitignore 不入库**):固件逆向、启动链、fb 双缓冲、HGL、一切踩坑与纪律(§ 章节持续累积) |
| `DEVELOPMENT_PLAN.md` | 路线图与分阶段计划(阶段 0 双系统 / A 稳定化 / B 状态栏通知中心 / B2 系统设置 / C MeowPanel / D Activity 框架 / E 网易云) | | `DEVELOPMENT_PLAN.md` | 路线图与分阶段计划(**自身已过时,以实际进度为准**;阶段 0 双系统 / A 稳定化 / B 状态栏通知中心 / B2 系统设置 / C MeowPanel / D Activity 框架 / E 网易云) |
| `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(IPC、路由表、导航栈、后台权限、媒体会话) | | `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(IPC、路由表、导航栈、后台权限、媒体会话) |
| `SYSTEM_SETTINGS.md` | 系统设置子系统设计(统一存储 + C API + 实体 UI + web 复用 + 导入导出) | | `SYSTEM_SETTINGS.md` | 系统设置子系统设计(统一存储 + C API + 实体 UI + web 复用 + 导入导出) |
| `MEOWOS_API.md` | 统一 API 蓝本(命名规范、返回码、设置 API、HTTP 映射、扩展指南) | | `MEOWOS_API.md` | 统一 API 蓝本(命名规范、返回码、设置 API、HTTP 映射、扩展指南) |
| `APPS.md` | MeowAPPs 应用规范(目录、app.json、生命周期、边界、日志、返回键) | | `APPS.md` | MeowAPPs 应用规范(目录、app.json、生命周期、边界、日志、返回键) |
| `GESTURE_BACK_DESIGN.md` / `KEY_CONTROL_DESIGN.md` / `PERF_SCHEDULER.md` | 左缘返回 / 按键契约与信号 / 帧率调度 | | `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`。 > 一次性/阶段性设计文档(已过时或未实施)移至 `devdocs/plan/` 归档(移动不删、保留历史;`devdocs/` 为本地开发档,不入库):`BOOT_SELECTOR_DESIGN`、`CHARGING_SCREEN`、`MAIN_MD3_PLAN`、`MD3_UI_SPEC`、`MEOW_TOUCH_DESIGN`、`MIGRATION_AC`、`BLUETOOTH_MANAGER_PLAN`、`APP_LOG_MANAGER`。
**文档收敛规则**:不再为每个功能新增设计 md;新功能在对应权威文档(`MEOW_APP_SDK.md`/`APPS.md`/`SYSTEM_SETTINGS.md` 等)补小节;新增决策/检查点写 `DEV_NOTES.md`;过时一次性设计进 `docs/plan/`。 **文档收敛规则**:不再为每个功能新增设计 md;新功能在对应权威文档(`MEOW_APP_SDK.md`/`APPS.md`/`SYSTEM_SETTINGS.md` 等)补小节;新增决策/检查点写 `devdocs/DEV_NOTES.md`;过时一次性设计进 `devdocs/plan/`。
## 当前架构与组件(2026-08 快照) ## 当前架构与组件(2026-08-30 快照)
- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`):类 systemd/systemctl 的独立守护进程,由 supervisor(meow_launcherd/bootmenu)`fork/exec` 托管。读 TF 卡 `MeowOS/services.d/<name>.conf`(目录式 `key=value`:`exec/args/env/enabled/restart/backoff_ms/max_restart/trigger`,未知键忽略、前向兼容);每个服务是 svcd 的**独立子进程**(崩溃隔离:服务挂不影响 svcd/supervisor);事件触发(`boot`/`card-mount`/`wifi-up`,1s 轮询);`-f` 前台运行便于 supervisor `waitpid`。mipsel 静态 ~99KB,进固件不入卡(拔卡 svcd 不消失)。 - **SDK 统一日志 + 通知(M2.52/53 已落地)**:`meow_log()` 按 app 分目录、每次启动新文件 `MeowOS/logs/apps/<app_id>/logs/<ts>_<pid>.log`,单文件 2MB 轮转、每 App 总量 50MB 删最旧;通知 API `meow_notify_*`(4 类型×每类型 4、媒体单例);**#5 SDK↔IPC 桥**(`meow_notify_ipc.c/.h`:app→launcher 中央存储,send/cancel 接入、回填 launcher `nid`,无 socket 降级不崩)。通知中心 UI / D-Bus 桥 / TF 转储为后续任务。
- **MeowOpt/(TF 卡)**:板载外部工具/二进制(如 `MeowOpt/mdnsd/mdnsd`),由 MeowOS 启动时从卡加载,拔卡降级不崩。 - **TF 卡多字体库**:`MeowOS/fonts/<family>/`(`font.png`+`font.idx`+`font.json` 三件套),默认 `MeowOS/fonts/default`;`font.file` 设置键选默认套;SDK `load_font` **优先读 env `MEOW_FONT_PNG/IDX`**(launcher 每次 `launch_app` 注入),app 未自带字库时**跟随系统 `font.file`**;≤16 套卡内(防爆);§5.7 字体管理 + 应用字库(多句柄,**草案未实现**)。已部署 4 套(思源宋体 / MiSans / GoogleSans / Meslo,位深 4)。
- **mDNS 方案A(已拍板)**:用**完整 `mdnsd`**(994KB,mipsel 静态)放 TF `MeowOpt/mdnsd/mdnsd`,由 meow_svcd 托管;SAResponder(885KB)因"单服务注册型"无法按名访问设备(`ingenic.local`)被排除。⚠️ 手机热点组播常在客户端间丢失 → mDNS 有风险,主机 DNS 兜底。 - **包名/目录安卓化**:`app.json` `package`(反域名**权威 app_id**,==目录名,缺省回退目录名)→ 示例 `os.meow.hello`;目录 `MeowAPPs/os.meow.hello`;数据 `MeowData/<pkg>/{data,cache}`。
- **SDK 已落地**:统一日志 `meow_log()`(按 app 分目录、每次启动新文件、单文件 2MB 轮转、每 App 总量 50MB 删最旧,M2.52)+ 通知 API `meow_notify_*`(4 类型×每类型 4、媒体单例,M2.53)。 - **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`,mipsel 静态 ~99KB,进固件不入卡):类 systemd/systemctl,由 supervisor(bootmenu/launcher)`fork/exec` 托管;读 TF `MeowOS/services.d/<name>.conf`(目录式 `key=value`,未知键忽略/前向兼容);服务=svcd **独立子进程**(崩溃隔离:服务挂不影响 svcd/supervisor);事件触发 `boot`/`card-mount`/`wifi-up`(1s 轮询);`-f` 前台便于 supervisor `waitpid`;状态机 DISABLED/STOPPED/RUNNING/STOPPING/FAILED + CLI。**接入 supervisor 启动链未做**。
- **mDNS 方案A(已拍板)**:完整 **`mdnsd`**(994KB,mipsel 静态)放 TF `MeowOS/opt/mdnsd/mdnsd`,由 meow_svcd 托管(需 root/setcap `cap_net_bind_service,cap_net_raw` 绑 UDP5353+组播 224.0.0.251);SAResponder(885KB)因"单服务注册型"无法按名访问设备(`ingenic.local`)被排除。⚠️ 手机热点组播常在客户端间丢失 → mDNS 有风险,主机 DNS 兜底。**`MeowOpt` 已废弃并入 `MeowOS/opt`**(二进制归 MeowOS/opt,拔卡降级不崩)。
- **boot 默认→板载**:**`/data/meow_boot_default`**(O_TRUNC 单值、不递增、权威);bootmenu 读它定默认;**meow.conf 不再承担 boot_default**(其 O_APPEND 累积 bug 已修,设备已清)。
- **状态栏 40→28px**:SDK `MUI_SBAR_H=28`(字号 20)+ launcher `SB_BAR_H=28`/`SB_FONT=20`;状态栏两条实现(SDK `meow_ui_sys_statusbar_render` / launcher `draw_statusbar`)**待 MD3 收敛为单一 SDK 全局状态栏**(launcher 退役 draw_statusbar 复用 SDK)。
- **键码/阈值修正**:`MEOW_KEY_PREVIOUS=165`(19 是另一码,[A-6] 定案);左缘返回阈值 **65px**([A-4] 90vs65 定案 65);亮度下限 1([A-3]);热键窗口下限 60ms([A-1]);日志路径统一 `MeowOS/logs`([A-7])。
- **TF 卡资产**:包管理 CLI `nyai`(部署 `MeowOS/opt/bin/nyai`,同 launcher 包语义);应用日志 `MeowOS/logs/apps/<id>/logs/<ts>_<pid>.log`;服务定义 `MeowOS/services.d/<name>.conf`。
- **mips-repo/**(宿主侧 mipsel 软件仓库):交叉编译产物 + `build.sh` + `manifest.md`,供后续固件/TF 复用(见「目录速览」)。 - **mips-repo/**(宿主侧 mipsel 软件仓库):交叉编译产物 + `build.sh` + `manifest.md`,供后续固件/TF 复用(见「目录速览」)。
- **仓库公开净化**:本地 main 单提交,HEAD 敏感 0 命中;`devdocs/`(本地开发文档 + WORK_STATE/DEV_NOTES)、`scripts/build/`(固件打包脚本)**gitignore 不入库**;`mips-repo/`(mipsel 软件仓库)入库。
## 目录速览 ## 目录速览
- `patch_v2/` — meow_launcherd(MeowOS 核心)、bootmenu(引导选单)、watchdog、meow_svcd(服务管理器)、sdk(meow_ui SDK 源码/产物);源码与构建脚本 - `patch_v2/` — meow_launcherd(MeowOS 核心)、bootmenu(引导选单)、watchdog、meow_svcd(服务管理器)、sdk(meow_ui SDK)、meowpkg(包管理);源码与构建脚本
- `mips-repo/` — mipsel 交叉编译软件仓库(mdnsd 等:`build.sh`+`manifest.md`+`bin/`),供后续固件/TF 复用 - `mips-repo/` — mipsel 交叉编译软件仓库(mdnsd 等:`README.md`+`bin/`),供后续固件/TF 复用
- `font/` — 预渲染字形系统(gen 生成器、meow_font 渲染库、atlas 产物) - `font/` — 预渲染字形系统(gen 生成器、meow_font 渲染库、atlas 产物)
- `fw_apps/` — 固件组装产物(`.upt` 不入库,由 build_fw_apps*.sh 重新打包) - `fw_apps/` — 固件组装产物(`.upt` 不入库,由 `scripts/build/build_fw_apps*.sh` 重新打包)
- `bootlogos/` — 用户定制启动图/关机 logo - `bootlogos/` — 用户定制启动图/关机 logo
- `device_test/` — 真机验证截图/证据(`.raw` 不入库) - `scripts/build/` — 固件打包脚本 `build_fw_apps_m*.sh`(**本地 gitignore 不入库**)
- `devdocs/` — 本地开发文档(ANALYSIS / WORK_STATE / DEV_NOTES / OPEN_QUESTIONS / FLASH_SSH / TEST_ON_DEVICE / device_test / plan 归档;**gitignore 不入库**)
- `ssh_keys/`, `.tools/` — SSH 密钥、交叉工具链(不入库,按需) - `ssh_keys/`, `.tools/` — SSH 密钥、交叉工具链(不入库,按需)
## 构建/刷机 ## 构建/刷机
- 固件打包:`build_fw_apps*.sh`(需交叉工具链 `zig-mipsel-cc` + musl.cc,见 ANALYSIS 第 10 节) - 固件打包:`scripts/build/build_fw_apps_m253.sh`(**现 M2.53 / 0.7.48**;需交叉工具链 `zig-mipsel-cc` + musl.cc,见 `devdocs/ANALYSIS.md` 第 10 节)
- 升级锁根:SD 卡根 `r3proii.upt`(固件由 `build_fw_apps*.sh` 打包成 `.upt` 再刷)
- 刷机:failsafe(关机后按住 音量加+电源)从 SD 卡根 `r3proii.upt` 升级;勿用播放器 UI 卡死的升级按钮 - 刷机:failsafe(关机后按住 音量加+电源)从 SD 卡根 `r3proii.upt` 升级;勿用播放器 UI 卡死的升级按钮
- 组件归属:**launcher / bootmenu / watchdog 为固件组件**(`/usr/bin`,只读 squashfs);`/usr/data` 下的同名文件仅 dev 手动 exec 快速下发,开机不用。**真机验证字体/包名/通知须重打固件 + 刷机 + reboot**。
- 回退:官方固件 / `backups/`(NAND 全量备份)重刷 - 回退:官方固件 / `backups/`(NAND 全量备份)重刷
## 关键纪律(红线) ## 关键纪律(红线)
@@ -53,4 +62,4 @@
- 日志/字库/媒体等大数据只写 TF 卡;板载只写 KB 级 `meow/settings.conf` - 日志/字库/媒体等大数据只写 TF 卡;板载只写 KB 级 `meow/settings.conf`
- 绝对不对挂载点 `rm -rf` - 绝对不对挂载点 `rm -rf`
详见 `ANALYSIS.md` 纪律章节。 详见 `devdocs/ANALYSIS.md` 纪律章节。
+12 -2
View File
@@ -34,11 +34,21 @@
| | `input.hotkey_window_ms` | int | 2500 | 组合/序列窗口 | | | `input.hotkey_window_ms` | int | 2500 | 组合/序列窗口 |
| | `input.touch_arm_delay_ms` | int | 700 | 触摸布防延迟(防幽灵) | | | `input.touch_arm_delay_ms` | int | 700 | 触摸布防延迟(防幽灵) |
| | `input.swipe_invert` | bool | false | 设置页滑动方向(0=手指上滑→往下翻;1=反向) | | | `input.swipe_invert` | bool | false | 设置页滑动方向(0=手指上滑→往下翻;1=反向) |
| **字体/主题** | `font.file` | path | `MeowAPPs/fonts/font.png` | 字体图集 | | **字体/主题** | `font.file` | path | `MeowOS/fonts/default/font.png` | 字体图集 |
| | `display.multi_font` | int 0/1 | 0 | **多字体**:1=按字符类别逐字符路由;0=单字体(只走 `font.file`,零额外内存/性能) |
| | `font.class_cjk` | path | *(空=跟全局)* | 多字体:CJK 表意+扩展 所用字体(family 相对路径) |
| | `font.class_cjk_sym` | path | *(空=跟全局)* | 多字体:全角/中文标点 所用字体 |
| | `font.class_num` | path | *(空=跟全局)* | 多字体:0-9 所用字体 |
| | `font.class_latin` | path | *(空=跟全局)* | 多字体:A-Za-z 所用字体 |
| | `font.class_latin_sym` | path | *(空=跟全局)* | 多字体:半角标点 所用字体 |
| | `font.class_other` | path | *(空=跟全局)* | 多字体:其他字符 所用字体 |
| | `font.size.title` / `font.size.body` | int | 28/20 | 标题/正文字号 | | | `font.size.title` / `font.size.body` | int | 28/20 | 标题/正文字号 |
| | `ui.style` | enum | `md3` | `md3`(Material You)/`miui` | | | `ui.style` | enum | `md3` | `md3`(Material You)/`miui` |
| **通知** | `notify.center_enabled` | bool | true | 下拉通知中心开关 | | **通知** | `notify.center_enabled` | bool | true | 下拉通知中心开关 |
| | `notify.app_quota` | int | 16 | 每应用通知配额 | | | `notify.app_quota` | int | 16 | 每应用通知配额 |
| | `notify.categories` | int | 4 | 通知类型数上限(每 App,M2.53)[源:审计 2026-08-30]|
| | `notify.category_quota` | int | 4 | 每类型活动通知上限(4×4=16/App)|
| | `notify.media_quota` | int | 1 | 媒体通知单例(每 App 仅 1 条)|
| **日志** | `log.level` | enum | `info` | `error/warn/info/debug` | | **日志** | `log.level` | enum | `info` | `error/warn/info/debug` |
| | `log.to_card` | bool | true | 写 TF 卡(无卡回退内存环) | | | `log.to_card` | bool | true | 写 TF 卡(无卡回退内存环) |
| **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(MeowOS 直写 CS43198 DAC;读回为追踪值) | | **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(MeowOS 直写 CS43198 DAC;读回为追踪值) |
@@ -92,7 +102,7 @@ int meow_settings_export(const char *path); /* 写一份 .conf 到 path */
int meow_settings_import(const char *path); /* 从 .conf 读入覆盖,校验 + 通知 */ int meow_settings_import(const char *path); /* 从 .conf 读入覆盖,校验 + 通知 */
``` ```
- **clamp/校验集中在 set**:如 `brightness` 自动 clamp ≥ `brightness_min`(默认 25);`screen_off_timeout >= 0`;枚举越界拒绝。 - **clamp/校验集中在 set**:如 `brightness` 自动 clamp ≥ `brightness_min`(默认 1;M2.39 由 25 改 1);`screen_off_timeout >= 0`;枚举越界拒绝。
- **变更通知**在 set 成功后对注册 listener 广播(key 字符串),实体 UI 据此重绘;web 端经 MeowPanel 长连接推送。 - **变更通知**在 set 成功后对注册 listener 广播(key 字符串),实体 UI 据此重绘;web 端经 MeowPanel 长连接推送。
## 4. 阶段 B2b:实体系统设置页(Launcher 内,用 B2a 已就位后端) ## 4. 阶段 B2b:实体系统设置页(Launcher 内,用 B2a 已就位后端)
+7 -4
View File
@@ -14,13 +14,16 @@
├── MeowData/ # 用户数据(歌曲、封面缓存、下载等) ├── MeowData/ # 用户数据(歌曲、封面缓存、下载等)
├── MeowAPPs/ # 应用 ├── MeowAPPs/ # 应用
│ ├── <app>/ # app.json + 二进制 + 资源 │ ├── <app>/ # app.json + 二进制 + 资源
│ ├── fonts/ # 预渲染字库 font.png + font.idx(可由 MeowOS 加载) │ ├── fonts/ # 默认字库 font.png + font.idx(font.file 缺省指此处;可由 MeowOS 加载)
│ ├── logs/ # 统一日志(meow_<boot>.log;无卡回退内存环)
│ └── backgrounds/ # 可选背景图 │ └── backgrounds/ # 可选背景图
├── MeowOpt/ # 板载外部工具/二进制(如 mdnsd/;卡上加载,拔卡降级不崩。与 MeowOS/opt 命名待归一,见 SPEC_REF 矛盾 §A-11[源:审计 2026-08-30])
└── MeowOS/ # MeowOS 专属(一切非必要文件) └── MeowOS/ # MeowOS 专属(一切非必要文件)
├── backups/ ├── backups/
│ └── settings/ │ └── settings/
│ └── setting.bak # 设置导出默认路径(SYSTEM_SETTINGS 一致) │ └── setting.bak # 设置导出默认路径(SYSTEM_SETTINGS 一致)
├── fonts/<family>/ # 多字体库(每套一个子目录;font.file 选默认套)
├── logs/ # 统一日志:系统 meow_<bootts>.log + 应用 apps/<app_id>/logs/<ts>_<pid>.log
├── services.d/ # 服务配置(meow_svcd 的 <name>.conf)
├── opt/ # 外围可选工具(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd…) ├── opt/ # 外围可选工具(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd…)
├── lib/ # 可选共享库(opt 工具的依赖 .so) ├── lib/ # 可选共享库(opt 工具的依赖 .so)
├── tools/ # 调试/运维(可不设,并入 opt) ├── tools/ # 调试/运维(可不设,并入 opt)
@@ -48,8 +51,8 @@
- **移工具**:从 rootfs 移除 `MeowOS/opt/` 对应二进制;MeowOS 启动时若卡在则 PATH 前插 `/usr/data/mnt/sd_0/MeowOS/opt/bin:$PATH`(卡不在跳过)。用 `meowtool` 包装提供绝对路径引用。 - **移工具**:从 rootfs 移除 `MeowOS/opt/` 对应二进制;MeowOS 启动时若卡在则 PATH 前插 `/usr/data/mnt/sd_0/MeowOS/opt/bin:$PATH`(卡不在跳过)。用 `meowtool` 包装提供绝对路径引用。
- **框架零卡依赖审查**:确认 launcher/bootmenu 没有任何"卡必须在"的硬依赖(settings 在 /data、字体/应用/日志对卡可缺省/回退)。 - **框架零卡依赖审查**:确认 launcher/bootmenu 没有任何"卡必须在"的硬依赖(settings 在 /data、字体/应用/日志对卡可缺省/回退)。
- **字库**:卡 `MeowAPPs/fonts/`(无卡回退点阵,已有)。 - **字库**:多字体库 `MeowOS/fonts/<family>/`(每套 `font.png/font.idx/font.json`,`font.file` 选默认套,缺省 `MeowOS/fonts/default/font.png`;无卡回退点阵,已有。[源:审计 2026-08-30])。
- **日志**:卡 `MeowAPPs/logs/`(无卡内存环,已有)。 - **日志**:卡 `MeowOS/logs/`(系统日志 `meow_<bootts>.log` + 应用日志 `apps/<app_id>/logs/<ts>_<pid>.log`;无卡内存环,已有。[源:审计 2026-08-30])。
- **设置**:板载 `/data/meow/settings.conf`(小文件跨卡保留),导出默认 `MeowOS/backups/settings/setting.bak`。 - **设置**:板载 `/data/meow/settings.conf`(小文件跨卡保留),导出默认 `MeowOS/backups/settings/setting.bak`。
## 5. 体积收益 ## 5. 体积收益
+128 -7
View File
@@ -19,17 +19,21 @@
/usr/data/mnt/sd_0/ /usr/data/mnt/sd_0/
├── Music/ # 播放器标准索引目录 ├── Music/ # 播放器标准索引目录
├── MeowData/<pkg>/{data,cache} # 应用专属数据(exFAT 无软链,用 env 注入) ├── MeowData/<pkg>/{data,cache} # 应用专属数据(exFAT 无软链,用 env 注入)
├── MeowAPPs/ # 应用(<app>/app.json+二进制+资源, fonts/, logs/, backgrounds/) ├── MeowAPPs/ # 应用(<package>=app.json.package=目录名;app.json+二进制+资源, fonts/, backgrounds/;日志已入 MeowOS/logs[源:审计 2026-08-30])
├── MeowOpt/ # 板载外部工具/二进制(如 mdnsd/,卡上加载,拔卡降级不崩)`[DEV_NOTES][meow_svcd.md]` ├── MeowOpt/ # 板载外部工具/二进制(如 mdnsd/,卡上加载,拔卡降级不崩)`[DEV_NOTES][meow_svcd.md]`
└── MeowOS/ # MeowOS 专属(一切非必要文件) └── MeowOS/ # MeowOS 专属(一切非必要文件)
├── backups/settings/setting.bak # 设置导出默认路径 ├── backups/settings/setting.bak # 设置导出默认路径
├── fonts/<family>/ # 多字体库(每套一个子目录,见 §5.2)
│ ├── font.png # 预渲染灰度 PNG(bit depth 4|8,gen/gen_font.py 产物)
│ ├── font.idx # 索引(MFNI v2,gen/gen_font.py 产物)
│ └── font.json # 元数据 + sha256 完整性校验(schema 见 §5.2)
├── logs/ # 系统日志 meow_<bootts>.log + apps/...(见 §3 日志) ├── logs/ # 系统日志 meow_<bootts>.log + apps/...(见 §3 日志)
├── notifications/(转储区,保留最新 10MB,不被电源事件清) ├── notifications/(转储区,保留最新 10MB,不被电源事件清)
├── opt/{bin,nyai,sources.list}, lib/, tools/ ├── opt/{bin,nyai,sources.list}, lib/, tools/
└── themes/charging/<id>/ # 充电屏主题(theme.json manifest) └── themes/charging/<id>/ # 充电屏主题(theme.json manifest)
``` ```
`[TF_CARD_LAYOUT §1][APPS 附录][NOTIFICATION_SYSTEM §5]` `[TF_CARD_LAYOUT §1][APPS 附录][NOTIFICATION_SYSTEM §5]`
- 环境注入(launcher 每次启动 app 前):`MEOW_DATA_DIR`/`MEOW_CACHE_DIR`/`MEOW_LOG_DIR`/`MEOW_LOG_APP`/`MEOW_LOG_LEVEL`/`MEOW_ACTIVITY`/`MEOW_CHAIN`;XDG:`XDG_DATA_HOME`/`XDG_CACHE_HOME`/`XDG_CONFIG_HOME`(=MeowData/<pkg>/{data,cache,config})`[APPS 附录 §2][FRAMEWORK_DESIGN 附录 D2]` - 环境注入(launcher 每次启动 app 前):`MEOW_DATA_DIR`/`MEOW_CACHE_DIR`/`MEOW_LOG_DIR`/`MEOW_LOG_APP`(app_id=package)/`MEOW_LOG_LEVEL`/`MEOW_ACTIVITY`/`MEOW_CHAIN`;XDG:`XDG_DATA_HOME`/`XDG_CACHE_HOME`/`XDG_CONFIG_HOME`(=MeowData/<pkg>/{data,cache,config})`[APPS 附录 §2][FRAMEWORK_DESIGN 附录 D2]`
### 1.3 板载 vs TF 划分原则(拔卡降级) ### 1.3 板载 vs TF 划分原则(拔卡降级)
- 板载只放**稳定框架+KB 级设置**;日志/字库/媒体等大件一律 TF 卡 `[TF_CARD_LAYOUT §0][SYSTEM_SETTINGS §2]` - 板载只放**稳定框架+KB 级设置**;日志/字库/媒体等大件一律 TF 卡 `[TF_CARD_LAYOUT §0][SYSTEM_SETTINGS §2]`
@@ -42,7 +46,7 @@
## 2. IPC / 接口 ## 2. IPC / 接口
### 2.1 launcher↔app env 注入 ### 2.1 launcher↔app env 注入
`MEOW_LOG_DIR`(卡日志目录,无卡不设)/`MEOW_LOG_APP`(app_id)/`MEOW_LOG_LEVEL`(本 app 级别,系统下发)/`MEOW_ACTIVITY`<target>/`MEOW_CHAIN`<id0>;<id1>;...;<top> `[APPS §3.5][APP_LOG_MANAGER §3][FRAMEWORK_DESIGN §3]` `MEOW_LOG_DIR`(卡日志目录,无卡不设)/`MEOW_LOG_APP`(app_id=app.json.package,缺省回退目录名)/`MEOW_LOG_LEVEL`(本 app 级别,系统下发)/`MEOW_ACTIVITY`<target>/`MEOW_CHAIN`<id0>;<id1>;...;<top> `[APPS §3.5][APP_LOG_MANAGER §3][FRAMEWORK_DESIGN §3]`
### 2.2 meow_ipc 消息协议(AF_UNIX,全小端 LE) ### 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]` - 帧头 16B:`magic=0x4D4F5701`('MEOW'|1)/`version=1`/`type`/`req_id`/`len`;`len` 上限 **MEOW_IPC_MAX=4096**;列表编码 `[u16 count]`+NUL 串 `[FRAMEWORK_DESIGN §1]`
@@ -87,7 +91,7 @@
- 主题:`MEOW_THEME_LIGHT/DARK/AUTO`;`meow_theme{bg,fg,accent,dim,font_title=28,font_body=20}` `[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_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_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_ui_set_statusbar(1/0)`/`meow_ui_statusbar_height()`(非全屏=28,全屏=0)/`meow_ui_set_fullscreen(on)`;状态栏高 **MUI_SBAR_H=28**(用户定 40→28,字号随之为 20)`[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]` - 日志:`MEOW_LOG_DEBUG=0/INFO/WARN/ERROR`;`meow_log(lvl,fmt,...)`/`meow_log_set_level`/`meow_log_get_level` `[meow_ui.h §9]`
- 通知:见 §4。 - 通知:见 §4。
@@ -101,7 +105,8 @@
- 生命周期:`meow_ui_run(&ev)`(阻塞)/`meow_ui_request_exit()`;`meow_ui_time_ms()`(单调 ms) - 生命周期:`meow_ui_run(&ev)`(阻塞)/`meow_ui_request_exit()`;`meow_ui_time_ms()`(单调 ms)
### 3.3 实现定案(相对设计稿)`[MEOW_APP_SDK §12][meow_ui.h]` ### 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" - [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(后续用户定 40→28);[D7] 音量源=settings.conf `audio.volume`;[D8] 蓝牙读 `hciconfig` 30s 缓存;[D9] 状态读取器跑在 `meow_ui_run` 1s 节流(无独立线程);[D10] 全局返回默认语义="APP 未注册 on_swipe 时 SDK 默认退出;已注册交 APP"
- **构建取向(已知设计,非缺陷)**:`build_sdk.sh` 用 `-I../meow_launcherd` 引 `meow_ipc.h`(`meow_notify_ipc.c` 复用共享协议头),故 SDK 编译期依赖 launcher 头、**非独立库**;**运行时无碍** —— App 只链 `meow_ui.a`(含 meow_font.o),不会链入 launcher 符号。SDK 编译依赖 launcher 头属已知设计,勿当作耦合错误。
--- ---
@@ -140,6 +145,15 @@
- **日/夜/自动**: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]` - **日/夜/自动**: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]` - 背景图系统:`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]` - 主界面背景:深色用 background(可选色)、浅色用 surface(白) `[DEV_NOTES #13]`
- **TF 卡多字体库(M2.5x)**:
- 目录 **`<卡根>/MeowOS/fonts/<family>/`**(每套一个子目录),内含三件套:`font.png`(预渲染灰度 PNG,bit depth 4|8)+ `font.idx`(MFNI v2 索引),二者均由 `font/gen/gen_font.py` 生成;`font.json` 为元数据+完整性校验。**缺任一件 → 该套不进列表 + 日志**。
- **font.json schema(定版)**:`family`(同目录名,信息用)/`name`(下拉显示名,缺则回退目录名)/`glyph_count`(信息用)/`size_list`(信息用)/`bit_depth`(4|8,信息用)/`sha256_png`(font.png 的 sha256 低 64hex)/`sha256_idx`(font.idx 的 sha256)/`source`(可选)/`license`(可选)。校验失败**仅 WARN 不硬失败**(该套仍可选);缺 font.json 或解析不出 `name` → 跳过。
- **加载优先级**:`font.file`(设置键,默认 `MeowOS/fonts/default/font.png`)指向的字体**存在且能 load → 用它**;否则回退卡默认 `MeowOS/fonts/default/font.png` → 固件 `/usr/share/meowos/font/font.png`。相对路径按卡根 `/usr/data/mnt/sd_0/` 解析。全失败 → 字库不可用,`ui_draw` 回退 5x7 点阵(系统不死)。
- **默认字库目录**:默认字体统一放 **`<卡根>/MeowOS/fonts/default/`**(`font.png`+`font.idx`,可选 `font.json`),不再用 `MeowAPPs/fonts/font.png`;设备旧 `MeowAPPs/fonts/` **保留**(勿删,避免破坏老约定/已有文件),仅新引用一律指向 `MeowOS/fonts/default`。
- **`font.file` 变更即 unload+reload 生效**(设置页回调);拔卡 → 回退固件内置。
- **preset 动态化**:启动/进设置页扫描一次 `MeowOS/fonts/*/`,重建「字体文件」下拉(默认字体 + ≤16 套卡内字体;上限防爆)。扫描仅此二时机,勿放热路径。
- **MeowPanel 预留**:`meow_font_import_zip()`/`meow_font_export_zip()` 为 TODO 声明(不实现),供 MeowPanel 后续接入(`.zip` 导入校验→解压到 `MeowOS/fonts/<family>/`、导出打包)。
- **多字体(M2.5x,`display.multi_font`)**:按字符类别(CJK/CJK_SYM/NUM/LATIN/LATIN_SYM/OTHER)逐字符路由,每类可各选一套字库(`font.class_*`,默认空=跟随全局 `font.file`);同 family 去重、同时装载不同字体 ≤4 套(超出 WARN「过多不同字体会增内存」并截断);开关关 = 单字体(现状)。详见 §5.7.7「多字体」。
### 5.3 通知配额/转储 ### 5.3 通知配额/转储
- **每 App 最多 4 类型;每类型活动 ≤4;媒体单例(每 App 仅 1 条,可多 App)**;系统事件抽象为 App(`app_id=system`) 走同一配额 `[NOTIFICATION_SYSTEM §7][meow_ui.h][DEV_NOTES #11]` - **每 App 最多 4 类型;每类型活动 ≤4;媒体单例(每 App 仅 1 条,可多 App)**;系统事件抽象为 App(`app_id=system`) 走同一配额 `[NOTIFICATION_SYSTEM §7][meow_ui.h][DEV_NOTES #11]`
@@ -155,13 +169,119 @@
- `<pkg>/` = 程序主体+app.json+资源;`MeowData/<pkg>/{data,cache}` = 数据;首次启动自动建 data/cache 并经 env 注入,exFAT 无软链 `[APPS 附录 §1/§2]` - `<pkg>/` = 程序主体+app.json+资源;`MeowData/<pkg>/{data,cache}` = 数据;首次启动自动建 data/cache 并经 env 注入,exFAT 无软链 `[APPS 附录 §1/§2]`
- `.mpk` = tar.gz(app.json + meow 二进制 + 资源),**不含 data/cache**;导入校验 app.json/哈希→解压→数据 init→登入列表 `[APPS 附录 §3]` - `.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]` - 包管理 **CLI `nyai`**(静态 MIPS,部署 `MeowOS/opt/bin/nyai`):install/remove/list/info/search/index/export/upgrade/scan;源 `MeowOS/opt/nyai/sources.list`;索引 `Packages`(Package/Version/Filename/Size/SHA256/Depends/Description)`[APPS 附录 §5]`
- 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + 扩展 `framework`/`immersive`/`icon`/`route.<id>`/`service.<svc>`/`fullscreen` `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2]` - 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + **`package`(反域名权威 app_id;==目录名;缺省回退目录名)** + 扩展 `framework`/`immersive`/`icon`/`route.<id>`/`awake.<id>`(keep_awake)/`service.<svc>`/`fullscreen`(**未解析**,见矛盾 §A-12) `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2][源:审计 2026-08-30]`
### 5.6 冷启动充电屏 ### 5.6 冷启动充电屏
- 触发(**三者同时**):冷启动(`poweroff` 写 `/data/meow_cold_boot=1`;`reboot` 写 0)+ 在充电(AC online=1 或 battery status=Charging)+(开机瞬间一次性快照,判定后不回头)`[CHARGING_SCREEN §0]` - 触发(**三者同时**):冷启动(`poweroff` 写 `/data/meow_cold_boot=1`;`reboot` 写 0)+ 在充电(AC online=1 或 battery status=Charging)+(开机瞬间一次性快照,判定后不回头)`[CHARGING_SCREEN §0]`
- 热启动(reboot、含充电态 reboot)一律**不进**充电屏,走引导选单 `[CHARGING_SCREEN §0]` - 热启动(reboot、含充电态 reboot)一律**不进**充电屏,走引导选单 `[CHARGING_SCREEN §0]`
- 主题:`theme.charging=<id>`(默认 default);`MeowOS/themes/charging/<id>/theme.json`;加载失败回退内置纯代码默认主题 `[CHARGING_SCREEN §1/§2]` - 主题:`theme.charging=<id>`(默认 default);`MeowOS/themes/charging/<id>/theme.json`;加载失败回退内置纯代码默认主题 `[CHARGING_SCREEN §1/§2]`
### 5.7 字体管理 + 应用字库接口(扩展 → SDK 统一接口 / 应用自带字库 / 多句柄)
> 在 §5.2「TF 卡多字体库(M2.5x)」基础上**扩展**:把 MeowOS 字体管理器定义为可用字库的**唯一权威来源**,向 SDK 提供统一查询/选用接口,并支持**应用自带字库**与**多句柄(多字体)**。
> ⚠️ **凡 `meow_font_*_h` / `meow_ui_*_h` / 列表枚举 / 句柄加载类接口均为「草案/待定案」**:当前 SDK 只有单字体 `meow_font_*` 与 `meow_ui_text*`(用当前默认字体),本小节为新功能、**未实现**,勿当作已落地。
> `[源:meow_font.h][源:agent-79 实现][源:meow_ui.h]`
> **app 未自带字库 → 跟随系统 `font.file`(M2.x 已落地)**:launcher 每次 `launch_app` 前把「当前生效字体」注入为环境变量 `MEOW_FONT_PNG`/`MEOW_FONT_IDX`(绝对路径,可选 `MEOW_FONT_APP=<family>`);SDK `meow_ui_gfx.c load_font()` **优先读 env**,无 env 则回退卡默认 `MeowOS/fonts/default` → 固件。§5.7.3 的 `load_handle`(app 自带字库)**未实现**,故当前 app 只能跟随系统字体;一旦实现自带字库,其加载优先级应高于 env。
- **5.7.1 字体管理器(launcher 侧)= 可用字库唯一权威来源**
- 唯一维护并扫描 `MeowOS/fonts/<family>/`(+默认),产出「字体列表」(family slug / 显示名 / glyph_count / size_list / bit_depth)。**SDK 不自行发现字库**,一律经本接口取 launcher 扫描结果。
- 扫描与校验规则沿用 §5.2:三件套 `font.png/font.idx/font.json` **缺任一件 → 该套跳过 + 日志**;`font.json` 解析 `name`(缺回退目录名)/`glyph_count`/`size_list`/`bit_depth`;`sha256_png/sha256_idx` 仅 **WARN** 不硬失败(该套仍可选)。
- 列表上限沿用 `FONT_PRESETS_MAX = 17`(1 默认 + ≤16 卡内套装,防爆);顺序 = 默认(0) + 扫描顺序。扫描时机不变(启动 / 进设置页),**勿放热路径** `[源:agent-79 实现]`。
- 权威列表数据模型(SDK 侧快照,枚举时填充):
```c
typedef struct meow_font_info {
char family[64]; /* family slug = MeowOS/fonts/<family>/ 目录名 */
char name[64]; /* font.json "name",缺则回退 family */
int glyph_count; /* font.json "glyph_count",信息用 */
int size_list[16]; /* font.json "size_list"(原生面字号,0 结束) */
int size_count; /* 有效字号数 */
int bit_depth; /* 4 | 8 */
char path_png[320]; /* 系统库:相对卡根 MeowOS/fonts/<family>/font.png */
} meow_font_info;
```
- **5.7.2 应用可读 TF 卡字体列表(统一规范接口,app 面向)** `[源:agent-79 实现][源:meow_font.h]`
```c
int meow_font_get_available_count(void); /* 可用字库数(含默认) */
int meow_font_enumerate(int idx, meow_font_info *out); /* 按序号取,越界返回 0 */
typedef int (*meow_font_enum_cb)(const meow_font_info *info, void *ctx);
int meow_font_enumerate_cb(meow_font_enum_cb cb, void *ctx); /* 回调式,返回套数 */
int meow_font_select_by_id(int id); /* 选该套为 app 会话默认字体 */
```
- 语义:`id` = 列表序号(§5.7.1 快照);`0` = **跟随系统默认**(用户 `font.file` 所选,app 会话不覆盖);`>0` = **app 会话内固定用该套**(覆盖 `font.file` 至 app 结束)。成功 0,越界/加载失败返回负值。
- **列表快照是临时的**:launcher 仅启动/进设置页重扫,期间拔卡或改卡内字库不刷新;app 应**每次会话重新枚举**、按 `family` 持久化,勿缓存数值 id。
- 覆盖范围:系统库 `MeowOS/fonts/<family>/` 与默认(含固件 `/usr/share/meowos/font` 回退)都反映在列表里;app 侧「选用已有字库」**只用本接口,不直接拼路径**。
- **5.7.3 应用自带字库(按 handle 加载)** `[源:meow_font.h]`
- 目录:`MeowAPPs/<pkg>/fonts/<handle>/{font.png,font.idx,font.json}`;`<pkg>` = `app.json.package`(反域名 app_id,见 §5.5),`<handle>` = 应用自设名字(如 `lyrics`/`reader`)。
- `app.json` 声明(**可选**):`"fonts": ["lyrics","reader"]`。作用 = MeowPanel 打包校验 / launcher 装包校验 / SDK 提供取证;**不声明也可运行时按 handle 加载**。
- SDK 接口(草案):
```c
int meow_font_load_handle(const char *pkg, const char *handle); /* 返回字体句柄(>0),失败负值;pkg=NULL=当前 app */
void meow_font_unload_handle(int h);
int meow_font_handle_info(int h, meow_font_info *out); /* 查句柄对应信息 */
```
- **应用自带字库校验更严**:三件套缺任一或 `font.json` 解析不出 → 加载**失败**(app 自声明字库,坏库应显式失败,而非静默回退点阵/默认)。
- 与系统库**并存可混用**:系统套件经 §5.7.2 `meow_font_select_by_id` 激活即成合法 handle;应用自带经 `load_handle` 取得 handle;两者在同一 handle 空间。
- **5.7.4 多句柄 / 多字体(不同控件用不同字体)** `[源:meow_font.h][源:meow_ui.h]`
- 低层 `meow_font_*` 新增 `_h` 句柄版本(显式传 handle):
```c
void meow_font_drawtext_h(uint16_t *fb,int x,int y,const char *utf8,uint16_t color,int size,int handle);
void meow_font_drawtext_outline_h(uint16_t *fb,int x,int y,const char *utf8,
uint16_t color,uint16_t outline_color,int ol,int size,int handle);
int meow_font_width_text_h(const char *utf8,int size,int handle);
int meow_font_line_height_h(int size,int handle);
int meow_font_ascent_h(int size,int handle);
int meow_font_has_size_h(int size,int handle);
```
- **兼容关键**:现有无 `_h` 的 `meow_font_*`(drawtext/width_text/line_height/...)语义变更为「作用于**当前句柄**」(默认 = 系统 `font.file` 那套);`meow_font_load(png,idx)` 仍是装载系统默认的入口;新增 `meow_font_use(handle)` 设定当前句柄。**不改任何现有签名 → 纯扩展、向后兼容。**
```c
void meow_font_use(int handle); /* 设定后续无 _h meow_font_* 所用字体 */
int meow_font_get_current(void);
```
- SDK `meow_ui_*` 高层文本同理:
```c
void meow_ui_use_font(int handle); /* 设定后续 meow_ui_text* 字体 */
int meow_ui_font_get_current(void); /* 取当前 meow_ui_text* 所用句柄 */
void meow_ui_text_h(int x,int y,const char *utf8,int size,uint32_t color,int handle);
void meow_ui_text_right_h(int right_x,int y,const char *utf8,int size,uint32_t color,int handle);
void meow_ui_text_center_h(int cx,int y,const char *utf8,int size,uint32_t color,int handle);
int meow_ui_text_width_h(const char *utf8,int size,int handle);
int meow_ui_text_height_h(int size,int handle); /* = meow_font_line_height_h */
```
现有 `meow_ui_text/text_right/text_center`(无 handle)= 用当前字体(`meow_ui_use_font` 设;默认系统默认)。**向后兼容。** 建议 app 切库前用 `meow_font_get_current()` / `meow_ui_font_get_current()` 求值保存,画完再 `meow_ui_use_font(旧)` 显式恢复,避免句柄状态泄漏影响后续绘制。
- **5.7.5 多句柄用法示例(歌词页用 A 库、正文用 B 库)**
```c
/* 应用自带 lyrics(歌词页) + 系统字库(正文)混用 */
int hLyrics = meow_font_load_handle(NULL, "lyrics"); /* MeowAPPs/<pkg>/fonts/lyrics/ */
meow_ui_use_font(hLyrics);
meow_ui_text_center(240, 300, "歌词行…", 20, 0xFFFFFF); /* 用 lyrics 库 */
int hBody = meow_font_select_by_id(2); /* 系统字库第 2 套(正文用) */
meow_ui_use_font(hBody);
meow_ui_text(20, 60, "正文…", 20, 0xFFFFFF); /* 用系统库 */
/* 量宽/行高须与最终实际字形一致:用同 handle 的 _h 版 */
int w = meow_ui_text_width_h("正文…", 20, hBody);
int lh = meow_ui_text_height_h(20, hBody);
```
- 说明:句柄状态为 app 进程内**全局**;同一屏需混排时,推荐每段绘制前显式 `meow_ui_use_font`,或全部走 `*_h` 显式传 handle,避免依赖上次绘制遗留的句柄状态。
- **5.7.6 MeowPanel 上传 / 导出(.zip,预留)** `[源:meow_font.h]`
- 复用已声明的 `meow_font_import_zip(zip_path)` / `meow_font_export_zip(family,out_zip)`(§5.2 已列为 TODO,未实现)。
- **系统字库导入**:`.zip` 内含 `font.png+font.idx+font.json` 三件套;校验三件套就绪 + `font.json` 可解析;sha256 沿用 `font.json` 约定(建议与运行时同口径 = **WARN**,仅缺件/不可解析硬失败);`family` 取自 `font.json.family` 或 zip 名;解压到 `MeowOS/fonts/<family>/`;**覆盖冲突需先确认**;导入后应触发 launcher 重扫(刷新 §5.7.1 列表)。
- **应用字库导入**:目标 `MeowAPPs/<pkg>/fonts/<handle>/`,与 `app.json "fonts"` 声明对齐,校验同上。
- **导出**:系统库把 `MeowOS/fonts/<family>/` 三件套打包;应用库把 `MeowAPPs/<pkg>/fonts/<handle>/` 打包。
- 约定:`.zip` 布局与校验沿用 `font.json` schema(§5.2);导入/导出后的一致性(列表刷新、应用声明同步)归 MeowPanel + launcher 协作,本小节仅列接口语义。
- **5.7.7 多字体(按字符类别逐字符路由,`display.multi_font`,M2.5x 新增)** `[源:meow_font.h][源:MEOWOS_SPEC_REF §5.2/§5.5]`
- **背景**:一款字体难兼顾中文(大图集、字形全)与拉丁/数字(干净、省内存)。多字体让**不同字符类别各用一套字库**,按 codepoint 逐字符路由取字形。
- **设置键**(launcher 读,SDK 经 env 读同一映射):
- `display.multi_font`(int **0/1,默认 0=单字体**):1=按字符类别逐字符路由;0=只走 `font.file`(现状,零额外内存/性能开销)。
- 6 个类别字体(SET_STR,值 = 字体 family 相对路径,**默认空 = 跟随全局 `font.file`**):`font.class_cjk`(CJK 表意+扩展)、`font.class_cjk_sym`(全角/中文标点)、`font.class_num`(0-9)、`font.class_latin`(A-Za-z)、`font.class_latin_sym`(半角标点)、`font.class_other`(其他)。
- **字符分类器**(`meow_font_class_of(cp)`,按 codepoint):`MEOW_FCLS_CJK`/`_CJK_SYM`/`_NUM`/`_LATIN`/`_LATIN_SYM`/`_OTHER`。规则(ASCII 优先 + Unicode 区间):0x30-39→NUM、0x41-5A/61-7A→LATIN、<0x80(其余)→LATIN_SYM;0x3000-303F/0xFF00-FFEF/0x2014/0x2018-201D/0x2026/0x00B7→CJK_SYM;CJK 表意+扩展+兼容表意区(0x3400-4DBF/4E00-9FFF/20000-2FA1F…)→CJK;其余(假名/韩文/其他文字/扩展区)→OTHER。
- **逐字符路由**:`meow_font_drawtext`/`_outline`/`width_text`/`line_height`/`ascent`/`has_size` 在 multi_font 开启时**逐字符分类 → 用类别对应槽的字库取字形与度量**(宽度/行高按该字体);缺某类字体/槽未载 → **回退全局**(slot0);该字符其字体无字形 → **豆腐缺字**(可经 `meow_font_has_glyph(cp)` 预判)。开关关 → 一律 slot0,与单字体一致。
- **SDK 接口**:`meow_font_load_slot(slot,png,idx)`/`meow_font_unload_slot(slot)`/`meow_font_enable_multifont(on)`/`meow_font_set_class_slot(cls,slot)`/`meow_font_config_multifont(on,global_png,global_idx,cfg[],&distinct,&truncated)`;`MEOW_MAX_SLOTS=5`(slot0=全局 + ≤4 类别)。`meow_font_config_multifont` 统一做**同 png 去重**(含与全局相同 → follow slot0)与**≤4 套上限**(超出 → 置 `*truncated=1`,调用方 WARN「过多不同字体会增内存」,已装载者保留、新类别回退全局)。
- **launcher env 注入**(launch_app,app 跟随系统字体):`MEOW_MULTI_FONT=0/1`;`MEOW_FONT_PNG/IDX`(全局,保留);6 类 `MEOW_FONT_CLS_<类>_PNG/IDX`(命中则覆盖全局;类值为空=跟随全局不注入)。SDK `meow_ui_gfx` 读 env 构建同一映射;无 env/关 → 单字体。
- **持久化**:类别字体设置在 settings.conf(板载,无卡也可存),经 `meow_settings_*` 读写,随设置页变更即重配。
- **内存**(推断解码 atlas = `plane_w×plane_h×(depth 4→0.5 / 8→1.0)` 字节;每套另加 glyph 数组 ≈ glyph_count×16B、idx 头保留约 128KB,idx 原始缓冲载毕即释)。实测仓库字库(depth 4):misans(CJK)≈2.44MiB、source_han_sans(CJK)≈2.53MiB、meslo(Latin)≈1.68MiB、google_sans(Latin)≈1.12MiB;按用户估算口径(**depth 8**):CJK ≈ 5-6MB、Latin ≈ 0.3-0.5MB(后者为窄覆盖 Latin 专库)。**典型「1CJK+1Latin」增量**:全局=CJK,另加一套 Latin → 增加 ≈0.3-1.7MB(depth 8 约 0.5MB;实测 depth4 latin 1.1-1.7MB);全局=Latin 另加一套 CJK → 增加 ≈2.4-2.5MB(depth 8 约 5-6MB)。`display.multi_font=0` 时仅全局一套,无任何额外装载。
--- ---
## 6. 标志性 TODO / 未决(文档未定,勿凭猜实现) ## 6. 标志性 TODO / 未决(文档未定,勿凭猜实现)
@@ -180,8 +300,9 @@
- **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-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-6 KEY_PREVIOUS(code)**:KEY_CONTROL §3 明注「KEY_PREVIOUSSONG 是 **165**;19 是另一码」;meow_ui.h `#define MEOW_KEY_PREVIOUS 19 /* KEY_PREVIOUSSONG */` 注释标错。→ **19 的标签应改**(值 19 非 prevsong;165 才是),属 meow_ui.h 注释误导。
- **A-7 app 日志路径**:APP_LOG_MANAGER §4 写 `apps/<app_id>.log`(单文件);meow_ui.h §9 / DEV_NOTES #9 为 **`apps/<包名>/logs/<启动时间>_<pid>.log`**(每次启动新文件)——用户已纠正,勿用单文件。→ 以 meow_ui.h/DEV_NOTES(M2.51)为准,APP_LOG_MANAGER 目录小节过时。 - **A-7 app 日志路径**:APP_LOG_MANAGER §4 写 `apps/<app_id>.log`(单文件);meow_ui.h §9 / DEV_NOTES #9 为 **`apps/<包名>/logs/<启动时间>_<pid>.log`**(每次启动新文件)——用户已纠正,勿用单文件。→ 以 meow_ui.h/DEV_NOTES(M2.51)为准,APP_LOG_MANAGER 目录小节过时。
- **A-8 状态栏高**:MEOW_APP_SDK §3.1「slim ~20-40px」;实际定案 **40px**([D6] MIGRATION_AC §11.1)。→ 取 40。 - **A-8 状态栏高**:MEOW_APP_SDK §3.1「slim ~20-40px」;实际定案 **40px**([D6] MIGRATION_AC §11.1),后被用户改为 **28px**(MUI_SBAR_H=28,字号 20,图标 ink 16/中线 14,见 M2.x 状态栏压缩)。→ 现取 28。
- **A-9 SDK 通知转储**:NOTIFICATION_SYSTEM §8「创建 10s 后自动转储到 TF」;meow_ui.h 现仅「活动表+meow_log 打日志,真实投递 UI/D-Bus 为后续」。→ 10s 转储/TF 尚未实现,属"已定未做"。 - **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-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)并存,命名仅大小写之差、语义可能重叠。→ **两者是否为同一用途/是否需归并待确认**;当前按各自文档记录,勿混用。 - **A-11 `MeowOS/opt/` vs `MeowOpt/`**:§1.2 树中 `MeowOS/opt/{bin,nyai,sources.list},lib/,tools/`(nyai 包管 + 工具)与新增顶层 `MeowOpt/`(板载外部工具/二进制,如 mdnsd)并存,命名仅大小写之差、语义可能重叠。→ **两者是否为同一用途/是否需归并待确认**;当前按各自文档记录,勿混用。
- **A-12 `fullscreen` 字段未解析**:§5.5 与 MEOW_APP_SDK §9 将 `fullscreen` 列为 app.json 字段,但 `meow_parse_app_json()` **并未读取该键**(实现只解析 name/exec/icon/package/framework/immersive/route./awake./service.)。→ 文档与实现不一致,属"已定未做";若要用需在解析器补读并处理。
`[各源标注见上]` `[各源标注见上]`
+1
View File
@@ -211,6 +211,7 @@ def write_idx(path, bit_depth, faces, glyph_tabs):
tab_sorted = sorted(tab, key=lambda g: g[0]) tab_sorted = sorted(tab, key=lambda g: g[0])
buf = bytearray() buf = bytearray()
for cp, x, y, w, h, adv, dx, dy in tab_sorted: for cp, x, y, w, h, adv, dx, dy in tab_sorted:
dy = max(dy, 0) # 少数全角/符号字形墨迹略高于 la 锚点,得到负 dy;u8 仅能表达非负,钳到 0(整体下移 1-3px,肉眼不可见)
buf += struct.pack("<IHHHHHbB", cp, x, y, w, h, adv, dx, dy) buf += struct.pack("<IHHHHHbB", cp, x, y, w, h, adv, dx, dy)
tables.append(buf) tables.append(buf)
cur += len(buf) cur += len(buf)
+238 -63
View File
@@ -11,6 +11,11 @@
* *
* 绘制模型:drawtext(fb,x,y,utf8,color,size),(x,y)=行首升线顶部;baseline=y+ascent; * 绘制模型:drawtext(fb,x,y,utf8,color,size),(x,y)=行首升线顶部;baseline=y+ascent;
* 每字形 blit 于 (x+pen+dx, y+dy);pen 累加 advance。color 为 RGB565(R=bit15..11)。 * 每字形 blit 于 (x+pen+dx, y+dy);pen 累加 advance。color 为 RGB565(R=bit15..11)。
*
* 多字体(M2.5x):槽位模型。slot 0 = meow_font_load() 装载的全局/默认字库(font.file);
* 额外字库经 meow_font_load_slot() 装载。meow_font_enable_multifont(1) 开启后,
* drawtext/width_text/line_height/... 逐字符 meow_font_class_of() 分类 → 用该类别
* 对应槽的字库取字形与度量;缺该类别/槽未载 → 回退 slot 0。关闭(默认)一律 slot 0。
*/ */
#include "meow_font.h" #include "meow_font.h"
@@ -71,17 +76,23 @@ typedef struct {
meow_glyph *glyphs; /* 按 cp 升序 */ meow_glyph *glyphs; /* 按 cp 升序 */
} meow_face; } meow_face;
/* 一个槽 = 一套已装载的字库(一个 PNG plane + 若干 face)。 */
typedef struct { typedef struct {
unsigned fb_w, fb_h, fb_pitch;
size_t plane_rowbytes; size_t plane_rowbytes;
unsigned depth; /* 4 或 8 */ unsigned depth; /* 4 或 8 */
unsigned plane_w, plane_h; /* 画布尺寸 */ unsigned plane_w, plane_h; /* 画布尺寸 */
unsigned char *plane; /* 打包像素面 */ unsigned char *plane; /* 打包像素面 */
int face_count; int face_count;
meow_face *faces; meow_face *faces;
} meow_state; int loaded; /* 是否已装载 */
} meow_font_slot;
static meow_state g; /* framebuffer 几何(全局,作用于所有槽;绘制前 meow_font_set_fb 设置一次)。 */
static unsigned g_fb_w, g_fb_h, g_fb_pitch;
static meow_font_slot g_slots[MEOW_MAX_SLOTS];
static int g_multifont = 0; /* 0=单字体(默认) */
static int g_class_slot[MEOW_FCLS_COUNT]; /* 每字符类 → 槽;默认全 0(跟随全局) */
/* PNG 内部为大端;idx(自定格式)为小端。 */ /* PNG 内部为大端;idx(自定格式)为小端。 */
static uint32_t be32(const unsigned char *p) { static uint32_t be32(const unsigned char *p) {
@@ -220,15 +231,16 @@ static unsigned char *png_decode_gray(const unsigned char *png, size_t len,
return out; return out;
} }
/* ---------------- idx 解析 ---------------- */ /* ---------------- idx 解析(写入指定槽) ---------------- */
static int load_idx(const unsigned char *buf, size_t len) { static int load_idx(const unsigned char *buf, size_t len, int slot) {
meow_font_slot *st = &g_slots[slot];
if (len < 16 || memcmp(buf, "MFNI", 4) != 0) return -1; if (len < 16 || memcmp(buf, "MFNI", 4) != 0) return -1;
unsigned depth = buf[6]; unsigned depth = buf[6];
int nfaces = buf[7]; int nfaces = buf[7];
unsigned png_w = le32(buf + 8), png_h = le32(buf + 12); unsigned png_w = le32(buf + 8), png_h = le32(buf + 12);
(void)png_w; (void)png_h; (void)png_w; (void)png_h;
if (nfaces < 1 || nfaces > 32) return -1; if (nfaces < 1 || nfaces > 32) return -1;
if (depth != g.depth) return -2; /* 位深须与 PNG 一致 */ if (depth != st->depth) return -2; /* 位深须与 PNG 一致 */
size_t face_off = 16; size_t face_off = 16;
meow_face *faces = (meow_face *)calloc((size_t)nfaces, sizeof(meow_face)); meow_face *faces = (meow_face *)calloc((size_t)nfaces, sizeof(meow_face));
@@ -271,20 +283,21 @@ static int load_idx(const unsigned char *buf, size_t len) {
return -5; return -5;
} }
} }
g.faces = faces; st->faces = faces;
g.face_count = nfaces; st->face_count = nfaces;
return 0; return 0;
} }
/* ---------------- 面/字形查找 ---------------- */ /* ---------------- 面/字形查找(槽感知) ---------------- */
static meow_face *face_for_size(int size) { static meow_face *face_for_size(int slot, int size) {
if (g.face_count <= 0) return NULL; meow_font_slot *st = &g_slots[slot];
if (st->face_count <= 0) return NULL;
meow_face *best = NULL; meow_face *best = NULL;
int best_delta = -1; int best_delta = -1;
for (int i = 0; i < g.face_count; i++) { for (int i = 0; i < st->face_count; i++) {
int d = g.faces[i].px - size; int d = st->faces[i].px - size;
if (d < 0) d = -d; if (d < 0) d = -d;
if (best_delta < 0 || d < best_delta) { best_delta = d; best = &g.faces[i]; } if (best_delta < 0 || d < best_delta) { best_delta = d; best = &st->faces[i]; }
} }
return best; return best;
} }
@@ -300,11 +313,12 @@ static meow_glyph *find_glyph(meow_face *f, uint32_t cp) {
return NULL; return NULL;
} }
/* ---------------- 像素采样(depth 感知) ---------------- */ /* ---------------- 像素采样(depth 感知,槽感知) ---------------- */
static inline unsigned sample_px(unsigned x, unsigned y) { static inline unsigned sample_px(int slot, unsigned x, unsigned y) {
if (x >= g.plane_w || y >= g.plane_h) return 0; meow_font_slot *st = &g_slots[slot];
if (g.depth == 8) return g.plane[(size_t)y * g.plane_w + x]; if (x >= st->plane_w || y >= st->plane_h) return 0;
unsigned char b = g.plane[((size_t)y * g.plane_w + x) >> 1]; if (st->depth == 8) return st->plane[(size_t)y * st->plane_w + x];
unsigned char b = st->plane[((size_t)y * st->plane_w + x) >> 1];
unsigned nh = (x & 1) ? (b & 0x0F) : (b >> 4); unsigned nh = (x & 1) ? (b & 0x0F) : (b >> 4);
return nh * 17; return nh * 17;
} }
@@ -320,10 +334,10 @@ static inline uint16_t blend565(uint16_t bg, uint16_t fg, unsigned a) {
return (uint16_t)((r << 11) | (gc << 5) | b); return (uint16_t)((r << 11) | (gc << 5) | b);
} }
/* ---------------- 字形 blit(含缩放、裁剪、alpha 混合) ---------------- */ /* ---------------- 字形 blit(含缩放、裁剪、alpha 混合;槽感知) ---------------- */
static void blit_glyph(uint16_t *fb, meow_face *face, const meow_glyph *gr, static void blit_glyph(uint16_t *fb, int slot, meow_face *face, const meow_glyph *gr,
int pen_x, int y, uint16_t color, unsigned scaleq) { int pen_x, int y, uint16_t color, unsigned scaleq) {
int stride = (int)(g.fb_pitch / 2); /* uint16 元素每行数 */ int stride = (int)(g_fb_pitch / 2); /* uint16 元素每行数 */
/* 目标尺寸 = 源尺寸 * scaleq / 4096 */ /* 目标尺寸 = 源尺寸 * scaleq / 4096 */
unsigned gw, gh; unsigned gw, gh;
int dx, dy; int dx, dy;
@@ -343,15 +357,15 @@ static void blit_glyph(uint16_t *fb, meow_face *face, const meow_glyph *gr,
for (unsigned oy = 0; oy < gh; oy++) { for (unsigned oy = 0; oy < gh; oy++) {
int ay = py + (int)oy; int ay = py + (int)oy;
if (ay < 0) continue; if (ay < 0) continue;
if (ay >= (int)g.fb_h) break; if (ay >= (int)g_fb_h) break;
int sy = (int)((uint64_t)oy * sh / gh); int sy = (int)((uint64_t)oy * sh / gh);
int base = face->plane_x + gr->x; int base = face->plane_x + gr->x;
for (unsigned ox = 0; ox < gw; ox++) { for (unsigned ox = 0; ox < gw; ox++) {
int ax = px + (int)ox; int ax = px + (int)ox;
if (ax < 0) continue; if (ax < 0) continue;
if (ax >= (int)g.fb_w) break; if (ax >= (int)g_fb_w) break;
int sx = (int)((uint64_t)ox * sw / gw); int sx = (int)((uint64_t)ox * sw / gw);
unsigned a = sample_px((unsigned)(base + sx), unsigned a = sample_px(slot, (unsigned)(base + sx),
(unsigned)(face->plane_y + gr->y + sy)); (unsigned)(face->plane_y + gr->y + sy));
if (!a) continue; if (!a) continue;
size_t idx = (size_t)ay * (size_t)stride + (size_t)ax; size_t idx = (size_t)ay * (size_t)stride + (size_t)ax;
@@ -386,13 +400,13 @@ static uint32_t advance_for_missing(meow_face *f) {
} }
static void draw_tofu(uint16_t *fb, int x, int y, int w, int h, uint16_t color) { 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); int stride = (int)(g_fb_pitch / 2);
for (int oy = 0; oy < h; oy++) { for (int oy = 0; oy < h; oy++) {
int ay = y + oy; int ay = y + oy;
if (ay < 0 || ay >= (int)g.fb_h) break; if (ay < 0 || ay >= (int)g_fb_h) break;
for (int ox = 0; ox < w; ox++) { for (int ox = 0; ox < w; ox++) {
int ax = x + ox; int ax = x + ox;
if (ax < 0 || ax >= (int)g.fb_w) break; if (ax < 0 || ax >= (int)g_fb_w) break;
if (oy == 0 || oy == h - 1 || ox == 0 || ox == w - 1) { if (oy == 0 || oy == h - 1 || ox == 0 || ox == w - 1) {
size_t idx = (size_t)ay * (size_t)stride + (size_t)ax; size_t idx = (size_t)ay * (size_t)stride + (size_t)ax;
fb[idx] = blend565(fb[idx], color, 255); fb[idx] = blend565(fb[idx], color, 255);
@@ -401,23 +415,86 @@ static void draw_tofu(uint16_t *fb, int x, int y, int w, int h, uint16_t color)
} }
} }
/* ---------------- 字符分类器(multi_font 路由依据) ---------------- */
int meow_font_class_of(uint32_t cp) {
/* 数字 */
if (cp >= '0' && cp <= '9') return MEOW_FCLS_NUM;
/* 拉丁字母(基本拉丁) */
if ((cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z')) return MEOW_FCLS_LATIN;
/* 半角标点/空格/控制符(ASCII 非字母数字) */
if (cp < 0x80) return MEOW_FCLS_LATIN_SYM;
/* 全角 / 中文标点 */
if ((cp >= 0x3000 && cp <= 0x303F) || /* CJK 符号与标点(含全角空格 U+3000) */
(cp >= 0xFF00 && cp <= 0xFFEF) || /* 半/全角形式(全角数字字母符号) */
cp == 0x2014 || cp == 0x2018 || cp == 0x2019 || /* — ‘’ */
cp == 0x201C || cp == 0x201D || cp == 0x2026 || /* “” … */
cp == 0x00B7) /* · */
return MEOW_FCLS_CJK_SYM;
/* CJK 表意文字 + 扩展 + 兼容表意 */
if ((cp >= 0x4E00 && cp <= 0x9FFF) ||
(cp >= 0x3400 && cp <= 0x4DBF) ||
(cp >= 0x20000 && cp <= 0x2A6DF) ||
(cp >= 0x2A700 && cp <= 0x2B73F) ||
(cp >= 0x2B740 && cp <= 0x2B81F) ||
(cp >= 0x2B820 && cp <= 0x2CEAF) ||
(cp >= 0x2CEB0 && cp <= 0x2EBEF) ||
(cp >= 0x30000 && cp <= 0x3134F) ||
(cp >= 0xF900 && cp <= 0xFAFF) ||
(cp >= 0x2F800 && cp <= 0x2FA1F))
return MEOW_FCLS_CJK;
return MEOW_FCLS_OTHER;
}
/* ---------------- 路由:一个 codepoint 应使用的槽(multi off → slot0) ---------------- */
static int route_slot(uint32_t cp) {
if (!g_multifont) return 0;
int cls = meow_font_class_of(cp);
if (cls < 0 || cls >= MEOW_FCLS_COUNT) return 0;
int s = g_class_slot[cls];
if (s >= 0 && s < MEOW_MAX_SLOTS && g_slots[s].loaded) return s;
return 0; /* 缺该类别字库/未装载 → 回退全局 slot0 */
}
/* 取某码位在路由落点槽内、对应最近字号的面 + scaleq;无可用字库返回 NULL。
* 顺带带回落点槽(供 blit 采样;不需要者传 NULL)。 */
static meow_face *routed_face(uint32_t cp, int size, unsigned *scaleq, int *slot_out) {
int slot = route_slot(cp);
if (slot_out) *slot_out = slot;
meow_font_slot *st = &g_slots[slot];
if (!st->loaded) return NULL;
meow_face *f = face_for_size(slot, size);
if (!f) return NULL;
if (scaleq) *scaleq = (size == f->px) ? 4096u : (unsigned)(((uint64_t)size << 12) / (uint64_t)f->px);
return f;
}
/* ---------------- 公共 API ---------------- */ /* ---------------- 公共 API ---------------- */
void meow_font_set_fb(unsigned fb_w, unsigned fb_h, unsigned pitch_bytes) { 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; g_fb_w = fb_w; g_fb_h = fb_h; g_fb_pitch = pitch_bytes;
}
void meow_font_unload_slot(int slot) {
if (slot < 0 || slot >= MEOW_MAX_SLOTS) return;
meow_font_slot *st = &g_slots[slot];
if (st->faces) {
for (int i = 0; i < st->face_count; i++) free(st->faces[i].glyphs);
free(st->faces);
}
free(st->plane);
st->faces = NULL; st->plane = NULL; st->face_count = 0;
st->plane_w = st->plane_h = 0; st->plane_rowbytes = 0; st->depth = 0;
st->loaded = 0;
} }
void meow_font_unload(void) { void meow_font_unload(void) {
if (g.faces) { for (int i = 0; i < MEOW_MAX_SLOTS; i++) meow_font_unload_slot(i);
for (int i = 0; i < g.face_count; i++) free(g.faces[i].glyphs); g_multifont = 0;
free(g.faces); for (int i = 0; i < MEOW_FCLS_COUNT; i++) g_class_slot[i] = 0;
}
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) { int meow_font_load_slot(int slot, const char *png_path, const char *idx_path) {
meow_font_unload(); if (slot < 0 || slot >= MEOW_MAX_SLOTS) return -9;
meow_font_unload_slot(slot);
size_t plen = 0, ilen = 0; size_t plen = 0, ilen = 0;
unsigned char *png = read_file(png_path, &plen); unsigned char *png = read_file(png_path, &plen);
if (!png) return -1; if (!png) return -1;
@@ -430,46 +507,91 @@ int meow_font_load(const char *png_path, const char *idx_path) {
free(png); free(png);
if (!plane) { free(idx); return -2; } if (!plane) { free(idx); return -2; }
g.plane = plane; meow_font_slot *st = &g_slots[slot];
g.depth = depth; st->plane = plane;
g.plane_w = w; st->depth = depth;
g.plane_h = h; st->plane_w = w;
g.plane_rowbytes = rowbytes; st->plane_h = h;
st->plane_rowbytes = rowbytes;
if (load_idx(idx, ilen) != 0) { if (load_idx(idx, ilen, slot) != 0) {
free(idx); free(idx);
meow_font_unload(); meow_font_unload_slot(slot);
return -3; return -3;
} }
free(idx); free(idx);
st->loaded = 1;
return 0; return 0;
} }
int meow_font_load(const char *png_path, const char *idx_path) {
meow_font_unload(); /* 全局字库(重)装:清全部槽 + 复位多字体配置 */
return meow_font_load_slot(0, png_path, idx_path);
}
void meow_font_enable_multifont(int on) { g_multifont = on ? 1 : 0; }
void meow_font_set_class_slot(meow_fclass cls, int slot) {
if (cls < 0 || cls >= MEOW_FCLS_COUNT) return;
g_class_slot[cls] = (slot >= 0 && slot < MEOW_MAX_SLOTS && g_slots[slot].loaded) ? slot : 0;
}
int meow_font_has_size(int size) { int meow_font_has_size(int size) {
for (int i = 0; i < g.face_count; i++) int lo = g_multifont ? 0 : 0;
if (g.faces[i].px == size) return 1; int hi = g_multifont ? MEOW_MAX_SLOTS : 1;
for (int s = lo; s < hi; s++) {
meow_font_slot *st = &g_slots[s];
if (!st->loaded) continue;
for (int i = 0; i < st->face_count; i++)
if (st->faces[i].px == size) return 1;
}
return 0;
}
int meow_font_has_glyph(uint32_t cp) {
int slot = route_slot(cp);
meow_font_slot *st = &g_slots[slot];
if (!st->loaded) return 0;
for (int i = 0; i < st->face_count; i++)
if (find_glyph(&st->faces[i], cp)) return 1;
return 0; return 0;
} }
int meow_font_ascent(int size) { int meow_font_ascent(int size) {
meow_face *f = face_for_size(size); if (g_multifont) {
int best = 0;
for (int s = 0; s < MEOW_MAX_SLOTS; s++) {
meow_face *f = face_for_size(s, size);
if (f && f->ascent > best) best = f->ascent;
}
return best;
}
meow_face *f = face_for_size(0, size);
return f ? f->ascent : 0; return f ? f->ascent : 0;
} }
int meow_font_line_height(int size) { int meow_font_line_height(int size) {
meow_face *f = face_for_size(size); if (g_multifont) {
int best = 0;
for (int s = 0; s < MEOW_MAX_SLOTS; s++) {
meow_face *f = face_for_size(s, size);
if (f) { int h = f->ascent + f->descent; if (h > best) best = h; }
}
return best;
}
meow_face *f = face_for_size(0, size);
return f ? (f->ascent + f->descent) : 0; return f ? (f->ascent + f->descent) : 0;
} }
int meow_font_width_text(const char *utf8, int size) { 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; const unsigned char *p = (const unsigned char *)utf8;
int total = 0; int total = 0;
while (*p) { while (*p) {
int cp = utf8_next(&p); int cp = utf8_next(&p);
if (cp < 0) continue; if (cp < 0) continue;
unsigned scaleq;
meow_face *f = routed_face((uint32_t)cp, size, &scaleq, NULL);
if (!f) continue;
meow_glyph *gr = find_glyph(f, (uint32_t)cp); meow_glyph *gr = find_glyph(f, (uint32_t)cp);
unsigned adv = gr ? gr->advance : advance_for_missing(f); unsigned adv = gr ? gr->advance : advance_for_missing(f);
if (scaleq == 4096) total += (int)adv; if (scaleq == 4096) total += (int)adv;
@@ -480,17 +602,18 @@ int meow_font_width_text(const char *utf8, int size) {
void meow_font_drawtext(uint16_t *fb, int x, int y, const char *utf8, void meow_font_drawtext(uint16_t *fb, int x, int y, const char *utf8,
uint16_t color, int size) { 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; const unsigned char *p = (const unsigned char *)utf8;
int pen = 0; int pen = 0;
while (*p) { while (*p) {
int cp = utf8_next(&p); int cp = utf8_next(&p);
if (cp < 0) continue; if (cp < 0) continue;
unsigned scaleq;
int slot;
meow_face *f = routed_face((uint32_t)cp, size, &scaleq, &slot);
if (!f) continue;
meow_glyph *gr = find_glyph(f, (uint32_t)cp); meow_glyph *gr = find_glyph(f, (uint32_t)cp);
if (gr && gr->w > 0 && gr->h > 0) { if (gr && gr->w > 0 && gr->h > 0) {
blit_glyph(fb, f, gr, x + pen, y, color, scaleq); blit_glyph(fb, slot, f, gr, x + pen, y, color, scaleq);
} else if (!gr) { } else if (!gr) {
/* 缺字:画空心方块 */ /* 缺字:画空心方块 */
unsigned adv = advance_for_missing(f); unsigned adv = advance_for_missing(f);
@@ -511,22 +634,74 @@ void meow_font_drawtext_outline(uint16_t *fb, int x, int y, const char *utf8,
if (ol <= 0) { meow_font_drawtext(fb, x, y, utf8, color, size); return; } 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 odx[8] = {-1,1,-1,1,0,0,-1,1};
static const int ody[8] = {-1,-1,1,1,-1,1,0,0}; static const int ody[8] = {-1,-1,1,1,-1,1,0,0};
/* 先将每个字形画 8 方向偏移描边,再画原色:逐字符处理以保证 advance 一致性。 */ /* 先将每个字形画 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; const unsigned char *p = (const unsigned char *)utf8;
int pen = 0; int pen = 0;
while (*p) { while (*p) {
int cp = utf8_next(&p); int cp = utf8_next(&p);
if (cp < 0) continue; if (cp < 0) continue;
unsigned scaleq;
int slot;
meow_face *f = routed_face((uint32_t)cp, size, &scaleq, &slot);
if (!f) continue;
meow_glyph *gr = find_glyph(f, (uint32_t)cp); meow_glyph *gr = find_glyph(f, (uint32_t)cp);
if (gr && gr->w > 0 && gr->h > 0) { if (gr && gr->w > 0 && gr->h > 0) {
for (int k = 0; k < 8; k++) for (int k = 0; k < 8; k++)
blit_glyph(fb, f, gr, x + pen + odx[k] * ol, y + ody[k] * ol, outline_color, scaleq); blit_glyph(fb, slot, f, gr, x + pen + odx[k] * ol, y + ody[k] * ol, outline_color, scaleq);
} }
int adv = gr ? gr->advance : (int)advance_for_missing(f); unsigned adv = gr ? gr->advance : advance_for_missing(f);
pen += scaleq == 4096 ? adv : (int)(((uint64_t)adv * scaleq) >> 12); pen += scaleq == 4096 ? (int)adv : (int)(((uint64_t)adv * scaleq) >> 12);
} }
meow_font_drawtext(fb, x, y, utf8, color, size); meow_font_drawtext(fb, x, y, utf8, color, size);
} }
/* ---------------- 多字体配置(去重 + ≤4 套上限,launcher 与 SDK 共用) ---------------- */
int meow_font_config_multifont(int on,
const char *global_png, const char *global_idx,
const meow_font_class_cfg cfg[MEOW_FCLS_COUNT],
int *distinct, int *truncated) {
if (distinct) *distinct = 0;
if (truncated) *truncated = 0;
(void)global_idx; /* 全局字库已由 meow_font_load(slot0) 装载,idx 此处不重读 */
/* 复位映射(保留已装载槽;未开则纯单字体) */
g_multifont = 0;
for (int i = 0; i < MEOW_FCLS_COUNT; i++) g_class_slot[i] = 0;
int g0 = g_slots[0].loaded;
if (!on) { if (distinct) *distinct = g0 ? 1 : 0; return 0; }
/* 已装载的不同字库(png 路径去重)。首项 = 全局(slot0)。 */
const char *d_png[MEOW_MAX_SLOTS];
int d_slot[MEOW_MAX_SLOTS];
int d_n = 0;
d_png[0] = global_png ? global_png : "";
d_slot[0] = 0;
d_n = 1;
g_multifont = 1;
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
const char *png = cfg[c].png;
if (!png || !png[0]) continue; /* 空 = 跟随全局 */
int found = -1;
if (global_png && !strcmp(png, global_png)) found = 0;
for (int k = 0; k < d_n && found < 0; k++)
if (!strcmp(png, d_png[k])) found = d_slot[k]; /* 同 family 去重 */
if (found >= 0) { g_class_slot[c] = found; continue; }
/* 新的一套不同字体:上限 MEOW_MAX_SLOTS(含全局)→ 截断 + WARN */
if (d_n >= MEOW_MAX_SLOTS) {
if (truncated) *truncated = 1;
g_class_slot[c] = 0; /* 该类别回退全局 */
continue;
}
int slot = d_n; /* slot0 已被全局占用 */
const char *idx = cfg[c].idx ? cfg[c].idx : "";
if (meow_font_load_slot(slot, png, idx) != 0) { /* 装载失败 → 回退全局 */
g_class_slot[c] = 0;
continue;
}
d_png[d_n] = png; d_slot[d_n] = slot; d_n++;
g_class_slot[c] = slot;
}
if (distinct) *distinct = d_n;
return 0;
}
+56
View File
@@ -51,6 +51,62 @@ void meow_font_drawtext(uint16_t *fb, int x, int y, const char *utf8,
void meow_font_drawtext_outline(uint16_t *fb, int x, int y, const char *utf8, 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); uint16_t color, uint16_t outline_color, int ol, int size);
/* ---- 字符分类器(multi_font 逐字符路由的依据;按 codepoint 归类)---- */
typedef enum {
MEOW_FCLS_CJK = 0, /* CJK 表意文字 + 扩展 / 兼容表意(U+3400-4DBF/4E00-9FFF/20000-2FA1F/…) */
MEOW_FCLS_CJK_SYM = 1, /* 全角/中文标点(U+3000-303F、U+FF00-FFEF、U+2014/2018-201D/2026、U+00B7) */
MEOW_FCLS_NUM = 2, /* 0-9 */
MEOW_FCLS_LATIN = 3, /* A-Za-z(基本拉丁字母) */
MEOW_FCLS_LATIN_SYM = 4, /* 半角标点(ASCII 非字母数字:空格/括号/引号/运算符/控制符) */
MEOW_FCLS_OTHER = 5, /* 其他(假名/韩文/他文字/扩展区等) */
MEOW_FCLS_COUNT = 6
} meow_fclass;
/* 把一个 codepoint 归类为上述之一。 */
int meow_font_class_of(uint32_t cp);
/* 校验码位是否在「将用于渲染它的字库」中存在字形(1=有,0=缺字/字库未加载→渲染为豆腐)。
* multi_font 开启时按分类路由到对应字库,关闭时仅查全局字库(slot 0)。 */
int meow_font_has_glyph(uint32_t cp);
/* ---- 多字体(MEOW_FCLS* 逐字符路由)----
* 槽位模型:slot 0 = meow_font_load() 装载的全局/默认字库(font.file);额外字库经
* meow_font_load_slot() 装载后,用 meow_font_set_class_slot() 把某字符类指到某一槽。
* meow_font_drawtext/width_text/line_height/... 在 meow_font_enable_multifont(1) 开启时
* 逐字符分类 → 用该类别对应槽的字库取字形与度量;缺该类别字库/未加载 → 回退 slot 0。
* 关闭(默认)时一律走 slot 0,行为与单字体完全一致(零额外开销)。
* 同 family 去重、≤4 套上限由 meow_font_config_multifont() 统一处理(launcher 与 SDK 共用)。 */
#define MEOW_MAX_SLOTS 5 /* slot0(全局) + ≤4 套类别字库;超出则截断并置 *truncated=1(调用方 WARN) */
int meow_font_load_slot(int slot, const char *png_path, const char *idx_path); /* 0=成功,负值失败 */
void meow_font_unload_slot(int slot);
void meow_font_enable_multifont(int on); /* 0=单字体(默认);1=逐字符分类路由 */
void meow_font_set_class_slot(meow_fclass cls, int slot); /* slot 越界/载入失败 → 该类别回退 slot 0 */
/* 一个字符类的候选字库路径(绝对路径,已由调用方按卡根解析;png=NULL/空 = 跟随全局)。 */
typedef struct {
const char *png;
const char *idx;
} meow_font_class_cfg;
/* 依据开关 + 各类候选配置多字体:同 png 路径去重(含与全局相同 → 直接用 slot 0),
* 独立字库数(含全局)> MEOW_MAX_SLOTS 时截断并置 *truncated=1(此前已装载的保留)。
* 返回 0 成功,负值失败;*distinct = 实际装载的不同字库数(含全局)。 */
int meow_font_config_multifont(int on,
const char *global_png, const char *global_idx,
const meow_font_class_cfg cfg[MEOW_FCLS_COUNT],
int *distinct, int *truncated);
/* ---- MeowPanel 预留:TF 卡字体库 .zip 导入/导出(TODO,未实现)----
* 当前仅声明占位,供 MeowPanel 后续接入;不实现、不接 zip 库。
* 调用方约定(待实现):导入=校验 .zip 内含 font.png+font.idx+font.json 三件套,
* 校验通过后解压到 TF 卡 MeowOS/fonts/<family>/(family 取自 font.json 的 family 或 zip 名,
* 冲突时覆盖需先确认);导出=把 MeowOS/fonts/<family>/ 三件套打包为 <out_zip>。
* 目录布局与 font.json schema 见 docs/MEOWOS_SPEC_REF.md §1.2/§5.2。
* 返回码:MEOW_OK=0 成功,负值错误码(沿用本模块失败负值约定)。 */
int meow_font_import_zip(const char *zip_path); /* TODO:MeowPanel */
int meow_font_export_zip(const char *family, const char *out_zip); /* TODO:MeowPanel */
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+271
View File
@@ -0,0 +1,271 @@
/*
* test_multi_font.c — MeowOS 多字体功能宿主导出单测。
*
* 验证两块(MEOWOS_SPEC_REF §5.7 多字体 / §5.2 分类路由):
* (1) meow_font_class_of() 字符分类器:各代表 codepoint 归类正确。
* (2) 逐字符路由:meow_font_drawtext/width_text/line_height/has_glyph 在 multi_font
* 开启时按类别用对应槽字库;关闭(默认)时与单字体一致;family 去重;≤4 套上限截断。
*
* 为避免依赖仓库真实字体(不同机器/来源不确定),此处用「假 atlas/stub」:离线合成两张
* 微型位图字体(合法 PNG 灰度8bit + MFNI v2 idx),各含 1-3 个字形、不同 advance,故
* 路由结果可精确断言。
* - lat(slot0/全局):'A' adv=10、'0' adv=10、' '(0x20) adv=10 —— 只有拉丁/数字/空格。
* - cjk(slot1):'中'(0x4E2D) adv=30 —— 只有 CJK。
*
* 用法:宿主 gcc -I. test_multi_font.c meow_font.c -lz -o test_multi_font && ./test_multi_font
* 退出码 0=全过;非 0=某断言失败(打印 FAIL)。
*/
#include "meow_font.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <zlib.h>
/* ---------------- 微型合成字库(假 atlas/stub) ---------------- */
static void wr_be32(unsigned char **p, uint32_t v) { (*p)[0]=v>>24;(*p)[1]=v>>16;(*p)[2]=v>>8;(*p)[3]=v; *p+=4; }
static void wr_le16(unsigned char **p, uint16_t v) { (*p)[0]=v&0xFF;(*p)[1]=v>>8; *p+=2; }
static void wr_le32(unsigned char **p, uint32_t v) { (*p)[0]=v&0xFF;(*p)[1]=(v>>8)&0xFF;(*p)[2]=(v>>16)&0xFF;(*p)[3]=(v>>24)&0xFF; *p+=4; }
static void png_chunk2(unsigned char *dst, size_t *off, const char *type,
const unsigned char *data, size_t dlen) {
unsigned char *o = dst + *off;
wr_be32(&o, (uint32_t)dlen);
memcpy(o, type, 4);
o += 4;
if (dlen) { memcpy(o, data, dlen); o += dlen; }
/* CRC = crc32(type) 续 crc32(data) */
uint32_t c = crc32(0L, (const Bytef *)type, 4);
if (dlen) c = crc32(c, (const Bytef *)data, (uInt)dlen);
wr_be32(&o, c);
*off += 12 + dlen;
}
/* 生成一张合法 PNG:w x h、灰度8bit、非隔行、filter=0 全零像素。返回 malloc 缓冲。 */
static unsigned char *make_png(int w, int h, size_t *outlen) {
const unsigned char sig[8] = {0x89,'P','N','G','\r','\n',0x1a,'\n'};
/* IHDR data (13B) */
unsigned char ihdr[13];
ihdr[0]=w>>24; ihdr[1]=w>>16; ihdr[2]=w>>8; ihdr[3]=w;
ihdr[4]=h>>24; ihdr[5]=h>>16; ihdr[6]=h>>8; ihdr[7]=h;
ihdr[8]=8; /* bit depth 8 */
ihdr[9]=0; /* color type 0 = gray */
ihdr[10]=0; ihdr[11]=0; ihdr[12]=0; /* compression/filter/interlace */
int rowbytes = w; /* depth8 gray */
size_t rawlen = (size_t)(rowbytes + 1) * h;
unsigned char *raw = (unsigned char *)calloc(1, rawlen); /* 每行 filter 字节=0 + 像素=0 */
if (!raw) return NULL;
for (int y = 0; y < h; y++) raw[y * (rowbytes + 1)] = 0;
uLongf destlen = compressBound((uLong)rawlen);
unsigned char *idat = (unsigned char *)malloc(destlen);
if (compress2(idat, &destlen, raw, (uLong)rawlen, Z_DEFAULT_COMPRESSION) != Z_OK) {
free(raw); free(idat); return NULL;
}
free(raw);
size_t total = 8 + 12 + 13 + 12 + destlen + 12;
unsigned char *out = (unsigned char *)malloc(total);
if (!out) { free(idat); return NULL; }
size_t off = 0;
memcpy(out + off, sig, 8); off += 8;
png_chunk2(out, &off, "IHDR", ihdr, 13);
png_chunk2(out, &off, "IDAT", idat, destlen);
png_chunk2(out, &off, "IEND", NULL, 0);
free(idat);
*outlen = off;
return out;
}
/* 生成 MFNI v2 idx:1 个面(px 字号),planes 覆盖全 PNG,含 nglyphs 个字(cp/adv 升序)。 */
static unsigned char *make_idx(int px, int ascent, int descent, int plane_w, int plane_h,
const uint32_t *cps, const int *advs, int nglyphs,
size_t *outlen) {
size_t len = 16 + 20 + (size_t)nglyphs * 16;
unsigned char *out = (unsigned char *)calloc(1, len);
unsigned char *o = out;
memcpy(o, "MFNI", 4); o += 4;
wr_le16(&o, 2); /* version=2 */
*o++ = 8; /* depth */
*o++ = 1; /* nfaces */
wr_le32(&o, (uint32_t)plane_w); wr_le32(&o, (uint32_t)plane_h);
/* face */
wr_le16(&o, (uint16_t)px); /* px */
wr_le16(&o, 0); /* plane_x */
wr_le16(&o, 0); /* plane_y */
wr_le16(&o, (uint16_t)plane_w); /* plane_w */
wr_le16(&o, (uint16_t)plane_h); /* plane_h */
wr_le16(&o, (uint16_t)ascent); /* ascent (s16) */
wr_le16(&o, (uint16_t)descent); /* descent (s16) */
wr_le16(&o, (uint16_t)nglyphs); /* glyph_count */
wr_le32(&o, 16 + 20); /* goff */
/* glyphs 升序 */
for (int i = 0; i < nglyphs; i++) {
wr_le32(&o, cps[i]); /* cp */
wr_le16(&o, 0); /* x */
wr_le16(&o, 0); /* y */
wr_le16(&o, (uint16_t)px); /* w */
wr_le16(&o, (uint16_t)px); /* h */
wr_le16(&o, (uint16_t)advs[i]); /* advance */
*o++ = 0; /* dx */
*o++ = 0; /* dy */
}
*outlen = len;
return out;
}
/* 写一套字库 <base>.png/.idx,返回 0 成功。 */
static int write_font(const char *base, int px, int ascent, int descent,
const uint32_t *cps, const int *advs, int nglyphs) {
char pngp[512], idxp[512];
snprintf(pngp, sizeof pngp, "%s.png", base);
snprintf(idxp, sizeof idxp, "%s.idx", base);
size_t plen = 0, ilen = 0;
unsigned char *png = make_png(64, 64, &plen);
if (!png) { fprintf(stderr, "make_png failed\n"); return -1; }
unsigned char *idx = make_idx(px, ascent, descent, 64, 64, cps, advs, nglyphs, &ilen);
if (!idx) { free(png); return -1; }
FILE *f = fopen(pngp, "wb");
if (!f) { free(png); free(idx); return -1; }
fwrite(png, 1, plen, f); fclose(f);
f = fopen(idxp, "wb");
if (!f) { free(png); free(idx); return -1; }
fwrite(idx, 1, ilen, f); fclose(f);
free(png); free(idx);
return 0;
}
/* ---------------- 断言 ---------------- */
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (!(cond)) { g_fail++; fprintf(stderr, "FAIL: %s (line %d)\n", msg, __LINE__); } \
else printf("ok : %s\n", msg); \
} while (0)
/* ---------------- 主测 ---------------- */
int main(void) {
/* 造两套 stub 字库 */
const uint32_t lat_cps[] = {0x20, 0x30, 0x41}; /* 空格 '0' 'A' */
const int lat_adv[] = {10, 10, 10};
const uint32_t cjk_cps[] = {0x4E2D}; /* '中' */
const int cjk_adv[] = {30};
if (write_font("/tmp/mf_lat", 20, 20, 6, lat_cps, lat_adv, 3) != 0) return 2;
if (write_font("/tmp/mf_cjk", 20, 25, 8, cjk_cps, cjk_adv, 1) != 0) return 2;
/* ---- (1) 字符分类器 ---- */
printf("\n== 分类器 meow_font_class_of ==\n");
CHECK(meow_font_class_of('A') == MEOW_FCLS_LATIN, "A -> LATIN");
CHECK(meow_font_class_of('z') == MEOW_FCLS_LATIN, "z -> LATIN");
CHECK(meow_font_class_of('5') == MEOW_FCLS_NUM, "5 -> NUM");
CHECK(meow_font_class_of(' ') == MEOW_FCLS_LATIN_SYM, "spc -> LATIN_SYM");
CHECK(meow_font_class_of(',') == MEOW_FCLS_LATIN_SYM, ", -> LATIN_SYM");
CHECK(meow_font_class_of('(') == MEOW_FCLS_LATIN_SYM, "( -> LATIN_SYM");
CHECK(meow_font_class_of('.') == MEOW_FCLS_LATIN_SYM, ". -> LATIN_SYM");
CHECK(meow_font_class_of(0x4E2D) == MEOW_FCLS_CJK, "U+4E2D -> CJK");
CHECK(meow_font_class_of(0x9FFF) == MEOW_FCLS_CJK, "U+9FFF -> CJK");
CHECK(meow_font_class_of(0x3400) == MEOW_FCLS_CJK, "U+3400(ExtA) -> CJK");
CHECK(meow_font_class_of(0x20000) == MEOW_FCLS_CJK, "U+20000(ExtB) -> CJK");
CHECK(meow_font_class_of(0x3001) == MEOW_FCLS_CJK_SYM, "U+3001(、) -> CJK_SYM");
CHECK(meow_font_class_of(0x3000) == MEOW_FCLS_CJK_SYM, "U+3000(全角空格) -> CJK_SYM");
CHECK(meow_font_class_of(0xFF0C) == MEOW_FCLS_CJK_SYM, "U+FF0C(,) -> CJK_SYM");
CHECK(meow_font_class_of(0xFF10) == MEOW_FCLS_CJK_SYM, "U+FF10(0) -> CJK_SYM");
CHECK(meow_font_class_of(0x2026) == MEOW_FCLS_CJK_SYM, "U+2026(…) -> CJK_SYM");
CHECK(meow_font_class_of(0x00B7) == MEOW_FCLS_CJK_SYM, "U+00B7(·) -> CJK_SYM");
CHECK(meow_font_class_of(0x3042) == MEOW_FCLS_OTHER, "U+3042(あ) -> OTHER");
CHECK(meow_font_class_of(0xAC00) == MEOW_FCLS_OTHER, "U+AC00(가) -> OTHER");
CHECK(meow_font_class_of(0xE9) == MEOW_FCLS_OTHER, "U+00E9(é) -> OTHER");
/* ---- (2) 多字体路由 ---- */
printf("\n== 单字体基线(multi off,全局=lat)==\n");
int r = meow_font_load("/tmp/mf_lat.png", "/tmp/mf_lat.idx");
CHECK(r == 0, "load global lat");
CHECK(meow_font_has_size(20), "lat has size 20");
CHECK(meow_font_width_text("A", 20) == 10, "w(A)=10 (lat)");
CHECK(meow_font_width_text("0", 20) == 10, "w(0)=10 (lat)");
/* lat 无 '中' → 缺字回退空格 adv=10 */
CHECK(meow_font_width_text("中", 20) == 10, "w(中)=10 fallback (lat no cjk)");
CHECK(meow_font_width_text("A中", 20) == 20, "w(A中)=20 (single font)");
CHECK(meow_font_has_glyph(0x4E2D) == 0, "has_glyph(中)=0 (lat lacks)");
CHECK(meow_font_has_glyph('A') == 1, "has_glyph(A)=1 (lat has)");
CHECK(meow_font_line_height(20) == 26, "lh(20)=26 (lat ascent20+desc6)");
printf("\n== 开启 multi_font(class_cjk -> cjk 槽)==\n");
meow_font_class_cfg cfg[MEOW_FCLS_COUNT];
memset(cfg, 0, sizeof cfg);
cfg[MEOW_FCLS_CJK].png = "/tmp/mf_cjk.png";
cfg[MEOW_FCLS_CJK].idx = "/tmp/mf_cjk.idx";
char gpng[64], gidx[64];
snprintf(gpng, sizeof gpng, "/tmp/mf_lat.png");
snprintf(gidx, sizeof gidx, "/tmp/mf_lat.idx");
int distinct = 0, truncd = 0;
r = meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd);
CHECK(r == 0, "config_multifont ok");
CHECK(distinct == 2, "distinct=2 (lat + cjk)");
CHECK(truncd == 0, "not truncated");
CHECK(meow_font_width_text("A", 20) == 10, "w(A)=10 (routed -> global/lat, latin class)");
CHECK(meow_font_width_text("0", 20) == 10, "w(0)=10 (num class -> global)");
CHECK(meow_font_width_text("中", 20) == 30, "w(中)=30 (routed -> cjk slot)");
CHECK(meow_font_width_text("A中", 20) == 40, "w(A中)=40 (per-char routing)");
CHECK(meow_font_has_glyph(0x4E2D) == 1, "has_glyph(中)=1 (routed -> cjk)");
CHECK(meow_font_line_height(20) == 33, "lh(20)=33 (max(lat26, cjk33))");
/* 关闭 → 立即回到单字体 */
meow_font_config_multifont(0, gpng, gidx, cfg, &distinct, &truncd);
CHECK(meow_font_width_text("A中", 20) == 20, "off -> w(A中)=20 again");
CHECK(meow_font_has_glyph(0x4E2D) == 0, "off -> has_glyph(中)=0 again");
/* drawtext smoke:吃一个混合串不崩(当前 cfg 仍为合法 cjk 配置) */
printf("\n== drawtext smoke(不影响断言,仅验证不崩)==\n");
meow_font_set_fb(64, 64, 128);
static uint16_t buf[64 * 64];
memset(buf, 0, sizeof buf);
meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd);
meow_font_drawtext(buf, 0, 0, "A中0,", 0xFFFF, 20);
printf("drew 'A中0,' ok\n");
printf("\n== family 去重 / 上限 ==\n");
/* latin 类别也指到 cjk → 应去重,distinct 仍为 2 */
memset(cfg, 0, sizeof cfg);
cfg[MEOW_FCLS_CJK].png = "/tmp/mf_cjk.png"; cfg[MEOW_FCLS_CJK].idx = "/tmp/mf_cjk.idx";
cfg[MEOW_FCLS_LATIN].png = "/tmp/mf_cjk.png"; cfg[MEOW_FCLS_LATIN].idx = "/tmp/mf_cjk.idx";
meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd);
CHECK(distinct == 2, "dedupe: latin==cjk path -> distinct=2");
CHECK(truncd == 0, "dedupe: not truncated");
/* 超上限:6 类各指向一个不同、可加载的字体(除全局)→ 第 5 个起截断。
* MEOW_MAX_SLOTS=5(global + ≤4 类别),故最多 5 套不同字库。 */
{
/* 生成 5 个额外不同字库(不同 advance,保证去重键不同) */
const uint32_t fcps[] = {0x4E2D};
int fav = 100;
char paths[8][64], idxs[8][64];
for (int i = 2; i < 7; i++) { /* 生成 /tmp/mf_trunc2..6 */
snprintf(paths[i], sizeof paths[i], "/tmp/mf_trunc%d", i);
if (write_font(paths[i], 20, 25, 8, fcps, &fav, 1) != 0) return 2;
snprintf(paths[i], sizeof paths[i], "/tmp/mf_trunc%d.png", i);
snprintf(idxs[i], sizeof idxs[i], "/tmp/mf_trunc%d.idx", i);
fav += 50;
}
char pp[6][64];
/* 6 类全部指向不同非全局字库 */
snprintf(pp[MEOW_FCLS_CJK], sizeof pp[0], "/tmp/mf_trunc2.png");
snprintf(pp[MEOW_FCLS_CJK_SYM], sizeof pp[0], "/tmp/mf_trunc3.png");
snprintf(pp[MEOW_FCLS_NUM], sizeof pp[0], "/tmp/mf_trunc4.png");
snprintf(pp[MEOW_FCLS_LATIN], sizeof pp[0], "/tmp/mf_trunc5.png");
snprintf(pp[MEOW_FCLS_LATIN_SYM], sizeof pp[0], "/tmp/mf_trunc6.png");
snprintf(pp[MEOW_FCLS_OTHER], sizeof pp[0], "/tmp/mf_trunc2.png"); /* 与 CJK 同 → 去重,不新增 */
for (int i = 0; i < 6; i++) { cfg[i].png = pp[i]; cfg[i].idx = NULL; }
/* 修 idx:write_font 已生对应 .idx,需给出 */
char ppi[6][64];
const char *maps[6] = {"/tmp/mf_trunc2", "/tmp/mf_trunc3", "/tmp/mf_trunc4",
"/tmp/mf_trunc5", "/tmp/mf_trunc6", "/tmp/mf_trunc2"};
for (int i = 0; i < 6; i++) {
snprintf(ppi[i], sizeof ppi[i], "%s.idx", maps[i]);
cfg[i].idx = ppi[i];
}
meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd);
CHECK(truncd == 1, "6 distinct -> truncated=1");
CHECK(distinct == MEOW_MAX_SLOTS, "distinct capped at MEOW_MAX_SLOTS");
}
printf("\n%s (%d failures)\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS", g_fail);
return g_fail ? 1 : 0;
}
+1 -1
View File
@@ -2,7 +2,7 @@
目标设备:R3 Pro II / MeowOS 固件(**mipsel-linux-muslsf**,静态,无 glibc 依赖)。 目标设备:R3 Pro II / MeowOS 固件(**mipsel-linux-muslsf**,静态,无 glibc 依赖)。
用途:存放针对该设备的交叉编译二进制与构建信息,便于**复用**到后续固件 / TF 卡 用途:存放针对该设备的交叉编译二进制与构建信息,便于**复用**到后续固件 / TF 卡
(MeowLib / MeowOpt 目录)。 (MeowOpt 目录;`MeowLib` 未定义/未用——`MeowOpt` 与 `MeowOS/opt` 命名需归一:代码用 `MeowOS/opt`,文档 `MeowOpt` 指卡上外部工具目录。[源:审计 2026-08-30])。
## 仓库结构(每工具一个目录) ## 仓库结构(每工具一个目录)
``` ```
-1
View File
@@ -1 +0,0 @@
{"name":"Hello App","exec":"/usr/data/mnt/sd_0/MeowAPPs/hello/hello","version":"0.1","immersive":1}
@@ -0,0 +1 @@
{"name":"Hello App","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello","package":"os.meow.hello","version":"0.1","framework":1,"immersive":1}
Binary file not shown.
+57 -39
View File
@@ -13,7 +13,7 @@
* *
* M2 诊断 · 信号记录 hook(仅记录,不改行为;排查「HibyOS 播放器 UI 触发关机/升级 → * M2 诊断 · 信号记录 hook(仅记录,不改行为;排查「HibyOS 播放器 UI 触发关机/升级 →
* 卡最后一帧但系统没真关机/SSH 能连」第一步):supervisor 加 bm_log() 写 TF 卡 * 卡最后一帧但系统没真关机/SSH 能连」第一步):supervisor 加 bm_log() 写 TF 卡
* MeowAPPs/logs/meow_boot_<bootts>.log(卡不可用降级 stderr),记录 supervisor * MeowOS/logs/meow_boot_<bootts>.log(卡不可用降级 stderr),记录 supervisor
* **收到**的信号(SIGTERM/SIGINT/SIGQUIT/SIGHUP——handler 只置标志,主循环 log 后 * **收到**的信号(SIGTERM/SIGINT/SIGQUIT/SIGHUP——handler 只置标志,主循环 log 后
* signal(s,SIG_DFL)+raise(s) 恢复默认重抛,与未加 hook 一致终止)、**发出**给播放器的 * signal(s,SIG_DFL)+raise(s) 恢复默认重抛,与未加 hook 一致终止)、**发出**给播放器的
* 信号(SIGSTOP/SIGCONT/SIGTERM/SIGKILL)、以及**播放器退出原因**(WTERMSIG/WEXITSTATUS)。 * 信号(SIGSTOP/SIGCONT/SIGTERM/SIGKILL)、以及**播放器退出原因**(WTERMSIG/WEXITSTATUS)。
@@ -95,7 +95,7 @@
#define DEV_TOUCH "/dev/input/event1" /* goodix-ts(未 grab,可共享读) */ #define DEV_TOUCH "/dev/input/event1" /* goodix-ts(未 grab,可共享读) */
#define FB_DEV "/dev/fb0" #define FB_DEV "/dev/fb0"
#define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs" #define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs"
#define CONF_FILE MEOWAPPS "/meow.conf" /* TF 卡配置:boot_default/boot_timeout_ms/boot_menu */ #define CONF_FILE MEOWAPPS "/meow.conf" /* TF 卡配置:boot_timeout_ms/boot_menu/bg_color;boot_default 权威在板载 /data/meow_boot_default */
#define MODE_FILE "/tmp/meow_boot_mode" /* 开机模式标记(tmpfs,开机清空) */ #define MODE_FILE "/tmp/meow_boot_mode" /* 开机模式标记(tmpfs,开机清空) */
#define PLAYER_BIN "/usr/bin/hiby_player" #define PLAYER_BIN "/usr/bin/hiby_player"
#define PLAYER_LOG "/usr/data/meow_player.log" #define PLAYER_LOG "/usr/data/meow_player.log"
@@ -410,7 +410,7 @@ static int amixer_vol(int numid)
return v; return v;
} }
/* 40px 状态栏:行1=时钟(左) 电量% WIFI BT(右),行2=VOL nn%(左),y=40 分隔线 */ /* 28px 状态栏:行1=时钟(左) 电量% WIFI BT(右),行2=VOL nn%(左),y=28 分隔线(40→28,用户定) */
static void draw_statusbar(uint32_t bg) static void draw_statusbar(uint32_t bg)
{ {
char timebuf[16], right[48]; char timebuf[16], right[48];
@@ -441,16 +441,16 @@ static void draw_statusbar(uint32_t bg)
char bt[8] = ""; char bt[8] = "";
if (bluetooth_present()) snprintf(bt, sizeof bt, "BT"); if (bluetooth_present()) snprintf(bt, sizeof bt, "BT");
draw_text(8, 4, timebuf, 1, 0x9FB8D9); draw_text(8, 2, timebuf, 1, 0x9FB8D9);
if (batt[0]) strcat(right, batt); if (batt[0]) strcat(right, batt);
if (wifi[0]) { if (right[0]) strcat(right, " "); strcat(right, wifi); } if (wifi[0]) { if (right[0]) strcat(right, " "); strcat(right, wifi); }
if (bt[0]) { if (right[0]) strcat(right, " "); strcat(right, bt); } if (bt[0]) { if (right[0]) strcat(right, " "); strcat(right, bt); }
if (right[0]) { if (right[0]) {
int rx = fb_xres - 8 - (int)strlen(right) * 6; int rx = fb_xres - 8 - (int)strlen(right) * 6;
draw_text(rx, 4, right, 1, 0x9FB8D9); draw_text(rx, 2, right, 1, 0x9FB8D9);
} }
if (vol[0]) draw_text(8, 22, vol, 1, 0x9FB8D9); if (vol[0]) draw_text(8, 14, vol, 1, 0x9FB8D9);
fill_rect(0, 40, fb_xres, 1, 0x333355); /* 分隔线(移至 y=40) */ fill_rect(0, 28, fb_xres, 1, 0x333355); /* 分隔线(移至 y=28) */
} }
/* 只刷时钟数字(每秒一次,不弹 amixer/不碰菜单体) */ /* 只刷时钟数字(每秒一次,不弹 amixer/不碰菜单体) */
@@ -463,43 +463,70 @@ static void draw_clock(uint32_t bg)
snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d",
tm.tm_hour, tm.tm_min, tm.tm_sec); tm.tm_hour, tm.tm_min, tm.tm_sec);
int w = (int)strlen(timebuf) * 6; int w = (int)strlen(timebuf) * 6;
fill_rect(4, 2, 8 + w, 12, bg); fill_rect(4, 0, 8 + w, 12, bg);
draw_text(8, 4, timebuf, 1, 0x9FB8D9); draw_text(8, 2, timebuf, 1, 0x9FB8D9);
} }
/* ================================================================== */ /* ================================================================== */
/* 配置:boot_default / boot_timeout_ms / boot_menu(从 TF 卡尽力读) */ /* 配置:boot_default / boot_timeout_ms / boot_menu / bg_color */
/* boot_default:权威 = 板载 /data/meow_boot_default(O_TRUNC 单值、不写 TF 卡);
* meow.conf 的 boot_default= 仅作无板载标记时的兜底(不新增写入,见 set_boot_default_meow)。 */
/* ================================================================== */ /* ================================================================== */
static int cfg_boot_default = DEF_BOOT_DEFAULT; static int cfg_boot_default = DEF_BOOT_DEFAULT;
static long cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS; static long cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS;
static int cfg_boot_menu = DEF_BOOT_MENU; static int cfg_boot_menu = DEF_BOOT_MENU;
static uint32_t cfg_bg = DEF_BG; static uint32_t cfg_bg = DEF_BG;
/* 读板载 /data/meow_boot_default(开机默认权威标记)。返回 0/1;-1=不存在/非法。 */
static int read_boot_default_onboard(void)
{
int fd = open("/data/meow_boot_default", O_RDONLY);
if (fd < 0) return -1;
char buf[8];
int n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) return -1;
buf[n] = 0;
int v = atoi(buf);
return (v == 0 || v == 1) ? v : -1;
}
static void load_config(void) static void load_config(void)
{ {
cfg_boot_default = DEF_BOOT_DEFAULT; cfg_boot_default = DEF_BOOT_DEFAULT;
cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS; cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS;
cfg_boot_menu = DEF_BOOT_MENU; cfg_boot_menu = DEF_BOOT_MENU;
cfg_bg = DEF_BG; cfg_bg = DEF_BG;
int has_conf = 1;
FILE *f = fopen(CONF_FILE, "r"); FILE *f = fopen(CONF_FILE, "r");
if (!f) { fprintf(stderr, "bootmenu: no %s, using defaults\n", CONF_FILE); return; } if (!f) { fprintf(stderr, "bootmenu: no %s, using defaults\n", CONF_FILE); has_conf = 0; }
char buf[128]; if (has_conf) {
while (fgets(buf, sizeof buf, f)) { char buf[128];
if (!strncmp(buf, "boot_default=", 13)) { while (fgets(buf, sizeof buf, f)) {
cfg_boot_default = atoi(buf + 13); if (!strncmp(buf, "boot_default=", 13)) {
if (cfg_boot_default != 0 && cfg_boot_default != 1) cfg_boot_default = DEF_BOOT_DEFAULT; /* 板载 /data/meow_boot_default 为权威;此处仅作无板载标记时的兜底(不再新增写入)。 */
} else if (!strncmp(buf, "boot_timeout_ms=", 16)) { cfg_boot_default = atoi(buf + 13);
long t = atol(buf + 16); if (cfg_boot_default != 0 && cfg_boot_default != 1) cfg_boot_default = DEF_BOOT_DEFAULT;
if (t >= 500 && t <= 60000) cfg_boot_timeout_ms = t; } else if (!strncmp(buf, "boot_timeout_ms=", 16)) {
} else if (!strncmp(buf, "boot_menu=", 10)) { long t = atol(buf + 16);
int m = atoi(buf + 10); if (t >= 500 && t <= 60000) cfg_boot_timeout_ms = t;
if (m == 0 || m == 1) cfg_boot_menu = m; } else if (!strncmp(buf, "boot_menu=", 10)) {
} else if (!strncmp(buf, "bg_color=", 9)) { int m = atoi(buf + 10);
unsigned int v; if (m == 0 || m == 1) cfg_boot_menu = m;
if (sscanf(buf + 9, "%x", &v) == 1) cfg_bg = v & 0xFFFFFF; } else if (!strncmp(buf, "bg_color=", 9)) {
unsigned int v;
if (sscanf(buf + 9, "%x", &v) == 1) cfg_bg = v & 0xFFFFFF;
}
} }
fclose(f);
}
/* 板载标记优先(权威):覆盖 meow.conf 兜底/编译默认。 */
int ob = read_boot_default_onboard();
if (ob == 0 || ob == 1) {
if (ob != cfg_boot_default)
fprintf(stderr, "bootmenu: boot_default onboard=%d (override %d)\n", ob, cfg_boot_default);
cfg_boot_default = ob;
} }
fclose(f);
fprintf(stderr, "bootmenu: cfg boot_default=%d timeout_ms=%ld boot_menu=%d bg=%06x\n", 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); cfg_boot_default, cfg_boot_timeout_ms, cfg_boot_menu, cfg_bg);
} }
@@ -519,10 +546,10 @@ static unsigned long long now_ms(void)
* - 借鉴 launcher log_open_card/logmsg 思路,但**不共享 /tmp/meow_bootts** * - 借鉴 launcher log_open_card/logmsg 思路,但**不共享 /tmp/meow_bootts**
* (launcher 缓存的是完整基名 meow_<ts>,直接复读会拼成 meow_boot_meow_…); * (launcher 缓存的是完整基名 meow_<ts>,直接复读会拼成 meow_boot_meow_…);
* 每次自行按 /proc/uptime 推开机时间戳,同一 boot 内文件名恒定。 * 每次自行按 /proc/uptime 推开机时间戳,同一 boot 内文件名恒定。
* - 写 /usr/data/mnt/sd_0/MeowAPPs/logs/meow_boot_<bootts>.log(O_APPEND|O_CREAT 0644)。 * - 写 /usr/data/mnt/sd_0/MeowOS/logs/meow_boot_<bootts>.log(O_APPEND|O_CREAT 0644)。
* - 卡不可用(未挂载/不可写)→ 降级 stderr。**只记录,不改任何行为。** * - 卡不可用(未挂载/不可写)→ 降级 stderr。**只记录,不改任何行为。**
* ================================================================== */ * ================================================================== */
#define BM_LOG_DIR "/usr/data/mnt/sd_0/MeowAPPs/logs" #define BM_LOG_DIR "/usr/data/mnt/sd_0/MeowOS/logs"
static int bm_log_fd = -1; static int bm_log_fd = -1;
static char bm_log_file[256] = ""; static char bm_log_file[256] = "";
@@ -1287,21 +1314,12 @@ static void write_mode(int choice)
/* Bug 1:选 MeowOS → 把它持久为开机默认(写 spec 标记 + meow.conf boot_default=0),与 launcher /* Bug 1:选 MeowOS → 把它持久为开机默认(写 spec 标记 + meow.conf boot_default=0),与 launcher
* «回到HibyOS»(set_boot_default_hiby 写 boot_default=1)对称。效果:本次选中即进 MeowOS 主屏,且 * «回到HibyOS»(set_boot_default_hiby 写 boot_default=1)对称。效果:本次选中即进 MeowOS 主屏,且
* 下一次开机选单默认 MeowOS(不再「选 MeowOS 仍落回 HibyOS」)。追加一行,bootmenu 逐行读、最后一行生效。 */ * 下一次开机选单默认 MeowOS(不再「选 MeowOS 仍落回 HibyOS」)。
* 权威标记 = 板载 /data/meow_boot_default(O_TRUNC 单值);meow.conf 不再写/承载 boot_default。 */
static void set_boot_default_meow(void) static void set_boot_default_meow(void)
{ {
int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644); 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"); } 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));
}
} }
/* ================================================================== */ /* ================================================================== */
+7 -7
View File
@@ -27,8 +27,8 @@ ssh_cmd 'echo "instances=$(ps | grep hiby_player | grep -v grep | grep -v sh | w
echo "==> 推送文件" 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/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/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/os.meow.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 scp -i "$KEY" -o StrictHostKeyChecking=no "$ROOT/apps_assets/apps/os.meow.hello/app.json" root@$IP:/data/hello_app.json
echo "==> 设备侧组装" echo "==> 设备侧组装"
ssh_cmd ' ssh_cmd '
@@ -36,12 +36,12 @@ set -e
if ! grep -q "sd_0" /proc/mounts; then if ! grep -q "sd_0" /proc/mounts; then
echo "!! TF 卡未挂载"; mount /dev/mmcblk0p1 /usr/data/mnt/sd_0 2>/dev/null || echo "!! 挂载失败" echo "!! TF 卡未挂载"; mount /dev/mmcblk0p1 /usr/data/mnt/sd_0 2>/dev/null || echo "!! 挂载失败"
fi fi
mkdir -p /usr/data/mnt/sd_0/MeowAPPs/hello /data/overlay mkdir -p /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello /data/overlay
cp -f /data/hello.bin /usr/data/mnt/sd_0/MeowAPPs/hello/hello cp -f /data/hello.bin /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello
cp -f /data/hello_app.json /usr/data/mnt/sd_0/MeowAPPs/hello/app.json cp -f /data/hello_app.json /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/app.json
chmod 755 /usr/data/mnt/sd_0/MeowAPPs/hello/hello chmod 755 /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello
chmod 755 /usr/data/meow_watchdog chmod 755 /usr/data/meow_watchdog
ls /usr/data/mnt/sd_0/MeowAPPs/hello/ ls /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/
rm -rf /data/overlay/theme1 /data/overlay/str_zh /data/overlay/str_en 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/layout/theme1 /data/overlay/theme1
cp -a /usr/resource/str/simplified_chinese /data/overlay/str_zh cp -a /usr/resource/str/simplified_chinese /data/overlay/str_zh
+56
View File
@@ -51,6 +51,62 @@ void meow_font_drawtext(uint16_t *fb, int x, int y, const char *utf8,
void meow_font_drawtext_outline(uint16_t *fb, int x, int y, const char *utf8, 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); uint16_t color, uint16_t outline_color, int ol, int size);
/* ---- 字符分类器(multi_font 逐字符路由的依据;按 codepoint 归类)---- */
typedef enum {
MEOW_FCLS_CJK = 0, /* CJK 表意文字 + 扩展 / 兼容表意(U+3400-4DBF/4E00-9FFF/20000-2FA1F/…) */
MEOW_FCLS_CJK_SYM = 1, /* 全角/中文标点(U+3000-303F、U+FF00-FFEF、U+2014/2018-201D/2026、U+00B7) */
MEOW_FCLS_NUM = 2, /* 0-9 */
MEOW_FCLS_LATIN = 3, /* A-Za-z(基本拉丁字母) */
MEOW_FCLS_LATIN_SYM = 4, /* 半角标点(ASCII 非字母数字:空格/括号/引号/运算符/控制符) */
MEOW_FCLS_OTHER = 5, /* 其他(假名/韩文/他文字/扩展区等) */
MEOW_FCLS_COUNT = 6
} meow_fclass;
/* 把一个 codepoint 归类为上述之一。 */
int meow_font_class_of(uint32_t cp);
/* 校验码位是否在「将用于渲染它的字库」中存在字形(1=有,0=缺字/字库未加载→渲染为豆腐)。
* multi_font 开启时按分类路由到对应字库,关闭时仅查全局字库(slot 0)。 */
int meow_font_has_glyph(uint32_t cp);
/* ---- 多字体(MEOW_FCLS* 逐字符路由)----
* 槽位模型:slot 0 = meow_font_load() 装载的全局/默认字库(font.file);额外字库经
* meow_font_load_slot() 装载后,用 meow_font_set_class_slot() 把某字符类指到某一槽。
* meow_font_drawtext/width_text/line_height/... 在 meow_font_enable_multifont(1) 开启时
* 逐字符分类 → 用该类别对应槽的字库取字形与度量;缺该类别字库/未加载 → 回退 slot 0。
* 关闭(默认)时一律走 slot 0,行为与单字体完全一致(零额外开销)。
* 同 family 去重、≤4 套上限由 meow_font_config_multifont() 统一处理(launcher 与 SDK 共用)。 */
#define MEOW_MAX_SLOTS 5 /* slot0(全局) + ≤4 套类别字库;超出则截断并置 *truncated=1(调用方 WARN) */
int meow_font_load_slot(int slot, const char *png_path, const char *idx_path); /* 0=成功,负值失败 */
void meow_font_unload_slot(int slot);
void meow_font_enable_multifont(int on); /* 0=单字体(默认);1=逐字符分类路由 */
void meow_font_set_class_slot(meow_fclass cls, int slot); /* slot 越界/载入失败 → 该类别回退 slot 0 */
/* 一个字符类的候选字库路径(绝对路径,已由调用方按卡根解析;png=NULL/空 = 跟随全局)。 */
typedef struct {
const char *png;
const char *idx;
} meow_font_class_cfg;
/* 依据开关 + 各类候选配置多字体:同 png 路径去重(含与全局相同 → 直接用 slot 0),
* 独立字库数(含全局)> MEOW_MAX_SLOTS 时截断并置 *truncated=1(此前已装载的保留)。
* 返回 0 成功,负值失败;*distinct = 实际装载的不同字库数(含全局)。 */
int meow_font_config_multifont(int on,
const char *global_png, const char *global_idx,
const meow_font_class_cfg cfg[MEOW_FCLS_COUNT],
int *distinct, int *truncated);
/* ---- MeowPanel 预留:TF 卡字体库 .zip 导入/导出(TODO,未实现)----
* 当前仅声明占位,供 MeowPanel 后续接入;不实现、不接 zip 库。
* 调用方约定(待实现):导入=校验 .zip 内含 font.png+font.idx+font.json 三件套,
* 校验通过后解压到 TF 卡 MeowOS/fonts/<family>/(family 取自 font.json 的 family 或 zip 名,
* 冲突时覆盖需先确认);导出=把 MeowOS/fonts/<family>/ 三件套打包为 <out_zip>。
* 目录布局与 font.json schema 见 docs/MEOWOS_SPEC_REF.md §1.2/§5.2。
* 返回码:MEOW_OK=0 成功,负值错误码(沿用本模块失败负值约定)。 */
int meow_font_import_zip(const char *zip_path); /* TODO:MeowPanel */
int meow_font_export_zip(const char *family, const char *out_zip); /* TODO:MeowPanel */
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
Binary file not shown.
+596 -77
View File
@@ -1,12 +1,25 @@
/* meow_launcherd.c — R3 Pro II Plan B zero-patch app launcher daemon. /* meow_launcherd.c — R3 Pro II Plan B zero-patch app launcher daemon.
*
* M2.53 亮度接管修正(开机/切到 MeowOS 亮度被重置为 25/16):
* - 根因:HibyOS 播放器运行/恢复画面会把它自己的背光下限(16/25)写进 sysfs,
* MeowOS 主屏接管时又以 read_brightness() 读到的该值当「当前亮度」
* (saved_brightness / meow_saved_brightness),从不重应用用户 display.brightness,
* 故用户设的低亮度(<25,最低到 1)被覆盖。
* - 修法:run_menu 主屏接管(had_player==0,播放器已死=真接管)时调
* apply_user_brightness():读 settings display.brightness(经新导出的
* meow_settings_brightness_clamped(),clamp 下限 brightness_min 默认 1、上限 100)
* → write_brightness 强写回,覆盖播放器残留的 16/25。
* - 边界:播放器会话期间的覆盖菜单(had_player==1)不压回——播电影时亮度归播放器管;
* ensure_backlight/restore_backlight 仍用 saved_brightness(覆盖会话=播放器真实值),
* 不改为用户值,避免菜单关闭瞬间抢播放器背光。
* *
* M2.46-2 状态栏 5 问题(2026-08-29): * M2.46-2 状态栏 5 问题(2026-08-29):
* ① 亮度图标主题色(on_surface_variant 灰 → on_surface 主色)+ 拖动亮度滑条时状态栏百分比实时跟: * ① 亮度图标主题色(on_surface_variant 灰 → on_surface 主色)+ 拖动亮度滑条时状态栏百分比实时跟:
* 根因 s_commit brightness 分支只 write_brightness 未同步 g_sys.brightness/未重绘;补 * 根因 s_commit brightness 分支只 write_brightness 未同步 g_sys.brightness/未重绘;补
* g_sys.brightness=v + paint_sb_right()(与随后 meow_sys_refresh 从 sysfs 回读一致不打架)。 * g_sys.brightness=v + paint_sb_right()(与随后 meow_sys_refresh 从 sysfs 回读一致不打架)。
* ② 右簇音/电顺序互换:最右由「音量」改「电量」(绘制顺序先电量后音量,右→左:电→音→蓝牙→WiFi→亮度)。 * ② 右簇音/电顺序互换:最右由「音量」改「电量」(绘制顺序先电量后音量,右→左:电→音→蓝牙→WiFi→亮度)。
* ③ 图标视觉对齐:sb_blit_icon 按 ink 边界居中 + 主图标等比等大(SB_ICON_INK=24),音量不再「小一圈/ * ③ 图标视觉对齐:sb_blit_icon 按 ink 边界居中 + 主图标等比等大(SB_ICON_INK=16),音量不再「小一圈/
* 下边距」;ink 垂直中心对齐状态栏文字中心(g_sb_icon_cy=SB_ANCHOR_Y+字行高/2),图文同中线。 * 下边距」;ink 垂直中心对齐状态栏文字中心(g_sb_icon_cy=SB_ICON_CY=14,见 sb_text_y),图文同中线。
* ④ WiFi 图标随信号分档:新增 wifi_signal_level() 读 /proc/net/wireless wlan0 level(dBm), * ④ WiFi 图标随信号分档:新增 wifi_signal_level() 读 /proc/net/wireless wlan0 level(dBm),
* ≥-60 强 / [-60,-75) 中 / <-75 弱,选 SB_ICON_WIFI_STRONG/MEDIUM/WEAK(新位图追加进 sb_icons.h); * ≥-60 强 / [-60,-75) 中 / <-75 弱,选 SB_ICON_WIFI_STRONG/MEDIUM/WEAK(新位图追加进 sb_icons.h);
* 强弱档保留自然 ink(少一格即语义),满格/强档走等大;wifi_on 为假不画(保持现状)。 * 强弱档保留自然 ink(少一格即语义),满格/强档走等大;wifi_on 为假不画(保持现状)。
@@ -765,15 +778,25 @@ static void draw_text(int x, int y, const char *s, int scale, uint32_t color)
/* ================================================================== */ /* ================================================================== */
/* M2.6 meow_font 集成(agent-28 字体子系统,font/meow_font.a + libz.a)*/ /* M2.6 meow_font 集成(agent-28 字体子系统,font/meow_font.a + libz.a)*/
/* 字库放 TF 卡 MeowAPPs/fonts/font.png+font.idx(或固件 /usr/share/ */ /* 字库放 TF 卡 MeowOS/fonts/default/font.png+font.idx(或固件 /usr/share/ */
/* meowos/font/);MeowOS 启动 ensure_card_mounted 后再 load,卡晚 */ /* meowos/font/);MeowOS 启动 ensure_card_mounted 后再 load,卡晚 */
/* 挂载时轮询重试。没加载成功时 ui_draw 回退 5x7 点阵(ASCII)。 */ /* 挂载时轮询重试。没加载成功时 ui_draw 回退 5x7 点阵(ASCII)。 */
/* ================================================================== */ /* ================================================================== */
#define UI_FONT_PNG_CARD MEOWAPPS "/fonts/font.png" #define UI_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/font.png"
#define UI_FONT_IDX_CARD MEOWAPPS "/fonts/font.idx" #define UI_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/font.idx"
#define UI_FONT_PNG_SYS "/usr/share/meowos/font/font.png" #define UI_FONT_PNG_SYS "/usr/share/meowos/font/font.png"
#define UI_FONT_IDX_SYS "/usr/share/meowos/font/font.idx" #define UI_FONT_IDX_SYS "/usr/share/meowos/font/font.idx"
/* M2.5x:TF 卡多字体库 —— 字体目录 = <卡根>/MeowOS/fonts/<family>/(font.png+font.idx+font.json)。
* <卡根> 默认 /usr/data/mnt/sd_0(card_present() 判定;--fontscan 可指定宿主根以便单测)。 */
#define MEOW_FONTS_CFG "MeowOS/fonts"
#define FONT_PRESETS_MAX 17 /* 1 默认 + ≤16 卡内字体(防爆) */
static const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */
static const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签 */
static char font_preset_val_buf[FONT_PRESETS_MAX][320]; /* 值持久存储(指向上面指针数组) */
static char font_preset_label_buf[FONT_PRESETS_MAX][64]; /* 标签持久存储 */
static int font_presets_n = 0; /* 运行时动态计数(默认=1,仅"默认字体") */
static int mfont_ready = 0; static int mfont_ready = 0;
static int mfont_forced = 0; /* font.file 变更→强制真实(un)load,跳过 SDK 复用捷径 */
/* 24-bit RGB → RGB565(与 put_px/meow_font 一致) */ /* 24-bit RGB → RGB565(与 put_px/meow_font 一致) */
static uint16_t rgb565(uint32_t c) static uint16_t rgb565(uint32_t c)
@@ -787,23 +810,154 @@ static void meow_font_bind_fb(void)
meow_font_set_fb(fb_xres, fb_yres, fb_line); meow_font_set_fb(fb_xres, fb_yres, fb_line);
} }
/* 尝试加载字库(卡上 + 固件);成功返回 1。 /* 把 font.file 存的「卡根相对/绝对」路径解析为设备绝对路径。
* 相对路径以 /usr/data/mnt/sd_0/ 为卡根(与 MEOWAPPS 同源);绝对路径原样。
* 返回 0 成功,-1 参数无效。 */
static int meow_font_resolve_path(const char *ref, char *out, int olen)
{
if (!ref || !ref[0]) return -1;
if (ref[0] == '/') { snprintf(out, olen, "%s", ref); return 0; }
snprintf(out, olen, "/usr/data/mnt/sd_0/%s", ref);
return 0;
}
/* 解析 font.file 指向的 png + 派生同目录 idx(font.png → font.idx)。
* 仅当指向的 png 存在(access R_OK)返回 0;否则 -1(交由回退链)。
* 返回 0:out_png/out_idx 已填设备绝对路径。 */
static int meow_font_resolve_selected(char *out_png, int png_len, char *out_idx, int idx_len)
{
char ref[MEOW_SET_MAX_LEN];
if (meow_settings_get_str("font.file", ref, sizeof ref) != 0) return -1;
if (meow_font_resolve_path(ref, out_png, png_len) != 0) return -1;
if (access(out_png, R_OK) != 0) return -1; /* 指向不存在 → 回退卡默认/固件 */
snprintf(out_idx, idx_len, "%s", out_png); /* 同目录 font.idx(标准三件套布局) */
size_t n = strlen(out_idx);
if (n > 4 && !strcmp(out_idx + n - 4, ".png")) {
out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd'; out_idx[n-1] = 'x'; out_idx[n] = 0;
}
return 0;
}
/* 尝试加载字库;成功返回 1。优先级(M2.5x):
* 1) font.file(设置键,真正接管加载;相对路径按卡根解析)→ 存在且能 load 即用;
* 2) 卡 MeowOS/fonts/default/font.png(默认字库);
* 3) 固件 /usr/share/meowos/font/font.png。
* A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。 * A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。
* 若 SDK 侧 g_mfont_ready==1(已加载),直接复用,避免重复 unload+reload;否则(卡晚挂载) * 当 font.file 指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload;
* 这里按原逻辑重试(launcher 的 mfont_ready 是独立标志,用于 CN/EN 文案选择与 ui_draw 判定)。 */ * 当 font.file 指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。
* mfont_forced=1(font.file 变更触发)时无条件真实 reload,跳过上述两条捷径。
* 全失败 → mfont_ready=0,ui_draw 回退 5x7 点阵(系统不死)。 */
static int meow_font_ensure(void) static int meow_font_ensure(void)
{ {
if (mfont_ready) return 1; if (mfont_ready && !mfont_forced) return 1;
if (meow_gfx_mfont_ready()) { mfont_ready = 1; return 1; } /* SDK 已加载,复用 */ mfont_forced = 0;
char fpng[320], fidx[320];
int have_sel = (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0);
/* SDK 已加载且所选字库为「默认卡字库」(或未设置)→ 复用,避免重复解码。 */
if (meow_gfx_mfont_ready() && (!have_sel || !strcmp(fpng, UI_FONT_PNG_CARD))) {
mfont_ready = 1;
logmsg("meow_font: reuse sdk-loaded font");
return 1;
}
/* font.file 优先。 */
if (have_sel && meow_font_load(fpng, fidx) == 0) {
mfont_ready = 1;
logmsg("meow_font: loaded font.file %s (%d/%d sizes)",
fpng, meow_font_has_size(28), meow_font_has_size(20));
return 1;
}
if (have_sel)
logmsg("meow_font: font.file %s load failed, fallback", fpng);
/* 卡默认 → 固件。 */
if (meow_font_load(UI_FONT_PNG_CARD, UI_FONT_IDX_CARD) == 0 || if (meow_font_load(UI_FONT_PNG_CARD, UI_FONT_IDX_CARD) == 0 ||
meow_font_load(UI_FONT_PNG_SYS, UI_FONT_IDX_SYS) == 0) { meow_font_load(UI_FONT_PNG_SYS, UI_FONT_IDX_SYS) == 0) {
mfont_ready = 1; mfont_ready = 1;
logmsg("meow_font: loaded (%d/%d sizes)", meow_font_has_size(28), meow_font_has_size(20)); logmsg("meow_font: loaded fallback (%d/%d sizes)",
meow_font_has_size(28), meow_font_has_size(20));
return 1; return 1;
} }
return 0; return 0;
} }
/* 求「当前生效字体」的 png/idx 绝对路径 + family,供 launch_app 注入 MEOW_FONT_PNG/IDX/APP。
* 优先级(与 meow_font_ensure 一致):font.file(resolve_selected)→ 卡默认 MeowOS/fonts/default。
* family 取自 MeowOS/fonts/<family>/font.png 目录名;无法判定则 "default"。
* 返回 0 成功(out_png/out_idx/out_family 已填),-1 无卡/无可用字体(调用方跳过注入,
* SDK 自身会回退卡默认→固件→点阵)。 */
static int meow_font_effective_paths(char *out_png, int plen, char *out_idx, int ilen,
char *out_family, int flen)
{
char fpng[400], fidx[400];
if (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0) {
snprintf(out_png, plen, "%s", fpng);
snprintf(out_idx, ilen, "%s", fidx);
const char *prefix = "/MeowOS/fonts/";
const char *p = strstr(fpng, prefix);
if (p) {
const char *q = p + strlen(prefix);
const char *end = strrchr(q, '/');
int rest = end ? (int)(end - q) : 0;
if (rest > 0 && rest < flen) { memcpy(out_family, q, rest); out_family[rest] = 0; }
else snprintf(out_family, flen, "default");
} else snprintf(out_family, flen, "default");
return 0;
}
/* 回退卡默认 MeowOS/fonts/default */
if (access(UI_FONT_PNG_CARD, R_OK) == 0) {
snprintf(out_png, plen, "%s", UI_FONT_PNG_CARD);
snprintf(out_idx, ilen, "%s", UI_FONT_IDX_CARD);
snprintf(out_family, flen, "default");
return 0;
}
return -1;
}
/* M2.5x 多字体:从 settings 读 display.multi_font + 6 类 font.class_*,把各自相对路径按卡根
* 解析为绝对路径,去重装载到 meow_font 槽位(同 family 去重 & ≤4 套上限由
* meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效;
* 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及
* 设置页变更 display.multi_font / font.class_* / font.file。 */
static void meow_font_reload_multifont(void)
{
static const char *cls_keys[MEOW_FCLS_COUNT] = {
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
"font.class_latin", "font.class_latin_sym", "font.class_other" };
int on = 0;
meow_settings_get_bool("display.multi_font", &on);
char gpng[400], gidx[400], gfam[96];
if (meow_font_effective_paths(gpng, sizeof gpng, gidx, sizeof gidx, gfam, sizeof gfam) != 0)
return; /* 无全局字体:无法多字体 */
/* 各类候选(静态缓冲保活;config_multifont 同步读取,不持指针) */
static char cpng[MEOW_FCLS_COUNT][400], cidx[MEOW_FCLS_COUNT][400];
meow_font_class_cfg cfg[MEOW_FCLS_COUNT];
memset(cfg, 0, sizeof cfg);
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
char ref[MEOW_SET_MAX_LEN];
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) == 0 && ref[0]) {
char abs[400];
if (meow_font_resolve_path(ref, abs, sizeof abs) == 0 && access(abs, R_OK) == 0) {
snprintf(cpng[c], sizeof cpng[c], "%s", abs);
snprintf(cidx[c], sizeof cidx[c], "%s", abs);
size_t n = strlen(cidx[c]);
if (n > 4 && !strcmp(cidx[c] + n - 4, ".png")) {
cidx[c][n-4]='.'; cidx[c][n-3]='i'; cidx[c][n-2]='d'; cidx[c][n-1]='x'; cidx[c][n]=0;
}
cfg[c].png = cpng[c]; cfg[c].idx = cidx[c];
}
}
}
int distinct = 0, truncated = 0;
if (meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated) != 0)
logmsg("multi_font: config failed -> single-font");
else if (truncated)
logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct);
else if (on)
logmsg("multi_font: enabled, %d distinct fonts (global=%s)", distinct, gfam);
}
/* 统一文本绘制:字库就绪用 meow_font(真字体/中文),否则回退 5x7 点阵。 /* 统一文本绘制:字库就绪用 meow_font(真字体/中文),否则回退 5x7 点阵。
* size 为字库字号(28=状态栏/大标题,20=正文/按钮);回退时按 scale 近似。 */ * size 为字库字号(28=状态栏/大标题,20=正文/按钮);回退时按 scale 近似。 */
static void ui_draw(int x, int y, const char *s, uint32_t color, int size) static void ui_draw(int x, int y, const char *s, uint32_t color, int size)
@@ -822,6 +976,13 @@ static int ui_width(const char *s, int size)
return (int)strlen(s) * 6 * ((size >= 16) ? 2 : 1); return (int)strlen(s) * 6 * ((size >= 16) ? 2 : 1);
} }
/* 文本高度(字库行高 or 回退 7×scale 估算),用于垂直居中 / 行高预算。 */
static int ui_text_height(int size)
{
if (mfont_ready) return meow_font_line_height(size);
return 7 * ((size >= 16) ? 2 : 1);
}
static int fb_open(void) static int fb_open(void)
{ {
int fd = open(FB_DEV, O_RDWR); int fd = open(FB_DEV, O_RDWR);
@@ -1159,7 +1320,8 @@ struct meow_svc {
}; };
struct app { struct app {
char id[MEOW_APP_ID_MAX]; /* M2.52:app_id=卡上目录名 de->d_name(与注册表 g_accts 一致),供 MEOW_LOG_APP 注入 */ char id[MEOW_APP_ID_MAX]; /* M2.52:app_id=app.json 的 package(权威,缺省回退卡上目录名 de->d_name),供 MEOW_LOG_APP 注入 */
char package[MEOW_APP_ID_MAX]; /* 安卓化:app.json 的 package 字段(反域名权威标识;目录名=package),scan_apps 据此定 app_id */
char name[17]; /* ASCII display name */ char name[17]; /* ASCII display name */
char exec[129]; /* absolute path */ char exec[129]; /* absolute path */
int ok; /* exec exists and X_OK */ int ok; /* exec exists and X_OK */
@@ -1418,6 +1580,156 @@ static int ensure_card_mounted(void)
return 0; return 0;
} }
/* ================================================================== */
/* M2.5x:TF 卡多字体库扫描 + preset 动态化 */
/* 字体目录 = <卡根>/MeowOS/fonts/<family>/,每套含三件套: */
/* font.png(预渲染灰度 PNG,bit depth 4|8)+ font.idx(MFNI v2 索引) */
/* + font.json(元数据 + 完整性校验)。缺任一件 → 该套不进列表 + 日志。 */
/* font.json schema(定版): */
/* { */
/* "family": "家族标识(同目录名)", // 信息用,不参与校验 */
/* "name": "下拉显示名", // 缺则回退目录名 */
/* "glyph_count": 10000, // 信息用 */
/* "size_list": [28,20], // 信息用 */
/* "bit_depth": 4, // 信息用(4|8) */
/* "sha256_png": "<64hex>", // font.png 校验 */
/* "sha256_idx": "<64hex>", // font.idx 校验 */
/* "source": "...", // 可选:来源 */
/* "license": "..." // 可选:许可 */
/* } */
/* 校验:sha256 不匹配仅 WARN 不硬失败(仍可选);缺三件套或 font.json */
/* 解析不出 name → 跳过 + 日志。仅启动/进设置页扫描一次(勿放热路径)。 */
/* ================================================================== */
/* 计算文件 sha256(走 busybox/宿主 sha256sum;输出小写 64hex)。返回 0 成功。 */
static int font_sha256_file(const char *path, char *out, int olen)
{
out[0] = 0;
/* 单引号转义防 shell 注入(路径含 ' 时安全;转义后超长则截断 → sha256sum 失败走 WARN) */
char esc[1024]; int j = 0;
for (const char *p = path; *p && j < (int)sizeof esc - 4; ) {
if (*p == '\'') { memcpy(esc + j, "'\\''", 4); j += 4; p++; }
else esc[j++] = *p++;
}
esc[j] = 0;
char cmd[1080];
snprintf(cmd, sizeof cmd, "sha256sum '%s' 2>/dev/null", esc);
FILE *f = popen(cmd, "r");
if (!f) return -1;
char line[160];
int ok = -1;
if (fgets(line, sizeof line, f)) {
int i = 0, k = 0;
while (line[i] == ' ' || line[i] == '\t') i++; /* 跳前导空白 */
while (line[i] && k < 64) {
char c = line[i++];
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
out[k++] = c;
else break;
}
out[k] = 0;
ok = (k == 64) ? 0 : -1;
}
pclose(f);
return ok;
}
/* 比较两个 sha256 十六进制串(大小写不敏感)。均为 64hex 才比对,否则视为不匹配。 */
static int font_sha_eq(const char *a, const char *b)
{
if (!a || !b || strlen(a) < 64 || strlen(b) < 64) return 0;
for (int i = 0; i < 64; i++) {
char ca = a[i], cb = b[i];
if (ca >= 'A' && ca <= 'F') ca += 32;
if (cb >= 'A' && cb <= 'F') cb += 32;
if (ca != cb) return 0;
}
return 1;
}
/* 解析 font.json:提取 name / sha256_png / sha256_idx(文件缺失或解析不出则留空)。 */
static void meow_font_json_probe(const char *json_path, char *name, int nlen,
char *sha_png, int slen, char *sha_idx, int xlen)
{
name[0] = 0; sha_png[0] = 0; sha_idx[0] = 0;
FILE *f = fopen(json_path, "r");
if (!f) return;
char buf[256];
while (fgets(buf, sizeof buf, f)) {
if (!name[0]) json_str(buf, "name", name, nlen);
if (!sha_png[0]) json_str(buf, "sha256_png", sha_png, slen);
if (!sha_idx[0]) json_str(buf, "sha256_idx", sha_idx, xlen);
}
fclose(f);
}
/* 探测一个 <dirpath>/<family> 套件是否合法:三件套齐全 → 解析 label + 校验 sha。
* 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 */
static int meow_font_probe_family(const char *dirpath, const char *family,
char *label, int llen)
{
char ppath[400], ipath[400], jpath[400];
snprintf(ppath, sizeof ppath, "%s/font.png", dirpath);
snprintf(ipath, sizeof ipath, "%s/font.idx", dirpath);
snprintf(jpath, sizeof jpath, "%s/font.json", dirpath);
if (access(ppath, R_OK) != 0 || access(ipath, R_OK) != 0 || access(jpath, R_OK) != 0) {
logmsg("fonts: skip '%s' (缺 font.png/font.idx/font.json)", family);
return 0;
}
char name[64] = "", jsha_png[80] = "", jsha_idx[80] = "";
meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png,
jsha_idx, sizeof jsha_idx);
snprintf(label, llen, "%s", name[0] ? name : family); /* 显示名:name 或回退目录名 */
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */
char apng[80] = "", aidx[80] = "";
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
logmsg("fonts: '%s' sha256(png) 不可用,跳过校验", family);
else if (jsha_png[0] && !font_sha_eq(apng, jsha_png))
logmsg("fonts: '%s' WARN sha256_png 不匹配(仍可选)", family);
if (font_sha256_file(ipath, aidx, sizeof aidx) != 0)
logmsg("fonts: '%s' sha256(idx) 不可用,跳过校验", family);
else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx))
logmsg("fonts: '%s' WARN sha256_idx 不匹配(仍可选)", family);
logmsg("fonts: add '%s' (%s)", label, family);
return 1;
}
/* 扫描 <card_root>/MeowOS/fonts/ 下每个家族子目录,重建字体 preset 列表(默认 + 卡内合法套件)。
* 上限 FONT_PRESETS_MAX(1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。 */
static void meow_font_scan_presets_root(const char *card_root)
{
font_presets_n = 0;
font_presets[0] = "MeowOS/fonts/default/font.png"; /* 默认字库 = MeowOS/fonts/default */
font_preset_labels[0] = "默认字体";
font_presets_n = 1;
char root[340];
snprintf(root, sizeof root, "%s/%s", card_root, MEOW_FONTS_CFG);
DIR *d = opendir(root);
if (!d) return; /* 无卡/无字库目录:仅默认 */
struct dirent *de;
while ((de = readdir(d)) && font_presets_n < FONT_PRESETS_MAX) {
if (de->d_name[0] == '.') continue;
char dirpath[400], label[64];
snprintf(dirpath, sizeof dirpath, "%s/%s", root, de->d_name);
if (meow_font_probe_family(dirpath, de->d_name, label, sizeof label) == 0)
continue; /* 缺组件/损坏 → 跳过该套 */
int i = font_presets_n;
snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i],
"%s/%s/font.png", MEOW_FONTS_CFG, de->d_name);
snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label);
font_presets[i] = font_preset_val_buf[i];
font_preset_labels[i] = font_preset_label_buf[i];
font_presets_n++;
}
closedir(d);
}
/* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。 */
static void meow_font_scan_presets(void)
{
meow_font_scan_presets_root("/usr/data/mnt/sd_0");
}
/* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */ /* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */
static int wlan_connected(void) static int wlan_connected(void)
{ {
@@ -1491,6 +1803,7 @@ static void meow_parse_app_json(FILE *f, struct app *a)
{ {
char buf[256]; char buf[256];
char name[17] = "", exec[129] = "", icon[129] = ""; char name[17] = "", exec[129] = "", icon[129] = "";
char package[MEOW_APP_ID_MAX] = "";
int fram = 0, immersive = 0; int fram = 0, immersive = 0;
struct app tmp; struct app tmp;
memset(&tmp, 0, sizeof tmp); memset(&tmp, 0, sizeof tmp);
@@ -1498,6 +1811,7 @@ static void meow_parse_app_json(FILE *f, struct app *a)
if (!name[0]) json_str(buf, "name", name, sizeof name); if (!name[0]) json_str(buf, "name", name, sizeof name);
if (!exec[0]) json_str(buf, "exec", exec, sizeof exec); if (!exec[0]) json_str(buf, "exec", exec, sizeof exec);
if (!icon[0]) json_str(buf, "icon", icon, sizeof icon); if (!icon[0]) json_str(buf, "icon", icon, sizeof icon);
if (!package[0]) json_str(buf, "package", package, sizeof package); /* 安卓化权威 app_id */
/* route.<id>=<chain>(activity 路由,M1) */ /* route.<id>=<chain>(activity 路由,M1) */
char *rp = strstr(buf, "route."); char *rp = strstr(buf, "route.");
if (rp && tmp.nroute < MEOW_MAX_ROUTES) { if (rp && tmp.nroute < MEOW_MAX_ROUTES) {
@@ -1594,6 +1908,7 @@ static void meow_parse_app_json(FILE *f, struct app *a)
} }
strncpy(a->name, name, 16); strncpy(a->name, name, 16);
strncpy(a->exec, exec, 128); strncpy(a->exec, exec, 128);
strncpy(a->package, package, sizeof a->package - 1);
a->framework = fram ? '1' : 0; a->framework = fram ? '1' : 0;
a->immersive = immersive ? '1' : 0; a->immersive = immersive ? '1' : 0;
strncpy(a->icon, icon, 128); strncpy(a->icon, icon, 128);
@@ -1627,10 +1942,14 @@ static int scan_apps(struct app apps[MAX_APPS])
memset(a, 0, sizeof *a); memset(a, 0, sizeof *a);
memcpy(a, &a0, sizeof a0); memcpy(a, &a0, sizeof a0);
a->ok = (access(a->exec, X_OK) == 0); a->ok = (access(a->exec, X_OK) == 0);
/* M2.52:记 app_id=目录名(与注册表 g_accts 同步一致),launch_app 注入 MEOW_LOG_APP 用 */ /* 安卓化:app_id=app.json 的 package(权威),缺省回退目录名 de->d_name;
strncpy(a->id, de->d_name, sizeof a->id - 1); * 两者都存在且不同则 WARN(正常情况二者相等)。launch_app 注入 MEOW_LOG_APP 用 */
/* M1 路由注册表同步(app_id = 目录名 de->d_name) */ const char *aid = a0.package[0] ? a0.package : de->d_name;
meow_acct_sync(de->d_name, a); strncpy(a->id, aid, sizeof a->id - 1);
if (a0.package[0] && strcmp(a0.package, de->d_name) != 0)
logmsg("WARN: app '%s' package '%s' != dir '%s'", a0.name, a0.package, de->d_name);
/* M1 路由注册表同步(app_id = a->id,即 package 优先) */
meow_acct_sync(a->id, a);
n++; n++;
} }
closedir(d); closedir(d);
@@ -2269,18 +2588,17 @@ static void meow_sys_light(void)
* line 2: VOL nn% (left) * line 2: VOL nn% (left)
* M2.11:值全部取自 meow_sys_state 快照(g_sys),不再每帧读 amixer/sysfs; * M2.11:值全部取自 meow_sys_state 快照(g_sys),不再每帧读 amixer/sysfs;
* 音量 = settings audio.volume(非 CS43198 死镜像);蓝牙 = 实际 powered; * 音量 = settings audio.volume(非 CS43198 死镜像);蓝牙 = 实际 powered;
* WiFi = operstate 实际 up。纯黑背景 = menu_bg;分隔线 y=40。*/ * WiFi = operstate 实际 up。纯黑背景 = menu_bg;分隔线 y=28(栏高 40→28)。*/
/* ---- 状态栏固定完整格重绘(M2.17:修「整个状态栏部件脏区残留」)---- /* ---- 状态栏固定完整格重绘(M2.17:修「整个状态栏部件脏区残留」)----
* 根因:M2.16 各部件/数字更新只把「当前内容包围盒」标为脏区并用它清底——旧内容超出新包围盒的 * 根因:M2.16 各部件/数字更新只把「当前内容包围盒」标为脏区并用它清底——旧内容超出新包围盒的
* 部分(时钟数字 '0' 底弧落于新 '1' 包围盒外、电量%/WiFi/BT 出现或消失、音量数字)未被重绘 → * 部分(时钟数字 '0' 底弧落于新 '1' 包围盒外、电量%/WiFi/BT 出现或消失、音量数字)未被重绘 →
* 残留;按音量键全屏刷新才清。修法:状态栏(0,0,480×40)用一条固定完整格:字段更新时整条清底 + * 残留;按音量键全屏刷新才清。修法:状态栏(0,0,480×28)用一条固定完整格:字段更新时整条清底 +
* 重绘时钟/右侧 + 整条标脏(SDK back_buf 做最终态 → meow_ui_flush 原子 blit 整条,无中间态)。 * 重绘时钟/右侧 + 整条标脏(SDK back_buf 做最终态 → meow_ui_flush 原子 blit 整条,无中间态)。
* 任何旧内容残留被整条覆盖。480×40≈38KB 极小,每秒一 blit 无碍;分隔线 y=40 单独绘制不在此列。 * 任何旧内容残留被整条覆盖。480×28≈27KB 极小,每秒一 blit 无碍;分隔线 y=28 单独绘制不在此列。
* (逐字段固定格在 480px 宽 + 「蓝牙」CJK 全字段同现时与时钟格重叠,故取整条退化——§18.46。) */ * (逐字段固定格在 480px 宽 + 「蓝牙」CJK 全字段同现时与时钟格重叠,故取整条退化——§18.46。) */
#define SB_BAR_Y 0 #define SB_BAR_Y 0
#define SB_BAR_H 40 /* rows 0..39;分隔线在 y=40 不在此 */ #define SB_BAR_H 28 /* rows 0..27;分隔线在 y=28 不在此(40→28,用户定) */
#define SB_ANCHOR_Y 2 /* 字形升线锚点 y */ #define SB_FONT 20 /* 状态栏字体 28→20(28px 栏放不下 28 号;垂直居中锚点见 sb_text_y) */
#define SB_FONT 28
static int g_sb_clock_x, g_sb_clock_y, g_sb_clock_w, g_sb_clock_h; static int g_sb_clock_x, g_sb_clock_y, g_sb_clock_w, g_sb_clock_h;
static int g_sb_right_x, g_sb_right_y, g_sb_right_w, g_sb_right_h; static int g_sb_right_x, g_sb_right_y, g_sb_right_w, g_sb_right_h;
@@ -2363,13 +2681,21 @@ static const meow_ui_theme_t *s_ui_theme(void)
/* ---- 状态栏右簇图标化(MD3)---- 布局常量 */ /* ---- 状态栏右簇图标化(MD3)---- 布局常量 */
#define SB_TGAP 6 /* 图标右缘 ↔ 文字间隙 */ #define SB_TGAP 6 /* 图标右缘 ↔ 文字间隙 */
#define SB_XGAP 10 /* 相邻两项(右→左)间隙 */ #define SB_XGAP 10 /* 相邻两项(右→左)间隙 */
#define SB_ICON_INK 24 /* 主图标目标 ink 高(px):按 ink 边界等比缩放对齐,音量/亮度等图标视觉一致 */ #define SB_ICON_INK 16 /* 主图标目标 ink 高(px):24→16(28px 栏等比缩放,留上下边距) */
#define SB_CLK_MARGIN 14 /* 右簇左缘相对时钟右缘须留出的安全间距 */ #define SB_CLK_MARGIN 14 /* 右簇左缘相对时钟右缘须留出的安全间距 */
/* 图标 ink 垂直中心线 = 状态栏文字垂直中心(机测 mfont28 数字在 SB_ANCHOR_Y=2 下中心 ≈ y=17)。 /* 图标 ink 垂直中心线 = 状态栏文字垂直中心(28px 栏文字按 (28-行高)/2 锚定后中心≈14,见 sb_text_y)。
* 图标 ink 对齐此线 → 与数字/文字同中线(修「图标比文字低」);如需微调只改这一个常量。 */ * 图标 ink 对齐此线 → 与数字/文字同中线(修「图标比文字低」);如需微调只改这一个常量。 */
#define SB_ICON_CY 17 #define SB_ICON_CY 14
static int g_sb_icon_cy = SB_ICON_CY; static int g_sb_icon_cy = SB_ICON_CY;
/* 状态栏文字垂直锚 y(行首升线顶):(栏高-行高)/2 把文字垂直居中于 28px 栏;
* 下限 1 防极端行高(>栏高)时算出负 y 溢出顶端(不越界优先)。 */
static int sb_text_y(void)
{
int y = (SB_BAR_H - ui_text_height(SB_FONT)) / 2;
return (y < 1) ? 1 : y;
}
/* 状态栏 MD3 图标 blit:把 sb_icons.h 的 32x32 单通道 alpha 位图画进状态栏(fb = SDK back_buf)。 /* 状态栏 MD3 图标 blit:把 sb_icons.h 的 32x32 单通道 alpha 位图画进状态栏(fb = SDK back_buf)。
* x = 图标槽左缘(槽宽 SB_ICON_SIZE);fg = 24-bit RGB 前景色(主题 on_surface / on_surface_variant)。 * x = 图标槽左缘(槽宽 SB_ICON_SIZE);fg = 24-bit RGB 前景色(主题 on_surface / on_surface_variant)。
* M2.46-2 视觉对齐(音量图标小一圈/下边距、图标与文字垂直中心): * M2.46-2 视觉对齐(音量图标小一圈/下边距、图标与文字垂直中心):
@@ -2436,7 +2762,7 @@ static int sb_r_place(int *xr, int icon, const char *txt, uint32_t col, int ink_
int tw = txt ? ui_width(txt, SB_FONT) : 0; int tw = txt ? ui_width(txt, SB_FONT) : 0;
int x = *xr - (iconw + gap + tw); int x = *xr - (iconw + gap + tw);
if (icon >= 0) { sb_blit_icon(icon, x, col, ink_h); x += iconw + gap; } if (icon >= 0) { sb_blit_icon(icon, x, col, ink_h); x += iconw + gap; }
if (txt) ui_draw(x, SB_ANCHOR_Y, txt, col, SB_FONT); if (txt) ui_draw(x, sb_text_y(), txt, col, SB_FONT);
*xr -= (iconw + gap + tw) + SB_XGAP; *xr -= (iconw + gap + tw) + SB_XGAP;
return *xr + SB_XGAP; return *xr + SB_XGAP;
} }
@@ -2489,8 +2815,8 @@ static void sb_redraw(void)
struct tm tm; struct tm tm;
localtime_r(&now, &tm); localtime_r(&now, &tm);
snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
ui_draw(8, SB_ANCHOR_Y, timebuf, sb_fg, SB_FONT); ui_draw(8, sb_text_y(), timebuf, sb_fg, SB_FONT);
/* 图标 ink 垂直中心 = 状态栏文字中心(SB_ICON_CY=机测 17):保证图标与数字/文字同中线。 */ /* 图标 ink 垂直中心 = 状态栏文字中心(SB_ICON_CY=14,28px 栏文字中心):同中线。 */
g_sb_icon_cy = SB_ICON_CY; g_sb_icon_cy = SB_ICON_CY;
/* 右簇 MD3 图标化(从右端 fb_xres-8 往左):[电量图标+%] [音量图标+%] [蓝牙图标] [WiFi图标] [亮度图标+%]。 /* 右簇 MD3 图标化(从右端 fb_xres-8 往左):[电量图标+%] [音量图标+%] [蓝牙图标] [WiFi图标] [亮度图标+%]。
* M2.46-2:音量/电量顺序互换(最右由音量改为电量);亮度由 on_surface_variant 改为主色 on_surface; * M2.46-2:音量/电量顺序互换(最右由音量改为电量);亮度由 on_surface_variant 改为主色 on_surface;
@@ -2538,10 +2864,10 @@ static void sb_redraw(void)
if (xr - bw > 8 + ui_width(timebuf, SB_FONT) + SB_CLK_MARGIN) if (xr - bw > 8 + ui_width(timebuf, SB_FONT) + SB_CLK_MARGIN)
sb_r_place(&xr, SB_I_BRIGHT, bright, sb_fg, SB_ICON_INK); /* 亮度:图标+%(主色,与时钟不重叠才画) */ sb_r_place(&xr, SB_I_BRIGHT, bright, sb_fg, SB_ICON_INK); /* 亮度:图标+%(主色,与时钟不重叠才画) */
} }
g_sb_clock_x = 4; g_sb_clock_y = SB_ANCHOR_Y; g_sb_clock_x = 4; g_sb_clock_y = sb_text_y();
g_sb_clock_w = 12 + ui_width(timebuf, SB_FONT); g_sb_clock_h = SB_BAR_H; g_sb_clock_w = 12 + ui_width(timebuf, SB_FONT); g_sb_clock_h = SB_BAR_H;
g_sb_right_x = xr + SB_XGAP; g_sb_right_x = xr + SB_XGAP;
g_sb_right_y = SB_ANCHOR_Y; g_sb_right_w = (fb_xres - 8) - g_sb_right_x + 4; g_sb_right_y = sb_text_y(); g_sb_right_w = (fb_xres - 8) - g_sb_right_x + 4;
g_sb_right_h = SB_BAR_H; g_sb_right_h = SB_BAR_H;
meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 整条标脏 */ meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 整条标脏 */
} }
@@ -2555,7 +2881,7 @@ static void draw_statusbar(void)
{ {
sb_redraw(); sb_redraw();
const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 分隔线随主题 outline */ const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 分隔线随主题 outline */
fill_rect(0, 40, fb_xres, 1, th->outline); /* separator(整条之外,单独绘制) */ fill_rect(0, SB_BAR_H, fb_xres, 1, th->outline); /* separator(整条之外,单独绘制;栏底=28) */
} }
/* returns 0 on success (app ran and exited), -1 on launch failure */ /* returns 0 on success (app ran and exited), -1 on launch failure */
@@ -2903,23 +3229,12 @@ static void sys_do_reboot(int cmd)
for (;;) usleep(1000); /* 防御:永不返回 */ for (;;) usleep(1000); /* 防御:永不返回 */
} }
/* 回HibyOS:写 /data/meow_boot_default(规格标记)+ meow.conf boot_default=1 /* 回HibyOS:写板载 /data/meow_boot_default=1(开机默认权威标记,O_TRUNC 单值、不写 TF 卡),
* (bootmenu 读 meow.conf 定默认),再 reboot 回选单并默认 HibyOS。 */ * 再 reboot 回选单并默认 HibyOS。meow.conf 不再写/承载 boot_default(bootmenu 以板载标记为准)。 */
static void set_boot_default_hiby(void) static void set_boot_default_hiby(void)
{ {
char conf[256]; int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int fd;
fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { write(fd, "1\n", 2); close(fd); logmsg("pm: /data/meow_boot_default=1"); } if (fd >= 0) { write(fd, "1\n", 2); close(fd); logmsg("pm: /data/meow_boot_default=1"); }
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=1\n", 15) < 0) logmsg("pm: write boot_default failed");
close(fd);
logmsg("pm: boot_default=1 appended to meow.conf");
} else {
logmsg("pm: meow.conf open failed: %s", strerror(errno));
}
} }
/* 电源菜单(M2.6):2×2 网格,dirty 绘制消频闪;输入修复: /* 电源菜单(M2.6):2×2 网格,dirty 绘制消频闪;输入修复:
@@ -3265,6 +3580,20 @@ static volatile sig_atomic_t sigterm_flag = 0;
static long sim_exit_tap_ms = 0; static long sim_exit_tap_ms = 0;
static int sim_exit_tap_done = 0; static int sim_exit_tap_done = 0;
/* M2.53:MeowOS 主屏接管时把亮度强制压回用户设置值(display.brightness,经
* meow_settings_brightness_clamped() —— 下限 brightness_min 默认 1、上限 100),
* 覆盖 HibyOS 播放器写入 sysfs 的背光下限(16/25)。仅应在「MeowOS 真正接管画面」
* (播放器已死、主屏常驻,即 run_menu 的 had_player==0)时调用;播放器会话期间的
* 覆盖菜单(had_player==1)不去抢亮度——尊重播放器背光(§红线)。 */
static void apply_user_brightness(void)
{
int v = meow_settings_brightness_clamped();
if (v <= 0) v = 60; /* 防御:clamp 后不可能 ≤0,仅兜底 */
write_brightness(v);
g_sys.brightness = v;
logmsg("apply user brightness (MeowOS takeover) -> %d", v);
}
/* real menu loop */ /* real menu loop */
static void run_menu(struct app apps[MAX_APPS], int napps, int state, static void run_menu(struct app apps[MAX_APPS], int napps, int state,
long sim_tap_ms, long sim_exit_ms) long sim_tap_ms, long sim_exit_ms)
@@ -3306,8 +3635,9 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
fbfd = fb_open(); fbfd = fb_open();
meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */ meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */
/* M2.6 加载字库(卡上 MeowAPPs/fonts/,卡片已挂载则该步成功;未挂载由下方轮询补载) */ /* M2.6 加载字库(卡上 MeowOS/fonts/default/,卡片已挂载则该步成功;未挂载由下方轮询补载) */
meow_font_ensure(); meow_font_ensure();
meow_font_reload_multifont(); /* M2.5x 多字体(读 settings display.multi_font + 6 类;关=单字体) */
/* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份 /* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份
* 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才 * 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才
* 冻结 + 备份,退出时恢复其帧。 */ * 冻结 + 备份,退出时恢复其帧。 */
@@ -3325,6 +3655,11 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
} }
ensure_backlight(); /* only if the panel is dark, never yank brightness */ ensure_backlight(); /* only if the panel is dark, never yank brightness */
unblank_panel(); /* player frozen: it can no longer re-blank */ unblank_panel(); /* player frozen: it can no longer re-blank */
/* M2.53:MeowOS 主屏接管(播放器已死,had_player==0)时强制把亮度压回用户
* display.brightness 设定值。HibyOS 播放器运行/恢复画面会把它自己的背光下限
* (16/25)写进 sysfs;若以它为「当前亮度」,用户设的低亮度(<25)会被覆盖。
* 覆盖菜单(had_player==1,播放器会话中)不压回——播电影时亮度归播放器管。 */
if (!had_player) apply_user_brightness();
if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */ if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */
draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */ draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */
fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */ fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */
@@ -3465,6 +3800,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
/* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */ /* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */
int was_nocard = (state == 1); int was_nocard = (state == 1);
if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); } if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); }
if (mfont_ready) meow_font_reload_multifont(); /* M2.5x:卡挂载后重配多字体(此前无全局可配) */
if (was_nocard || state != 0 || napps == 0) { if (was_nocard || state != 0 || napps == 0) {
struct app fresh[MAX_APPS]; struct app fresh[MAX_APPS];
int fn = scan_apps(fresh); int fn = scan_apps(fresh);
@@ -4008,9 +4344,9 @@ static const char *const hk_preset_labels[] = { "电源+音量减", "连按序
static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" }; static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" };
static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" }; static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" };
#define SEQ_PRESETS_N 2 #define SEQ_PRESETS_N 2
static const char *const font_presets[] = { "MeowAPPs/fonts/font.png" }; /* font.file 行(字体文件)不使用编译期静态表:其 presets/preset_labels 指向字体区
static const char *const font_preset_labels[] = { "默认字体" }; * (font_presets/font_preset_labels, 见本文件字体区) 的运行时动态数组,计数由
#define FONT_PRESETS_N 1 * s_row_presets() 动态返回(默认 1,进设置页扫描 TF 卡后更新)。见 SMENU 行。 */
static const char *const out_presets[] = { "hw:0,0" }; static const char *const out_presets[] = { "hw:0,0" };
static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" }; static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" };
#define OUT_PRESETS_N 1 #define OUT_PRESETS_N 1
@@ -4074,7 +4410,16 @@ static const srow_t SMENU[] = {
{ "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 }, { "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 },
/* 字体主题 */ /* 字体主题 */
{ "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, FONT_PRESETS_N }, { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
/* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/<family>/ 列表;
* 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */
{ "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 },
{ "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 }, { "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 },
{ "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 }, { "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 },
{ "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 }, { "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 },
@@ -4120,7 +4465,41 @@ static int s_sel_track_y = -1;
#define S_SLIDER_W (SET_CONTENT_W) #define S_SLIDER_W (SET_CONTENT_W)
/* ---- 行高/位置/导航辅助 ---- */ /* ---- 行高/位置/导航辅助 ---- */
static int s_setting_idx(int i) { return SMENU[i].ctrl != SC_HEADER; } /* M2.5x 多字体:6 个「字体文件」行仅在 display.multi_font=1 时可见(开时显示)。
* 可见性与分组的标题行无关(标题恒显示)。 */
static int s_row_visible(int i)
{
const srow_t *r = &SMENU[i];
if (r->key && !strncmp(r->key, "font.class_", 11)) {
int on = 0; meow_settings_get_bool("display.multi_font", &on);
return on;
}
return 1;
}
/* 可见行数(= 压缩后占用的行数;标题行计入,隐藏的 class 行不计) */
static int s_vis_count(void)
{
int n = 0;
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) n++;
return n;
}
/* SMENU 下标 → 压缩位置(第几个可见行);不可见返回 -1 */
static int s_vis_of(int i)
{
if (i < 0 || i >= SMENU_N || !s_row_visible(i)) return -1;
int p = 0;
for (int k = 0; k < i; k++) if (s_row_visible(k)) p++;
return p;
}
/* 压缩位置 → SMENU 下标 */
static int s_vis_at(int p)
{
int c = 0;
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) { if (c == p) return i; c++; }
return SMENU_N - 1;
}
static int s_setting_idx(int i) { return i >= 0 && i < SMENU_N && SMENU[i].ctrl != SC_HEADER && s_row_visible(i); }
static int s_first_setting(void) static int s_first_setting(void)
{ {
for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i; for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i;
@@ -4132,10 +4511,23 @@ static int s_next_setting(int i)
return s_first_setting(); /* 回绕 */ return s_first_setting(); /* 回绕 */
} }
/* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组
* (TF 卡 MeowOS/fonts/<family>/ 扫描结果,见字体区 font_presets/font_presets_n),
* 其余行用编译期静态表(r->presets / r->preset_n)。 */
static const char *const *s_row_presets(const srow_t *r, int *n)
{
if (r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11))) {
if (n) *n = font_presets_n;
return font_presets;
}
if (n) *n = r->preset_n;
return r->presets;
}
/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */ /* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */
static int s_clamp_scroll_range(int scroll) static int s_clamp_scroll_range(int scroll)
{ {
int total = SMENU_N * SROW_H; int total = s_vis_count() * SROW_H;
if (total <= SET_CONTENT_H) return 0; if (total <= SET_CONTENT_H) return 0;
int max_scroll = total - SET_CONTENT_H; int max_scroll = total - SET_CONTENT_H;
if (scroll < 0) scroll = 0; if (scroll < 0) scroll = 0;
@@ -4147,20 +4539,22 @@ static int s_clamp_scroll_range(int scroll)
static int s_clamp_scroll(int sel, int scroll) static int s_clamp_scroll(int sel, int scroll)
{ {
scroll = s_clamp_scroll_range(scroll); scroll = s_clamp_scroll_range(scroll);
int top = sel * SROW_H; int pos = s_vis_of(sel);
if (pos < 0) pos = s_vis_of(s_first_setting());
int top = pos * SROW_H;
int bot = top + SROW_H; int bot = top + SROW_H;
if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */ if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */
if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */ if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */
return s_clamp_scroll_range(scroll); return s_clamp_scroll_range(scroll);
} }
/* 行 y 落在 content 坐标 cy 时对应的行号;越界返回 -1 */ /* 行 y 落在 content 坐标 cy 时对应的行号(压缩位置 → SMENU 下标);越界返回 -1 */
static int s_row_at_content_y(int cy) static int s_row_at_content_y(int cy)
{ {
if (cy < 0) return -1; if (cy < 0) return -1;
int idx = cy / SROW_H; int pos = cy / SROW_H;
if (idx >= SMENU_N) return -1; if (pos >= s_vis_count()) return -1;
return idx; return s_vis_at(pos);
} }
/* ---- 值格式化/展示 ---- */ /* ---- 值格式化/展示 ---- */
@@ -4231,8 +4625,15 @@ static void s_display_value(const srow_t *r, char *buf, int len)
/* SC_PRESET */ /* SC_PRESET */
char cur[MEOW_SET_MAX_LEN]; char cur[MEOW_SET_MAX_LEN];
if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; } if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; }
for (int i = 0; i < r->preset_n; i++) { /* M2.5x 多字体:class 行空值 = 跟随全局 font.file */
if (!strcmp(r->presets[i], cur)) { if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) {
snprintf(buf, len, "跟随全局");
return;
}
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
for (int i = 0; i < pn; i++) {
if (!strcmp(pps[i], cur)) {
snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur); snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur);
return; return;
} }
@@ -4947,6 +5348,24 @@ static void s_commit(const srow_t *r, int save_now)
!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) { !strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */ paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */
} }
/* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。
* 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走
* font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */
else if (!strcmp(r->key, "font.file")) {
mfont_ready = 0;
mfont_forced = 1;
if (!meow_font_ensure())
logmsg("font.file: reload failed -> dotmatrix fallback");
else
logmsg("font.file: reloaded");
meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */
meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */
}
/* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */
else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) {
meow_font_reload_multifont();
meow_dirty_full();
}
if (save_now) meow_settings_save(); if (save_now) meow_settings_save();
} }
static void s_set_int_value(const srow_t *r, int v, int save_now) static void s_set_int_value(const srow_t *r, int v, int save_now)
@@ -4993,13 +5412,15 @@ static void draw_settings_page(int sel, int scroll_off)
/* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。 /* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。
* 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底; * 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底;
* 状态栏(0-40)与标题区(40-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */ * 状态栏(0-28)与标题区(28-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant); fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant);
int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT; int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT;
s_sel_track_y = -1; s_sel_track_y = -1;
for (int i = 0; i < SMENU_N; i++) { for (int i = 0; i < SMENU_N; i++) {
int sy = SET_CONTENT_Y + i * SROW_H - scroll_off; int pos = s_vis_of(i);
if (pos < 0) continue; /* 隐藏行(多字体关闭时 6 个 class 行) */
int sy = SET_CONTENT_Y + pos * SROW_H - scroll_off;
if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */ if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */
const srow_t *r = &SMENU[i]; const srow_t *r = &SMENU[i];
int selected = (i == sel); int selected = (i == sel);
@@ -5055,8 +5476,9 @@ static void draw_settings_page(int sel, int scroll_off)
} }
/* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */ /* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */
if (SMENU_N * SROW_H > SET_CONTENT_H) { int vis_total = s_vis_count() * SROW_H;
int total = SMENU_N * SROW_H; if (vis_total > SET_CONTENT_H) {
int total = vis_total;
int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total; int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total;
if (thumb_h < 24) thumb_h = 24; if (thumb_h < 24) thumb_h = 24;
int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H); int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H);
@@ -5077,6 +5499,20 @@ static void draw_settings_page(int sel, int scroll_off)
} }
} }
/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */
static const char *s_preset_value(const srow_t *r, int pick)
{
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
if (is_cls) {
if (pick == 0) return ""; /* 跟随全局 font.file */
pick--;
}
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
if (pick < 0 || pick >= pn) return is_cls ? "" : (pps && pn > 0 ? pps[0] : "");
return pps[pick];
}
/* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)---- /* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)----
* ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。 * ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。
* 返回选中索引;取消返回 -1。阻塞(自开输入设备)。 * 返回选中索引;取消返回 -1。阻塞(自开输入设备)。
@@ -5100,14 +5536,24 @@ static int s_dropdown(const srow_t *r)
} else { } else {
char cstr[MEOW_SET_MAX_LEN]; char cstr[MEOW_SET_MAX_LEN];
meow_settings_get_str(r->key, cstr, sizeof cstr); meow_settings_get_str(r->key, cstr, sizeof cstr);
n = r->preset_n; int pn = 0;
if (n > S_NET_MAX) n = S_NET_MAX; const char *const *pps = s_row_presets(r, &pn);
for (int i = 0; i < n; i++) { int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
names[i] = r->presets[i]; int base = is_cls ? 1 : 0; /* class 行 0 号留给「跟随全局」 */
snprintf(labels[i], sizeof labels[i], "%s", int cnt = pn; if (cnt > S_NET_MAX - base) cnt = S_NET_MAX - base;
(r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : r->presets[i]); n = base + cnt;
item[i] = labels[i]; if (is_cls) {
if (!strcmp(r->presets[i], cstr)) cur = i; names[0] = "";
snprintf(labels[0], sizeof labels[0], "跟随全局字体");
item[0] = labels[0];
if (!cstr[0]) cur = 0;
}
for (int i = 0; i < cnt; i++) {
names[base + i] = pps[i];
snprintf(labels[base + i], sizeof labels[base + i], "%s",
(r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]);
item[base + i] = labels[base + i];
if (!strcmp(pps[i], cstr)) cur = base + i;
} }
} }
if (n <= 0) return -1; if (n <= 0) return -1;
@@ -5130,7 +5576,7 @@ static int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int
int opt_row = 48, pad = 20, title_h = 40; int opt_row = 48, pad = 20, title_h = 40;
int d_h = title_h + n * opt_row + pad; int d_h = title_h + n * opt_row + pad;
int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2; int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2;
if (d_y < 40) d_y = 40; if (d_y < SB_BAR_H) d_y = SB_BAR_H; /* 下拉不压状态栏(栏高 40→28,随常量) */
if (H - d_y - d_h < 20) d_y = H - d_h - 20; if (H - d_y - d_h < 20) d_y = H - d_h - 20;
struct keypoll kp_power, kp_keys; struct keypoll kp_power, kp_keys;
@@ -5260,6 +5706,10 @@ static void run_settings_page(void)
int scroll_off = 0; int scroll_off = 0;
int H = fb_yres; int H = fb_yres;
/* M2.5x:进设置页扫描一次 TF 卡 MeowOS/fonts/<family>/,刷新「字体文件」下拉。
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */
meow_font_scan_presets();
struct keypoll kp_power, kp_keys; struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
@@ -5585,7 +6035,7 @@ static void run_settings_page(void)
int pick = s_dropdown(r); int pick = s_dropdown(r);
if (pick >= 0) { if (pick >= 0) {
if (r->ctrl == SC_ENUM) meow_settings_set_enum(r->key, pick); if (r->ctrl == SC_ENUM) meow_settings_set_enum(r->key, pick);
else meow_settings_set_str(r->key, r->presets[pick]); else meow_settings_set_str(r->key, s_preset_value(r, pick));
s_commit(r, 1); s_commit(r, 1);
} }
/* M2.38 Bug3:s_dropdown 返回后彻底排空本页 tfd + /* M2.38 Bug3:s_dropdown 返回后彻底排空本页 tfd +
@@ -5636,7 +6086,7 @@ static void run_settings_page(void)
if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; } if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; }
draw_settings_page(sel, scroll_off); draw_settings_page(sel, scroll_off);
/* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化), /* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化),
* 状态栏(0-40)由 draw_statusbar/draw_clock 各自维护,不在此列。 * 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。
* 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */ * 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */
if (after_modal) { meow_dirty_full(); after_modal = 0; } if (after_modal) { meow_dirty_full(); after_modal = 0; }
else meow_dirty_add(0, 42, fb_xres, fb_yres - 42); else meow_dirty_add(0, 42, fb_xres, fb_yres - 42);
@@ -6280,14 +6730,17 @@ static int launch_app(struct app *a)
char *argv[] = { a->exec, NULL }; char *argv[] = { a->exec, NULL };
char env_path[320], env_log[320], env_data[320], env_cache[320]; char env_path[320], env_log[320], env_data[320], env_cache[320];
char env_app[MEOW_APP_ID_MAX + 16], env_lvl[48]; char env_app[MEOW_APP_ID_MAX + 16], env_lvl[48];
char *envp[12]; char env_fpng[400], env_fidx[400], env_fapp[96];
char env_mf[24];
char env_cls[MEOW_FCLS_COUNT][2][400];
char *envp[32]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),class(≤12),data,cache */
int ne = 0; int ne = 0;
const char *cp = getenv("PATH"); const char *cp = getenv("PATH");
snprintf(env_path, sizeof env_path, "PATH=%s", cp ? cp : "/usr/bin:/bin"); snprintf(env_path, sizeof env_path, "PATH=%s", cp ? cp : "/usr/bin:/bin");
envp[ne++] = env_path; envp[ne++] = env_path;
envp[ne++] = "HOME=/root"; envp[ne++] = "HOME=/root";
if (log_dir[0]) { snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", log_dir); envp[ne++] = env_log; } if (log_dir[0]) { snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", log_dir); envp[ne++] = env_log; }
/* M2.52:MEOW_LOG_APP=app_id(目录名,与注册表一致;无 id 则跳过,SDK 兜底 "app") */ /* M2.52:MEOW_LOG_APP=app_id(=app.json.package,缺省回退目录名;与注册表一致;无 id 则跳过,SDK 兜底 "app") */
if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; } if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; }
/* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3), /* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3),
* SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */ * SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */
@@ -6299,6 +6752,55 @@ static int launch_app(struct app *a)
snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]); snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]);
envp[ne++] = env_lvl; envp[ne++] = env_lvl;
} }
/* M2.x:注入「当前生效字体」= 系统 font.file(launcher 已 load 的那套),
* SDK meow_ui_gfx.load_font 优先读 MEOW_FONT_PNG/IDX,app 未自带字库(§5.7 load_handle
* 未实现)时跟随系统字体,不再硬编码 MeowAPPs/fonts。无卡/无可用字体则跳过注入,
* SDK 自行回退卡默认→固件→点阵。 */
{
static const char *cls_env_png[MEOW_FCLS_COUNT] = {
"MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_NUM_PNG",
"MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_OTHER_PNG" };
static const char *cls_env_idx[MEOW_FCLS_COUNT] = {
"MEOW_FONT_CLS_CJK_IDX", "MEOW_FONT_CLS_CJK_SYM_IDX", "MEOW_FONT_CLS_NUM_IDX",
"MEOW_FONT_CLS_LATIN_IDX", "MEOW_FONT_CLS_LATIN_SYM_IDX", "MEOW_FONT_CLS_OTHER_IDX" };
static const char *cls_keys[MEOW_FCLS_COUNT] = {
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
"font.class_latin", "font.class_latin_sym", "font.class_other" };
char fpng[400], fidx[400], fam[96];
int have_global;
int mf_on = 0;
meow_settings_get_bool("display.multi_font", &mf_on);
snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0);
envp[ne++] = env_mf;
have_global = (meow_font_effective_paths(fpng, sizeof fpng, fidx, sizeof fidx,
fam, sizeof fam) == 0);
if (have_global) {
snprintf(env_fpng, sizeof env_fpng, "MEOW_FONT_PNG=%s", fpng);
snprintf(env_fidx, sizeof env_fidx, "MEOW_FONT_IDX=%s", fidx);
snprintf(env_fapp, sizeof env_fapp, "MEOW_FONT_APP=%s", fam);
envp[ne++] = env_fpng;
envp[ne++] = env_fidx;
envp[ne++] = env_fapp;
logmsg("launch: font env %s/%s (family=%s)", fpng, fidx, fam);
}
/* M2.5x 多字体:每类注入 png/idx(命中则覆盖全局;类值为空=跟随全局,不注入)。
* SDK 读这些 env 构建多字体映射;同 family 去重由 SDK 侧 meow_font_config_multifont 处理。 */
if (mf_on)
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
char ref[MEOW_SET_MAX_LEN], abs[400], idxabs[400];
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) != 0 || !ref[0]) continue;
if (meow_font_resolve_path(ref, abs, sizeof abs) != 0 || access(abs, R_OK) != 0) continue;
snprintf(idxabs, sizeof idxabs, "%s", abs);
size_t n4 = strlen(idxabs);
if (n4 > 4 && !strcmp(idxabs + n4 - 4, ".png")) {
idxabs[n4-4]='.'; idxabs[n4-3]='i'; idxabs[n4-2]='d'; idxabs[n4-1]='x'; idxabs[n4]=0;
}
snprintf(env_cls[c][0], sizeof env_cls[c][0], "%s=%s", cls_env_png[c], abs);
snprintf(env_cls[c][1], sizeof env_cls[c][1], "%s=%s", cls_env_idx[c], idxabs);
envp[ne++] = env_cls[c][0];
envp[ne++] = env_cls[c][1];
}
}
char pkg[64]; char pkg[64];
if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) { if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) {
snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg); snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg);
@@ -6510,7 +7012,8 @@ static void usage(void)
" [--sim-power-pick N] [--sim-no-exec]\n" " [--sim-power-pick N] [--sim-no-exec]\n"
" [--dumpkeys N] [--scan] [--sysdump]\n" " [--dumpkeys N] [--scan] [--sysdump]\n"
" [--pkg-install <pkg.mpk>] [--pkg-export <pkg> <out.mpk>]\n" " [--pkg-install <pkg.mpk>] [--pkg-export <pkg> <out.mpk>]\n"
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"); " [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
" [--fontscan <card_root>]\n");
} }
int main(int argc, char **argv) int main(int argc, char **argv)
@@ -6519,6 +7022,7 @@ int main(int argc, char **argv)
int do_scan = 0, do_sysdump = 0, sim_vol = 0; int do_scan = 0, do_sysdump = 0, sim_vol = 0;
long ipc_only_ms = 0; long ipc_only_ms = 0;
const char *appinfo_path = NULL; const char *appinfo_path = NULL;
const char *fontscan_root = NULL; /* --fontscan <root>:宿主/qemu 测试字形扫描 */
const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL; const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL;
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]); if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]);
@@ -6537,6 +7041,7 @@ int main(int argc, char **argv)
snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]); snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]);
} }
else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i]; else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i];
else if (!strcmp(argv[i], "--fontscan") && i + 1 < argc) fontscan_root = argv[++i];
else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i]; else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i];
else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; } else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; }
else { usage(); return 1; } else { usage(); return 1; }
@@ -6556,6 +7061,20 @@ int main(int argc, char **argv)
* menu open so edits apply without a daemon restart) */ * menu open so edits apply without a daemon restart) */
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */ settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
/* M2.5x:启动扫描一次 TF 卡 MeowOS/fonts/<family>/,填充「字体文件」下拉 preset。
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */
meow_font_scan_presets();
if (fontscan_root) {
/* M2.5x 测试钩子:扫描指定卡根下的 MeowOS/fonts/<family>/ 并打印 preset(qemu/宿主可测)。
* 用于验证 font.json 解析/三件套/缺件跳过/sha 校验 + preset 动态化逻辑。 */
meow_font_scan_presets_root(fontscan_root);
printf("fonts=%d\n", font_presets_n);
for (int i = 0; i < font_presets_n; i++)
printf(" %d %s -> %s\n", i, font_preset_labels[i], font_presets[i]);
return 0;
}
if (sim_vol > 0) { if (sim_vol > 0) {
/* M2.14 sim:验证线性渐变——step=1/2/5 下设定音量按 1% 逐级爬升/回落, /* M2.14 sim:验证线性渐变——step=1/2/5 下设定音量按 1% 逐级爬升/回落,
* 每 1% 经 meow_dac_vol_write 写一次真实 DAC,打点进 log(stderr)。 */ * 每 1% 经 meow_dac_vol_write 写一次真实 DAC,打点进 log(stderr)。 */
+24 -1
View File
@@ -77,6 +77,9 @@ static const meow_setting_def g_defs[] = {
{ "display.theme_dark_start", SET_INT, "0", 0, 1439, NULL, NULL }, { "display.theme_dark_start", SET_INT, "0", 0, 1439, NULL, NULL },
{ "display.theme_dark_end", SET_INT, "0", 0, 1439, NULL, NULL }, { "display.theme_dark_end", SET_INT, "0", 0, 1439, NULL, NULL },
{ "display.background", SET_STR, "black",0, 0, NULL, NULL }, { "display.background", SET_STR, "black",0, 0, NULL, NULL },
/* M2.5x 多字体:1=按字符类别逐字符路由(配合下方 6 类 font.class_*);
* 0=单字体(只走 font.file,零额外内存/性能开销,等价现状)。 */
{ "display.multi_font", SET_INT, "0", 0, 1, NULL, NULL },
/* 电源 */ /* 电源 */
{ "power.longpress_menu_ms", SET_INT, "3000", 0, 0, NULL, NULL }, { "power.longpress_menu_ms", SET_INT, "3000", 0, 0, NULL, NULL },
{ "power.forceoff_ms", SET_INT, "8000", 0, 0, NULL, NULL }, { "power.forceoff_ms", SET_INT, "8000", 0, 0, NULL, NULL },
@@ -92,7 +95,15 @@ static const meow_setting_def g_defs[] = {
/* M2.38:设置页滑动方向切换(0=标准:手指上滑→列表往下翻;1=反向) */ /* M2.38:设置页滑动方向切换(0=标准:手指上滑→列表往下翻;1=反向) */
{ "input.swipe_invert", SET_BOOL, "0", 0, 0, NULL, NULL }, { "input.swipe_invert", SET_BOOL, "0", 0, 0, NULL, NULL },
/* 字体/主题 */ /* 字体/主题 */
{ "font.file", SET_STR, "MeowAPPs/fonts/font.png", 0,0, NULL, NULL }, { "font.file", SET_STR, "MeowOS/fonts/default/font.png", 0,0, NULL, NULL },
/* M2.5x 多字体:6 个字符类别各一字体(SET_STR,值=字体 family 相对路径,缺省空=跟随全局 font.file)。
* 仅 display.multi_font=1 时生效;同一 family 去重;上限 4 套不同字体(超出 WARN)。 */
{ "font.class_cjk", SET_STR, "", 0,0, NULL, NULL },
{ "font.class_cjk_sym", SET_STR, "", 0,0, NULL, NULL },
{ "font.class_num", SET_STR, "", 0,0, NULL, NULL },
{ "font.class_latin", SET_STR, "", 0,0, NULL, NULL },
{ "font.class_latin_sym", SET_STR, "", 0,0, NULL, NULL },
{ "font.class_other", SET_STR, "", 0,0, NULL, NULL },
{ "font.size.title", SET_INT, "28", 0, 0, NULL, NULL }, { "font.size.title", SET_INT, "28", 0, 0, NULL, NULL },
{ "font.size.body", SET_INT, "20", 0, 0, NULL, NULL }, { "font.size.body", SET_INT, "20", 0, 0, NULL, NULL },
{ "ui.style", SET_ENUM, "md3", 0, 0, "md3,miui", "Material You,MIUI" }, { "ui.style", SET_ENUM, "md3", 0, 0, "md3,miui", "Material You,MIUI" },
@@ -205,6 +216,18 @@ static int clamp_brightness(int v)
return v; return v;
} }
/* M2.53:返回当前用户亮度设定(display.brightness)经 clamp_brightness 后的值
* (下限 brightness_min 默认 1、上限 100)。MeowOS 主屏接管时用它压回用户亮度,
* 覆盖 HibyOS 播放器写入的背光下限 16/25。settings 未 init/键缺失回退默认 101 再 clamp
* (→100,不闪屏)。纯函数,宿主机可直接测(MEOW_SETTINGS_FILE 可编译期覆盖)。 */
int meow_settings_brightness_clamped(void)
{
meow_setting *b = find_set("display.brightness");
int v = 101; /* 默认表 display.brightness 默认 */
if (b && b->cur[0]) v = atoi(b->cur);
return clamp_brightness(v);
}
/* ---- 按类型把字符串值 apply 到 cur(读文件/导入时用;含 clamp/校验)---- */ /* ---- 按类型把字符串值 apply 到 cur(读文件/导入时用;含 clamp/校验)---- */
static void apply_value(meow_setting *s, const char *v) static void apply_value(meow_setting *s, const char *v)
{ {
+6
View File
@@ -70,6 +70,12 @@ 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_enum(const char *key, int *out); /* 返回索引(名字->索引) */
int meow_settings_get_bool(const char *key, int *out); /* 0/1 */ int meow_settings_get_bool(const char *key, int *out); /* 0/1 */
/* M2.53:返回当前用户亮度设定(display.brightness)经 clamp_brightness 后的值
* (下限 display.brightness_min 默认 1=极暗但可见、上限 100=防闪屏)。
* MeowOS 主屏接管时用它把亮度压回用户值,覆盖 HibyOS 播放器写入的背光下限 16/25。
* settings 未初始化/键缺失时回退默认 101 再 clamp(→100);纯函数,宿主机可直接测。 */
int meow_settings_brightness_clamped(void);
int meow_settings_set_int (const char *key, int v); /* clamp 到取值域 */ int meow_settings_set_int (const char *key, int v); /* clamp 到取值域 */
int meow_settings_set_str (const char *key, const char *v);/* 长度截断 MEOW_SET_MAX_LEN */ int meow_settings_set_str (const char *key, const char *v);/* 长度截断 MEOW_SET_MAX_LEN */
int meow_settings_set_enum(const char *key, int idx); /* 校验枚举合法,越界拒绝 */ int meow_settings_set_enum(const char *key, int idx); /* 校验枚举合法,越界拒绝 */
+2 -2
View File
@@ -19,8 +19,8 @@ echo "== 2) 推送 ov.tgz / uinput_tap / hello =="
[ -f /tmp/ov.tar.gz ] || "$ROOT/patch_v2/make_ov.sh" [ -f /tmp/ov.tar.gz ] || "$ROOT/patch_v2/make_ov.sh"
$SCP /tmp/ov.tar.gz root@$DEV:/data/ov.tgz $SCP /tmp/ov.tar.gz root@$DEV:/data/ov.tgz
$SCP "$ROOT/patch_v2/uinput_tap/uinput_tap" root@$DEV:/data/uinput_tap $SCP "$ROOT/patch_v2/uinput_tap/uinput_tap" root@$DEV:/data/uinput_tap
$SCP "$ROOT/patch_v2/apps_assets/apps/hello/hello" root@$DEV:/data/hello.bin $SCP "$ROOT/patch_v2/apps_assets/apps/os.meow.hello/hello" root@$DEV:/data/hello.bin
$SCP "$ROOT/patch_v2/apps_assets/apps/hello/app.json" root@$DEV:/data/hello_app.json $SCP "$ROOT/patch_v2/apps_assets/apps/os.meow.hello/app.json" root@$DEV:/data/hello_app.json
$SSH root@$DEV 'chmod 755 /data/uinput_tap /data/hello.bin; md5sum /data/ov.tgz /data/uinput_tap /data/hello.bin' $SSH root@$DEV 'chmod 755 /data/uinput_tap /data/hello.bin; md5sum /data/ov.tgz /data/uinput_tap /data/hello.bin'
echo "== 3) 设备侧: 解包+bind+安全重启 ==" echo "== 3) 设备侧: 解包+bind+安全重启 =="
+4 -2
View File
@@ -9,10 +9,12 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架
rm -f meow_ui.a *.o rm -f meow_ui.a *.o
OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_ui_sys stb_image" # -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移)
OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_ui_sys meow_ui_vol stb_image"
CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd"
for f in $OBJS; do for f in $OBJS; do
echo "== cc $f ==" echo "== cc $f =="
"$CC" -Os -static -c "$f.c" -o "$f.o" "$CC" $CFLAGS -c "$f.c" -o "$f.o"
done done
# meow_font.o(复用 launcher 字库,打进 meow_ui.a → 应用只链一个库) # meow_font.o(复用 launcher 字库,打进 meow_ui.a → 应用只链一个库)
+243
View File
@@ -0,0 +1,243 @@
/*
* meow_notify_ipc.c — MeowOS SDK 通知 → launcher IPC 桥(任务 #5,SDK↔IPC 关键断点)。
*
* 让 meow_notify_send() 的通知真正经 AF_UNIX IPC 送到 launcher 中央存储(meow_notif_post)
* 并回读 MSG_NOTIFY_ACK 拿 launcher 分配的通知 id(nid)。协议帧与负载结构复用 launcher 的
* meow_ipc.h(同源一份、杜绝两端漂移):
* - 帧头 16B:magic=0x4D4F5701 / version=1 / type / req_id / len(全小端 LE,mipsel 原生);
* - MSG_NOTIFY(0x0020) ↑ 负载 = m_notify_t;MSG_NOTIFY_ACK(0x0021) ↓ 负载 = m_ack_t(status=nid);
* - MSG_NOTIFY_DISMISS(0x0022) ↑ 负载 = uint32 nid。
*
* 设计要点:
* - 只做「一次性 connect + send + read ACK」,不维护连接/订阅循环(与 meow_ipc.c 客户端同型);
* - 映射层(SDK 枚举 ↔ IPC 字段)集中在本文件:分类枚举 CAT_* ↔ 字符串
* "general"/"message"/"email"/"event"/"progress"/"media";timeout_ms → expire_timeout:
* >0 该毫秒、0=不自动过期、-1(ONGOING)=0xFFFFFFFF(并置 ongoing=1);媒体类型置 media=1;
* - 优雅降级:无 socket(connect 失败)/写失败/ACK 超时(SO_RCVTIMEO)/非 ACK 应答 →
* 返回 0(调用方 meow_ui_notify.c 保留本地 id 兜底),绝不崩;
* - 线程安全:app_id 经 pthread_once 缓存;socket 路径每次调用读 env MEOW_IPC_SOCK 覆盖
* (宿主/test 用),缺省 meow_ipc.h 的 MEOW_IPC_PATH(/usr/data/meow_ipc.sock)。
*
* 约定:与 meow_ui_notify.c 同风格(M2.x 中文注释);本文件为纯传输层,不调 meow_log,
* 失败诊断由调用方 meow_ui_notify.c 的 notify_deliver() 统一打点。
*/
#include "meow_notify_ipc.h"
#include "meow_ipc.h" /* launcher 端共享协议:帧头/m_notify_t/m_ack_t/MSG_* —— 防漂移 */
#include <stdio.h> /* snprintf */
#include <stdlib.h> /* getenv */
#include <string.h>
#include <errno.h> /* EINTR */
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
/* ACK 读超时(ms):launcher 主循环为 IPC 单写者(eventfd 唤醒,正常 <10ms 响应),
* 放宽容错 500ms;超时视为降级(返回 0,不崩)。 */
#define MEOW_NOTIFY_IPC_TIMEOUT_MS 500
/* app_id(env MEOW_LOG_APP,缺省 "app";与 meow_ui_log.c 的取值口径一致)。 */
static pthread_once_t g_once = PTHREAD_ONCE_INIT;
static char g_app_id[MEOW_APP_ID_MAX] = "app";
static void ipc_init(void)
{
const char *s = getenv("MEOW_LOG_APP");
if (s && s[0]) snprintf(g_app_id, sizeof g_app_id, "%s", s);
}
/* socket 路径:优先 env MEOW_IPC_SOCK(宿主/qemu sim 覆盖),缺省 MEOW_IPC_PATH。 */
static const char *ipc_sock_path(void)
{
const char *s = getenv("MEOW_IPC_SOCK");
return (s && s[0]) ? s : MEOW_IPC_PATH;
}
const char *meow_notify_ipc_app_id(void)
{
pthread_once(&g_once, ipc_init);
return g_app_id;
}
/* ============================================================ */
/* 映射层: SDK meow_notify 枚举/字段 ↔ IPC m_notify_t 字段 */
/* ============================================================ */
/* SDK 分类枚举 → IPC 字符串(与 meow_ui.h §9.1 的 MEOW_NOTIFY_CAT_* 序号对齐;
* 越界钳位为 "general"——同 meow_notify_category_str 的防御语义)。 */
static const char *cat_to_str(meow_notify_category cat)
{
static const char *t[] = { "general", "message", "email", "event", "progress", "media" };
if ((int)cat >= 0 && (int)cat < (int)MEOW_NOTIFY_CAT_COUNT) return t[cat];
return t[MEOW_NOTIFY_CAT_GENERAL];
}
/* SDK timeout_ms → IPC expire_timeout(存储/显示端语义):
* >0 → 该毫秒数后过期;0 → 不自动过期(NEVER);<0 → 常驻(ONGOING)= 0xFFFFFFFF。 */
static uint32_t timeout_to_expire(int timeout_ms)
{
if (timeout_ms < 0) return 0xFFFFFFFFu; /* ONGOING:常驻,不自动过期 */
return (uint32_t)timeout_ms; /* 0(NEVER)/ >0(毫秒) */
}
/* 构建 IPC 负载 m_notify_t(app_icon/actions/hints v1 留空,M2b D-Bus 桥再填)。
* 仅在持有调用者互斥之外、发送前调用(纯 CPU 拷贝,不阻塞)。 */
static void build_m_notify(const meow_notify *n, uint32_t replaces_id, m_notify_t *mn)
{
memset(mn, 0, sizeof *mn);
snprintf(mn->app_id, sizeof mn->app_id, "%s", meow_notify_ipc_app_id());
snprintf(mn->category, sizeof mn->category, "%s", cat_to_str(n->cat));
snprintf(mn->summary, sizeof mn->summary, "%s", n->title ? n->title : "");
snprintf(mn->body, sizeof mn->body, "%s", n->body ? n->body : "");
snprintf(mn->target_activity, sizeof mn->target_activity, "%s",
n->target_activity ? n->target_activity : "");
mn->replaces_id = replaces_id; /* 0=新建;>0=更新(不存在则落为新建,见 launcher) */
mn->expire_timeout = timeout_to_expire(n->timeout_ms);
mn->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress);
mn->media = (n->cat == MEOW_NOTIFY_CAT_MEDIA) ? 1 : 0;
mn->ongoing = (n->timeout_ms == MEOW_NOTIFY_TIMEOUT_ONGOING) ? 1 : 0;
}
/* ============================================================ */
/* 传输层(AF_UNIX SOCK_STREAM;一次性 connect + send + read) */
/* ============================================================ */
/* 连接 socket。失败返回 -1(不崩;调用方降级)。 */
static int conn_path(const char *path)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) {
close(fd);
return -1;
}
return fd;
}
/* 写满 n 字节(EINTR 重试);任一次写失败返回 0。 */
static int write_all(int fd, const uint8_t *p, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, p + off, n - off);
if (w < 0) { if (errno == EINTR) continue; return 0; }
if (w == 0) return 0;
off += (size_t)w;
}
return 1;
}
/* 读满 n 字节(超时/断开返回 0)。 */
static int read_all(int fd, uint8_t *p, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t r = read(fd, p + off, n - off);
if (r < 0) { if (errno == EINTR) continue; return 0; }
if (r == 0) return 0;
off += (size_t)r;
}
return 1;
}
/* 组装并写出 16B(LE)帧头 + 负载。 */
static int send_frame(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &len, 4);
if (!write_all(fd, hdr, sizeof hdr)) return 0;
if (len && payload && !write_all(fd, payload, len)) return 0;
return 1;
}
/* 给 conn 设 ACK 读超时(失败忽略——超时降级由 read_all 兜底)。 */
static void set_ack_timeout(int fd)
{
struct timeval tv;
tv.tv_sec = MEOW_NOTIFY_IPC_TIMEOUT_MS / 1000;
tv.tv_usec = (MEOW_NOTIFY_IPC_TIMEOUT_MS % 1000) * 1000;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
}
uint32_t meow_notify_ipc_send(const meow_notify *n, uint32_t replaces_id)
{
m_notify_t mn;
int fd;
if (!n) return 0;
build_m_notify(n, replaces_id, &mn);
fd = conn_path(ipc_sock_path());
if (fd < 0) return 0; /* 无 socket:优雅降级 */
set_ack_timeout(fd);
if (!send_frame(fd, MSG_NOTIFY, 0x4E4F5449u /* "NOTI" */, &mn, (uint32_t)sizeof mn)) {
close(fd); /* 写失败:降级 */
return 0;
}
/* 读回 16B 应答头 + m_ack_t,取 launcher 分配的通知 id(a.status)。 */
uint8_t rhdr[MEOW_IPC_HDR_LEN];
uint32_t nid = 0;
if (read_all(fd, rhdr, sizeof rhdr)) {
uint32_t rmagic, rlen;
uint16_t rver, rtype;
memcpy(&rmagic, rhdr + 0, 4);
memcpy(&rver, rhdr + 4, 2);
memcpy(&rtype, rhdr + 6, 2);
memcpy(&rlen, rhdr + 12, 4);
if (rmagic == MEOW_IPC_MAGIC && rver == MEOW_IPC_VERSION &&
rtype == MSG_NOTIFY_ACK && rlen >= sizeof(m_ack_t)) {
uint8_t pay[sizeof(m_ack_t)];
if (read_all(fd, pay, sizeof(m_ack_t))) {
m_ack_t a;
memcpy(&a, pay, sizeof a);
if (a.code == MEOW_OK) nid = a.status; /* 落库成功 → nid */
}
}
}
close(fd);
return nid; /* 非 ACK/超时 → 0(降级) */
}
void meow_notify_ipc_dismiss(uint32_t nid)
{
int fd;
if (nid == 0) return;
fd = conn_path(ipc_sock_path());
if (fd < 0) return; /* 无 socket:静默 */
set_ack_timeout(fd);
if (!send_frame(fd, MSG_NOTIFY_DISMISS, 0x4E4F5444u /* "NOTD" */, &nid, (uint32_t)sizeof nid)) {
close(fd); /* 写失败:静默 */
return;
}
/* 读回应答(launcher 对 DISMISS 回 MSG_ACK,见 meow_ipc.c cmd_dismiss)。虽为
* best-effort,但读回确保 launcher 已处理完撤下再关连接(避免写后即关的丢帧竞态)。 */
uint8_t rhdr[MEOW_IPC_HDR_LEN];
if (read_all(fd, rhdr, sizeof rhdr)) {
uint32_t rmagic, rlen;
uint16_t rver, rtype;
memcpy(&rmagic, rhdr + 0, 4);
memcpy(&rver, rhdr + 4, 2);
memcpy(&rtype, rhdr + 6, 2);
memcpy(&rlen, rhdr + 12, 4);
if (rmagic == MEOW_IPC_MAGIC && rver == MEOW_IPC_VERSION &&
rtype == MSG_ACK && rlen >= sizeof(m_ack_t)) {
uint8_t pay[sizeof(m_ack_t)];
(void)read_all(fd, pay, sizeof(m_ack_t)); /* 读到即确认;内容不关心 */
}
}
close(fd);
}
+44
View File
@@ -0,0 +1,44 @@
/*
* meow_notify_ipc.h — MeowOS SDK 通知 → launcher IPC 桥(内部头,不随 meow_ui.h 发布给应用)。
*
* 定位(任务 #5 SDK↔IPC 桥关键断点):让框架 app 调 meow_notify_send() 的通知真正经
* AF_UNIX IPC 送到 launcher(meow_launcherd)中央存储(meow_notif.c 的 meow_notif_post),
* 通知中心(阶段 B · M5)才能显示。消息通道(FRAMEWORK_DESIGN §1 / meow_ipc.h):
* - MSG_NOTIFY 0x0020 ↑:推送/更新通知(负载 m_notify_t);
* - MSG_NOTIFY_ACK 0x0021 ↓:落库结果(m_ack_t,status=launcher 分配的通知 id nid);
* - MSG_NOTIFY_DISMISS 0x0022 ↑:撤下通知(负载 uint32 nid)。
*
* 约定:只做「一次性发送 + 读 ACK」,不造 IPC 订阅循环;SDK 本地活动表(meow_ui_notify.c)
* 保留(app 进程内即时查询/配额自检),真正的通知中心数据在 launcher 中央存储,二者以
* 本接口经 IPC 同步。全部失败路径优雅降级(无 socket/写失败/超时 → 返回 0,不崩)。
*
* 本接口只暴露「SDK 类型出/进 + 状态码」,不把 m_notify_t / 帧格式泄漏给 meow_ui_notify.c;
* 映射层(SDK 枚举 ↔ IPC 字符串/uint32)实现在 meow_notify_ipc.c 内部。
*/
#ifndef MEOW_NOTIFY_IPC_H
#define MEOW_NOTIFY_IPC_H
#include <stdint.h>
#include "meow_ui.h" /* meow_notify */
#ifdef __cplusplus
extern "C" {
#endif
/* 当前 app 的 app_id(env MEOW_LOG_APP,缺省 "app";pthread_once 缓存,线程安全)。 */
const char *meow_notify_ipc_app_id(void);
/* 把一条 SDK meow_notify 映射为 m_notify_t 并一次性发送 MSG_NOTIFY,阻塞读
* MSG_NOTIFY_ACK 取 launcher 分配的通知 id(nid)。replaces_id:0=新建;>0=更新已存在
* 通知(与 freedesktop 语义一致)。成功返回 nid(>0);无 socket/写失败/超时/非 ACK →
* 返回 0(调用方保留本地 id 兜底,不崩)。 */
uint32_t meow_notify_ipc_send(const meow_notify *n, uint32_t replaces_id);
/* 一次性发送 MSG_NOTIFY_DISMISS 撤下 launcher 侧通知。best-effort:写失败/无 socket
* 静默(不崩、不回读 ACK)。 */
void meow_notify_ipc_dismiss(uint32_t nid);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_NOTIFY_IPC_H */
+1 -1
View File
@@ -32,7 +32,7 @@ extern "C" {
#define MEOW_KEY_VOLUMEDOWN 114 #define MEOW_KEY_VOLUMEDOWN 114
#define MEOW_KEY_VOLUMEUP 115 #define MEOW_KEY_VOLUMEUP 115
#define MEOW_KEY_PLAYPAUSE 164 #define MEOW_KEY_PLAYPAUSE 164
#define MEOW_KEY_PREVIOUS 19 /* KEY_PREVIOUSSONG */ #define MEOW_KEY_PREVIOUS 165 /* KEY_PREVIOUSSONG=165(上一曲;19 是另一码,勿混用) */
#define MEOW_KEY_NEXT 163 /* KEY_NEXTSONG */ #define MEOW_KEY_NEXT 163 /* KEY_NEXTSONG */
/* 滑动方向(on_swipe 的 dir 参数) */ /* 滑动方向(on_swipe 的 dir 参数) */
+49 -2
View File
@@ -29,10 +29,14 @@
#define FBIO_WAITFORVSYNC 0x4620 #define FBIO_WAITFORVSYNC 0x4620
#define FB_DEV "/dev/fb0" #define FB_DEV "/dev/fb0"
#define MEOW_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowAPPs/fonts/font.png" #define MEOW_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/font.png"
#define MEOW_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowAPPs/fonts/font.idx" #define MEOW_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/font.idx"
#define MEOW_FONT_PNG_SYS "/usr/share/meowos/font/font.png" #define MEOW_FONT_PNG_SYS "/usr/share/meowos/font/font.png"
#define MEOW_FONT_IDX_SYS "/usr/share/meowos/font/font.idx" #define MEOW_FONT_IDX_SYS "/usr/share/meowos/font/font.idx"
/* 默认字库(卡 MeowOS/fonts/default)与固件内置之外,launcher 会把「当前生效字体」
* 经环境变量下发给 app(见 meow_launcherd.c launch_app / meow_font_effective_paths):
* MEOW_FONT_PNG / MEOW_FONT_IDX = 绝对路径;MEOW_FONT_APP = <family> 或 "default"。
* SDK load_font 优先读 env(匹配系统 font.file),无则回退卡默认 → 固件。 */
#define MUI_DEFAULT_W 480 #define MUI_DEFAULT_W 480
#define MUI_DEFAULT_H 720 #define MUI_DEFAULT_H 720
@@ -572,13 +576,55 @@ int meow_gfx_is_device(void) { return g_fb_mem != NULL; }
int meow_gfx_mfont_ready(void) { return g_mfont_ready; } int meow_gfx_mfont_ready(void) { return g_mfont_ready; }
/* ================= 初始化/销毁 ================= */ /* ================= 初始化/销毁 ================= */
/* 字库加载优先级(M2.x):
* 1) 环境变量 MEOW_FONT_PNG/MEOW_FONT_IDX(launcher 注入的「当前生效字体」= 系统 font.file)
* —— app 未自带字库(§5.7 load_handle 未实现)时跟随系统 font.file;
* 2) 卡默认 MeowOS/fonts/default/font.png(不再硬编码 MeowAPPs/fonts);
* 3) 固件 /usr/share/meowos/font/font.png。
* 全失败 → g_mfont_ready=0,text_draw 回退 5x7 点阵(系统不死)。 */
static int load_font(void) static int load_font(void)
{ {
const char *ep = getenv("MEOW_FONT_PNG");
const char *ei = getenv("MEOW_FONT_IDX");
if (ep && ep[0] && ei && ei[0] && meow_font_load(ep, ei) == 0) return 1;
if (meow_font_load(MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) return 1; if (meow_font_load(MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) return 1;
if (meow_font_load(MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) return 1; if (meow_font_load(MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) return 1;
return 0; return 0;
} }
/* M2.5x 多字体:读 launcher 注入的环境变量,构建 6 字符类 → 字库的映射(同 family 去重、
* ≤4 套上限由 meow_font_config_multifont 统一处理)。launcher 注入(见 meow_launcherd.c
* launch_app):MEOW_MULTI_FONT=0/1;MEOW_FONT_PNG/IDX=全局(保留);每类
* MEOW_FONT_CLS_<X>_PNG/IDX(命中则覆盖全局;未注入 = 该类跟随全局)。
* 仅当全局字库已装载(g_mfont_ready)才配置;关/无 env → 单字体,零额外开销。 */
static void meow_gfx_load_multifont(void)
{
static const char *cls_env[MEOW_FCLS_COUNT][2] = {
{ "MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_IDX" },
{ "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_CJK_SYM_IDX" },
{ "MEOW_FONT_CLS_NUM_PNG", "MEOW_FONT_CLS_NUM_IDX" },
{ "MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_IDX" },
{ "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_LATIN_SYM_IDX" },
{ "MEOW_FONT_CLS_OTHER_PNG", "MEOW_FONT_CLS_OTHER_IDX" },
};
const char *emf = getenv("MEOW_MULTI_FONT");
int on = emf && emf[0] == '1';
const char *gp = getenv("MEOW_FONT_PNG");
const char *gi = getenv("MEOW_FONT_IDX");
meow_font_class_cfg cfg[MEOW_FCLS_COUNT];
memset(cfg, 0, sizeof cfg);
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
const char *p = getenv(cls_env[c][0]);
const char *idx = getenv(cls_env[c][1]);
if (p && p[0] && idx && idx[0]) { cfg[c].png = p; cfg[c].idx = idx; }
}
int distinct = 0, truncated = 0;
if (meow_font_config_multifont(on, gp, gi, cfg, &distinct, &truncated) != 0)
meow_log(MEOW_LOG_WARN, "multi_font: config failed, single-font fallback");
else if (truncated)
meow_log(MEOW_LOG_WARN, "multi_font: >%d distinct fonts -> capped (more memory!)", distinct);
}
int meow_ui_init(const meow_ui_opts *opts) int meow_ui_init(const meow_ui_opts *opts)
{ {
if (g_init_ok) return 0; if (g_init_ok) return 0;
@@ -623,6 +669,7 @@ int meow_ui_init(const meow_ui_opts *opts)
meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line); meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line);
g_mfont_ready = load_font(); g_mfont_ready = load_font();
if (g_mfont_ready) meow_gfx_load_multifont(); /* M2.5x 多字体(读 env;关/无 env = 单字体) */
const char *dp = getenv("MEOW_UI_DUMP"); const char *dp = getenv("MEOW_UI_DUMP");
if (dp && *dp) g_dump_path = strdup(dp); if (dp && *dp) g_dump_path = strdup(dp);
+2 -2
View File
@@ -219,9 +219,9 @@ static void *input_thread(void *arg)
else if (!val && t_down) touch_release(); else if (!val && t_down) touch_release();
} else if (code == MEOW_KEY_POWER || code == MEOW_KEY_VOLUMEDOWN || } else if (code == MEOW_KEY_POWER || code == MEOW_KEY_VOLUMEDOWN ||
code == MEOW_KEY_VOLUMEUP || code == MEOW_KEY_PLAYPAUSE || code == MEOW_KEY_VOLUMEUP || code == MEOW_KEY_PLAYPAUSE ||
code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 165) { code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 19) {
/* M2.22(A)/M2.24 纪律:物理键(电源 116 / 音量 114,115 / 播放 164 / /* M2.22(A)/M2.24 纪律:物理键(电源 116 / 音量 114,115 / 播放 164 /
* 上一曲 19,165 / 下一曲 163)全部是系统语义 —— watchdog/launcher 统一消费, * 上一曲 165(MEOW_KEY_PREVIOUS)/19(旧码) / 下一曲 163)全部是系统语义 —— watchdog/launcher 统一消费,
* 绝不下发 APP。应用不碰物理键、不把任何物理键映射成确认/exit/返回/翻页 * 绝不下发 APP。应用不碰物理键、不把任何物理键映射成确认/exit/返回/翻页
* (KEY_CONTROL_DESIGN §1.3 / APPS.md §3.4.1);APP 只接收 tap/swipe/tick。 * (KEY_CONTROL_DESIGN §1.3 / APPS.md §3.4.1);APP 只接收 tap/swipe/tick。
* 忽略该键,不发 MEOW_EV_KEY。 */ * 忽略该键,不发 MEOW_EV_KEY。 */
+62 -16
View File
@@ -12,15 +12,20 @@
* (替换);媒体类型单例(每 app 仅保留最近 1 条,重发替换——可多媒体 App)。 * (替换);媒体类型单例(每 app 仅保留最近 1 条,重发替换——可多媒体 App)。
* - 超时语义 [实现定案]:timeout_ms > 0 为该毫秒数后过期;0 = 不自动过期(直到 * - 超时语义 [实现定案]:timeout_ms > 0 为该毫秒数后过期;0 = 不自动过期(直到
* meow_notify_cancel);-1 = 常驻(ONGOING),同样不自动过期。 * meow_notify_cancel);-1 = 常驻(ONGOING),同样不自动过期。
* - 落点:本阶段内部维护活动通知表 + 用既有 meow_log() 打日志作为落点;真实投递到 * - 落点(任务 #5 SDK↔IPC 桥):本表登记后,经 IPC 把 MSG_NOTIFY(0x0020) 送到
* 通知中心 UI / D-Bus 是后续任务——meow_notify 结构已对齐 freedesktop 字段 * launcher 中央存储(meow_notif.c 的 meow_notif_post),回读 MSG_NOTIFY_ACK(0x0021)
* (summary/body/…)为此预留,但不写死后端。 * 取 launcher 分配的通知 id(nid,存进 slot->ipc_id,供后续 replaces_id/dismiss 用)。
* 通知中心 UI / D-Bus / TF 转储由 launcher 侧处理;SDK 本地活动表保留(app 进程内
* 即时查询/配额自检)。无 socket/写失败/超时 → 优雅降级(保留本地 id 兜底,不崩)。
* meow_notify 结构已对齐 freedesktop 字段(summary/body/…)为此预留。
* *
* 线程安全:pthread_mutex(同 meow_ui_log.c,launcher 前后台进程都会在已链 -lpthread * 线程安全:pthread_mutex(同 meow_ui_log.c,launcher 前后台进程都会在已链 -lpthread
* 的应用里调用)。不依赖 /dev/fb0、不依赖板子,可宿主编译。内部只复用 * 的应用里调用),且 IPC 发送/读 ACK 在持锁之外进行(避免 socket I/O 阻塞其它查询)。
* meow_ui_time_ms()(单调毫秒)与 meow_log()(统一日志)两个既有 SDK 能力。 * 不依赖 /dev/fb0、不依赖板子,可宿主编译。内部只复用 meow_ui_time_ms()(单调毫秒)、
* meow_log()(统一日志)与 meow_notify_ipc_*(IPC 桥,纯传输层)三个既有/新增 SDK 能力。
*/ */
#include "meow_ui.h" #include "meow_ui.h"
#include "meow_notify_ipc.h" /* 任务 #5:通知 → launcher 中央存储的 IPC 桥(映射+传输) */
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -32,7 +37,8 @@
/* 活动通知槽(内部;标题/正文等复制副本,不持 app 外部指针,避免栈字符串悬垂) */ /* 活动通知槽(内部;标题/正文等复制副本,不持 app 外部指针,避免栈字符串悬垂) */
typedef struct { typedef struct {
uint32_t id; /* 唯一 id;0=空闲槽 */ uint32_t id; /* 本地唯一 id(app 侧句柄);0=空闲槽 */
uint32_t ipc_id; /* launcher 中央存储分配的通知 id(nid);0=未同步/降级 */
int active; /* 1=活动(未取消未过期),0=空闲 */ int active; /* 1=活动(未取消未过期),0=空闲 */
meow_notify_category cat; meow_notify_category cat;
meow_notify_urgency urg; meow_notify_urgency urg;
@@ -85,6 +91,7 @@ static void notify_purge_expired(void)
if (s->timeout_ms > 0 && (now - s->created_ms) >= (uint64_t)s->timeout_ms) { if (s->timeout_ms > 0 && (now - s->created_ms) >= (uint64_t)s->timeout_ms) {
s->active = 0; s->active = 0;
s->id = 0; s->id = 0;
s->ipc_id = 0;
} }
} }
} }
@@ -93,6 +100,7 @@ static void notify_purge_expired(void)
static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uint64_t now) static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uint64_t now)
{ {
s->id = id; s->id = id;
s->ipc_id = 0; /* 初值=未同步;发送后由 notify_deliver 回填 launcher 分配的 nid */
s->active = 1; s->active = 1;
s->cat = n->cat; s->cat = n->cat;
s->urg = ((int)n->urgency >= 0 && (int)n->urgency < (int)MEOW_NOTIFY_URG_COUNT) s->urg = ((int)n->urgency >= 0 && (int)n->urgency < (int)MEOW_NOTIFY_URG_COUNT)
@@ -106,12 +114,23 @@ static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uin
s->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress); s->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress);
} }
/* 落点:本阶段用统一日志标出一条投递;真实后端(通知中心 UI / D-Bus)后续替换此处。 */ /* 落点(任务 #5):把通知经 IPC MSG_NOTIFY(0x0020) 送到 launcher 中央存储
static void notify_deliver(const notify_slot_t *s, const char *ev) * (meow_notif.c 的 meow_notif_post),回读 MSG_NOTIFY_ACK(0x0021) 取 launcher 分配的
* 通知 id(nid)。返回 nid(>0=成功);无 socket/写失败/超时 → 返回 0(调用方保留本地 id
* 兜底,不崩)。调用方须在持锁外调用(socket I/O 不占 g_mu,避免阻塞其它查询)。
* replaces_id:0=新建;>0=更新 launcher 侧已存在通知(freedesktop 语义)。 */
static uint32_t notify_deliver(const meow_notify *n, uint32_t replaces_id)
{ {
meow_log(MEOW_LOG_INFO, "[notify] %s id=%u type=%s urgency=%s title=%s", uint32_t nid = meow_notify_ipc_send(n, replaces_id);
ev, (unsigned)s->id, meow_notify_category_str(s->cat), if (nid) {
meow_notify_urgency_str(s->urg), s->title); meow_log(MEOW_LOG_INFO, "[notify] delivered nid=%u replaces=%u cat=%s title=%s",
(unsigned)nid, (unsigned)replaces_id, meow_notify_category_str(n->cat),
n->title ? n->title : "");
} else {
meow_log(MEOW_LOG_INFO, "[notify] degraded (no launcher) cat=%s title=%s",
meow_notify_category_str(n->cat), n->title ? n->title : "");
}
return nid;
} }
/* ============================================================ */ /* ============================================================ */
@@ -132,7 +151,7 @@ int meow_notify_register_type(meow_notify_category cat)
uint32_t meow_notify_send(const meow_notify *n) uint32_t meow_notify_send(const meow_notify *n)
{ {
uint32_t id; uint32_t id, nid, replaces_id = 0;
uint64_t now, oldest; uint64_t now, oldest;
int i, slot_idx, count, oldest_idx; int i, slot_idx, count, oldest_idx;
@@ -155,11 +174,16 @@ uint32_t meow_notify_send(const meow_notify *n)
if (n->cat == MEOW_NOTIFY_CAT_MEDIA) { if (n->cat == MEOW_NOTIFY_CAT_MEDIA) {
/* 媒体单例:复用已存在的媒体槽(重发替换同 app 上一条媒体通知) */ /* 媒体单例:复用已存在的媒体槽(重发替换同 app 上一条媒体通知) */
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
if (g_slots[i].active && g_slots[i].cat == MEOW_NOTIFY_CAT_MEDIA) { slot_idx = i; break; } if (g_slots[i].active && g_slots[i].cat == MEOW_NOTIFY_CAT_MEDIA) {
slot_idx = i;
replaces_id = g_slots[i].ipc_id; /* 更新该媒体通知(0=降级/未同步) */
break;
}
} else { } else {
count = notify_count_cat(n->cat); count = notify_count_cat(n->cat);
if (count >= MEOW_NOTIFY_MAX_PER_TYPE) { if (count >= MEOW_NOTIFY_MAX_PER_TYPE) {
/* 满则丢该类型最旧一条(替换) */ /* 满则丢该类型最旧一条(更新):先取被丢最旧的 launcher nid 作 replaces_id,
* 再释放槽(顺序不可反,否则 ipc_id 已被清 0)。 */
oldest_idx = -1; oldest_idx = -1;
oldest = UINT64_MAX; oldest = UINT64_MAX;
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
@@ -167,7 +191,13 @@ uint32_t meow_notify_send(const meow_notify *n)
oldest = g_slots[i].created_ms; oldest = g_slots[i].created_ms;
oldest_idx = i; oldest_idx = i;
} }
if (oldest_idx >= 0) { g_slots[oldest_idx].active = 0; g_slots[oldest_idx].id = 0; } if (oldest_idx >= 0) {
replaces_id = g_slots[oldest_idx].ipc_id; /* 复用被丢最旧的 launcher nid */
if (replaces_id == 0) replaces_id = 0; /* 未同步时按新建处理 */
g_slots[oldest_idx].active = 0;
g_slots[oldest_idx].id = 0;
g_slots[oldest_idx].ipc_id = 0;
}
} }
} }
if (slot_idx < 0) { if (slot_idx < 0) {
@@ -184,26 +214,42 @@ uint32_t meow_notify_send(const meow_notify *n)
id = g_next_id++; id = g_next_id++;
if (g_next_id == 0) g_next_id = 1; /* 防回绕 */ if (g_next_id == 0) g_next_id = 1; /* 防回绕 */
notify_fill(&g_slots[slot_idx], n, id, now); notify_fill(&g_slots[slot_idx], n, id, now);
notify_deliver(&g_slots[slot_idx], "send");
pthread_mutex_unlock(&g_mu); pthread_mutex_unlock(&g_mu);
/* 落点(持锁外):IPC 送 launcher 中央存储,回读 nid;无 launcher 则降级(nid=0)。 */
nid = notify_deliver(n, replaces_id);
/* 回填 launcher 分配的 nid(槽可能已被并发 cancel/过期清掉——仅在时回填)。 */
if (nid) {
pthread_mutex_lock(&g_mu);
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
if (g_slots[i].active && g_slots[i].id == id) { g_slots[i].ipc_id = nid; break; }
pthread_mutex_unlock(&g_mu);
}
return id; return id;
} }
int meow_notify_cancel(uint32_t id) int meow_notify_cancel(uint32_t id)
{ {
int i, rc = 0; int i, rc = 0;
uint32_t ipc_id = 0;
if (id == 0) return 0; if (id == 0) return 0;
pthread_mutex_lock(&g_mu); pthread_mutex_lock(&g_mu);
notify_purge_expired(); notify_purge_expired();
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) { for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) {
if (g_slots[i].active && g_slots[i].id == id) { if (g_slots[i].active && g_slots[i].id == id) {
ipc_id = g_slots[i].ipc_id; /* launcher 侧 nid;0=未同步/降级 */
g_slots[i].active = 0; g_slots[i].active = 0;
g_slots[i].id = 0; g_slots[i].id = 0;
g_slots[i].ipc_id = 0;
rc = 1; rc = 1;
break; break;
} }
} }
pthread_mutex_unlock(&g_mu); pthread_mutex_unlock(&g_mu);
/* 落点:经 IPC MSG_NOTIFY_DISMISS(0x0022) 撤下 launcher 中央存储对应通知
* (best-effort;无 launcher/未同步则静默降级)。 */
if (rc && ipc_id) meow_notify_ipc_dismiss(ipc_id);
return rc; return rc;
} }
+15 -18
View File
@@ -14,6 +14,7 @@
*/ */
#include "meow_ui_sys.h" #include "meow_ui_sys.h"
#include "meow_ui_internal.h" #include "meow_ui_internal.h"
#include "meow_ui_vol.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -23,7 +24,6 @@
#include <time.h> #include <time.h>
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" #define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf"
#define VOL_DEFAULT 60
/* 蓝牙 powered 缓存刷新间隔(hciconfig 较贵) */ /* 蓝牙 powered 缓存刷新间隔(hciconfig 较贵) */
#define BT_REFRESH_MS 30000 #define BT_REFRESH_MS 30000
@@ -80,22 +80,15 @@ static int hci_powered(void)
return up; return up;
} }
/* 音量:读 settings.conf 的 audio.volume(launcher 同源,watchdog 写同文件)。 */ /* 音量:读 settings.conf 的 audio.volume(launcher 同源,watchdog 写同文件)。
* 与 launcher 口径一致:0-100,且尊重 audio.volume_max 上限 clamp(vol_clamp 同义)。
* 注意 settings 落盘为 "key = value"(带空格),必须按 '=' 切分解析,不能朴素前缀匹配——
* 否则永远读不到(回退默认 60,即「状态栏音量数字不对、卡在 60」的根因)。 */
static int settings_vol(void) static int settings_vol(void)
{ {
FILE *f = fopen(MEOW_SETTINGS_FILE, "r"); int vol = meow_ui_vol_read(MEOW_SETTINGS_FILE, "audio.volume", MEOW_UI_VOL_DEFAULT);
if (!f) return VOL_DEFAULT; int vol_max = meow_ui_vol_read(MEOW_SETTINGS_FILE, "audio.volume_max", MEOW_UI_VOL_MAX_DEF);
int vol = VOL_DEFAULT; return meow_ui_vol_clamp(vol, vol_max);
char buf[128];
while (fgets(buf, sizeof buf, f)) {
if (!strncmp(buf, "audio.volume=", 13)) {
int v = atoi(buf + 13);
if (v >= 0 && v <= 100) vol = v;
break;
}
}
fclose(f);
return vol;
} }
int meow_ui_sys_init(void) int meow_ui_sys_init(void)
@@ -210,21 +203,25 @@ void meow_ui_sys_statusbar_render(void)
/* 整条清底:覆盖 app 可能画进顶行的内容(z 最顶) */ /* 整条清底:覆盖 app 可能画进顶行的内容(z 最顶) */
meow_ui_fill_rect(0, 0, w, h, th->bg); meow_ui_fill_rect(0, 0, w, h, th->bg);
/* 文字垂直锚 y:(栏高-行高)/2 居中;下限 1 防极端行高 > 栏高时负 y 溢出顶端(不越界优先)。 */
int ty = (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2;
if (ty < 1) ty = 1;
/* 时钟(左):每秒现取 localtime */ /* 时钟(左):每秒现取 localtime */
char timebuf[16]; char timebuf[16];
time_t t = time(NULL); time_t t = time(NULL);
struct tm tm; struct tm tm;
localtime_r(&t, &tm); localtime_r(&t, &tm);
snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
meow_ui_text(8, (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2, timebuf, MUI_SBAR_FONT, th->fg); meow_ui_text(8, ty, timebuf, MUI_SBAR_FONT, th->fg);
/* 右侧信息串:batt% WIFI 蓝牙 VOL nn% */ /* 右侧信息串:batt% WIFI 蓝牙 VOL nn% */
char right[96]; char right[96];
statusbar_right_str(right, sizeof right); statusbar_right_str(right, sizeof right);
if (right[0]) if (right[0])
meow_ui_text_right(w - 8, (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2, right, MUI_SBAR_FONT, th->fg); meow_ui_text_right(w - 8, ty, right, MUI_SBAR_FONT, th->fg);
/* 分隔线(栏内最底 1px,主题次要色);内容 inset=statusbar_height()=40 从 y=40 起即干净 */ /* 分隔线(栏内最底 1px,主题次要色);内容 inset=statusbar_height()=28 从 y=28 起即干净 */
meow_ui_fill_rect(0, h - 1, w, 1, th->dim); meow_ui_fill_rect(0, h - 1, w, 1, th->dim);
g_statusbar_force = 0; g_statusbar_force = 0;
+7 -5
View File
@@ -10,7 +10,8 @@
* - 电量 /sys/class/power_supply/battery/{capacity,status} * - 电量 /sys/class/power_supply/battery/{capacity,status}
* - WiFi /sys/class/net/wlan0/operstate * - WiFi /sys/class/net/wlan0/operstate
* - 蓝牙 实际 powered(hciconfig -a hci0 的 UP;spawn 贵 → 缓存 30s 刷一次) * - 蓝牙 实际 powered(hciconfig -a hci0 的 UP;spawn 贵 → 缓存 30s 刷一次)
* - 音量 /usr/data/meow/settings.conf 的 audio.volume(launcher 同文件) * - 音量 /usr/data/meow/settings.conf 的 audio.volume(launcher 同文件;
* 0-100 并 clamp 到 audio.volume_max 上限,与 launcher vol_clamp 口径一致)
* - 时钟 localtime_r(每秒在绘制时现取) * - 时钟 localtime_r(每秒在绘制时现取)
*/ */
#ifndef MEOW_UI_SYS_H #ifndef MEOW_UI_SYS_H
@@ -28,9 +29,10 @@ typedef struct meow_sys_state {
int vol; /* 音量 0-100 */ int vol; /* 音量 0-100 */
} meow_sys_state; } meow_sys_state;
/* 顶部状态栏几何(与 launcher SB_BAR_H=40 / SB_FONT=28 对齐,视觉统一)。 */ /* 顶部状态栏几何(与 launcher SB_BAR_H=28 / SB_FONT=20 对齐,视觉统一)。
#define MUI_SBAR_H 40 * 高度 40→28(用户定),字号 28→20(28px 栏放不下 28 号;20 号按 (高-行高)/2 垂直居中)。 */
#define MUI_SBAR_FONT 28 #define MUI_SBAR_H 28
#define MUI_SBAR_FONT 20
/* 首次全量读取;失败仍返回 0(字段置 -1/0)。 */ /* 首次全量读取;失败仍返回 0(字段置 -1/0)。 */
int meow_ui_sys_init(void); int meow_ui_sys_init(void);
@@ -44,7 +46,7 @@ const meow_sys_state *meow_ui_sys_get(void);
/* 状态栏是否应绘制(= set_statusbar(1) 且 非 fullscreen)。 */ /* 状态栏是否应绘制(= set_statusbar(1) 且 非 fullscreen)。 */
int meow_ui_sys_statusbar_active(void); int meow_ui_sys_statusbar_active(void);
/* 状态栏高度(px):active 时 40,否则 0(供 meow_ui_statusbar_height 用)。 */ /* 状态栏高度(px):active 时 28,否则 0(供 meow_ui_statusbar_height 用)。 */
int meow_ui_sys_statusbar_height(void); int meow_ui_sys_statusbar_height(void);
/* 自上次渲染后状态栏数据是否已变(1s 轻刷/显隐切换置位);读取即清除。 /* 自上次渲染后状态栏数据是否已变(1s 轻刷/显隐切换置位);读取即清除。
+52
View File
@@ -0,0 +1,52 @@
/*
* meow_ui_vol.c — SDK 状态栏音量换算纯函数 + settings.conf 读取(A-0 核心数源)。
*
* 单独成 TU 的目的:
* 1. 纯函数(clamp / 键读取)不依赖 meow_ui 其它子系统,宿主 gcc 直接可测
* (test_meow_ui_vol.c),无需链整库/stub。
* 2. 把「音量从 settings 读数并 clamp 到 volume_max」这一语义钉死在单点,
* 与 launcher 的 vol_clamp(meow_launcherd.c)口径对齐,杜绝两端音量漂移。
*
* 数据源:/usr/data/meow/settings.conf 扁平 key=value。launcher meow_settings.c
* 落盘为 "key = value"(带空格),本解析兼容无空格/多空格,与 launcher 读循环
* 及 sdk meow_ui_theme.c 的 get_setting_str 同款解析。
*/
#include "meow_ui_vol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int meow_ui_vol_clamp(int vol, int vol_max)
{
if (vol < 0) vol = 0;
if (vol > MEOW_UI_VOL_MAX) vol = MEOW_UI_VOL_MAX;
if (vol_max < 0) vol_max = 0; /* 异常上限兜底:不进负值 */
if (vol > vol_max) vol = vol_max;
return vol;
}
int meow_ui_vol_read(const char *path, const char *key, int def)
{
if (!path || !key) return def;
FILE *f = fopen(path, "r");
if (!f) return def;
int kl = (int)strlen(key);
char line[160];
char *found = NULL;
while (fgets(line, sizeof line, f)) {
char *eq = strchr(line, '=');
if (!eq) continue; /* 非 key=value 行(注释/空行)跳过 */
char *k = line;
while (*k == ' ' || *k == '\t') k++; /* key 左去空白 */
int n = (int)(eq - k);
while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; /* key 右去空白 */
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; /* key 不精确匹配 */
found = eq + 1; /* 命中所求键 */
break;
}
fclose(f);
if (!found) return def;
while (*found == ' ' || *found == '\t') found++; /* value 左去空白;atoi 天然停于非数字 */
return atoi(found);
}
+32
View File
@@ -0,0 +1,32 @@
/*
* meow_ui_vol.h — SDK 状态栏音量换算/读取(meow_ui_vol.c)。
* 不随 meow_ui.h 发布给应用;仅供 meow_ui_sys.c 与宿主单测(test_meow_ui_vol.c)使用。
*/
#ifndef MEOW_UI_VOL_H
#define MEOW_UI_VOL_H
/* 音量语义常量(与 launcher MEOW_VOL_MAX/DEFAULT、meow_settings 表逐一致):
* audio.volume 默认 60,取值域 0-100(硬上限 MEOW_UI_VOL_MAX);
* audio.volume_max 默认 100(软上限,clamp 上限,用户可调小);
* audio.volume_step 默认 1(跳步;只影响「每次音量键改多少」,与状态栏展示无关——
* SDK 只读当前值,步进由改动方(launcher)承担)。 */
#define MEOW_UI_VOL_DEFAULT 60
#define MEOW_UI_VOL_MAX 100
#define MEOW_UI_VOL_MAX_DEF 100
/* 纯函数:把软件音量夹到 [0, MEOW_UI_VOL_MAX],再夹到 volume_max 软上限。
* 语义与 launcher 的 vol_clamp(meow_launcherd.c)一致:
* - 硬上限 100(0-100 口径);
* - 尊重 audio.volume_max(用户把上限调小则音量被夹到该值);
* - volume_max 异常(<0)兜底为 0(不进负值)。
* 无文件/全局依赖,宿主 gcc 可直接测。 */
int meow_ui_vol_clamp(int vol, int vol_max);
/* 读 settings.conf 里一个 int 键。扁平 "key=value" 格式(launcher meow_settings.c
* 落盘为 "key = value",带空格;这里兼容无空格/多空格两种写式)。
* 解析:'=' 切分,key/value 去首尾空白,key 精确匹配(避免 audio.volume 误配
* audio.volume_max)。未命中/打开失败/键缺失回退 def。
* path 由调用方传入(设备传导 MEOW_SETTINGS_FILE;宿主单测指向临时文件)。 */
int meow_ui_vol_read(const char *path, const char *key, int def);
#endif /* MEOW_UI_VOL_H */
+145
View File
@@ -0,0 +1,145 @@
/*
* test_meow_ui_vol.c — 宿主单元测试:SDK 状态栏音量换算纯函数 + settings.conf 读取。
*
* 直接编译 meow_ui_vol.c(纯函数,无 meow_ui 依赖),用临时 settings 文件验证:
* V1 解析:launcher 落盘 "key = value"(带空格)形式能正确读到值(根因回归——
* 旧代码朴素前缀匹配 "audio.volume=" 永远读不到 → 卡默认 60);
* V2 解析:无空格 "key=value" / 多空格 / 头注释行 / 键缺失回退默认 / 精确匹配
* (audio.volume 不误配 audio.volume_max);
* V3 clamp:volume_max 上限、硬上限 100、负值兜底 0、上限异常(<0)兜底 0;
* V4 管道(镜像 meow_ui_sys.c settings_vol):读 + clamp 到 volume_max;
* V5 展示串:与 meow_ui_sys.c statusbar_right_str 同款 "VOL %d%%",断言显示值即
* 当前夹后音量(含 step 语义:a.volume_step 只决定步进,展示原样不/舍入)。
*
* 编译(宿主 gcc,mipsel 无关):
* gcc -o test_meow_ui_vol test_meow_ui_vol.c meow_ui_vol.c -I.
*/
#include "meow_ui_vol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int failures = 0;
static void check(const char *name, int cond)
{
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
if (!cond) failures++;
}
/* 写一份临时 settings 文件(lines 为已含 '\n' 的行内容;NULL 结束) */
static const char *tmp_path;
static void write_settings(const char **lines)
{
FILE *f = fopen(tmp_path, "w");
if (!f) { perror("fopen"); exit(2); }
for (int i = 0; lines[i]; i++) fputs(lines[i], f);
fclose(f);
}
/* 镜像 meow_ui_sys.c 的 settings_vol():读 audio.volume + volume_max,再 clamp。 */
static int settings_vol_of(const char *path)
{
int vol = meow_ui_vol_read(path, "audio.volume", MEOW_UI_VOL_DEFAULT);
int vol_max = meow_ui_vol_read(path, "audio.volume_max", MEOW_UI_VOL_MAX_DEF);
return meow_ui_vol_clamp(vol, vol_max);
}
/* 状态栏右侧 VOL 串组装(与 meow_ui_sys.c statusbar_right_str 同款)。 */
static void vol_display(int vol, int vol_max, char *out, size_t n)
{
snprintf(out, n, "VOL %d%%", meow_ui_vol_clamp(vol, vol_max));
}
int main(void)
{
char base[64];
snprintf(base, sizeof base, "/tmp/meow_vol_%d.conf", (int)getpid());
tmp_path = base;
/* ---- V1 解析:launcher 落盘格式 "key = value"(带空格)——根因回归 ---- */
{
const char *L[] = { "# meow-settings 1\n", "audio.volume = 82\n", NULL };
write_settings(L);
check("V1 launcher 空格格式 audio.volume=82 -> 82",
meow_ui_vol_read(tmp_path, "audio.volume", -1) == 82);
}
/* ---- V2 解析:无空格 / 多空格 / 注释 / 缺键 / 精确匹配 ---- */
{
const char *L[] = { "audio.volume=40\n", NULL };
write_settings(L);
check("V2 无空格 audio.volume=40 -> 40",
meow_ui_vol_read(tmp_path, "audio.volume", -1) == 40);
const char *L2[] = { " audio.volume = 7 \n", NULL };
write_settings(L2);
check("V2 多空格 audio.volume = 7 -> 7",
meow_ui_vol_read(tmp_path, "audio.volume", -1) == 7);
const char *L3[] = { "display.brightness = 100\n", NULL };
write_settings(L3);
check("V2 键缺失 -> 默认 -1", meow_ui_vol_read(tmp_path, "audio.volume", -1) == -1);
const char *L4[] = { "audio.volume_max = 90\n", NULL };
write_settings(L4);
check("V2 audio.volume 不误配 audio.volume_max -> 默认",
meow_ui_vol_read(tmp_path, "audio.volume", -1) == -1);
check("V2 单独读 audio.volume_max=90 -> 90",
meow_ui_vol_read(tmp_path, "audio.volume_max", -1) == 90);
}
/* ---- V3 clamp 纯函数 ---- */
check("V3 clamp(60,100)=60", meow_ui_vol_clamp(60, 100) == 60);
check("V3 clamp(80,60)=60 (尊重 volume_max)", meow_ui_vol_clamp(80, 60) == 60);
check("V3 clamp(80,100)=80", meow_ui_vol_clamp(80, 100) == 80);
check("V3 clamp(150,100)=100 (硬上限)", meow_ui_vol_clamp(150, 100) == 100);
check("V3 clamp(150,80)=80", meow_ui_vol_clamp(150, 80) == 80);
check("V3 clamp(-5,100)=0 (负值兜底)", meow_ui_vol_clamp(-5, 100) == 0);
check("V3 clamp(60,0)=0 (上限 0)", meow_ui_vol_clamp(60, 0) == 0);
check("V3 clamp(60,-10)=0 (上限异常兜底)", meow_ui_vol_clamp(60, -10) == 0);
check("V3 clamp(120,255)=100 (硬上限优先于大上限)", meow_ui_vol_clamp(120, 255) == 100);
/* ---- V4 管道:读 + clamp 到 volume_max(镜像 settings_vol)---- */
{
const char *L[] = { "audio.volume = 82\n", "audio.volume_max = 100\n", NULL };
write_settings(L);
check("V4 vol=82 max=100 -> 82", settings_vol_of(tmp_path) == 82);
const char *L2[] = { "audio.volume = 82\n", "audio.volume_max = 60\n", NULL };
write_settings(L2);
check("V4 vol=82 max=60 -> 60 (clamp)", settings_vol_of(tmp_path) == 60);
const char *L3[] = { "audio.volume = 150\n", "audio.volume_max = 100\n", NULL };
write_settings(L3);
check("V4 vol=150 max=100 -> 100 (硬上限)", settings_vol_of(tmp_path) == 100);
const char *L4[] = { "audio.volume = -3\n", "audio.volume_max = 100\n", NULL };
write_settings(L4);
check("V4 vol=-3 -> 0", settings_vol_of(tmp_path) == 0);
const char *L5[] = { "display.brightness = 100\n", NULL }; /* 无音量键 */
write_settings(L5);
check("V4 无 audio.volume -> 默认 60", settings_vol_of(tmp_path) == MEOW_UI_VOL_DEFAULT);
}
/* ---- V5 展示串:VOL %d%(step 只决定步进,展示原样不/舍入)---- */
{
char s[16];
vol_display(60, 100, s, sizeof s);
check("V5 vol=60 -> \"VOL 60%\"", !strcmp(s, "VOL 60%"));
vol_display(80, 60, s, sizeof s);
check("V5 vol=80 max=60 -> \"VOL 60%\" (显示夹后值)", !strcmp(s, "VOL 60%"));
vol_display(-5, 100, s, sizeof s);
check("V5 vol=-5 -> \"VOL 0%\"", !strcmp(s, "VOL 0%"));
vol_display(37, 100, s, sizeof s);
check("V5 vol=37 -> \"VOL 37%\" (step 值原样展示)", !strcmp(s, "VOL 37%"));
}
unlink(tmp_path);
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}
+202
View File
@@ -0,0 +1,202 @@
/*
* test_notify_integration.c — 宿主集成测试:meow_notify_send/cancel 全链路(任务 #5)。
*
* 直接把 meow_ui_notify.c + meow_notify_ipc.c 编进来,仅 stub 两个 SDK 能力
* (meow_log 空实现 / meow_ui_time_ms 单调毫秒),用本地 AF_UNIX stub socket 回环:
*
* S1 send(general,timeout=0) → 返回本地 id(1),stub 回 nid=101,active_count=1;
* S2 send(media) → media=1、category=media(经 stub 捕获帧校验);
* S3 同类型再发(未满配额)→ 返回本地 id(3);
* S4 cancel(本地id=1) → 返回 1,stub 收到 MSG_NOTIFY_DISMISS(负载 nid=101);
* S5 配额满(每类型4)后发第 5 条 → 丢最旧:该 5 条 send 的 replaces_id == 被丢最旧的 nid;
* S6 无 launcher(connect 失败)→ send 仍返回本地 id(>0)、不崩;cancel 也静默不崩。
*
* 编译(宿主 gcc):
* gcc -o test_notify_integration test_notify_integration.c \
* meow_ui_notify.c meow_notify_ipc.c -I. -I../meow_launcherd -lpthread
*/
#include "meow_ui.h"
#include "meow_notify_ipc.h"
#include "meow_ipc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <pthread.h>
/* ---- stub SDK 依赖(避免链整库) ---- */
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
uint64_t meow_ui_time_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
/* ---- stub server 捕获(每次连接一帧) ---- */
#define MAXCAP 16
typedef struct { int type; m_notify_t n; uint32_t dismiss_id; uint32_t nid; } srec_t;
static srec_t g_rec[MAXCAP];
static int g_nrec = 0;
static int g_stop = 0;
static int g_listen_fd = -1;
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static void *server(void *arg)
{
(void)arg;
while (!g_stop) {
int c = accept(g_listen_fd, NULL, NULL);
if (c < 0) {
if (errno == EAGAIN && g_stop) break; /* 非阻塞:listener 被关闭 */
if (errno == EBADF) break;
usleep(1000);
continue;
}
uint8_t hdr[MEOW_IPC_HDR_LEN];
size_t off = 0;
while (off < sizeof hdr) { ssize_t r = read(c, hdr + off, sizeof hdr - off); if (r <= 0) break; off += (size_t)r; }
if (off < sizeof hdr) { close(c); continue; }
uint32_t type, len, req_id;
memcpy(&req_id, hdr + 8, 4);
memcpy(&len, hdr + 12, 4);
memcpy(&type, hdr + 6, 2);
if (len > MEOW_IPC_MAX) { close(c); continue; }
uint8_t pay[MEOW_IPC_MAX];
off = 0;
while (off < len) { ssize_t r = read(c, pay + off, len - off); if (r <= 0) break; off += (size_t)r; }
pthread_mutex_lock(&g_mu);
int idx = g_nrec < MAXCAP ? g_nrec++ : (MAXCAP - 1);
memset(&g_rec[idx], 0, sizeof g_rec[idx]);
g_rec[idx].type = (int)type;
if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) memcpy(&g_rec[idx].n, pay, sizeof(m_notify_t));
if (type == MSG_NOTIFY_DISMISS && len >= 4) memcpy(&g_rec[idx].dismiss_id, pay, 4);
/* 分配 nid = 100 + NOTIFY 序号 */
uint32_t seq = 0;
for (int i = 0; i <= idx; i++) if (g_rec[i].type == MSG_NOTIFY) seq++;
g_rec[idx].nid = (type == MSG_NOTIFY) ? (100 + seq) : 0;
uint32_t ack_nid = g_rec[idx].nid;
pthread_mutex_unlock(&g_mu);
if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS) {
m_ack_t a; memset(&a, 0, sizeof a); a.code = MEOW_OK; a.status = ack_nid;
uint8_t rhdr[MEOW_IPC_HDR_LEN];
uint32_t mag = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION, t = (uint16_t)
(type == MSG_NOTIFY ? MSG_NOTIFY_ACK : MSG_ACK); uint32_t rl = sizeof a;
memcpy(rhdr+0,&mag,4); memcpy(rhdr+4,&ver,2); memcpy(rhdr+6,&t,2); memcpy(rhdr+8,&req_id,4); memcpy(rhdr+12,&rl,4);
size_t w0 = 0; while (w0 < sizeof rhdr) { ssize_t w = write(c, rhdr+w0, sizeof rhdr-w0); if (w<=0) break; w0 += (size_t)w; }
size_t w1 = 0; while (w1 < rl) { ssize_t w = write(c, (uint8_t*)&a+w1, rl-w1); if (w<=0) break; w1 += (size_t)w; }
}
close(c);
}
return NULL;
}
static int g_listen_made = 0;
static int make_listen(char *path, size_t n)
{
snprintf(path, n, "/tmp/meow_notify_intg_%d.sock", (int)getpid());
unlink(path);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un a; memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX; strncpy(a.sun_path, path, sizeof a.sun_path - 1);
if (bind(fd, (struct sockaddr*)&a, sizeof a) < 0 || listen(fd, 4) < 0) { close(fd); return -1; }
fcntl(fd, F_SETFL, O_NONBLOCK); /* 非阻塞 listener:accept() 可被 g_stop 截断 */
g_listen_fd = fd; g_listen_made = 1;
return 0;
}
static void stop_server(void)
{
if (g_listen_made) { g_stop = 1; close(g_listen_fd); g_listen_made = 0; }
}
static srec_t rec_at(int i)
{
srec_t r; pthread_mutex_lock(&g_mu); r = g_rec[i]; pthread_mutex_unlock(&g_mu); return r;
}
static int failures = 0;
static void check(const char *name, int cond)
{
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
if (!cond) failures++;
}
int main(void)
{
setenv("MEOW_LOG_APP", "intgapp", 1);
char sock[108];
if (make_listen(sock, sizeof sock) != 0) { fprintf(stderr, "cannot listen\n"); return 2; }
setenv("MEOW_IPC_SOCK", sock, 1);
pthread_t th; pthread_create(&th, NULL, server, NULL);
/* S1 通用通知 */
meow_notify nt; memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.urgency = MEOW_NOTIFY_URG_NORMAL;
nt.title = "t1"; nt.timeout_ms = 0;
uint32_t id1 = meow_notify_send(&nt);
check("S1 send 返回本地 id=1", id1 == 1);
check("S1 active_count=1", meow_notify_active_count() == 1);
check("S1 stub 回 nid=101", rec_at(0).nid == 101);
/* S2 媒体通知:单例 + media=1 */
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_MEDIA; nt.title = "music"; nt.timeout_ms = -1;
uint32_t id2 = meow_notify_send(&nt);
check("S2 媒体返本地 id=2", id2 == 2);
check("S2 category=media", !strcmp(rec_at(1).n.category, "media"));
check("S2 media=1", rec_at(1).n.media == 1);
check("S2 expire=0xFFFFFFFF", rec_at(1).n.expire_timeout == 0xFFFFFFFFu);
/* S3 同类型再发(general 未满 4)→ 新本地 id */
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.title = "t3"; nt.timeout_ms = 0;
uint32_t id3 = meow_notify_send(&nt);
check("S3 再发返本地 id=3", id3 == 3);
check("S3 第2条general nid=103", rec_at(2).nid == 103);
/* S4 cancel(id1) → DISMISS nid=101 */
int rc = meow_notify_cancel(id1);
check("S4 cancel 返回 1", rc == 1);
check("S4 stub 收到 MSG_NOTIFY_DISMISS", rec_at(3).type == MSG_NOTIFY_DISMISS);
check("S4 dismiss 负载 nid=101", rec_at(3).dismiss_id == 101);
check("S4 本地表已撤(count=2)", meow_notify_active_count() == 2);
/* S5 配额满 → 丢最旧 + replaces_id 复用被丢 nid。
当前 general 有 id3(1 条);再发 4 条到 4 条(第 4 条触顶后丢最旧)。 */
for (int i = 0; i < 4; i++) {
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.title = "quota"; nt.timeout_ms = 0;
(void)meow_notify_send(&nt);
}
/* 现在 general 应仍有 4 条(第5条丢最旧 id3=本地,即被降级替换) */
check("S5 配额满后每类型仍=4", meow_notify_count_in_type(MEOW_NOTIFY_CAT_GENERAL) == 4);
/* 被丢最旧 = id3(第2条 general,本地 id=3),其 launcher nid=103(rec[2])。
触发丢最旧的是 S5 循环第 4 次 send = 第 7 条 NOTIFY(rec[7]),其 replaces_id==103。 */
check("S5 丢最旧后 send replaces_id=被丢nid(103)", rec_at(7).n.replaces_id == 103);
stop_server();
pthread_join(th, NULL);
/* S6 无 launcher 降级 */
char dead[108]; snprintf(dead, sizeof dead, "/tmp/meow_notify_intg_dead_%d.sock", (int)getpid());
unlink(dead);
setenv("MEOW_IPC_SOCK", dead, 1);
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_EMAIL; nt.title = "no-launcher"; nt.timeout_ms = 0;
uint32_t id6 = meow_notify_send(&nt);
check("S6 无 launcher 仍返回本地 id(>0)", id6 > 0);
check("S6 无 launcher active_count 正常", meow_notify_active_count() >= 1);
check("S6 cancel 无 launcher 静默不崩", meow_notify_cancel(id6) == 1);
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}
+281
View File
@@ -0,0 +1,281 @@
/*
* test_notify_ipc.c — 宿主测试:任务 #5 SDK↔IPC 桥(meow_notify_ipc.{h,c})。
*
* 用本地 AF_UNIX stub socket(回环)验证:
* T1 枚举↔字符串映射:CAT_PROGRESS→"progress"、CAT_MEDIA→"media" + media=1;
* T2 timeout 映射:-1(ONGOING)→expire=0xFFFFFFFF + ongoing=1;0(NEVER)→expire=0;
* >0→该毫秒 + ongoing=0;
* T3 回读 MSG_NOTIFY_ACK 拿 launcher 分配的 nid;replaces_id 原样透传;progress 钳位;
* T4 无 socket 降级:connect 失败 → meow_notify_ipc_send 返回 0,不崩;
* T5 dismiss:meow_notify_ipc_dismiss(nid) 发出 MSG_NOTIFY_DISMISS 帧(负载=nid)。
*
* 编译(宿主 gcc,不依赖 meow_ui.a 其余 TU):
* gcc -o test_notify_ipc test_notify_ipc.c meow_notify_ipc.c \
* -I. -I../meow_launcherd -lpthread
* 运行:./test_notify_ipc (全部 PASS 退出码 0,任一 FAIL 退出码 1)
*/
#include "meow_notify_ipc.h"
#include "meow_ui.h"
#include "meow_ipc.h" /* 帧头 / m_notify_t / m_ack_t / MSG_*(同源校验) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <pthread.h>
/* ---- 捕获:stub 服务端解析到的帧内容(mutex 保护;客户端返回后读取) ---- */
typedef struct {
uint32_t type; /* 收到的帧类型 */
m_notify_t n; /* MSG_NOTIFY 负载 */
uint32_t dismiss_id; /* MSG_NOTIFY_DISMISS 负载 */
uint32_t req_id;
uint32_t nid_to_return; /* ACK 中回填的 status */
int got; /* 已收到一帧 */
} cap_t;
static cap_t g_cap;
static pthread_mutex_t g_cap_mu = PTHREAD_MUTEX_INITIALIZER;
static void cap_store(uint32_t type, uint32_t req_id, const uint8_t *pay, uint32_t len,
uint32_t nid_to_return)
{
pthread_mutex_lock(&g_cap_mu);
memset(&g_cap, 0, sizeof g_cap);
g_cap.type = type;
g_cap.req_id = req_id;
g_cap.nid_to_return = nid_to_return;
if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) memcpy(&g_cap.n, pay, sizeof(m_notify_t));
if (type == MSG_NOTIFY_DISMISS && len >= 4) memcpy(&g_cap.dismiss_id, pay, 4);
g_cap.got = 1;
pthread_mutex_unlock(&g_cap_mu);
}
/* ---- stub 服务端线程:accept 一个连接 → 读一帧 → 校验由主线程做,这里只收 + 回 ACK ---- */
static int g_listen_fd = -1;
static void *server_thread(void *arg)
{
(void)arg;
int c = accept(g_listen_fd, NULL, NULL);
if (c < 0) return NULL;
/* 读 16B 头 */
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t off = 0;
while (off < sizeof hdr) {
ssize_t r = read(c, hdr + off, sizeof hdr - off);
if (r <= 0) { close(c); return NULL; }
off += (size_t)r;
}
uint32_t magic, len;
uint16_t ver, type;
memcpy(&magic, hdr, 4);
memcpy(&ver, hdr + 4, 2);
memcpy(&type, hdr + 6, 2);
memcpy(&len, hdr + 12, 4);
uint32_t req_id; memcpy(&req_id, hdr + 8, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) { close(c); return NULL; }
if (len > MEOW_IPC_MAX) { close(c); return NULL; }
uint8_t pay[MEOW_IPC_MAX];
off = 0;
while (off < len) {
ssize_t r = read(c, pay + off, len - off);
if (r <= 0) { close(c); return NULL; }
off += (size_t)r;
}
cap_store(type, req_id, pay, len, (uint32_t)strtoul(getenv("TEST_NID"), NULL, 0));
/* MSG_NOTIFY → 回 m_ack_t(code=OK, status=nid_to_return);
* MSG_NOTIFY_DISMISS → 回 m_ack_t(code=OK, status=0,同 launcher 行为)。 */
if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS) {
m_ack_t a;
memset(&a, 0, sizeof a);
a.code = MEOW_OK;
a.status = (type == MSG_NOTIFY) ? (uint32_t)strtoul(getenv("TEST_NID"), NULL, 0) : 0;
uint8_t rhdr[MEOW_IPC_HDR_LEN];
uint32_t rmagic = MEOW_IPC_MAGIC;
uint16_t rver = MEOW_IPC_VERSION, rtype = (uint16_t)(type == MSG_NOTIFY ? MSG_NOTIFY_ACK : MSG_ACK);
uint32_t rlen = sizeof a;
memcpy(rhdr + 0, &rmagic, 4);
memcpy(rhdr + 4, &rver, 2);
memcpy(rhdr + 6, &rtype, 2);
memcpy(rhdr + 8, &req_id, 4);
memcpy(rhdr + 12, &rlen, 4);
off = 0;
while (off < sizeof rhdr) { ssize_t w = write(c, rhdr + off, sizeof rhdr - off); if (w <= 0) break; off += (size_t)w; }
off = 0;
while (off < rlen) { ssize_t w = write(c, (uint8_t*)&a + off, rlen - off); if (w <= 0) break; off += (size_t)w; }
}
close(c);
return NULL;
}
static int failures = 0;
static void check(const char *name, int cond)
{
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
if (!cond) failures++;
}
/* 建 listening socket,返回 fd;路径写入 sock_path。 */
static int arb_listen(char *sock_path, size_t slen)
{
static int count = 0;
snprintf(sock_path, slen, "/tmp/meow_notify_ipc_%d_%d.sock", (int)getpid(), count++);
unlink(sock_path);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return -1; }
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, sock_path, sizeof a.sun_path - 1);
if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { perror("bind"); close(fd); return -1; }
if (listen(fd, 1) < 0) { perror("listen"); close(fd); return -1; }
return fd;
}
int main(void)
{
setenv("MEOW_LOG_APP", "testapp", 1); /* 固定 app_id,供 stub 校验 */
/* ---------- T1/T2/T3:映射 + ACK 回 nid ---------- */
{
char sock[108];
g_listen_fd = arb_listen(sock, sizeof sock);
if (g_listen_fd < 0) { fprintf(stderr, "cannot create listener\n"); return 2; }
setenv("MEOW_IPC_SOCK", sock, 1);
setenv("TEST_NID", "0x1234", 1);
pthread_t th;
pthread_create(&th, NULL, server_thread, NULL);
meow_notify nt;
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_PROGRESS;
nt.urgency = MEOW_NOTIFY_URG_NORMAL;
nt.title = "下载中";
nt.body = "70%";
nt.timeout_ms = 5000; /* >0 → expire=5000, ongoing=0 */
nt.progress = 150; /* 钳位 → 100 */
uint32_t nid = meow_notify_ipc_send(&nt, 0);
pthread_join(th, NULL);
close(g_listen_fd);
check("T3 send 返回 launcher nid(0x1234)", nid == 0x1234);
pthread_mutex_lock(&g_cap_mu);
int got = g_cap.got;
const m_notify_t *cn = &g_cap.n;
pthread_mutex_unlock(&g_cap_mu);
check("T3 stub 收到 MSG_NOTIFY 帧", got && g_cap.type == MSG_NOTIFY);
check("T1 CAT_PROGRESS→category=progress", !strcmp(cn->category, "progress"));
check("T2 timeout>0→expire_timeout=5000", cn->expire_timeout == 5000);
check("T2 timeout>0→ongoing=0", cn->ongoing == 0);
check("T1 media=0(非媒体)", cn->media == 0);
check("T1 progress 钳位 150→100", cn->progress == 100);
check("T2 replaces_id 透传=0", cn->replaces_id == 0);
check("T1 app_id=testapp", !strcmp(cn->app_id, "testapp"));
check("T1 summary=下载中", !strcmp(cn->summary, "下载中"));
}
/* ---------- T1/T2:媒体类型 + ONGOING(-1) ---------- */
{
char sock[108];
g_listen_fd = arb_listen(sock, sizeof sock);
setenv("MEOW_IPC_SOCK", sock, 1);
setenv("TEST_NID", "0x9999", 1);
pthread_t th;
pthread_create(&th, NULL, server_thread, NULL);
meow_notify nt;
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_MEDIA;
nt.urgency = MEOW_NOTIFY_URG_CRITICAL;
nt.title = "Now Playing";
nt.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING; /* -1 */
uint32_t nid = meow_notify_ipc_send(&nt, 0);
pthread_join(th, NULL);
close(g_listen_fd);
check("T3 send(media/ongoing) 返回 nid(0x9999)", nid == 0x9999);
check("T1 CAT_MEDIA→category=media", !strcmp(g_cap.n.category, "media"));
check("T1 CAT_MEDIA→media=1", g_cap.n.media == 1);
check("T2 ONGOING(-1)→expire=0xFFFFFFFF", g_cap.n.expire_timeout == 0xFFFFFFFFu);
check("T2 ONGOING(-1)→ongoing=1", g_cap.n.ongoing == 1);
}
/* ---------- T2:NEVER(0) 超时 ---------- */
{
char sock[108];
g_listen_fd = arb_listen(sock, sizeof sock);
setenv("MEOW_IPC_SOCK", sock, 1);
setenv("TEST_NID", "0x1", 1);
pthread_t th;
pthread_create(&th, NULL, server_thread, NULL);
meow_notify nt;
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_GENERAL;
nt.title = "x";
nt.timeout_ms = MEOW_NOTIFY_TIMEOUT_NEVER; /* 0 */
(void)meow_notify_ipc_send(&nt, 0);
pthread_join(th, NULL);
close(g_listen_fd);
check("T2 NEVER(0)→expire=0 & ongoing=0",
g_cap.n.expire_timeout == 0 && g_cap.n.ongoing == 0);
check("T1 CAT_GENERAL→category=general", !strcmp(g_cap.n.category, "general"));
}
/* ---------- T3:replaces_id 透传 ---------- */
{
char sock[108];
g_listen_fd = arb_listen(sock, sizeof sock);
setenv("MEOW_IPC_SOCK", sock, 1);
setenv("TEST_NID", "0xBEEF", 1);
pthread_t th;
pthread_create(&th, NULL, server_thread, NULL);
meow_notify nt;
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_MESSAGE;
nt.title = "repl";
nt.timeout_ms = 0;
(void)meow_notify_ipc_send(&nt, 0x42u);
pthread_join(th, NULL);
close(g_listen_fd);
check("T3 replaces_id 透传=0x42", g_cap.n.replaces_id == 0x42u);
}
/* ---------- T4:无 socket 降级不崩 ---------- */
{
/* 指向不存在路径 */
char dead[108];
snprintf(dead, sizeof dead, "/tmp/meow_notify_ipc_dead_%d.sock", (int)getpid());
unlink(dead);
setenv("MEOW_IPC_SOCK", dead, 1);
meow_notify nt;
memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_EMAIL;
nt.title = "no-socket";
nt.timeout_ms = 0;
uint32_t nid = meow_notify_ipc_send(&nt, 0);
check("T4 无 socket→返回 0(降级不崩)", nid == 0);
meow_notify_ipc_dismiss(0x1234); /* 无 socket 静默,不崩 */
check("T4 dismiss 无 socket 静默不崩", 1);
}
/* ---------- T5:dismiss 发 MSG_NOTIFY_DISMISS ---------- */
{
char sock[108];
g_listen_fd = arb_listen(sock, sizeof sock);
setenv("MEOW_IPC_SOCK", sock, 1);
pthread_t th;
pthread_create(&th, NULL, server_thread, NULL);
meow_notify_ipc_dismiss(0xABCDu);
pthread_join(th, NULL);
close(g_listen_fd);
check("T5 stub 收到 MSG_NOTIFY_DISMISS 帧", g_cap.got && g_cap.type == MSG_NOTIFY_DISMISS);
check("T5 dismiss 负载=nid=0xABCD", g_cap.dismiss_id == 0xABCDu);
}
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}
+150
View File
@@ -0,0 +1,150 @@
/*
* test_notify_post.c — 宿主交叉验证:SDK send 的 IPC 帧 → launcher 真·中央存储。
*
* 把 SDK 的 meow_notify_ipc.c(映射+传输)与 launcher 的 meow_notif.c(真·中央存储,
* meow_notif_post/dismiss)接到一起,stub 掉 meow_settings 后端;服务端复刻
* meow_launcherd.c:6427-6440 的 m_notify_t→meow_notif_t 映射 + MSG_NOTIFY_ACK 应答。
*
* 验证:SDK meow_notify_ipc_send() 产出的帧被 meow_notif_post() 真实落库并回分配 nid>0;
* 随后 meow_notify_ipc_dismiss() 经 meow_notif_dismiss() 撤下(count 归 0)。
*
* 编译(宿主 gcc):
* gcc -o test_notify_post test_notify_post.c meow_notify_ipc.c \
* ../meow_launcherd/meow_notif.c -I. -I../meow_launcherd -lpthread
*/
#include "meow_notify_ipc.h"
#include "meow_ui.h"
#include "meow_ipc.h"
#include "meow_notif.h" /* launcher 真·中央存储 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <pthread.h>
/* ---- stub:settings 后端(键缺省 → meow_notif.c 用默认配额) ---- */
int meow_settings_get_int(const char *key, int *out) { (void)key; (void)out; return MEOW_ERR_NOENT; }
/* ---- 复刻 launcher 的 m_notify_t → meow_notif_t 映射(meow_launcherd.c:6427-6440) ----
* 注:与 launcher 同款 snprintf(dest 较 src 小会有 format-truncation 提示),launcher 自身
* 以其编译 flags 构建无警告;此处仅为宿主测试,用 pragma 压掉以免 -Werror 干扰。 */
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
#endif
static void launcher_map(const m_notify_t *mn, meow_notif_t *n)
{
memset(n, 0, sizeof *n);
n->id = mn->replaces_id; /* 0=>新建;>0=>替换(不存在则新建) */
snprintf(n->app_id, sizeof n->app_id, "%s", mn->app_id);
snprintf(n->category, sizeof n->category, "%s", mn->category);
snprintf(n->summary, sizeof n->summary, "%s", mn->summary);
snprintf(n->body, sizeof n->body, "%s", mn->body);
snprintf(n->app_icon, sizeof n->app_icon, "%s", mn->app_icon);
snprintf(n->target_activity, sizeof n->target_activity, "%s", mn->target_activity);
n->expire_timeout = mn->expire_timeout;
n->progress = mn->progress;
n->ongoing = mn->ongoing;
n->media = mn->media;
}
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
static int g_listen_fd = -1;
static int g_stop = 0;
static uint32_t g_posted_nid = 0;
static void reply_ack(int c, uint16_t type, uint32_t req_id, uint32_t status)
{
m_ack_t a; memset(&a, 0, sizeof a); a.code = MEOW_OK; a.status = status;
uint8_t rhdr[MEOW_IPC_HDR_LEN];
uint32_t mag = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION, t = type; uint32_t rl = sizeof a;
memcpy(rhdr+0,&mag,4); memcpy(rhdr+4,&ver,2); memcpy(rhdr+6,&t,2); memcpy(rhdr+8,&req_id,4); memcpy(rhdr+12,&rl,4);
size_t o = 0; while (o < sizeof rhdr) { ssize_t w = write(c, rhdr+o, sizeof rhdr-o); if (w<=0) break; o += (size_t)w; }
o = 0; while (o < rl) { ssize_t w = write(c, (uint8_t*)&a+o, rl-o); if (w<=0) break; o += (size_t)w; }
}
static void *server(void *arg)
{
(void)arg;
meow_notif_init();
while (!g_stop) {
int c = accept(g_listen_fd, NULL, NULL);
if (c < 0) { if (errno == EAGAIN && g_stop) break; if (errno == EBADF) break; usleep(1000); continue; }
uint8_t hdr[MEOW_IPC_HDR_LEN]; size_t o = 0;
while (o < sizeof hdr) { ssize_t r = read(c, hdr+o, sizeof hdr-o); if (r<=0) break; o += (size_t)r; }
if (o < sizeof hdr) { close(c); continue; }
uint32_t type, len, req_id; memcpy(&type, hdr+6, 2); memcpy(&req_id, hdr+8, 4); memcpy(&len, hdr+12, 4);
if (len > MEOW_IPC_MAX) { close(c); continue; }
uint8_t pay[MEOW_IPC_MAX]; o = 0;
while (o < len) { ssize_t r = read(c, pay+o, len-o); if (r<=0) break; o += (size_t)r; }
if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) {
m_notify_t mn; memcpy(&mn, pay, sizeof mn);
meow_notif_t n; launcher_map(&mn, &n);
uint32_t nid = 0;
int rc = meow_notif_post(&n, &nid);
if (rc == NOTIF_OK) g_posted_nid = nid;
reply_ack(c, MSG_NOTIFY_ACK, req_id, (rc == NOTIF_OK) ? nid : (uint32_t)rc);
fprintf(stderr, "[post-srv] post rc=%d nid=%u cat=%s\n", rc, nid, mn.category);
} else if (type == MSG_NOTIFY_DISMISS && len >= 4) {
uint32_t id; memcpy(&id, pay, 4);
int rc = meow_notif_dismiss(id);
reply_ack(c, MSG_ACK, req_id, 0);
fprintf(stderr, "[post-srv] dismiss id=%u rc=%d\n", id, rc);
}
close(c);
}
return NULL;
}
static int make_listen(char *path, size_t n)
{
snprintf(path, n, "/tmp/meow_notify_post_%d.sock", (int)getpid());
unlink(path);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un a; memset(&a, 0, sizeof a); a.sun_family = AF_UNIX;
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
if (bind(fd, (struct sockaddr*)&a, sizeof a) < 0 || listen(fd, 4) < 0) { close(fd); return -1; }
fcntl(fd, F_SETFL, O_NONBLOCK);
g_listen_fd = fd;
return 0;
}
static int failures = 0;
static void check(const char *name, int cond) { printf("%s %s\n", cond ? "PASS" : "FAIL", name); if (!cond) failures++; }
int main(void)
{
setenv("MEOW_LOG_APP", "intgapp", 1);
char sock[108];
if (make_listen(sock, sizeof sock) != 0) { fprintf(stderr, "cannot listen\n"); return 2; }
setenv("MEOW_IPC_SOCK", sock, 1);
pthread_t th; pthread_create(&th, NULL, server, NULL);
/* SDK send → launcher 真存储。media + ONGOING 走全部映射分支。 */
meow_notify nt; memset(&nt, 0, sizeof nt);
nt.cat = MEOW_NOTIFY_CAT_MEDIA; nt.urgency = MEOW_NOTIFY_URG_NORMAL;
nt.title = "正在播放"; nt.timeout_ms = -1; nt.progress = 40;
uint32_t nid = meow_notify_ipc_send(&nt, 0);
check("send 经 launcher 真存储回 nid>0", nid != 0);
check("send 拿到 launcher 分配的 nid(与 post 一致)", g_posted_nid == nid);
check("launcher 存储 count(media app)=1", meow_notif_count("intgapp") == 1);
/* SDK dismiss → launcher 撤下 */
meow_notify_ipc_dismiss(nid);
check("dismiss 后 launcher 存储 count=0", meow_notif_count("intgapp") == 0);
g_stop = 1; close(g_listen_fd);
pthread_join(th, NULL);
meow_notif_shutdown();
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}
+4 -4
View File
@@ -13,7 +13,7 @@
# ./verify_apps.sh devinfo 输入设备 abs 范围等调试信息 # ./verify_apps.sh devinfo 输入设备 abs 范围等调试信息
# #
# 场景: # 场景:
# scen 0 正常: MeowAPPs/hello/(hello + app.json 有效) # scen 0 正常: MeowAPPs/os.meow.hello/(hello + app.json 有效)
# scen 1 无卡: umount sd_0 (测试后必须 scen 0 恢复) # scen 1 无卡: umount sd_0 (测试后必须 scen 0 恢复)
# scen 2 无 MeowAPPs 目录 # scen 2 无 MeowAPPs 目录
# scen 3 MeowAPPs 空目录 # scen 3 MeowAPPs 空目录
@@ -93,12 +93,12 @@ wdtap() {
scen() { scen() {
local n=${1:-0} local n=${1:-0}
case $n in case $n in
0) s "mkdir -p $SD/MeowAPPs/hello; cp -f /data/hello.bin $SD/MeowAPPs/hello/hello; cp -f /data/hello_app.json $SD/MeowAPPs/hello/app.json; chmod 755 $SD/MeowAPPs/hello/hello; ls $SD/MeowAPPs/hello/; echo SCEN0_OK" ;; 0) s "mkdir -p $SD/MeowAPPs/os.meow.hello; cp -f /data/hello.bin $SD/MeowAPPs/os.meow.hello/hello; cp -f /data/hello_app.json $SD/MeowAPPs/os.meow.hello/app.json; chmod 755 $SD/MeowAPPs/os.meow.hello/hello; ls $SD/MeowAPPs/os.meow.hello/; echo SCEN0_OK" ;;
1) s "umount $SD 2>/dev/null || umount /usr/data/mnt/sd_0 2>/dev/null; grep sd_0 /proc/mounts || echo SCEN1_NOCARD_OK" ;; 1) s "umount $SD 2>/dev/null || umount /usr/data/mnt/sd_0 2>/dev/null; grep sd_0 /proc/mounts || echo SCEN1_NOCARD_OK" ;;
2) s "mv $SD/MeowAPPs $SD/MeowAPPs.bak 2>/dev/null; ls $SD/ 2>/dev/null; echo SCEN2_NODIR_OK" ;; 2) s "mv $SD/MeowAPPs $SD/MeowAPPs.bak 2>/dev/null; ls $SD/ 2>/dev/null; echo SCEN2_NODIR_OK" ;;
3) s "mkdir -p $SD/MeowAPPs; ls -la $SD/MeowAPPs/; echo SCEN3_EMPTY_OK" ;; 3) s "mkdir -p $SD/MeowAPPs; ls -la $SD/MeowAPPs/; echo SCEN3_EMPTY_OK" ;;
4) s "mkdir -p $SD/MeowAPPs/hello $SD/MeowAPPs/bad; cp -f /data/hello.bin $SD/MeowAPPs/hello/hello; cp -f /data/hello_app.json $SD/MeowAPPs/hello/app.json; chmod 755 $SD/MeowAPPs/hello/hello; echo '{\"name\":\"Bad App\"}' > $SD/MeowAPPs/bad/app.json; cat $SD/MeowAPPs/bad/app.json; echo SCEN4_BADJSON_OK" ;; 4) s "mkdir -p $SD/MeowAPPs/os.meow.hello $SD/MeowAPPs/bad; cp -f /data/hello.bin $SD/MeowAPPs/os.meow.hello/hello; cp -f /data/hello_app.json $SD/MeowAPPs/os.meow.hello/app.json; chmod 755 $SD/MeowAPPs/os.meow.hello/hello; echo '{\"name\":\"Bad App\"}' > $SD/MeowAPPs/bad/app.json; cat $SD/MeowAPPs/bad/app.json; echo SCEN4_BADJSON_OK" ;;
5) s "mkdir -p $SD/MeowAPPs/hello $SD/MeowAPPs/nonexec; cp -f /data/hello.bin $SD/MeowAPPs/hello/hello; cp -f /data/hello_app.json $SD/MeowAPPs/hello/app.json; chmod 755 $SD/MeowAPPs/hello/hello; echo '{\"name\":\"Ghost App\",\"exec\":\"/usr/data/mnt/sd_0/MeowAPPs/nonexec/nope\"}' > $SD/MeowAPPs/nonexec/app.json; cat $SD/MeowAPPs/nonexec/app.json; echo SCEN5_NOEXEC_OK" ;; 5) s "mkdir -p $SD/MeowAPPs/os.meow.hello $SD/MeowAPPs/nonexec; cp -f /data/hello.bin $SD/MeowAPPs/os.meow.hello/hello; cp -f /data/hello_app.json $SD/MeowAPPs/os.meow.hello/app.json; chmod 755 $SD/MeowAPPs/os.meow.hello/hello; echo '{\"name\":\"Ghost App\",\"exec\":\"/usr/data/mnt/sd_0/MeowAPPs/nonexec/nope\"}' > $SD/MeowAPPs/nonexec/app.json; cat $SD/MeowAPPs/nonexec/app.json; echo SCEN5_NOEXEC_OK" ;;
*) echo "usage: scen 0..5"; return 1 ;; *) echo "usage: scen 0..5"; return 1 ;;
esac esac
} }
+11 -11
View File
@@ -9,8 +9,8 @@ DEV="<alt_device_ip>"
KEY="$HOME/Project/R3ProII/ssh_keys/<device_key>" KEY="$HOME/Project/R3ProII/ssh_keys/<device_key>"
PATCH="$HOME/Project/R3ProII/patch_v2/hiby_player_patched_v2" PATCH="$HOME/Project/R3ProII/patch_v2/hiby_player_patched_v2"
WD="$HOME/Project/R3ProII/patch_v2/meow_watchdog/meow_watchdog" WD="$HOME/Project/R3ProII/patch_v2/meow_watchdog/meow_watchdog"
HELLO="$HOME/Project/R3ProII/patch_v2/apps_assets/apps/hello/hello" HELLO="$HOME/Project/R3ProII/patch_v2/apps_assets/apps/os.meow.hello/hello"
HJ="$HOME/Project/R3ProII/patch_v2/apps_assets/apps/hello/app.json" HJ="$HOME/Project/R3ProII/patch_v2/apps_assets/apps/os.meow.hello/app.json"
TAP="$HOME/Project/R3ProII/patch_v2/uinput_tap/uinput_tap" TAP="$HOME/Project/R3ProII/patch_v2/uinput_tap/uinput_tap"
KEYTAP=/data/keytap KEYTAP=/data/keytap
ASSETS="$HOME/Project/R3ProII/patch_v2/apps_assets" ASSETS="$HOME/Project/R3ProII/patch_v2/apps_assets"
@@ -40,17 +40,17 @@ deploy() {
s 'md5sum /data/hiby_player_patched_v2 | cut -c1-8' s 'md5sum /data/hiby_player_patched_v2 | cut -c1-8'
echo "== 设备侧组装 ==" echo "== 设备侧组装 =="
s 'if ! grep -q "sd_0" /proc/mounts; then mount /dev/mmcblk0p1 /usr/data/mnt/sd_0 2>/dev/null || echo mount_fail; fi s 'if ! grep -q "sd_0" /proc/mounts; then mount /dev/mmcblk0p1 /usr/data/mnt/sd_0 2>/dev/null || echo mount_fail; fi
mkdir -p /usr/data/mnt/sd_0/MeowAPPs/hello /data/overlay mkdir -p /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello /data/overlay
cp -f /data/hello.bin /usr/data/mnt/sd_0/MeowAPPs/hello/hello 2>/dev/null cp -f /data/hello.bin /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello 2>/dev/null
cp -f /data/hello_app.json /usr/data/mnt/sd_0/MeowAPPs/hello/app.json 2>/dev/null cp -f /data/hello_app.json /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/app.json 2>/dev/null
chmod 755 /usr/data/mnt/sd_0/MeowAPPs/hello/hello 2>/dev/null chmod 755 /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello 2>/dev/null
rm -rf /data/overlay/theme1 /data/overlay/str_zh /data/overlay/str_en 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/layout/theme1 /data/overlay/theme1
cp -a /usr/resource/str/simplified_chinese /data/overlay/str_zh cp -a /usr/resource/str/simplified_chinese /data/overlay/str_zh
cp -a /usr/resource/str/english /data/overlay/str_en cp -a /usr/resource/str/english /data/overlay/str_en
cp -f /data/hiby_player_patched_v2 /data/overlay/hiby_player cp -f /data/hiby_player_patched_v2 /data/overlay/hiby_player
chmod 755 /data/overlay/hiby_player chmod 755 /data/overlay/hiby_player
ls /usr/data/mnt/sd_0/MeowAPPs/hello/ 2>/dev/null' ls /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/ 2>/dev/null'
# 推 hello + 资源 # 推 hello + 资源
timeout 30 $SSH root@$DEV "cat > /data/hello.bin" < "$HELLO" timeout 30 $SSH root@$DEV "cat > /data/hello.bin" < "$HELLO"
timeout 30 $SSH root@$DEV "cat > /data/hello_app.json" < "$HJ" timeout 30 $SSH root@$DEV "cat > /data/hello_app.json" < "$HJ"
@@ -60,10 +60,10 @@ put $ASSETS/str/simplified_chinese/apps.ini /data/overlay/str_zh/apps.ini
put $ASSETS/str/english/apps.ini /data/overlay/str_en/apps.ini put $ASSETS/str/english/apps.ini /data/overlay/str_en/apps.ini
EOF EOF
timeout 30 sftp -i "$KEY" -o StrictHostKeyChecking=accept-new -b /tmp/vc.sftp root@$DEV timeout 30 sftp -i "$KEY" -o StrictHostKeyChecking=accept-new -b /tmp/vc.sftp root@$DEV
s 'cp -f /data/hello.bin /usr/data/mnt/sd_0/MeowAPPs/hello/hello s 'cp -f /data/hello.bin /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello
cp -f /data/hello_app.json /usr/data/mnt/sd_0/MeowAPPs/hello/app.json cp -f /data/hello_app.json /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/app.json
chmod 755 /usr/data/mnt/sd_0/MeowAPPs/hello/hello chmod 755 /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/hello
ls -la /usr/data/mnt/sd_0/MeowAPPs/hello/' ls -la /usr/data/mnt/sd_0/MeowAPPs/os.meow.hello/'
} }
restart() { restart() {