Compare commits

..
282 Commits
Author SHA1 Message Date
AskaEth db672cbe8f Merge branch 'feat/m26-sdk-image-stack-into-main-repo' 2026-11-06 21:35:50 +08:00
AskaEth 281f4153ee fix(sdk): M26 r1 P1 — ILP32 尺寸算术溢出(畸形 BMP 经公开入口 SIGSEGV)+ 加 S17 回归
评审在 ARM32(ILP32,与 mipsel 同数据模型)实测:54 字节 BMP 头声明 w=2^27、bpp=32
时 w*bpp 回绕成 0 → rowsize=0 → malloc(0) → 采样索引 row+sx*4 越界读 → SIGSEGV,
经 meow_img_load_file_dim 端到端可触发;同一文件在 LP64 宿主返回 NULL(不溢出),
合并前走 stb 也是 NULL。即这是相对旧路径的鲁棒性回归。

修复(只加闸,正常路径数值不变):

- meow_png_stream.c(P1 本体)
  · 新增 ILP32 守卫 MZP_MAX_DIM=1<<20 与 mzp_bytes_ok()(unsigned long long 验算,
    超过 SIZE_MAX 即拒)
  · BMP:声明尺寸先升 int/long long 校验(w/h 单边 ≤1<<20;h=INT_MIN 用
    -(long long)hi 取绝对值,不再有 -h 溢出);rowsize 用 64 位算并验;
    行偏移 off / 采样下标 sx 改 long long,sx 夹在 [0,w-1];按行 fseek 前加
    off > LONG_MAX 拒绝;目标 RGBA 字节数 64 位验后再 malloc
  · PNG:IHDR 尺寸原已 ≤65535,补上 tw*th*4 的 64 位校验(32768²×4 = 2^32 会回绕),
    emit_row 的 sy/sx 与输出行偏移改 64 位并夹紧
- meow_jpeg.c(顺手审计同类 32 位算术,评审要求「不要只修点名那处」)
  · mj_layout 返回 unsigned long long(单平面/三平面之和都可能越 2^32),
    越界返回 MJ_LAYOUT_BAD → mj_sof 各处重新布局后都检查
  · target_bytes 与分量 plane malloc 改 64 位验算;目标图字节超 MJ_PEAK_CAP 直接拒
  · mj_output:输出缓冲 64 位验;y*vh / x*vw 采样改 long long 并夹紧
  · meow_img_target_dims / mj_pick_n 的 (long)h*maxdim、8*maxdim 改 long long

- test_meow_img_stream.c:新增 S17(10 组畸形 BMP 声明尺寸 + 2 个合法正例 +
  PNG 回绕点),断言「后端拒绝 + 公开入口不崩(越界尺寸另断言 NULL 且不分配)」;
  文件头补 ILP32 交叉编译命令(宿主 LP64 看不到 32 位回绕,测试注释里明写这一点)

验证:同一测试在 zig arm-linux-musleabi(sizeof(long)=4)下 S1-S17 全绿(206 checks,
exit 0);把后端换回 r1 提交的旧实现,同一 ARM32 二进制 exit 139(说明 S17 真能抓);
宿主 LP64 S1-S17 全绿、B1-B9 全绿;静态库 --verify 仍 78 成员 / 全 MIPS32r2。

同步提醒:这些行与公开仓 ~/Proj/MeowOS-SDK/sdk/{meow_png_stream.c,meow_jpeg.c} 逐字节
同源,同一补丁必须由控制塔同步过去,否则立刻产生双边漂移。
2026-11-06 21:27:03 +08:00
AskaEth ee5796747d feat(sdk): M26 公开仓真缩解图像栈并入主仓 — JPEG DCT/PNG 逐行/BMP 跳行后端 + RGB565 页图
主仓 SDK 的图片层原本是 220 行 stb 直通(整幅解码后重采样)。公开仓 MeowOS-SDK
的同一子系统已被真实 App(tuku 阅读器)驱动并做过峰值实测与像素比对,故以公开仓
为权威方向并入主仓 meow_ui.a,两个方向的新功能都保留(逐文件 diff 确认公开仓是
严格超集:无一处「双方互有增减」)。

- 新增 meow_jpeg.c / meow_jpeg.h:DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT),
  渐进式只解到首个精细化扫描;不支持项(CMYK/YCCK、算术编码、过订阅 DHT、16bit
  量化表)返回 0 由调用方回退 stb
- 新增 meow_png_stream.c:libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸;
  BMP 按目标行 seek + 行内跳列(8/16/24/32bpp)
- meow_ui_img.c + include/meow_ui_image.h:新增 meow_img_pick_scale /
  load_file_step / load_file_step_ex / resample 四个公开入口;_dim 与 _mem_dim
  先进后端,失败/不支持才回退 stb(对外语义不变)
- include/meow_ui_draw.h + meow_ui_gfx.c:M32 RGB565 页图 5 函数
  (meow_gfx_rgb565 / rgba_to_rgb565 / blit_rgb565 / _scaled)。gfx 保留主仓的
  `#include "../meow_launcherd/meow_font.h"` 与 <time.h>(公开仓自 vendored
  meow_font.h 且无 <time.h>,机械覆盖会丢这两行)
- meow_jpeg.h 保持内部:不进 meow_ui.h 伞头,include/ 下无任何头暴露 meow_jpeg_*
- build_sdk.sh:OBJS 插入 meow_jpeg / meow_png_stream(32→34);成员数
  76→78(+meow_font.o +43 freetype),--verify 全为 ELF32 MIPS32 rel2/o32
- 测试:新增 test_meow_img_stream.c(S1-S16),test_meow_img_budget.c 同步公开仓
  版(B9 断言收紧为 p_new <= p_old);22 个既有宿主测试的编译注释行补
  meow_jpeg.c / meow_png_stream.c / -lz(meow_ui_img.c 现引用后端符号,不加即
  undefined reference)

宿主实测:S1-S16 全绿(S7 与 stb 网格像素一致:JPEG mad 4.35-13.69、PNG/BMP
mad 0.00;S9 回退矩阵 CMYK/1bpp BMP/Adam7 均拒绝且 stb 仍成功),B1-B9 全绿
(B9 PNG 峰值 25.65→0.61 MiB、BMP 14.65→0.59 MiB)。回归 test_meow_ui_widgets /
scroll / ime_fix / test_umbrella 全过。zlib 在应用链接期解析:meow_ui.a 只留
inflate* 未定义符号,四个 App 构建脚本原本就带 $SYSROOT/lib/libz.a,静态
linkcheck(meow_ui.a + libz.a -lpthread)通过。

注:主仓 patch_v2/reader_app 仍自带 vendored stb、不用 meow_img_*,本单不改变
阅读器的内存占用;RGB565 页图接入 reader_app 是另一个任务。
2026-11-06 20:44:22 +08:00
AskaEth 479d93a3bf Merge branch 'feat/m24-devkit-static-mipsel-debug-tools' 2026-11-06 20:03:08 +08:00
AskaEth adaed09f94 fix(devkit): M24 r2 评审 — 交付完整性闸门 + 补 sysctl + time64 文档更正
P2-1(与 P1 同类):licenses/ 与 README 缺失仍会假成功。
- 新增 meta_complete()/verify_meta():MANIFEST + README + 五份许可证副本
  (procps COPYING/COPYING.LIB、ncurses、lsof、musl-COPYRIGHT)缺一即不完整
- 纳入 skip 条件与 --verify-only;产物未变但 meta 缺失时就地 write_meta 补全(不重编),
  构建树不在则退回全量重建
- 回归测试新增 T6(删 licenses 后 --with-sources 必须恢复)、T7(--verify-only 必须拒绝)、
  T8(删 README 后恢复);共 20 项全过

P2-2:README.txt/MANIFEST.txt 的卡上路径全错(写成 ../src、scripts/build/…)。
- 全部改为卡内相对路径:src/、licenses/、MANIFEST.txt、src/DEVKIT.md、src/build_devkit.sh
- README 增加「目录内容」段并注明 src/ 是生成目录、勿手放文件

P2-3:valgrind 的排除理由(“无 MIPS32 后端”)是错的 —— 官方支持 MIPS32/Linux。
- 改为「未尝试」:交叉构建需 autotools + 目标 vDSO 细节、产物 10MB+,56MB 单核不实用

P2-4:busybox 1.31 实测有 pgrep/kill/killall,没有 pkill、没有 sysctl。
- 表格改准确;pgrep 保留(增量有限不交付);pkill 写明「busybox 无此 applet,
  killall 只能部分替代,本轮未做」
- 新增交付 sysctl(procps 4.0.4,148184 B,静态):MANIFEST/verify-only/真机
  (sysctl -a 428 键、kernel.hostname/panic 读取、-n)均已验证

P2-5:free -s <小数> 也受 time64 影响(usleep 亚秒被抹平),文档说反了。
- DEVKIT §9 增加一行(free -s 0.5 实测 0s、free -s 1 实测 1s);README 同步;
  “收益只有 vmstat -t 一列”改为包含 free -s

P3:
1. §9 机制改准:内核只写 8 字节 → tv_sec 高 32 位是内核写入的 nsec、offset 8 的
   tv_nsec 才是未初始化;nanosleep 是把 tv_sec 高半字当 nsec 读(0)
2. 删掉脚本里 top 的旧错误归因(时长溢出/59.4 GiB),与 DEVKIT 对齐
3. 测试耗时改为「热缓存约 15s,冷启动 1–2 分钟」(实测 28s 含多次构建/修复路径)
4. --verify-only 依赖清单写全(file/readelf/sha256sum/awk/sed/grep/wc/cut/basename)
5. licenses/ 增加 musl-COPYRIGHT(zig env → libc/musl/COPYRIGHT,取不到即失败),§6 补一行
6. htop 说明改准:亚秒刷新走 ncurses halfdelay()(select + 32 位 timeval),大概率不受
   time64 影响;受影响的是时钟显示与部分采样(仍注明未做运行时验证)
7. README/脚本注明 src/ 是生成目录
8. LGPL 摘要改为「源码(§6a) 或 共享库(§6b) 或 书面要约(§6c)」
9. ltrace/sysstat 从「不可行」改为「未做(成本高)」,与 perf 的硬阻塞区分

验证:test_build_devkit.sh 20/20;--verify-only 通过(7 个二进制 + MANIFEST + 5 份许可证);
独立工作区离线重建 64s/0 下载,七个 sha256 与前一次逐字节一致;设备实测 sysctl 与其余六项正常。
2026-11-06 19:53:45 +08:00
AskaEth 0531603779 fix(devkit): M24 r1 评审 — P1 --with-sources 静默漏源码 + time64 归因更正
P1(阻断):stamp 命中时 main 提前 return,--with-sources 在 return 之后,
于是「先构建再 --with-sources」会打印成功、退出码 0、却不产出 src/。

- 把源码整理抽成 stage_sources(),stamp 命中路径先兑现 WITH_SOURCES 再返回;
  整理后按钉住的 sha256 复核(sha 漂移/缺文件都重铺),任何失败即非零退出
- 新增 test_build_devkit.sh(12 项,秒级):stamp 命中下的 --with-sources、
  src 缺文件/被改坏的自愈、--verify-only 的 MANIFEST 篡改检测
- skip 条件额外要求 MANIFEST 与 bin 逐条 sha 一致;--clean 同时清 out/src;
  全新构建未带 --with-sources 时删除上次残留 src/(避免源码与二进制错配)

P2-a:top 排除理由改写。根因是 zig 0.17 musl 的 time64 ABI 错配(mips o32 是
ILP32 + 内核 4.4 无 time64 syscall,16B timespec 直传 32 位 syscall),不是
procps 的 bug;top 的 Mem 一栏是「百分比/总容量」不是 used GiB。新增 DEVKIT.md
§9 记录证据(vmstat -t 空列、usleep(0.5s)×10 实测 0s、sleep(1) 正常、ps lstart
正常),并说明 M16 的 patch_v2/sdk/meow_ui_time.c 可复用(本轮实测该 .o 独立
编译、仅 5 个 libc 未定义符号)但本轮不接入的取舍——需要单独 mission 做链接期
接管 + 全量回归,不是 -D 能修的。

P2-b:--with-sources 现在同时放 build_devkit.sh + DEVKIT.md(GPLv2 §3 的
"scripts used to control compilation");补 LGPL-2.1 §6 静态链接 libproc2 的
义务说明;MANIFEST/README.txt 写明 src/ 与 licenses/。

P2-c:lsof 内嵌 Termux musl 的 resolv.conf 路径记为工具链副作用(实测
lsof -i 约 3s 超时、-nP 瞬时;端口名/本机名不受影响),不换工具链。

P3:软链路径写实(/bin/ps、/usr/bin/{free,lsof,top});MANIFEST 用 zig version
而非 clang 版本;chmod 措辞改成检查挂载参数;--verify-only 增加 MANIFEST 逐条
sha256/体积比对且不再依赖 date;stamp 纳入 zig 完整版本;许可证复制失败即报错。

验证:test_build_devkit.sh 12/12 通过;--verify-only 通过(含 MANIFEST 对账);
独立工作区离线重建 85s/0 下载,六个二进制 sha256 与上一版逐字节一致;设备侧
二进制未变(sha 与 r1 真机验证过的一致)。
2026-11-06 19:29:27 +08:00
AskaEth b52e6a08bc feat(devkit): M24 静态 MIPS32r2 调试工具集 — vmstat/slabtop/free/ps/pmap/lsof
TF 卡 MeowOS/opt/devkit 用的交叉编译流水线:zig 0.17 musl 静态链接,
产物 fw_apps/devkit/{bin,MANIFEST.txt,licenses,README.txt}(gitignore 内)。

- build_devkit.sh:源码固定 版本+sha256+URL(ncurses 6.5 / procps-ng 4.0.4 /
  lsof 4.99.4),缓存命中零联网;冷启动 3 次下载约 80s,重跑 0 次联网约 65s,
  stamp 命中 1s 跳过;--force/--clean/--offline/--with-sources/--verify-only
- 自编静态 ncursesw 只为 slabtop(procps 的 slabtop 硬依赖 ncurses);
  terminfo 目录钉到设备 /usr/share/terminfo,交互式 slabtop 可用
- 逐项自检 file/readelf(static、ELF32、mips32r2、0 NEEDED),任一不过即失败
- 不交付 top(真机输出不可信)、htop(卡上已有 3.5.3 且我们的更大)
- DEVKIT.md:许可证留痕(procps=GPL-2+ 需提供源码、ncurses=MIT、lsof=Purdue)、
  可行性实测、上卡 PATH 与 busybox 同名关系、真机验证输出
2026-11-06 19:05:15 +08:00
AskaEth 2a2e6988ae Merge branch 'feat/m23-network-time-sync' 2026-11-06 13:43:24 +08:00
AskaEth 1f692ae2a7 fix(ntp): M22 r1 评审 P2×3 — 孤儿 ntpd 收尸 / 日志失败不吞 hwclock / 锁自证
r1 评审 verdict=p2-3items,三条均已按评审给的修法修掉,且都补了可证伪的回归断言。

P2-1 SIGTERM 后遗留孤儿 ntpd,之后每次静默 skip 且 rc=0
- attempt() 把子进程记进全局 NTPD_CHILD 并落盘 <锁目录>/ntpd.pid;
  新增 cleanup()(EXIT trap)先收尸子进程再释放锁;新增 on_signal()
  (INT/TERM/HUP)清理后 exit 143——旧代码的 TERM trap 只清锁、不 exit,
  被杀后还会继续跑完看门狗。
- 取代原来的 "pidof ntpd 就 skip":新增 reap_or_skip_ntpd(),用
  /proc/<pid>/cmdline 是否含本锁目录来区分「我们自己遗留的孤儿」(收尸)
  与「别人的常驻 ntpd」(让路)。崩溃后留下的 ntpd.pid 也据此校验,
  pid 被复用时不会误判成外部 daemon(不会永久 skip)。
- S45 stop 不再按陈旧 pid 文件盲杀:只从 <锁目录>/<pid> 取持有者,
  再核对 /proc/<pid>/cmdline 含 meow_ntpsync 才 kill。

P2-2 日志重定向失败导致 hwclock 根本不执行
- persist_rtc 改 ">/dev/null 2>>$LOG_FILE":hwclock 的 rc 与日志写入解耦。
- 新增 prepare_log():启动时在子 shell 里试写日志,不可写就整体退化 /dev/null。
  必须用子 shell——`:` 是 POSIX 特殊内建,它的重定向失败会让 dash 直接
  退出整个脚本(本单自测先踩到了,rc=2)。

P2-3 锁 mkdir→写 pid 有窗口,并发抑制非原子
- 锁的持有者改由 <锁目录>/<pid> 子目录自证(mkdir 原子),不再写 pid 文件;
  读到空锁时给 3s 宽限而不是立刻判死。
- 清失效锁改用 mv 到 <锁目录>.stale.$$(rename 原子),并发清锁时只有
  一个能成功,避免「抢锁失败方删掉胜方的锁」。
- 退出只删自己的锁:没有自己的 <pid> 子目录就不动锁目录。

验证
- 新增回归断言 11-16(run_tests.sh):SIGTERM 收尸/锁释放、遗留孤儿收尸、
  日志不可写仍写 RTC、锁被占用不抢、死锁可接管、pid 复用不阻断。
- 可证伪性:新套件跑旧代码(c9f0cba 的 meow_ntpsync)→ 6 条 FAIL
  (孤儿 alive、日志不可写 rc=1 且 hwclock 未执行、抢锁并发调 ntpd×3)。
  再跑「仅回退 STALE_NTPD 硬化」的变体 → pid 复用那条 FAIL
  (rc=0 但 RTC 未写,正是评审指出的静默失效)。
- dash 与 busybox ash 各 41/41 PASS,rc=0;dash -n / busybox sh -n 均 0 错。
- 时间断言放宽为负载容忍(宿主是多 agent 共享机,实测 sleep 5 能跑成 85s),
  假 ntpd 改为挂 3600s,保证上界仍能证明是看门狗掐断而非自然退出。

fw_content.conf: meow_ntpsync md5 ad324e3f→f3c3abe4,
S45meow_ntpsync md5 25454a3f→115e7592(50meow_ntpsync 未改,md5 不变)。
2026-11-06 13:34:45 +08:00
AskaEth c9f0cbaab6 feat(ntp): M22 网络校时 — 复用固件自带 ntpd + hwclock 回写 RTC
真机实测(0.8.47,2026-11-06)定位到的根因不是「没有 NTP 客户端」:
固件自带 ntp-4.2.8p14(/usr/sbin/ntpd)和 /etc/ntp.conf(4 个 pool.ntp.org),
但没有任何 init 脚本/进程启动它 —— 有客户端、没启动。

实测证据:
- RTC 存在且可读(/dev/rtc0,10003000.rtc),内核开机 hctosys=1,
  冷启动不回到 1970 → 掉电保持;RTC 存 UTC(since_epoch == date +%s)。
- 但 RTC 会走慢且无人纠正:实测系统时间比 NTP 慢 49 s;把系统时间人为
  拨错 4 h 后观察 65 s 无人纠正,RTC 也不跟随系统时间。
- UDP/123 出网正常(ntpd -q 一次成功 time set +48.877s);DNS/HTTP 正常。
- udhcpc 的 default.script 不处理 option 42,实测热点也不下发。
- ntpd -q 不会回写 RTC(步进后 sys-rtc 仍差 49 s)→ 必须 hwclock -w -u。
- ntpd 没有超时参数,无服务器时永久存活 → 必须自带看门狗。
- -p 形式的 server 首个 poll 要 ~64 s;配置里加 iburst 后 8 s 内返回。

实现(新增,不改任何基线文件):
- /usr/bin/meow_ntpsync:一次性校时器。复用 ntpd -q -g + 运行时生成的
  iburst 配置;内置 IP 兜底(阿里/腾讯/Cloudflare)→ DNS 名兜底;
  单次 15 s 看门狗、默认路由轮询、1 h 短路、进程锁、失败静默。
  成功严格校时后 hwclock -w -u 回写 RTC(RTC 存 UTC,必须 -u)。
- /etc/init.d/S45meow_ntpsync:开机只 setsid 后台拉起并立即返回
  (关键路径 +0 秒;后台最长约 150 s 自行退出),失败不阻断 init。
- /usr/share/udhcpc/default.script.d/50meow_ntpsync:只在 bound/renew
  触发(设备上唯一可靠的「网络就绪」事件),后台拉起后立即 exit 0;
  有 $ntpsrv(option 42)就优先用。基线 default.script 本就有 HOOK_DIR
  循环,目录原本为空。
- tests/run_tests.sh:宿主沙箱单测(假 ntpd/hwclock/网络探测)。

验证:dash 与 busybox sh 各 sh -n 通过;单测两套 shell 各 23/23 通过;
真机(/tmp 下运行,未装系统目录)首跑 8 s rc=0、ntpd slew 成功、
RTC 由 1793940395 写成 1793940452(49 s 偏差被纠正)、hwclock -u -r 吻合;
二次运行 0 s 短路;-b -t 3 无网络 5 s 退出 rc=1;
DHCP 灌入不可达服务器 192.0.2.x 仍靠 IP 兜底在 ~10 s 内成功。
2026-11-06 12:49:13 +08:00
AskaEth 2cfac55b4e Merge branch 'feat/m22-boot-logo-and-brightness-init-script' 2026-11-05 22:04:56 +08:00
AskaEth f9c2ea42a4 fix(boot): M21 r1 评审 P1 — bl_cap 把 cur=0 当「无有效值」照写 20(+P2 两处)
P1 根因(评审给的真机驱动证据链,已按其最小修法改):
  /module_driver/pwm_backlight.sh 是 `insmod pwm_backlight.ko ... default_brightness=0
  max_brightness=101`,模块 init 把 props.brightness 置 0 再 update_status(PWM 0 分支)→ 上电即灭屏;
  该模块由 S11module_driver_default 加载,rcS 自然序排在 S11jpeg_display_shell 之后。于是:
    - 函数入口那次 bl_cap:背光节点还不存在,写必然失败(no-op);
    - fb0 出现后画图前那次 bl_cap:读到 cur=0,旧条件 `cur <= cap` 命中「只降不升」跳过分支,
      20 从未写入 → 画图时刻亮度 0。
  物理后果两头都是缺陷:面板仍亮则 logo 依旧亮(原问题没修);面板灭则 S11~S21 无可见 logo,
  settings.conf 缺失时直到 launcherd 才亮屏(相对 stock「0.1s 后写 60」是回归)。

修改:
  - bl_cap 判定改为「仅 1 <= cur <= cap 才视为已有有效值跳过;0 / 空 / 非数字一律照写」。
    S22 抢写 1 的竞态保护不变(1 落在 1..20 → 仍跳过,不会被顶回 20)。
  - 删掉函数入口那次 bl_cap(节点未建,必然 no-op,且是重定向报错的来源);只保留 fb0 就绪后、
    画 logo 前那一次 —— 同一驱动脚本里 pwm_backlight.ko 先于 soc_fb.ko 加载,fb0 出现即节点就绪。
  - 写入成功时打一行 `backlight: -> 20 (was N)` 便于刷机后复核;`2>/dev/null` 移到 `>` 之前,
    节点未就绪时连重定向报错一起吞掉(P2-1)。
  - S22:失败路径日志由「保持 S11 低亮度」改为「不改背光(当前 brightness=N)」(P2-2);写入失败
    日志同样附当前读数;写入处的 2>/dev/null 次序一并改正。
  - 头部注释按时序真相重写(驱动默认 0 = 灭屏,不是「上电很亮」)。

实测(沙箱重写 sysfs 路径 + stub cmd_jpeg_display/nanddump/sleep,dash 与 busybox 各一遍):
  cur=0 → 画图时刻 20、结束 20(修复前 0/0);cur=1 → 1/1;cur=60、cur=101 → 20/20;
  竞态(fb0 就绪前外部写 1)→ 画图时刻 1、结束 1(未被顶回);节点不可写时 stderr 0 行、仍画图、rc=0。
  S22 19 用例全 rc=0(仅 usage rc=1,rcS 不可达)、sleep 调用 0、行为与改文案前一致。

S11 与 S22 内容均变 → fw_content.conf 两条 md5 同步更新为
  3ad33301fb9f0884102a47e17a1985ed / 1a0aa33d6f806f3f1648168cc1d8b953
(未改动 meow_watchdog 那条:main tip dfde95b 仍是 553840cd,与本单无关,已在报告中提示。)
2026-11-05 22:04:16 +08:00
AskaEth 0ab23161b6 fix(boot): M21 开机 logo 亮度 — S11 画前压到 <=20 + 新增 S22brightness 按用户设置恢复
冷启动 logo 很亮:stock S11 是画完 logo 之后才写死 60,画图那一刻用的是面板上电默认高亮;
而 S11 早于 S21mount_ubifs,那时 /usr/data 未挂载、读不到 settings.conf,只能写死一个值。

- S11jpeg_display_shell:以基线(249220f2)为底,把背光压制挪到画 logo 之前(jpeg_display
  函数体内、先于 cmd_jpeg_display),值 20(只降不升:S22 已写更暗则不覆盖);删除画完后
  写死 60 的那行;/dev/fb0 等待、mtd5 主题判定、后台 jpeg_display & 语义不变;
  主题判定 `==` 改 `=`(POSIX test,语义不变,便于宿主验证)。
- S22brightness(新):S21mount_ubifs 之后运行,读 /usr/data/meow/settings.conf 的
  display.brightness,按 display.brightness_min 与 sysfs max_brightness clamp 后写入;
  读不到/解析失败不改背光(保持 S11 低值)、无 sleep/重试、任何运行期失败 exit 0;幂等。
- scripts/build/fw_content.conf:登记两条(S11 覆盖基线同名文件、S22 新增),md5 实算,
  说明列写清 S11 < S21mount_ubifs < S22 的顺序依赖。
- scripts/build/README.md:补「覆盖基线已有文件」语义(无条件 cp -f,不校验基线 md5、
  也不跳过已存在文件;唯一基线校验仍是 hiby_player)。

固件需整包刷机才生效,未刷机、未写设备。
2026-11-05 22:04:16 +08:00
AskaEth 316d9d42c7 build(fw): 同步 watchdog 的 md5 到 M19 重编后的二进制
M19 把 meow_watchdog 重编为 ad8c1f2b(首次把 M6.23 的亮屏亮度修复真正编入),但
fw_content.conf 第 32 条仍 pin 旧值 553840cd,build_fw_apps.sh 的强校验会在该条中止。
本提交只改这一行 md5 与该条说明。

注:scripts/build/ 被 .gitignore:68 忽略,而这些文件本就已跟踪,故需 git add -f。
2026-11-05 21:54:23 +08:00
AskaEth dfde95b020 Merge branch 'feat/m20-watchdog-brightness-boot-fallback' 2026-11-05 21:41:25 +08:00
AskaEth f2551a28a2 fix(watchdog): M20 亮屏亮度不再回退固定 60(settings 读失败重试 + 未知快照语义拆分)— 重编出货二进制
真机 0.8.46(2026-11-05 21:09 日志)复现「开机/亮屏背光跳到固定值」:
  76646 [watchdog] power: panel ON (blank 0 + brightness 60)
而 /usr/data/meow/settings.conf 是 display.brightness = 1。根因链:
- 出货 /usr/bin/meow_watchdog(md5 553840cd…,与仓库内二进制逐字节相同)是
  2026-09-01 6a4d330 编出的旧版:不含 M6.23 的 settings 读取(strings 无
  display.brightness_min,日志仍是旧格式 "brightness %d")—— M6.23 只改了源码、
  没重编。旧版 wd_panel_on 无条件 write_brightness(wd_saved_bri>0?wd_saved_bri:60),
  wd_saved_bri 初值 60 且该 session 未熄屏过 → 亮屏写 60,覆盖 launcher 刚设的 1。
- 源码侧同样有 60 兜底:wd_settings_brightness() fopen 失败即 -1 → 回退快照 60。

改动(patch_v2/meow_watchdog/):
- wd_settings_brightness():fopen 失败重试 3×10ms。覆盖 settings.conf「写 tmp+rename」
  原子替换、开机早期 /usr/data 未就绪的瞬时窗口;只在失败时发生(成功路径 0 开销),
  最坏 20ms,远小于亮屏轮询粒度 REFRESH_MS=100ms。
- 新增 wd_bri_decide() 纯函数:settings(clamp 后)→ 本次运行期真实快照 → 不写
  (面板已亮,untouched)→ 安全低亮 10(面板黑且无任何值来源)。任何路径都不再写 60。
- wd_saved_bri 初值 60 → WD_BRI_UNKNOWN(0);wd_panel_off 只在读到正亮度时更新快照,
  读失败/被 launcher 抢先写 0 时保留上一次真实值(不再是 (br>0)?br:60)。
- panel ON/OFF 日志带 src=settings|snapshot|untouched|safe-low 与 user/saved/cur,
  真机一眼看出用的是哪一支。
- 背光路径加 MEOW_BACKLIGHT env 测试钩子(与 MEOW_SETTINGS_FILE 同款,真机无 env)。
- test_wd_fw.c 新增 21 条用例(决策 7 + settings 解析/重试 6 + panel OFF/ON 端到端 8);
  端到端用例真的读写背光文件,复现「settings=1 → 亮屏写 1」而不是 60。
- 重编 meow_watchdog:mipsel 静态 MIPS32r2 ELF,md5 ad8c1f2b…,0 warning。

验证:./build_watchdog.sh host → 38 pass / 0 fail;交叉编译(-Os -Wall -Wextra -static)
0 warning,产物静态 MIPS32r2。
生效条件:meow_watchdog 在 squashfs /usr/bin,必须刷固件(或等价的重打包流程)才生效。
后续(不在本单 scope):scripts/build/fw_content.conf 第 32 行仍 pin 旧 md5 553840cd…,
不更新会让 build_fw_apps.sh 的强校验失败。
2026-11-05 21:32:27 +08:00
AskaEth bcb11e012f fix(reader): stb_image 实现改由 meow_ui.a 提供,修 M6.45 收编后的重复定义
SDK 在 M6.45 收编 stb_image.c.o 进 meow_ui.a 后,reader 的 engine/rdimg.c 仍带
-DSTB_IMAGE_IMPLEMENTATION,链接期 stbi_* 与归档内 stb_image.o 重复定义,reader
无法重链。改为只编封装、实现取自归档;rdimg.c 用到的 stbi_info_from_memory /
stbi_load_from_memory 在归档中均已导出。
2026-11-05 21:12:42 +08:00
AskaEth 62b3603ade merge(sdk): 回补 M19 滚动池槽泄漏修复(wrap 上下文归还 + 44 断言回归) 2026-11-05 21:00:22 +08:00
AskaEth 72c6e9889d fix(sdk): 从公开仓回补 M19 滚动池修复(free 归还 wrap 槽)+ 宿主回归
meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽占一个,meow_scroll_free
只归还会话槽、从不归还 wrap 槽也不清 ctx.w → 进程内最多成功 wrap 8 次,之后恒 NULL。
主仓此前仍是有缺陷版本;本提交把公开 SDK 仓 c85877b 的修复按内容移植到主仓(唯一权威
源),meow_ui_scroll.c 结果与公开仓 sdk/meow_ui_scroll.c 逐字节一致,diff 仅 wrap 相关
hunk(之前两文件差异也只有这些 hunk,故无其它改动被覆盖)。

- meow_scroll 结构记 wrap_slot(create 置 -1),wrap 时写槽下标;free 按该下标清
  g_wrap_ctx[].w/lv 并归还 g_wrap_used,随后置 -1(二次 free 不触碰任何槽)。
  直接 create 的实例 wrap_slot=-1,free 不误清别人的槽。池满语义不变:8 个同时活跃时
  第 9 个 wrap 仍返回 NULL;free(NULL) 安全。
- test_meow_ui_scroll 断言 17 → 44:64 次 wrap/free(修复前 8/64、修复后 64/64)、
  free 后再 wrap、8 满 + 第 9 个 NULL + 释放其一即复用、交错 free 不串槽、直接 create
  不误清槽、list→listview→list 槽复用不残留 kind、free(NULL)/二次 free。
  头注释补 meow_ui_nav.c:原配方缺该 TU,链接期 meow_ui_nav_request_exit 未定义。

验证:LAUNCHER_DIR=主检出/patch_v2/meow_launcherd build_sdk_host.sh --verify
exit=0、0 warning/error、成员清单 76 项一致且全 MIPS32r2;宿主测试 44 pass / 0 fail;
同测试对修复前 meow_ui_scroll.c 为 13 pass / 19 fail(首条即 ok=8 first_fail=8)。
meow_ui.a 中 meow_ui_scroll.o 已变化,链 meow_ui.a 的 App 需重链。
2026-11-05 20:59:36 +08:00
AskaEth 8d2c098deb Merge branch 'feat/files-hello-row-height-followup' 2026-11-05 20:57:43 +08:00
AskaEth 9600f10352 fix(hello): demo 左滑列表行高按字体行高派生 — 首行标题不再丢失(M17)
根因(承接 M12/M15 finding):SDK meow_listview 双行 renderer 把
block = 2*meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2) 在行内垂直居中
(ty = 行顶 + (item - block)/2),标题只在 ty >= 视口顶时才画。demo 的 swipe 列表
显式 set_item_renderer(2,1,0)(双行档位)却 set_item_h(48):真机 source_han_serif
20px 行高 30 → block 62 → ty = 行顶 - 7,首行「notif 0」标题被裁剪守卫整条跳过,
只剩 swipe left 副标题。

修复:
- hello_internal.h:新增 HELLO_ROW_H_MIN(56) / HELLO_ROW_SUB_GAP(2,与 SDK
  MEOW_LISTV_SUB_GAP 同口径) + hello_row_h_for_line_h(line_h)
  = max(56, 2*line_h + 2) + hello_row_h()(取当前主题 font_body 的
  meow_ui_text_height)。本地派生,不依赖 M15 新增导出 meow_listview_item_h;
  显式传值在 M15 之前的 SDK(默认 48/56 档位)与 M15 之后(默认已派生 62)都成立且一致。
- ui/demo/sdk2/sdk2.c:set_item_h(48) 改传 hello_row_h()(行高 30 → 62)。
  列表视口 LV_H / 三节布局推进量不变(行高只影响行内几何与滚动量),其它 demo 区块
  (LazyColumn 列表、宫格、选择器)不受影响。

验证(宿主,未碰真机):
- zig-mipsel-cc -Os -static -Wall -Wextra(另用 -Werror 复验)交叉编译 hello:0 warning;
- 新宿主 test/test_row_text_fit.c(32 断言,纯头文件派生、不链 SDK):
  字库行高矩阵 14/21/26/27/30 下 block <= item、首行 ty >= 0;行高 30 → 62;
  旧 48 → ty=-7 的 bug 形态哨兵;全 PASS。

影响:meow_ui.a 未改 → 无需重编 SDK;本 App 需重链重部署(热更 hello 二进制)。
2026-11-05 20:57:13 +08:00
AskaEth e69b8efeeb fix(files): 目录列表行高按字体行高派生 — 20px 字库下首行标题不再空白(M17)
根因(承接 M12/M15 finding):SDK meow_listview 双行 renderer 把
block = 2*meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2) 在行内垂直居中
(ty = 行顶 + (item - block)/2),标题只在 ty >= 视口顶时才画。目录行是双行档位,
真机 source_han_serif 20px(ascent 24 + descent 6)行高 30 → block 62,而
browser_page_create 显式 set_item_h(56) → ty = 行顶 - 3:首行(行顶 == 视口顶)
标题被裁剪守卫整条跳过,只剩大小/时间副标题。

修复:
- files_internal.h:APP_ROW_H 降级为行高下限(56);新增 FILES_ROW_SUB_GAP(2,与
  SDK MEOW_LISTV_SUB_GAP 同口径) + files_row_h_for_line_h(line_h)
  = max(56, 2*line_h + 2) + files_row_h()(取当前主题 meow_theme_get()->font_body
  的 meow_ui_text_height)。本地派生,不依赖 M15 新增导出 meow_listview_item_h;
  显式传值在 M15 之前的 SDK(默认 56)与 M15 之后(默认已派生 62)都成立且一致。
- ui/browser.c:browser_page_create 的 set_item_h 改传 files_row_h()
  (行高 30 → 62)。SDK 的 draw/hit/max_scroll 全部按这一个值换算,滚动/命中天然
  同步;页脚几何(r_list 单独扣 APP_FOOTER_H)与行高无关。其它几何/交互不变。

验证(宿主,未碰真机):
- zig-mipsel-cc -Os -static -Wall -Wextra(另用 -Werror 复验)交叉编译 files:0 warning;
- 新宿主 test/test_row_text_fit.c(32 断言,纯头文件派生、不链 SDK):
  字库行高矩阵 14/21/26/27/30 下 block <= item、首行 ty >= 0;行高 30 → 62;
  旧 56 → ty=-3 的 bug 形态哨兵;全 PASS。

影响:meow_ui.a 未改 → 无需重编 SDK;本 App 需重链重部署(热更 files 二进制)。
2026-11-05 20:57:13 +08:00
AskaEth c2635daf3f Merge branch 'feat/sdk-time64-syscall-gaps' 2026-11-05 20:46:36 +08:00
AskaEth a7bd6c44d9 fix(sdk): 评审 p2 修正 — 负 nsec 判 EINVAL / 音频条件等待计数改 5 处
- meow_ui_time.c `__timedwait_cp`:`at->tv_nsec` 是有符号 long,原写法
  `>= 1000000000L` 会让负 nsec 通过(musl 原文用 `>= 1000000000UL`,负值按无符号
  比较同样判 EINVAL)。改为 `< 0 || >= 1000000000L`,与本文件 M11 的
  `__clock_nanosleep` 写法一致;同时避开 `-Wsign-compare`(UL 写法在 -Wextra 下
  会告警)。反汇编确认 llvm 合并成一条 `sltu`,与 musl 的 unsigned 比较等价。
- CLOCK_ABI.md:meow_ui_audio.c 的 pthread_cond_timedwait 调用点由「3 处」改为
  5 处(:312/:351/:417/:431/:499),§5/§6 两处同步;核对文中其它计数(四 App、
  23 项单测)无误。

验证:宿主 gcc -Wall -Wextra 0 warning,test_meow_time64 23 项 PASS、
test_meow_time 回归 PASS;交叉 zig -Wall -Wextra 0 warning;
build_sdk_host.sh --verify 76 成员全 MIPS32r2、0 warning;归档内 meow_ui_time.o
与新鲜交叉构建逐字节一致。
2026-11-05 20:46:26 +08:00
AskaEth 40ac1e40f0 docs(sdk): CLOCK_ABI 补 M16 全量清点/接管机制/验证
§6 把「同源残余」换成逐条清点:zig musl 源里 20 处
`#if defined(SYS_*_time64) && !defined(__ILP32__)` 守卫,分「受损(回退分支直传
time64 结构)」与「不受影响(回退分支自带 long[]/宽度判断)」两类,各给源文件:行号
与反汇编证据;纠正 M11 对 pselect/ppoll 的保守判断(它们本来就正确)。

§7 记录接管机制:_REDIR_TIME64 让应用引用 __x_time64(不是 API 名);同一 .o 的
libc 符号必须一起定义(只定义 __timedwait_cp 会与 libc 的 __timedwait.o 撞
duplicate symbol,lld 实测);hidden 不阻止静态链接接管但要保证本 TU 被拉入;
结构宽度前提(整链 zig 头)。附未修 7 项的 5 步配方与「不建议顺手全改」的理由。

§8 记录本单验证:宿主 23 项换算单测 + M11 回归、build_sdk_host.sh --verify
(76 成员/全 MIPS32r2/0 warning)、四个入口的 syscall 号与缓冲宽度反汇编、
真实 meow_ui.a 链接探针的符号绑定。真机确认仍待设备。
2026-11-05 20:46:26 +08:00
AskaEth 0412994953 test(sdk): time64→o32 time32 换算宿主单测(23 项)
test_meow_time64.c 覆盖 meow_ui_time.c 接管入口共用的纯换算层:

- 相对超时(futex):普通值、亚秒、INT32_MAX 上界、上/下溢饱和、负值透传;
- 绝对时刻(clock_settime/timerfd):int32 上下界接受、越界拒绝(对应 ENOTSUP);
- itimerspec 四字段不串位、任一越界整体失败且不写坏输出、反向展开对称;
- 结构宽度 before/after:同一块 16B time64 内存按内核 o32 只读 8B 时,
  0.4s 被读成 {0,0}(立即超时)、2.3s 被读成 2s 整;经本层转换后内核读到
  {2, 300000000}。

宿主编译/运行:
  gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/m.o
  gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time64.c /tmp/m.o -o test_meow_time64
  ./test_meow_time64        # == PASS ==
M11 的 test_meow_time.c 仍 PASS(回归)。
2026-11-05 20:46:26 +08:00
AskaEth 3b5ffece57 feat(sdk): 接管 mipsel time64 同源缺口 — futex 相对超时/timerfd/clock_settime
zig clang 给 mipsel 预定义 __ILP32__,打穿 musl 的 time64 回退守卫,以下入口把
16/32 字节 time64 结构直接交给 o32 time32 系统调用:

- thread/__timedwait.c:14:futex 相对超时(pthread_cond_timedwait / sem_timedwait /
  pthread_join 超时 / aio_suspend / 非 PI timedlock / rwlock timed)。内核只读前 8B,
  tv_nsec 落在 tv_sec 高半字(恒 0)→ 亚秒等待立即超时、整秒等待丢纳秒。SDK 的
  meow_ui_audio 三处条件等待直接受害。按 musl 原文接管 __timedwait_cp/__timedwait,
  只在发 futex 前把相对超时转成 time32;取消点仍走 libc 的 __syscall_cp。
- linux/timerfd.c:14/42:32B itimerspec 直传 → 内核按 16B 读、字段错位;接管
  create/settime/gettime(同一 .o 的三个符号必须一起定义,否则重复定义)。
- time/clock_settime.c:9:16B 直传 → nsec 恒 0;接管 __clock_settime64。

换算抽成平台无关的 meow_time32_{rel,abs}_from_parts() / itimerspec 转换
(meow_ui_internal.h),宿主可测。pselect/ppoll/select、getrusage、wait4、
clock_adjtime 逐条反汇编确认回退分支自带宽度转换,不受影响(纠正 M11 §6 的保守
估计)。未修项(sigtimedwait/semtimedop/timer_settime/gettime/utimensat/recvmmsg/
mq_timedsend/PI timedlock)的清单与配方见 CLOCK_ABI.md §6/§7。

验证:宿主 gcc -Wall -Wextra 0 warning;交叉 zig -Wall -Wextra 0 warning;
build_sdk_host.sh --verify 76 成员全 MIPS32r2;真实 meow_ui.a 链接探针确认
__pthread_cond_timedwait_time64 的 jal 指向本实现、futex=4238 且超时为 8B、
timerfd 4323/4322、clock_settime 4262。
2026-11-05 20:46:26 +08:00
AskaEth cfcb94b8d2 Merge branch 'feat/sdk-listview-row-height' 2026-11-05 20:38:09 +08:00
AskaEth 31c12892d6 test(sdk): 宿主回归 — --wrap 行高 30 下首行标题必须画出(listview 行高派生)
新 test_meow_ui_listview_rowh.c(35 断言)用链接期
-Wl,--wrap=meow_ui_text_height 把宿主 5x7 回退行高 14 换成真机 source_han_serif
20px 实测行高 30,驱动真实 SDK 渲染(同 M12 app/test/test_first_row_title_render.c
手法),断言全部读 back_buf 像素:

- T0 wrap 生效(meow_ui_text_height(font_body) == 30);
- T1 默认(不设 item_h):item_h == 62 = max(56, 2*30+2) ≥ 2*行高+间距;
  max_scroll/hit 按 62 换算;scroll=0 时首行标题带与副标题带都有文字像素;
  视口上方无标题泄漏;
- T2 裸 create(不调 renderer)同样派生;set_item_h(0) 忽略并保持派生;
- T3 显式 set_item_h(70)(ncm 语义):item_h/hit/max_scroll 全按 70,首行标题可见;
- T4 档位:tier(2) 恢复派生 62、tier(1) 48,单行档位不画副标题且标题可见;
- T5 pool 模式(set_vsource + 默认 renderer):同样派生 62,首行标题可见;
- T6 旧行为哨兵:显式 56 < block 62 时首行标题带 0 像素、副标题仍有 → 证明
  像素断言能区分「画出/被跳过」,即 A/B 复现 bug 形态。

回归(宿主 gcc -O1 -Wall -Wextra,全部 0 warning):
- 新 test:35 pass / 0 fail;
- listvirt 44 / widgets ALL PASS / md3 ALL / scroll 17 / select 6 / swipe 35 /
  lazygrid 32 全 PASS;
- 交叉 build_sdk_host.sh --verify:76 成员全 MIPS32r2 rel2,meow_ui_widget.c
  -Wall -Wextra 0 warning(meow_ui.a 变更 → 下游 App 重链)。
2026-11-05 20:38:02 +08:00
AskaEth 4d9ba46a7b fix(sdk): listview 默认行高按字体行高派生 — 双行列表首行标题不再被裁剪
根因(M12 finding / SDK 侧):meow_listview 双行 renderer 把「标题+副标题」
文本块 block = 2*meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2) 在行内
垂直居中(ty = 行顶 + (item-block)/2),且文本只在 `ty >= 视口顶` 时才画。
旧默认行高固定 56 假定行高 ≤ 27;真机 source_han_serif 20px(ascent 24 +
descent 6)行高 30 → block 62 > 56 → ty = 行顶 - 3,首行(行顶 == 视口顶)
标题被裁剪守卫整条跳过、副标题仍在 → 所有用默认行高的 App 两行列表首行
标题消失。

修复(patch_v2/sdk/meow_ui_widget.c):
- item_h=0 改为「派生」哨兵:listv_item_h() 在未显式定行高时按当前主题正文
  字体行高算 —— lines=2: max(56, 2*line_h+2)(行高 30 → 62)、lines=1:
  max(48, line_h)。draw/hit/max_scroll 共用同一函数,行几何恒一致;
- meow_listview_create 不再写死 56;set_item_renderer 不再写死 48/56;
  set_item_tier 恢复派生默认(仍是 1→≥48 / 2→≥56 的口径,字体大时自动长高);
- 显式 meow_listview_set_item_h(>0) 语义不变、优先且固定(ncm 传 70、files
  传 56 等调用方不受影响);set_item_h(<=0) 仍忽略,返回值改为「当前生效行高」;
- 新增 meow_listview_item_h() 读当前生效行高(App 自绘行装饰对齐用,避免再
  硬编码 56);
- src2 分支的双行判断补 `w->lines == 2`,与 pool 分支同口径:tier/renderer 设
  单行档位时明确不画副标题(此前单行档位 48px 里仍塞 62px 文本块 → 同样丢标题)。

同类路径评估(不改):
- LazyColumn/LazyRow(W_LIST):单行 label,item_h 由 create 显式给(默认 44),
  且 item_h 兼作横向列宽,按行高派生意味不明;默认 44 > 真机行高 30,当前不触发;
- lazygrid:格子几何由 App 显式给、文本底部对齐并整体 clip_push 裁剪,不受影响。

影响面:所有用默认行高的两行列表(hello/reader/files 及其它 App)在 20px 字库
下默认行高 56 → 62;显式 set_item_h 的调用方(ncm 70/launcher/files 等)零变化。
meow_ui.a 变更 → 下游 App 需重链(追加导出符号 meow_listview_item_h,ABI 兼容,
老 App 不重编也能跑)。

验证:见下一条 test 提交(宿主 --wrap 行高 30 + 像素断言 35 断言)。
2026-11-05 20:38:02 +08:00
AskaEth 805cfe11cc Merge branch 'feat/firmware-build-m646-killfix' 2026-11-05 20:16:38 +08:00
AskaEth c811913c06 build(fw): M6.46 固件 — 编入 M7 杀后台修复 + M11 SDK 时钟修复(0.8.46)
- launcher bb2d5f12 (863160B) 重编,链 meow_ui.a 4c4a1554:
  M7(bgmgr_can_kill 门控 framework→protected_app / 后台页 sub 指针持久化 /
  am kill 走 meow_bg_terminate 与页面按钮同通道) + M11(meow_ui_time 按 o32 time32 取时、
  链接期接管 __clock_gettime*)
- meow_amsd b698f176 (139252B) 重编:M7 am kill 同通道 + cmd_kill/cmd_start 失败原因细分
  (am CLI 9e323faf 保持不动:am.c 自 ca2290e 未变,与仓库发布物一致)
- fw_content.conf 8 条源 md5 对齐:bootmenu 4441822d / watchdog 553840cd / audiod b122c9b8 /
  meow_audio 891ab264 / netmgrd×2 20243ea1 / launcher bb2d5f12 / amsd b698f176;头注 M6.46/0.8.46
- build_fw_apps.sh:repack.sh 改 bash 显式执行(#!/usr/bin/env bash 在 su 下 bad interpreter)
- 打包 r3proii_m6_46.upt md5 66501aaf(42,930,176B):rootfs 38,756,352<=45,088,768,
  unpack 0 diff,87 files/1 folder 与 m6_45 一致
- 卡上写 r3proii.upt(66501aaf),原 0.8.45(3f4a8ad3) 备份 r3proii.upt.bak.0.8.45;
  写前写后 du -sk Music 均 199164544;未刷机(运行中仍 43ac2312 / 0.8.45)
2026-11-05 20:16:29 +08:00
AskaEth 278e7eaa93 merge(sdk): 收编公开 SDK 仓的图片预算解码,主仓成为 SDK 唯一权威源 2026-11-05 20:07:26 +08:00
AskaEth d9950d75eb Merge branch 'feat/ncm-list-first-row-title' 2026-11-05 20:04:49 +08:00
AskaEth e23bdca26a fix(sdk): 收编公开 SDK 仓独有改进(图片预算解码 + JPEG 解码期缩解),主仓成为唯一权威源
公开仓 ~/Proj/MeowOS-SDK 的 sdk/ 与主仓 patch_v2/sdk 无共同 git 历史(subtree split
重写了提交哈希,不做跨仓 merge/subtree pull),按内容逐文件裁决。公开仓侧的真增量只有
commit 21bffa1(图片预算解码)及其依赖;其余差异是公开仓的自包含打包(扁平 include/、
vendored 头与 meow_font.c)或主仓早已超越的旧快照。

一、取公开侧(纯追加,主仓该文件无独有改动)
- stb_image.h:新增 stbi_load_scaled / stbi_load_scaled_from_memory——JPEG 输出级按
  步长抽取行/列,整幅原尺寸输出缓冲从不物化;进升级 JPEG 各分量系数 IDCT 进像素平面后
  立即释放。scale<=1 与旧 API 等价,非 JPEG 忽略。主仓版本的 diff 全部是公开侧新增
  (无主仓独有 hunk),故整体替换。
- meow_ui_img.c:追加 img_pick_scale / img_finish_dim / meow_img_probe_file /
  meow_img_load_file_dim / meow_img_load_mem_dim。既有加载入口一字未改。
- include/meow_ui_image.h:追加上述三个公共声明。
- 新增 test_meow_img_budget.c + test_meow_img_budget_gen.py(--wrap malloc/free 打点
  统计瞬时峰值,B1-B9 覆盖矩阵)。

二、取主仓(公开侧为旧快照或自包含适配,并入会回退功能)
- meow_ui_ime.c / test_ime_fix.c:主仓 bug3 修复(IME 打开瞬间 200ms TAP 冷却窗 +
  drain/40ms/再 drain)及其回归用例为公开侧所无;公开侧同名文件不含该修复。
- meow_ui_input.c / meow_ui_internal.h:主仓 meow_input_touch_reset(drain 与叠加抑制
  进入沿共用)、FreeType 渲染/字形声明、meow_time_ms_from_parts。公开侧此三处无独有声明
  可并,取主仓即并集。
- meow_ui_gfx.c / meow_ui_sys.c / meow_cachegov.c:主仓时钟 ABI 根因修复(o32 time32,
  meow_ui_time.c 链接期接管 __clock_gettime*)已把这些文件的本地 now_ms 统一为
  meow_ui_time_ms();公开侧仍是各自 clock_gettime,回落即回退根因修复。
- meow_ui_dialog.c / meow_select.c:主仓 MD3 后续(对话框按钮胶囊 MEOW_SHAPE_FULL、
  bottom sheet 顶角 28)晚于公开侧快照,公开侧等于旧版。
- include/meow_ui_font.h:主仓多出 FreeType 段与 stdint 自洽 include,公开侧缺。
- build_sdk.sh / build_sdk_host.sh:主仓含 FreeType vendor 裁剪构建、meow_ui_time TU、
  launcher meow_font.a 打包;公开侧为自包含重写,不回流。
- 其余 test_*.c:差异仅为编译配方注释(公开仓用 -Iinclude、无 meow_ui_time.c、标注
  「需主仓组件故公开仓跳过」等),对主仓目录布局不成立,不改。

三、不引入(vendored 副本 / 打包产物)
- meow_font.c、include/meow_font.h、include/meow_ipc.h、include/meow_notif.h:公开仓
  为自包含 vendored 副本(meow_font.c 与主仓 font/meow_font.c 逐字节相同),主仓已有
  权威源,复制会制造双源。

裁决说明(两边都改过的文件):
- test_meow_ui_select.c:公开仓把 T1 探针从 x=10 改到 x=12(commit 432875d)。主仓 x=10
  在本树实测 6/6 PASS(容器卡左缘 8、文本左缘 24,x=10 已在卡内且避开圆角),无失败
  可修,故不跟进该改动,避免无谓 churn。
- 公开侧「meow_media_playctl」一类 API 名:仅出现在公开仓 vendored 头/文档,主仓
  sdk 无对应实现,未硬造。

四、验证(worktree / 分支 feat/sdk-absorb-public-deltas)
- build_sdk_host.sh --verify:0 warning / 0 error;成员清单 76 个(OBJS 32 +
  meow_font.o + freetype 43)一致,全部 ELF32 MIPS32 rel2 (o32)。
- nm 确认 meow_ui.a 导出 meow_img_probe_file / meow_img_load_file_dim /
  meow_img_load_mem_dim / stbi_load_scaled / stbi_load_scaled_from_memory。
- 宿主测试:对「未改动的 main SDK」跑同一套矩阵做基线对比,40 项同名结果逐项完全一致
  (22 PASS / 11 FAIL / 7 建链失败,失败项与失败计数一字不差);本提交新增
  test_meow_img_budget 为 PASS。补充按各测试正确配方单编:test_meow_img_color、
  test_meow_lyric、test_meow_lyric_view、test_meow_ui_ft(显式传入 MiSans 字体)、
  test_umbrella 均通过/0 warning。
- 既有失败全部是 Termux 环境问题,与本次合并无关(基线同样失败):/tmp 不可写导致
  mkdtemp/fopen/bind EACCES(config/vol/notif_open/notify_ipc/notify_integration/
  sb_collapse/overlay_suppress/input_keys),audiod 守护不在场(ui_audio_host),
  test_sdk_fixes 阻塞循环超时(基线同样 124)。
- hello_app 用新 meow_ui.a 重链成功(0 warning,静态 MIPS32r2),可执行文件
  471380→473636 字节(+2256,新解码路径)。

五、影响
meow_ui.a 内容变化(新增 4 个 MEOW_API + stb 解码路径改变)→ 需重编重链 hello / files /
reader / ncm 四 App 与 meow_launcherd。
2026-11-05 20:03:20 +08:00
AskaEth 22f9dab2ce fix(ncm): 列表首行标题不显示 — 行高按字体行高派生(M12)
根因(真机 seated,非数据/颜色/遮挡):SDK meow_listview 双行渲染把
「2*行高 + 2」的文本块在行内垂直居中,且标题只在 ty >= 视口顶时才画
(meow_ui_widget.c:2061 的越界守卫)。真机 source_han_serif 20px 面
ascent 24 + descent 6 → 行高 30 → 文本块 62px,而 ncm 行高 APP_ROW_H=56
→ ty = 行顶 - 3;首行行顶 == 视口顶 → ty < 视口顶 → 标题被整条跳过,
副标题(ty+32)仍在视口内照画。两类列表同用该路径 + 同一行高,故症状一致。

修复(不动 SDK):ncm_app_internal.h 新增 ncm_row_h()/ncm_row_h_for_line_h(),
行高 = max(56, 2*字体行高 + 2 + 8)(偶数;本机 30 → 70,卡高 62 == 文本块);
home/playlist/queue/cache/search 在 page create 取一次 g_item_h,统一用于
set_item_h 与卡片几何/可见行/页脚/补载视口换算。

回归:
- app/test/test_meow_row_text_fit.c(新):真机字库行高矩阵 14/21/26/27/30
  48 断言(文本块 ≤ 行高、首行 ty ≥ 0、卡高 ≥ 文本块、旧 56 哨兵)。
- app/test/test_first_row_title_render.c(新):--wrap 行高=30 驱动真实 SDK
  渲染 + 读回 PPM 像素计数:旧 56 → 首行标题 0 像素/副标题 186;新 70 →
  标题 84/副标题 100。
- test_meow_row_gap、plpage_sim 全 PASS;ncm_host 7 页出图与改动前逐字节相同
  (宿主行高 14 → 行高仍 56,证重构无视觉回归);5 页自检 PASS;0 warning。
- plpage_sim 临时目录改 $TMPDIR(Termux /tmp 不可写,原写死无法运行)。
- REPORT §43:根因/排除方向/修复/验证/真机验收要点。
2026-11-05 19:51:01 +08:00
AskaEth 98b9169b86 build(ncm): 宿主构建补 meow_ui_time.c(M11 漏编,main 上 ncm_host 链接失败)
M11 新增 patch_v2/sdk/meow_ui_time.c 并把 SDK 各处取时收敛到 meow_ui_time_ms,
但 build_ncm_host.sh 的 SDK_SRC 未同步 → main 上跑本脚本链接报
`undefined symbol: meow_ui_time_ms`(且失败链接会删掉已入库的 ncm_host 产物)。
补一个源文件即可,宿主分支只编 meow_ui_time_ms 的非 mips 实现。
2026-11-05 19:50:51 +08:00
AskaEth 4e60f440f6 Merge branch 'feat/sdk-clock-abi-fix' 2026-11-05 13:42:04 +08:00
AskaEth 38be5e78bc fix(sdk): 时钟 ABI 根因修复 — meow_ui_time 按 o32 time32 取时并链接期接管
真机 meow_ui_time_ms() 返回「低 32=秒×1000、高 32 随机」的根因不是 SDK 写法,
而是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾:
- zig 头(mips-linux-musl/bits/alltypes.h)无条件 64 位 time_t、struct timespec 16 字节;
- zig 的 clang 给 32 位 mips 预定义 __ILP32__,musl 的 time64 回退分支
  `#if defined(SYS_x_time64) && !defined(__ILP32__)`(本意只排除 x32)被编译掉;
- 于是 libc 的 __clock_gettime64 实为 o32 time32 实现(SYS_clock_gettime 4263),
  只写 8 字节 {int32 sec; int32 nsec},与头的 16 字节布局错配。

修复(patch_v2/sdk/meow_ui_time.c):
- meow_ui_time_ms() 直接按内核 o32 ABI 取 8 字节 time32,再升 64 位毫秒;
- 同 TU 强定义 __clock_gettime/__clock_gettime64(归档先于 -lc,链接期接管,
  time()/gettimeofday()/pthread 定时等待的内部取时随之修正);
- 同源缺陷一并修:__nanosleep_time64/__clock_nanosleep(_time64) 转换 time32 后重发
  (修复前 usleep(16000) 等亚秒睡眠立即返回、变忙等空转);
- gfx/sys/input/cachegov 四处重复 now_ms 收敛到 meow_ui_time_ms;
- build_sdk.sh OBJS 增加 meow_ui_time;新增 test_meow_time 宿主单测、
  tools/clock_abi_probe.c 探针、CLOCK_ABI.md 根因/验证/影响面文档;
- 26 个宿主测试的编译注释补 meow_ui_time.c(cachegov 测试补时钟源)。

验证:宿主单测 PASS;anim(32)/view_event 宿主用例全过;build_sdk_host.sh --verify
76 成员全 MIPS32r2、0 warning;探针链接后无 libc cgt_init/__vdsosym,
__time64/__timedwait_cp 的取时指向本 TU,睡眠走 4166/4265 且用本地 8 字节结构。
2026-11-05 13:41:52 +08:00
AskaEth 8a115f6583 Merge branch 'feat/ncm-offline-retry-storm' 2026-11-05 13:16:54 +08:00
AskaEth 294ed6ffc7 test(ncm): M10 宿主回归三套(44 断言)+ 构建脚本 + REPORT §42
- bench/retry_storm_sim.c(25 断言):真实 retry_ctl.c/meta_gate.c,100ms tick 断网
  120s→4 次/间隔 ≥30s;状态每 tick 抖动→2 次;时钟回退→不立即重试且间隔仍 ≥30s;
  worker 45s>间隔→不并发;META 空闲 3 帧/60s(旧 600)、播放 13 帧(≤1/5s)、
  状态抖动 ≤4 帧/秒、元数据变化/seek 立即报、超长标题截断不重报、回退不连发。
- bench/home_retry_sim.c(13 断言):真实 app/ui/home.c 的 home_tick 编进宿主
  (--gc-sections + 桩),断网 120s→4 次重试、单飞、全帧重绘 0 次;
  把真机形态的垃圾 64 位 now 喂给 home_tick 仍为 4 次/≥30s;回在线再断网不清基准。
- bench/meta_ipc_sim.c(6 断言):真实 ipc_media 线程 + 假 launcher AF_UNIX socket,
  空闲 2s 只收到 1 帧 META(旧 ~10 帧),状态翻转恰好多 1 帧,帧头合法。
- build_ncm.sh / build_ncm_host.sh:源列表加 retry_ctl.c、meta_gate.c(host 补
  mem_watch.c,否则 player_manager 链接失败);build_ncm_sims.sh 加三个 sim。
- REPORT §42:真机日志证据(时间戳低/高 32 位拆解、21016 帧 META)、根因三链、
  实现、验证(session_gate 88 / http_truncate 13 / core 139 全过)、真机验证建议。
2026-11-05 13:00:32 +08:00
AskaEth 7f4e18b0ed fix(ncm): META 改「变化+低频心跳」,不再每 100ms 刷 launcher(M10)
根因:ipc_media 主循环 `if (now - last_meta >= 1000)` 同样依赖 meow_ui_time_ms()
的坏 64 位值(REPORT §42)→ 条件恒真 → 每个 IPC 轮次都发 MSG_META。
launcher 日志 meow_20261105_122541.log:单次启动 21016 条
`ipc: META app=dev.player.ncm state=0 pos=0/0 -> ACK`,间隔 ~100ms,且全是
无变化的静止帧;launcher 每帧写 2 行日志+归并会话,SD 写放大到整机卡死。

修复:
- 新增 app/play/meta_gate.{h,c}(纯逻辑):状态/时长/标题/歌手/专辑/封面/歌词
  标志变化 → 立即报;播放中位置偏移 ≥5s 重锚(launcher 自己按 last_active 外推);
  心跳播放中 5s / 非播放 30s;硬下限 250ms(任何路径 ≤4 帧/秒);时钟回退不下溢;
  字符串按发送侧截断长度比较(超长标题不得恒判「变了」)。
- ipc_media.send_meta 改为「组快照 → 闸判定 → 才发」,删除 1s 定时器;
  时间改 ncm_monotonic_ms();首帧仍必发(注册媒体源);发送走 DEBUG 日志。
2026-11-05 13:00:28 +08:00
AskaEth 35a636ef42 fix(ncm): 离线重试改单调钟节奏闸,断网不再每个 tick 重试(M10)
根因(真机日志 19000100_000000_7534.log):meow_ui_time_ms() 在设备上返回坏值——
低 32 位是「开机秒数×1000」(无毫秒),高 32 位每次调用随机(REPORT §42)。
旧守卫 `!g_home_thr_on && (!g_offline_retry_ms || now - g_offline_retry_ms >= 30000)`
因此恒真:worker 一收工,下一个 tick 立刻再重试;真机 74s 内 10 次
(间隔 5-7s 只是失败请求本身的连接超时)。第二条链是 else 分支清零重试时刻,
online/offline 一抖动就再试。

修复:
- 新增 app/logic/retry_ctl.{h,c}:节奏闸只做差值,时钟由调用方给可信单调钟
  (home.c 改传 core 的 ncm_monotonic_ms());回退不下溢(重定基准、不重试);
  busy 单飞;在线/离线抖动不重置基准;首次进离线立即探测一次,之后严格 ≥30s。
- home_tick 不再使用入参 now(真机坏值无效化);新增 home_retry_reset()。
- 静默重试不再走完整 home_reload:新增 home_reload_ex(silent) 不预读盘
  (旧实现每轮在 UI 线程同步读 playlists.bin)、home_apply_ex(keep_scroll)
  保留滚动位置、worker 记 g_home_changed 决定是否需要重绘——旧实现每轮
  home_apply() 复位滚动+整帧重绘,是「歌单列表滑动特别卡」的直接来源。
- worker 线程 pthread_detach(旧代码每轮泄漏线程栈)。
2026-11-05 13:00:24 +08:00
AskaEth aa225a1973 tower: snapshot of uncommitted base checkout changes (worktree wt-10) 2026-11-05 12:47:10 +08:00
AskaEth 61ef0f6de9 Merge branch 'feat/ncm-offline-session-keep' 2026-11-05 12:01:33 +08:00
AskaEth 31879f3302 fix(ncm): 缓存命中改单请求出参,移除全局命中计数(评审 P2-2/P2-1)
问题(P2-2):离线判定用 `ncm_apicache_hits()` 前后差值判「本次是否缓存命中」——
①其它 worker 的命中会污染差值 → 误判「已回在线」;②盘缓存分支的 g_hits++ 在 g_mu 锁外,
跨线程自增/64 位读存在竞争与撕裂可能。

修法(出参方案):
- `api_post_cached(..., int *from_cache)`:命中置 1(未走网络),miss/失败为 0;
  四个调用点显式传参。
- 新增 `ncm_api_user_playlist_ex(..., int *out_from_cache)`;out_n 与原接口一致**必填**
  (NULL→NCM_ERR_ARG,修入口容忍 NULL、成功路径却无条件解引用的契约不一致,评审 P2-1);
  out_from_cache 可 NULL。旧 `ncm_api_user_playlist` 为薄包装(调用方零改动)。
- `appstate_load_playlists_ex` 透传该标志;home_worker 用本次请求的 from_cache 判定是否
  回在线;歌单页只在失败时同步离线态(成功可能是缓存命中,不据此清离线)。
- 删除 `ncm_apicache_hits()`/g_hits(无使用者,同时消除数据竞争);盘缓存命中分支两行
  合并为一行(行为不变,重构)。
- 测试:session_gate_sim 新增 D2 五例(miss→0 且真网络 +1 次 / 命中→1 且 0 请求 /
  失败→0 / 兼容入口不崩 / out_n 必填契约);playlist_race_sim、plpage_sim 补 `_ex` 桩。

验证:session_gate_sim 88 PASS/0 FAIL;test_ncm_core_host 139 PASS/0 FAIL;
http_truncate_sim 13 PASS/0 FAIL;核心 13 TU(-Wall -Wextra)0 warning。
2026-11-05 11:59:33 +08:00
AskaEth b1d607d43f fix(ncm): http_ret_norm 保留 NCM_ERR_JSON(评审 P2-1:畸形分块不再误报断网)
问题:http_run 在 body_feed 失败时返回 NCM_ERR_JSON(畸形 chunk 尺寸/帧体),
但 http_ret_norm 的白名单漏了它,落到 default 被改判 NCM_ERR_NET → 上层按网络类
进离线模式,明明是数据/协议错误却误报「网络不可用」,也会误导离线恢复重试。

修法:白名单补 NCM_ERR_JSON(已属 NCM 错误码的一律原样返回),并把「不得改判」
写进函数注释。测试:http_truncate_sim 新增畸形分块用例(chunk 尺寸行 "ZZ"),
期望 NCM_ERR_JSON;修前该用例会得到 -4。

验证:http_truncate_sim 13 PASS(原 8 截断 + 4 传输归一 + 1 本用例)。
2026-11-05 11:54:23 +08:00
AskaEth 2ca386973b test(ncm): M9 分类/登录门宿主单测(83 断言)+ 传输层归一 4 例 + REPORT §41
- bench/session_gate_sim.c(新):真实 core + ncm_http_request 桩,覆盖
  A 错误分类矩阵(-4/-5/-6/-17/裸 mbedtls vs -13/其它)、B HTTP 状态矩阵
  (401/403 vs 5xx/429 vs 3xx/4xx)、C 登录门 + 启动决策(无网→离线、无会话→扫码)、
  D 真实 ncm_api 状态/业务码映射(含 -460→SVC、301/302→AUTH)、
  E 真实 ncm_login_status_check/refresh 映射、F 会话密钥稳定性(换 MAC 仍可解、
  旧 MAC 密文可解且自动迁移)。83 PASS / 0 FAIL,0 真实请求。
- bench/http_truncate_sim.c:追加 4 例传输层归一(握手 fatal alert→NET、握手超时→TIMEOUT、
  读 fatal alert→NET、CA 失败→TLS),原 8 例截断守卫零回归(12 PASS)。
- build_ncm_sims.sh:window/search/plpage/apicache/cookie_refresh 链入 ncm_session.o
  (appstate/api/login 新依赖),新增 session_gate_sim 构建与运行。
- test_ncm_core.c:采用 M8 分支的 $TMPDIR 适配(Termux /tmp 不可写;合并时以 M8 为准)。
- 相关 sim 头注释同步链接命令(ncm_apicache.o/ncm_session.o)。
- REPORT §41:判定点调用链与行号、根因证据、实现、验证矩阵与诚实出入
  (mipsel/真机未跑;app.c 宿主不可编译的既有原因;403/-460 取舍)。

验证:test_ncm_core_host 139 PASS/0 FAIL;session_gate_sim 83 PASS;
http_truncate_sim 12 PASS;window/search/apicache/cookie_refresh sim 通过;
核心 12 TU + 应用 13 TU(-Wall -Wextra)0 warning。
2026-11-05 11:45:44 +08:00
AskaEth e0bd503261 fix(ncm): 会话密钥改用设备私有 .mkey,网卡/MAC 变化不再丢登录态(M9 启动无网根因)
根因(代码级):会话 cookie 加密密钥 = sha256(首选网卡 MAC + 盐),MAC 取自
/sys/class/net 首个非 lo 网卡。设备断网时 Wi-Fi 网卡可能从 /sys/class/net 消失,
或 readdir 顺序变化选到另一块网卡 → 派生密钥改变 → session.enc 解不开 →
ncm_ctx_load_cookie 失败 → cookie 为空 → g_st.logged_in=0 → 启动直接进扫码页。
这与「把网络不可达当登录过期」是同一症状的另一条路径,且发生在登录门之前。

改动:
- 新密钥优先 <data_dir>/.mkey(32B 随机,首次生成后稳定,与网络状态无关);
  .mkey 不可写(只读 FS/无权限)才回退旧 MAC 派生,保持极端环境可启动。
- 旧版 MAC 派生密文仍可解(cookie_decrypt_sel 二次尝试);load_session 命中旧密钥时
  自动重写为 .mkey 密钥(透明迁移,不改调用方)。
- ncm_store.h 同步说明:machine_key 仅作旧密文兼容,新写入不再使用。
2026-11-05 11:45:20 +08:00
AskaEth 707bbd8a02 feat(ncm): 断网保持登录态 + 离线模式(可见提示/缓存可播/联网自动回在线)(M9)
用户报告:「一断网就退出到扫码登录界面,不会保持账号态切离线,缓存列表形同虚设」。
上一条已把失败分成 NETWORK/AUTH;本条把结论落到 App 状态与 UI。

- 会话状态机(appstate.c):只有 NCM_ERR_AUTH(401/403/301/302)清 cookie;网络类失败
  (不可达/超时/TLS/5xx/-460)只置 g_st.offline,账号态与 session.enc 一律保留;
  appstate_ensure_uid 同语义(原先任何 AUTH 都清 cookie,网络故障不得进入该分支)。
- 启动(app.c):根页改由 ncm_session_boot_decide 决定——有本地会话就进 home(无网也是),
  只有无本地会话才进扫码;打印 boot 决策日志。
- 离线 UX:首页个人卡「离线模式」红标 + 页脚提示 + 空态「离线模式/可播放已缓存」;
  歌单页/搜索页/播放页/缓存页分别给出离线文案(缓存页点播照旧走 NCM_QUEUE_CACHE,本地可播)。
- 恢复:home 可见时每 30s 静默重试一次(强制绕过接口缓存,成功即回在线并重绘);
  任何真实网络成功(home/playlist worker)也会清离线态。
- 防误判:接口缓存命中(120s 旧响应)不算「回在线」——home_worker 用
  ncm_apicache_hits() 差分识别,避免离线提示被缓存命中闪掉。
2026-11-05 11:45:20 +08:00
AskaEth 8557da6fca feat(ncm): 失败分类 + 登录门决策核心(M9:断网不再被当会话过期)
根因:传输层/服务端暂不可用与鉴权失败共用「负错误码」一个通道,登录门只能按
NCM_ERR_AUTH 判断,任何上游映射偏差都会变成「退出登录」:
- ncm_login.c 把任何业务码 !=200(含 -460 风控)都当 AUTH;ncm_api.c 把 -460 也映射 AUTH;
- api/login 层完全不看 HTTP 状态:401/403 与 5xx/网关 HTML 都落进 JSON/缺字段分支,
  反过来 5xx 与网络故障无法区分;
- http 层握手/写入失败把 mbedtls 裸负码(如 -0x7280)直接返回,上层按 NCM_ERR_* 比不中。

改动:
- core/ncm_session.{h,c}(新):ncm_err_classify / ncm_err_is_network /
  ncm_http_status_class|err / ncm_gate_decide / ncm_session_boot_decide。
  语义:NETWORK={NET,TLS,TIMEOUT,SVC,历史裸码};AUTH 仅 NCM_ERR_AUTH;
  401/403→AUTH,408/425/429/5xx→SVC,其它非 2xx→HTTP;启动时有本地会话就先信它
  (无网→离线进 home,绝不进扫码)。
- NCM_ERR_SVC=-17(服务端 5xx/429、业务码 -460:暂不可用、可重试,不是登录失效)。
- ncm_http.c:http_ret_norm 在 done: 统一归一(超时/证书/其它→NET),裸 mbedtls 码不再穿透。
- ncm_api.c:api_status_gate 先按 HTTP 状态定性再解析 body;业务码加 302→AUTH、-460→SVC。
- ncm_login.c:status_check/refresh 先过状态门;301/302→AUTH、-460→SVC、其它非 200→HTTP
  (不再因一次未知响应把用户踢回扫码)。
- ncm_apicache:新增命中计数,供 App 区分「真网络成功」与「离线读到旧缓存」。
- build_core.sh / build_ncm.sh:核心源列表加 ncm_session.c。
2026-11-05 11:45:20 +08:00
AskaEth 03868d048a Merge branch 'feat/ncm-crossbuild-hotupdate' 2026-11-05 11:36:56 +08:00
AskaEth 7d4906828d build(ncm): 宿主交叉适配层 + 全量测试补跑(M8)
- 新增 build_ncm_cross_host.sh:同构 HOME 复用容器脚本,串起 sdk/vendor/host-mbedtls/core/tests/ncm
- libs_common.sh 工具链路径改为 MEOW_R3PROII_ROOT/$HOME 可覆盖,容器默认不变
- build_mbedtls.sh 递归清理 library/ 与 3rdparty/ 旧对象,修宿主/mipsel 混架构归档
- test_ncm_core.c 改用 $TMPDIR,Termux /tmp 不可写不再导致 store/cache/tag 用例失败
- REPORT §40:构建/测试/热更结果(ncm md5 1bbd2a55…,Music du 前后恒定)
2026-11-05 11:34:45 +08:00
AskaEth 2c69070cc4 Merge branch 'feat/ncm-cache-filename-actual-level' 2026-11-04 21:11:04 +08:00
AskaEth e60642217a test(ncm): M6 缓存命名宿主单测(63 PASS)+ core 全量用例 + REPORT §39
- 新增 test/test_ncm_cache.c + build_ncm_cache_host.sh(纯 libc:真实 ncm_cache.c +
  ncm_tag.c,仅桩 strlcpy/mkdir_p/档位映射,不依赖 vendor/mbedtls)
- 覆盖:实际档映射、降级后写入名、hires 命中已存 exhigh/lossless、低档不命中高档、
  旧命名命中/扫描/删除/双点名迁移、列表去重、LRU/清空/删除兼容、level_from_path 边界
- test_ncm_core.c 新增 test_cache_actual_naming(接入 core 全量测试集,同语义断言)
- REPORT §39:命名/命中/兼容方案、质量取舍与未执行项(vendor/交叉/真机)
2026-11-04 21:11:00 +08:00
AskaEth 68833cf642 feat(ncm): 下载落盘名用响应实际档 + 命中按文件名标实际档(M6 播放链路)
- song_download_attempt:URL/类型确定后算实际档,.part/open/commit 统一用实际档键
  (hires 请求被降级为 320k mp3 → song_<id>_exhigh.mp3),eff_level=实际档
- song_local_file:命中路径由 ncm_cache_level_from_path 取文件名档位并按容器钳顶
  (mp3 永不高于极高),旧 hires.mp3 正确显示「极高」
- 移除旧「stale-degraded 改名+强制重下」与「无档位键迁移」:老卡文件读可用、不迁移
- player_flow_sim 适配:补实际档/路径档位桩与 M5 链接桩;P7/P12 期望键改实际档,
  P13 改为旧命名命中即播、不重下不迁移
2026-11-04 21:11:00 +08:00
AskaEth 30ffe469c8 feat(ncm): 缓存实际档判定/文件名解析 + 查找档位下探(M6 core)
- ncm_cache_actual_level:请求档 + 容器/码率/位深 + 响应生效档 → 实际档(只降不升),
  作为写盘名/播放显示/已缓存列表的唯一事实源
- ncm_cache_level_from_path:文件名档位段解析(song_<id>_<档位>,含 sky_c51)
- ncm_cache_lookup:请求档精确 miss 后按实际档逐档下探(hires 命中已存 exhigh/lossless),
  只降不升,低档请求不命中高档实体
- ncm_cache_scan:actual_level 改用统一判定;同曲同实际档的旧名+新名去重展示
2026-11-04 21:11:00 +08:00
AskaEth 132622cbc1 Merge branch 'feat/launcher-bg-kill-button-dead' 2026-11-04 21:00:45 +08:00
AskaEth 6b52740ff1 fix: 后台页杀按钮点不动 — app.json framework=1 被误当「不可杀」门控 + am kill 无前台会话不可执行
根因(2026-11-04 真机 dev.player.ncm pid=7520):
- bg_page.c bgm_app_meta 把 app.json 的 framework 字段映射进 bgmgr_row.framework,而
  bgmgr_can_kill 把它当「系统/框架 App 禁止 kill」。但 framework=1 在 MeowOS 里意为「用
  meow_ui.a 的 SDK 框架 App」(docs/APP_DEV_GUIDE.md §9,推荐值,真机 4 个 App 全写 1)
  → 后台页每行「杀」按钮被置灰,bgm_row_tap 命中后直接 return 1、不调 meow_bg_terminate;
  「一键清除后台」同理 0 行可杀。整条链路无日志,故只能靠翻 app.json 才定位(全部设备日志里
  "后台管理页终止后台App" 出现 0 次 = 4 次进页从未真正杀过)。
- 面板 killfg 正常是因为走 am kill → amsd → MSG_AMS_KILL,无 framework 门控;但它其实靠
  amsd 1200ms×2 超时兜底 kill(pid,SIGTERM) 才杀成:util.c 原本在无前台 App 会话时只回
  ACK -2((uint32_t)MEOW_ERR_NOENT),ACK 及时到达时 amsd 判「launcher refused」完全不杀
  (ams_20260915_031339.log 实证),ACK 迟到才侥幸成功(2026-11-04 真机)。

修复:
- meow_bgmgr: 门控位 framework → protected_app(系统级受保护标记;当前无 App 置位,launcher
  自身组件本就不在 App 表/后台表内)。bg_page 不再把 a->framework 送进门控。
- bg_page: 行副文本改存 ctx 持久缓冲(原 rbuf[].sub 指向 bgm_rows 栈帧 → 悬垂,副标题乱码);
  门控拒绝/未命中后台表各留一行 logmsg,「点了没反应」从此可查。
- util.c MSG_AMS_KILL: 先查后台保活表 → meow_bg_terminate 就地走真退序列(与页面「杀」按钮
  同一通道,故 am kill 与页面按钮行为对称),无前台会话也能杀;否则仍走 pending / ACK -2。
- amsd cmd_kill/cmd_start: ipc 失败原因细分(connect failed / timeout, no reply / ...),日志
  不再笼统写 "launcher offline"(响应串保留该字样,MeowPanel api_ams_ctl 依此映射 503)。
- meow_bgplay: meow_bg_terminate 注释/日志写明「后台管理页 / am kill」双入口同通道。

测试(宿主 gcc,产物在 $TMPDIR):
- test_bgpage 40 checks:新增 framework=1 仍判可杀且按钮真 terminate(本 bug 回归)、
  受保护行按钮不杀但留诊断日志、sub 持久化 + 栈抖动不变(悬垂回归)。
- test_bgmgr 30 checks:protected_app 门控矩阵 + framework=1 可杀回归。
- test_bgplay 92 PASS:新增 4b meow_bg_terminate 全链路(受理→term_at_ms→tick SIGTERM→
  on_bg_dead→清槽;空/不在表分别 INVAL/NOENT);TEST_SVC_PATH 改取 $TMPDIR
  (Termux 无 /tmp,原 fputs(NULL) 直接 abort)。
- amsd 宿主集成(fake launcher 桩,4 例):ACK 0→terminate 且不盲杀;ACK -2→refused 不盲杀;
  无 launcher→direct SIGTERM (connect failed);挂起不回→direct SIGTERM (timeout, no reply)。
0warning:util.c 与 main 同告警逐行一致(仅 SDK 头既有 typedef 重定义),其余文件干净。
2026-11-04 21:00:40 +08:00
AskaEth a73bcb9841 Merge branch 'feat/ncm-cached-list-and-tag-embed' 2026-11-04 20:49:36 +08:00
AskaEth a9c94c7221 fix(ncm): tag 嵌入按实际容器拒绝 MP4/M4A(ID3v2 前缀会破坏 MP4 容器)
download 扩展名在 type 非 flac 时一律落 mp3;若服务端降级返回 MP4/M4A("....ftyp"),
给文件加 ID3v2 头会破坏容器。ncm_tag_embed_file 现读 12 字节 magic:fLaC → VORBIS_COMMENT,
"ftyp" → NCM_ERR_ARG 拒绝(调用方 commit 裸流),其余按 MP3 处理。
test_ncm_tag 增 2 条断言(拒绝 + 原文件不变)→ 53 PASS / 0 FAIL,0 warning。
2026-11-04 20:49:25 +08:00
AskaEth 39465ea1ef feat(ncm): 新写入的缓存文件嵌入歌曲元数据(FLAC/MP3,commit 前流式重写,失败降级裸流)
用户需求:保留元数据分离的同时把歌曲元数据嵌进缓存文件(否则外部播放器读不到任何信息)。
- 接入点 player_manager:下载完整(dl_rc==OK)→ stream_src_finish(读端先自然收尾)→
  ncm_tag_embed_file(.tmp, 实际扩展名, {name/art_name/album_name}) → dispose join 之后才
  commit。tag 重写是同目录 .tag.tmp + rename 换 inode,解码端全程持旧 fd,播放/seek/
  .part 续传字节语义零改动。
- 容器按实际 magic 分支(ncm_tag.c);峰值内存 64KB;失败只记 WARN、commit 裸流,
  播放主链路不受影响(用户红线)。
- 元数据分离体系不动:<cache_dir>/meta/<id>.bin 旁路照旧写读;LRU 只 stat 文件,
  无 size/etag 索引需回填(重写后 stat 自然一致)。
- test_ncm_core 新增 test_cache_tag:writer 写裸流 → 嵌入 ID3v2.3 / VORBIS_COMMENT →
  读回 → ncm_cache_scan(hires 名 + 320k mp3 体 → 实际档 EXHIGH,名称来自文件内 tag)
  → ncm_cache_remove → ncm_cache_clear。
- REPORT §38:设计/内存估算/验证/诚实遗留(本机缺 vendor 与交叉工具链,未跑整链与真机)。
2026-11-04 20:49:25 +08:00
AskaEth f392ee5c48 feat(ncm): 首页顶栏「已缓存」入口 + 已缓存列表页(歌名/歌手/实际档位/大小,点播/单曲删除/清空)
入口:首页 topbar 新增动作 3(MEOW_ICON_DOWNLOAD 位图,原「回播放页」顺延动作 4)→
push ACT_CACHE;左缘返回弹回首页(app.c 路由补齐 tap/swipe/touch/tick)。

列表页 app/ui/cache.c(对齐 queue.c 惯例:圆角卡/红 mini-vinyl/按压红条/当前播放行高亮):
- 行 = 歌名 / 歌手 · 实际档位 · 大小;空态/页脚(N 首 · 合计 X MB)。
- 点行 = 点播(NCM_QUEUE_CACHE 队列,上/下一首在缓存列表内循环);行右「删除」=单曲删除;
  顶栏「清空」=确认框 + ncm_cache_clear(与设置页「清理缓存」同语义)。
- 数据全在 worker:cache_reload → appstate_cache_list_load → ncm_cache_scan(读目录 +
  文件内 tag + 音频头探测)→ 快照;UI 线程零磁盘 IO。快照上限 512 条(最近访问优先,
  ≈116KB bss),更多条目按 mtime 保留最近的。

core:
- ncm_cache_scan / ncm_cache_remove(songs/ 池,song_<id>_<请求档>.<实际格式> 解析,
  跳过 .tmp/.part 与非数字 id;按 mtime 降序;历史双点名删除兜底)。
- ncm_api_level_from_key(缓存键后缀 ↔ 档位枚举,与 ncm_api_level_key 互逆)。
- **档位显示实际档而非请求档**:真机存在 song_171650_hires.mp3(无损请求被降级链落到
  exhigh mp3),故 scan 用 ncm_audio_probe_file 读实际容器参数后只降不升地推断
  (FLAC 16bit→无损 / 24bit→Hi-Res+,MP3 ≤128k→标准 / >128k→极高)。

播放:player_manager 对 NCM_QUEUE_CACHE 用条目真实键(id+请求档+实际扩展名)直读缓存
文件,不按用户当前档键重下;song_local_file/play_is_same_song/queue 页均支持第三种队列。
宿主 mock(NCM_PAGE=cache)造 3 条缓存 + 嵌入 tag + 同步扫描。
2026-11-04 20:49:25 +08:00
AskaEth 184ebd0ac9 feat(ncm): 缓存文件内嵌元数据模块 ncm_tag(FLAC VORBIS_COMMENT / MP3 ID3v2.3 写读 + 音频头探测)
core/ncm_tag.c/.h(纯 libc,宿主可单测,不依赖 mbedtls/jsmn):
- FLAC:在元数据末块之后插入 VORBIS_COMMENT(TITLE/ARTIST/ALBUM),旧 VORBIS_COMMENT
  替换、PADDING/PICTURE 原样保留;MP3:ID3v2.3 TIT2/TPE1/TALB(UTF-16LE+BOM,中英文
  与 emoji 均可),源已带 ID3v2 时先剥旧标签(不留双标签)。
- **按实际容器分支**(读 fLaC 魔数):缓存名档位段是请求档,真机存在
  song_<id>_hires.mp3(无损请求被降级链落到 exhigh mp3),不能按名字判容器。
- 写入 = 同目录 <path>.tag.tmp + 64KB 分块搬运 + fsync + rename 原子替换;任一步失败
  即 unlink 临时件、原文件保持不变(调用方 commit 裸流降级),峰值内存 64KB。
- ncm_audio_probe_file:只读文件头探测实际容器/位深/码率(FLAC STREAMINFO / MP3 首帧),
  「已缓存列表」据此显示实际档位而非请求档。
- test/test_ncm_tag.c + build_ncm_tag_host.sh:FLAC/MP3 构建+读回、中文 roundtrip、
  旧标签替换、容器判定、3MB 分块、失败降级、探测 —— 51 断言全过,0 warning。
2026-11-04 20:49:25 +08:00
AskaEth e56c837f4f Merge branch 'feat/sdk-cross-build-bring-up' 2026-11-04 20:26:22 +08:00
AskaEth 8be4634a61 feat(sdk): 本机 zig 交叉链跑通 build_sdk.sh — 新增 build_sdk_host.sh 环境适配+成员/架构自检
主仓 patch_v2/sdk/build_sdk.sh 此前只在 SDK 仓验证过:硬编码 ROOT="$HOME/Project/R3ProII"
取 .tools/zig-mipsel-cc 与 patch_v2/meow_launcherd/meow_font.a,默认 AR=/usr/bin/ar、
mktemp 落 /tmp。Termux 上三者都不成立(工具链在 ~/.tools、meow_font.a 在仓库内且 *.a 被
gitignore、ar 在 $PREFIX/bin、/tmp 不可写),脚本无法直接跑通。

- build_sdk_host.sh: 不改 build_sdk.sh,临时布局同构 $HOME(.tools 与 patch_v2/
  meow_launcherd 符号链接到真实位置),导出 AR/TMPDIR 后原样执行脚本;--verify 追加
  「成员清单 = 本次 *.o(OBJS + meow_font.o)+ libfreetype.a 对象」比对与全量架构校验
  (不复制 OBJS 列表,避免与 build_sdk.sh 漂移)
- FreeType 2.14.3 源: SDK_FREETYPE_SRC 指向本地源优先;未设且无 vendor 时仍走 savannah 下载
- 验证: 冷构建(zig cache 空) 55s,重编 11-14s;meow_ui.a 5,701,866B / 75 成员
  (OBJS 31 + meow_font.o + freetype 43),全 ELF32 MIPS32 rel2 o32;hello_app 按文档链路
  meow_ui.a + libz.a + -lpthread 链接成功(474,612B 静态 MIPS32r2),不调 meow_ft_* 时
  freetype 成员未被拉入(零增量成立)
- 产物不入库: vendor/、*.a、*.o 均被 .gitignore 覆盖,git status 仅本脚本
2026-11-04 20:25:54 +08:00
AskaEth 47c638d65b Merge branch 'feat/m6445-web-license-residual-sync' 2026-11-04 20:14:16 +08:00
AskaEth 20f4727714 feat: M6.44+M6.45 现场补全 — 补齐按扩展名过滤遗漏的 web 前端与 license 合规件
真机 0.8.45 现场组成补全(初次提现场按扩展名过滤漏掉 7 个文本文件),
内容逐字节取自容器现场 /mnt/mb/home/Aska/Project/R3ProII 并 md5 对拍一致:

- patch_v2/meowpanel/web/meowpanel.css          (+17)  趋势/App 详情卡样式
- patch_v2/meowpanel/web/mp_pages.js            (+289) 性能趋势/功耗曲线/App 详情/蓝牙详情
- patch_v2/meowpanel/web/mp_shared.js           (+49)  趋势 sparkline 通用逻辑
- patch_v2/reader_app/.gitignore                (+2)   seg/chapter smoke host
- patch_v2/reader_app/NOTICE                    (+23)  FreeType 2.14.3 + zlib 条目
- patch_v2/reader_app/resources/licenses/NOTICE (+23)  同 reader_app/NOTICE(md5 一致)
- patch_v2/reader_app/resources/licenses/FTL.txt (新增 6743B) FreeType 许可证全文

面板前端对应已收编的后端 mp_files.c/mp_net.c(文件搜索+蓝牙详情),
NOTICE/FTL.txt 为 LGPL/FreeType 合规产物。仅投运现场内容,未改内容。
2026-11-04 20:10:42 +08:00
AskaEth 2ffb92c038 Merge branch 'feat/m6445-dirty-snapshot-consolidation' 2026-11-04 20:09:20 +08:00
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
2026-11-04 20:02:28 +08:00
AskaEth e87ccdf061 Merge branch 'feat/m643-reader-streaming-panel-sync' 2026-11-04 20:00:59 +08:00
AskaEth f40abbd494 feat: M6.43 — reader分段流式读取(Phase1,只txt) + 面板前台管理/返回回桌面按钮
- reader分段(只txt): core/seg.{h,c} 512KB码位对齐切段(尾字完整)+manifest+按需读单段; book.c rd_book_open只布局每页所在段(治CPU100%/返回无效) load_seg只读当前段(非txt原路径); reader.c 进度实时保存(翻页/刮擦/字号/编码dirty+on_tick节流落盘+退出即时写,锚点存段号,崩溃不丢进度); rdtxt.c fread替代fgetc(去开书扫描卡顿); Phase2(双段拼接+4%释放)留待
- 面板: /api/ams/bg加fg/fg_count(后台保持向后兼容)+POST /api/ams/:pkg/killfg(前台救命:优先am kill,launcher拒则pid直信号SIGTERM->SIGKILL绕过门控)+前端/#/bg前台组'终止'绿徽标+确认弹窗(一键清仍只后台不误杀前台); POST /api/input/gesture(返回=左缘右滑/回桌面=底部上滑,注入滑动轨迹复用SDK手势)+前端屏幕控制页加返回/回桌面MD3按钮

0warning;宿主test_reader_core 72-0/seg_smoke 19-0/loaders_smoke/book_smoke/formats_smoke PASS;面板selftest/node check PASS
2026-11-04 19:47:10 +08:00
AskaEth a64ae06eb6 fix(reader): txt编码误判(UTF-8前窗截断判GB18030致乱码) + build_index无持久化(整本重排慢/UI阻塞卡) — ①rd_utf8_sample_ok结尾不完整序列按合法(中间非法仍拒,GBK不误判,新增t13回归) ②索引落盘MeowData/os.meow.reader/index/*.idx(失效键size/mtime/font/fs/ls/wh/enc,命中秒开,原子写+强自检弃坏缓存,编码/字号/行距重排重写) 重开<0.3s/首开建cache; test_reader_core 72-0, 0warning 2026-11-04 19:47:10 +08:00
AskaEth 68710d7ed8 fix(reader): 书索引build_index全量逐字FreeType排版优化 — rdft_advance加(px,cp)->advance缓存(开放寻址4096槽,与glyph缓存分离,load_font/set_px同清), 测量从O(全书字符)降到O(唯一码点~2600); 大书(10MB)打开 rd_book_open 23.9s->140ms(~170x), npages一致布局确定保留; 点条目不再用户态busy循环冻死(非驱动/锁屏, 是UI线程同步全量建索引); 0warning, 宿主test_reader_core 68-0/loaders_smoke/book_smoke/formats_smoke PASS 2026-09-04 21:01:56 +08:00
AskaEth 5e67100231 fix(reader): 书架scan_books改为递归扫子目录(Books/<subdir>/,深度≤2), 用户按目录组织书也能扫到 — 修复'书架为空'(书在Books/kh/子目录); reader热更 eacd0e6c 已基于此; 0warning 2026-09-04 20:48:55 +08:00
AskaEth 06f872faf2 fix(fw): M6.42 收编audiod开机路由武装瘦身(防楔死) — 推卡
- audiod 重编 d833abce(0warning, glibc-dynamic≤2.17): 去无条件apply_route(5)→target双写,当前端口正确则跳过强烧,仅未武装/端口不对/读失败才写目标;保留M2.91防挂死武装(numid=9语义/popen有界超时/D态抛弃/io_thread未动)
- SDK未变(meow_ui.a 207630cb沿用); launcher 9013f843未变; netmgrd 9c345b2e/meow_audio 6864ec38/amsd 57e19cde 未变
- 固件 r3proii_m6_42.upt 6d7c1168(0.8.42, rootfs 38719488<=45088768, unpack 0 diff); 已上卡+备份r3proii_prev_m6_42=2a966f2b+rm recovery_all+升级重启; 设备已回网0.8.42
- reader eacd0e6c/files 01ad3f01 卡上已热更未动(不在固件)
- 注: audiod瘦身是驱动楔死缓解,需用户后续多boot复测(不在本收编范围)
2026-09-04 20:48:02 +08:00
AskaEth 68f91f7de2 fix(audio): M6.42 — audiod开机路由武装瘦身(去无条件apply_route(5)→target双写, 当前端口正确则跳过强烧, 仅未武装/端口不对/读失败才写目标; 保留M2.91核心防挂死武装, numid=9语义/popen有界超时/D态抛弃/io_thread未动) — 降低stock hbc3000驱动楔死整机硬冻结触发面(reader后台+Files切换撞楔死驱动口); 0warning, 宿主test_audiod_host 100-0; 需真机多boot复测 2026-09-04 20:43:22 +08:00
AskaEth 4ce7a268a8 fix(fw): M6.41 收编启动失败原因透传+后台管理按钮点不动修复+主菜单微调 — 推卡(固件已上卡,设备待回网补reader热更+校准)
- launcher 9013f843(启动失败原因透传+后台管理按钮点不动修复+主菜单字体/样式微调) / reader 5a0c8776(左缘返回+view栈+字体瘦身+页位图释放)
- SDK未变(meow_ui.a 207630cb沿用),不重编meow_ui.a/不重链ncm/hello/files(files已热更01ad3f01不动)
- 固件 r3proii_m6_41.upt 2a966f2b(0.8.41, rootfs 38719488<=45088768, unpack 0 diff); 已上卡+备份r3proii_prev_m6_41=28191ae7+rm recovery_all+触发升级重启
- netmgrd 9c345b2e/audiod 9f47c8fd/meow_audio 6864ec38 未变; amsd 57e19cde 保持
- 注: 设备升级后未回网(WiFi已知问题,超~5分钟),reader热更+升级后验收+红线校准待设备回网后补
2026-09-04 19:50:55 +08:00
AskaEth ddc8d35eb9 fix: M6.41批 — 启动失败原因透传 + 阅读器左缘返回/阅读页接入view栈 + 后台管理按钮点不动 + 阅读器瘦身(fonts common+页位图释放) + files txt换行
- launcher(453): app_launcher.c g_launch_fail_reason+launch_fail()统一出口(5失败点 fork OOM/exec/no such file/missing dep/execpipe)+main_menu.c launch_fail_toast(缺依赖/CANNOT START: name(reason))+toast256
- launcher(457): bg_page.c bgm_enumerate跳过term_at_ms(杀后当下消失)+不可杀行按钮置灰+row_tap/top_action返回1; subpage.c 消warning
- reader(454): reader_on_swipe LEFT/RIGHT→返回(先关overlay否则page_pop回书架)+app.c书架根LEFT→exit
- reader(458瘦身A+B): app.json fonts full→common(省13MB atlas)+reader.c RD_PAGE_CACHE 3→2+g_anim_old懒分配/anim_done释放+on_enter/on_leave页位图离开释放重建
- reader(459导航): reader_open返回int+shelf点书push ACT_READER(阅读页接入view栈,on_enter/on_leave驱动) — 使454左缘返回+458瘦身B真正生效
- files(455): viewer.c viewer_cp_len+按码位折行(txt中文不冲出右缘)

0warning;宿主测试全过;files二进制已热更
2026-09-04 19:43:28 +08:00
AskaEth 626a87a30d fix(fw): M6.40 收编系统级音量翻页映射(MSG_KEY_PAGEUP/PAGEDOWN)+P1 MD3(select面板28/对话框按钮胶囊)+阅读器UI改版 — 推卡(固件已上卡,设备待回网验收)
- meow_ui.a 重编 207630cb(0warning): select面板28/对话框按钮胶囊h/2
- launcher f1980f4f(音量翻页系统映射) / hello 92c7ba17 / files 66bdb60d / reader 9c2a25e0(阅读器UI改版) / ncm e796282e
- 固件 r3proii_m6_40.upt 28191ae7(0.8.40, rootfs 38719488<=45088768, unpack 0 diff); 已上卡+备份r3proii_prev_m6_40=a671b8ba+rm recovery_all+触发升级重启
- netmgrd 9c345b2e/audiod 9f47c8fd/meow_audio 6864ec38 未变; amsd 57e19cde 保持
- 注: 设备升级后未回网(超8分钟),四App+面板热更与升级后验收待设备回网后补
2026-09-04 18:24:16 +08:00
AskaEth 2826c1e905 feat: M6.40批 — 阅读器UI改版(书架list·grid+空态骨架+界面设置+App设置页+header·footer+背景黑默认) + 系统级音量翻页映射 + P1 MD3(select面板28/对话框按钮胶囊) + MeowPanel写端点护栏 + SDK公开仓·文档同步
- reader: app.c/shelf.c/reader.c 全套UI; 新增 reader_conf(reader.conf)/settings(App设置页)/reader_ipc(音量翻页IPC响应); app.json加volume_page
- launcher: keys.c meow_volpage_*门 + util.c app_id→fd映射(HELLO记fd/断开清) + app_launcher.c 前台三处接线(进入sink/按键路径volpage_key/离开clear) → 前台声明volume_page的App音量键→MSG_KEY_PAGEUP/PAGEDOWN翻页(非阅读器前台音量照旧)
- sdk: meow_select.c 底部面板28 + meow_ui_dialog.c 对话框按钮胶囊(h/2) + test_meow_ui_select.c修采样
- meowpanel: /api/net/policy·/api/power/policy 加防自锁+policy默认关两条护栏(panel.policy_enabled默认0)
- 文档: MEOW_APP_SDK.md §12.6 + meowpanel README端点表

0warning;宿主测试全过;二进制留批量批次未入库
2026-09-04 18:13:18 +08:00
AskaEth b364b6371a fix(fw): M6.39 收编SDK LazyGrid/LazyColumn/LazyRow MD3容器化+SDK全局App内底部上滑回桌面(系统手势优先) — 推卡
- meow_ui.a 重编 0fb87579(0warning): meow_list/lazygrid 默认container=1 + meow_ui_input_is_home_swipe/MEOW_EV_HOME
- launcher a21243b6 / hello 96d14310 / files a9de683f / reader 3e8be7cb / ncm 47954e2c (重链新meow_ui.a)
- 固件 r3proii_m6_39.upt a671b8ba(0.8.39, rootfs 38715392<=45088768, unpack 0 diff)
- netmgrd 9c345b2e/audiod 9f47c8fd/meow_audio 6864ec38 未变; amsd 57e19cde 保持
2026-09-04 14:01:26 +08:00
AskaEth 501867b362 feat(sdk): M6.39批 — LazyGrid/LazyColumn/LazyRow MD3容器化 + SDK全局App内底部上滑回桌面(系统手势优先)
- meow_ui_widget.c: W_LIST/meow_list/LazyColumn/LazyRow默认container=1(MD3圆角卡surfaceContainer+tonal选中+卡内按压+去分隔线) + 新增meow_list_set_container; 平坦回退set_container(0)
- meow_lazygrid.c: lg_draw_cell容器化(卡内缩8,surfaceContainer圆角+tonal选中+按压)
- meow_ui_input.c: 新增meow_ui_input_is_home_swipe(镜像np_home_gesture:屏底28px热区+上移65px+竖向主导,与左缘返回/列表中滑互斥) + MEOW_EV_HOME分发→meow_ui_nav_request_exit(系统保留手势**优先于App on_swipe**,任意App内生效→MSG_NAV EXIT→批①保活回桌面; 不调request_exit避免进程退出断送保活)
- 声明: meow_ui_input.h / meow_ui_widgets.h 各追加
- 宿主测试: test_meow_ui_home_gesture 19-0 / test_meow_ui_lazygrid 32-0, 0warning
2026-09-04 13:56:11 +08:00
AskaEth d0a5901ffb fix(fw): M6.38 收编SDK对话框MD3细化+批②底部上滑回桌面+批6 ncm顶栏回播放页+批④面板后台列表+launcher UI微调+封面空artwork保留 — 推卡+面板热更
- meow_ui.a 重编 4c006844(0warning): meow_ui_dialog MEOW_SHAPE_EXTRA_LARGE 28
- launcher c9218411 / hello 63824093 / files c9309774 / reader 0d462111(未变,不拉对话框对象) / ncm 2b33b1dc
- 固件 r3proii_m6_38.upt 7b6ce4a5(0.8.38, rootfs 38715392<=45088768, unpack 0 diff)
- meowpanel 后端 fcb2f658 + web(index/css/js/mp_pages) 热更 MeowOS/opt/MeowPanel, /api/ams/bg 新增, 运行中新二进制
- netmgrd 9c345b2e/audiod 9f47c8fd/meow_audio 6864ec38 未变; amsd 57e19cde 保持
2026-09-04 13:23:56 +08:00
AskaEth a2aec783a7 feat(ui): M6.38批 — P1 MD3细化(SDK对话框28)+批②底部上滑回桌面+批6 ncm顶栏回播放页+批④面板后台列表+launcher UI微调(歌词页黑半透明底/播放页布局下移)+封面空artwork保留修复
- P1: meow_ui_dialog.c 对话框底框 radius_card→MEOW_SHAPE_EXTRA_LARGE(28) 三处(:103填充/:104描边/:361 BottomSheet); meow_ui_widgets.h 新增 MEOW_SHAPE_EXTRA_LARGE 28 宏; TextField/FAB/Slider/Progress 按设计文档保留
- 批②: np_gesture.h 新增 NP_GEST_HOME+np_home_gesture(底部热区上滑,与左缘返回对称互斥); now_playing.c 新增 np_draw_home_bar(屏底左缘pill指示条)+手势衔接; test_now_playing_widgets G5b
- 批6: ncm home.c 顶栏加回播放页按钮(MEOW_ICON_QUEUE_MUSIC)→push(ACT_PLAYER), 无音频也生效
- 批④: meowpanel /api/ams/bg 只读端点+前端后台列表页+杀/清复用kill通道
- UI微调: now_playing.c 播放页布局宏下移(NP_COVER_Y 84/NP_BTN_Y 612消底部留白)+歌词页背景统一黑色半透明叠层(弃封面取色底,删 np_draw_lyric_coverbg)
- 封面保留: now_playing.c/notif_center.c 空artwork保留上一张已解码封面(防暂停/开歌单致META报空artwork后封面闪没); test_np_cover C2断言更新+mirror同步

0warning;宿主测试 test_np_cover ALL PASS;launcher/ncm/panel 宿主测试无回归。二进制留批量批次未入库
2026-09-04 13:09:03 +08:00
AskaEth b3ce719742 fix(fw): M6.37 收编SDK默认列表MD3容器化(meow_listview_create默认container=1)重编meow_ui.a重链四App — 推卡
- meow_ui.a 重编 14f218d7(0warning): meow_listview_create默认container=1,行=surfaceContainer圆角卡+空隙+去分隔线+卡内tonal选中
- launcher e7fd28dd / hello c10a3ea5 / files 90a6e817 / reader 0d462111 / ncm 6f3967f8 (重链新meow_ui.a)
- 固件 r3proii_m6_37.upt a8dd8f54(0.8.37, rootfs 38715392<=45088768, unpack 0 diff)
- netmgrd 9c345b2e/audiod 9f47c8fd/meow_audio 6864ec38 未变; amsd 57e19cde 保持
2026-09-04 12:29:23 +08:00
AskaEth 8e68254ce4 style(sdk): SDK默认列表MD3容器化 — meow_listview_create默认container=1(行=surfaceContainer圆角卡+空隙+去分隔线+卡内tonal选中),hello/files/ncm/reader等用默认renderer的App开箱即MD3(用户2026-09-04反馈默认仍扁平);需平坦宿主可set_container(0)回退(宿主测试test_meow_ui_widgets/listvirt补set_container(0)锁扁平基线)。0warning,宿主测试全过(widgets ALL PASS/md3 PASS/swipe 35-0/listvirt 44-0)。meow_ui.a变需重编+重链四App 2026-09-04 12:24:16 +08:00
AskaEth 4d0a8450e9 fix(fw): M6.36 收编全面MD3对齐+批①叠加/列表外距/后台图标/后台圆角卡+重编meow_ui.a重链四App — 推卡
- meow_ui.a 重编 d02c0f57(0warning): MD3对齐(meow_ui_widget/widgets:容器/胶囊/shape)+批①叠加共享判定+meow_img_pixels+meow_ui_audio
- launcher 0c4f3c10: MD3全面对齐(subpage/settings/bg/MENU列表圆角卡8px外距/按钮胶囊/状态层/shape)+批①叠加/穿透根治(7处转场)+顶栏后台按钮图标+后台管理页圆角卡
- 四App热更: ncm df149a9c / hello f3ac1fc0 / files f37d66c7 / reader b6cbc050(reader四格式冒烟全过)
- 固件 r3proii_m6_36.upt 91ccf411(0.8.36, rootfs 38715392<=45088768, unpack 0 diff)
- netmgrd 9c345b2e/audiod 9f47c8fd/meow_audio 6864ec38 未变; amsd 57e19cde 已M6.35收编
2026-09-04 12:00:59 +08:00
AskaEth b712088daa style(md3): 全面MD3对齐P0-P3 — P0扁平行列表容器化(meow_listview_set_container容器模式:surfaceContainer圆角卡12dp+tonal状态层+去分隔线;sub_page/settings_page/bg_page圆角容器) P1按钮胶囊化+shape令牌(extrasmall/small/medium/large/full) P2状态层补齐 P3子控件(Switch off-track/NavBar pill64×32/Checkbox24/对话框28);宿主test_meow_md3等全过,0warning;meow_ui.a变M6.36需重编+四App重链 2026-09-04 11:49:59 +08:00
AskaEth 3fea237306 docs: 同步至 M6.35/0.8.35(App后台常驻框架+批③后台管理页+面板拆分合并页+MD3对齐在跑+third_party/md-material-web参考+redline纪律;不提网易云明面) 2026-09-04 11:40:00 +08:00
AskaEth 2db87b2c5c style(bt): 后台管理页行改MD3圆角卡+8px外距(bgm_row_card_y/h,卡高48行距56上下对称,删除分隔线,按压限卡内,按钮相对卡定位恒等)+test_bg_row_card PASS,0warning 2026-09-04 11:27:07 +08:00
AskaEth 7a276f609e fix(bt): 批①叠加/穿透根治 — /tmp/meow_notif_active从布尔标志改为内容=前台归属app_id(App用MEOW_LOG_APP比较:前台照常上屏收输入/后台常驻自抑+排空输入/空=全抑制),SDK present+input共用共享判定meow_ui_overlay_suppressed_active,launcher 7处转场改写;宿主test_overlay_suppress(T2c)/notif_gesture/overlay_gate/bg全过,0warning;meow_ui.a变M6.36需重编+重链四App 2026-09-04 04:05:41 +08:00
AskaEth 6bab304660 style(launcher): 主界面列表布局(home_layout=list)圆角卡加8px外距(MENU_LIST_ROW_GAP+menu_row_card_y/h卡上下对称内缩行距不变) — 圆角列表项紧贴修复;其余扁平行(Material标准)未动;test_menu_row_gap PASS,0warning,不动meow_ui.a 2026-09-04 03:53:09 +08:00
AskaEth 491db5069c style(launcher): 主界面顶栏'后台'按钮图标美化 — 去方形primary底改surface_container柔和圆角+矢量双卡层叠图标(后台应用堆叠语义)+小字'后台';命中矩形/入口不变,0warning 2026-09-04 03:51:30 +08:00
AskaEth b23c6d53c4 fix(fw): M6.35 收编批③后台管理页(按钮化)+amsd FG/BG+i18n — 推卡 2026-09-04 02:57:26 +08:00
AskaEth 5c174224f7 feat(bt): 批③后台管理页UI按钮化 — 每行右'杀'按钮(bgm_btn_rect精确命中,行区不误杀)+顶栏'一键清除后台'按钮(sub_page_t增row_draw/row_tap/top_action/top_action_cb全NULL守卫,wifi/bt/lock行为不变);test_bgpage 32项ALL PASS,0warning 2026-09-04 02:48:08 +08:00
AskaEth c69fb92284 refactor(i18n): settings/lock 剩余中文补抽为 meow_str_zh 键(内联键,默认=原中文显示不变) — settings_page([59]设置行/值格式/标题)+lock_settings(行/模态/动作)+lock_screen_ui(清空/退格/PIN锁定提示);顺带修2条既有-Wformat-zero-length;宿主settings/lock全过,0warning 2026-09-04 02:21:45 +08:00
AskaEth 211c5a556a feat(bt): 批③后台管理页 — bg_page(顶栏'后台'入口+sub_page_t列表[名字/CPU/内存/状态/tap=kill]+一键清除动作行+空态,2s实时)+meow_bgmgr(collect/can_kill !framework门控+kill_all不杀框架)+meow_bg_terminate复用回收序列;amsd FG/BG收编(重编57e19cde含handle_bg/fg);test_bgmgr 20项ALL PASS,0warning 2026-09-04 02:19:06 +08:00
AskaEth 2f22e26de7 fix(fw): M6.34 收编批①后台常驻+宽限期+audiod A3/DSD+meow_img_pixels+重编meow_ui.a重链四App — 推卡 2026-09-04 02:00:39 +08:00
AskaEth 3c5612f7c9 refactor(sdk/lyric)+misc: ①G5断言改成EXIT(播放页任意右滑回桌面,宿主全过) ②meow_img_pixels正式访问器上收(替代局部补全结构体,const化w/h,清3处警告) ③RECOVERY_PROMPT §五铁律补重链四App ④MEOW_APP_SDK.md §4.1取色/§5.9音频/§9后台字段/§16歌词组件;0warning;meow_ui.a变M6.34需重编+四App重链 2026-09-04 01:49:24 +08:00
AskaEth 45c633868f feat(bt): 批①宽限期补充 — 全App入后台表(bg_since_ms)+未声明App宽限30min(MEOW_BG_GRACE_MS)超时回收+声明常驻/媒体活跃不清+槽满LRU回收最旧未声明;test_bgplay全过/0warning;devdocs APP_BACKGROUND_KEEPALIVE_P1 §9;M6.34需先重编meow_ui.a(meow_img_pixels) 2026-09-04 01:48:31 +08:00
AskaEth effce6d0db feat(audiod): A3+DSD — ①24/32bit高清byte-exact(偏好S32) ②独立AUDIO_FMT_DOP DoP DSD透传+失败报错关流(拒BT) ③音量收编v2双组写PCM+DSD_VOLUME平方锥度audiod权威 ④/api/audio/monitor监控+前端音频页监控卡;宿主100/0,0warning;SDK meow_ui_audio ABI变大M6.34需重编meow_ui.a+重链四App 2026-09-04 00:56:57 +08:00
AskaEth 2dc4cad454 refactor(panel): 前后端按模块拆分+屏幕/控制合并页 — 后端meowpanel.c 6372行拆src/*.c+mp.h(235函数原型,20端点output byte-identical,0warning)+前端拆mp_shared/mp_pages/meowpanel三层;屏幕·遥控合并(左屏实时画面+右MD3 SVG矢量按钮无emoji);重编后热更TF卡 2026-09-04 00:56:13 +08:00
AskaEth f3ae06d7cd feat(bt): 批①后台常驻框架实施 — meow_media_app_resident按app会话判活跃+should_keep三参(keepalive||bg_audio&&resident)+background_keepalive声明解析透传+amsd EV BG/FG上报;宿主test_media39/test_bgplay/test_bg_keep19全过,0warning;落盘APP_BACKGROUND_KEEPALIVE_P1;降配(nice)待真机未做 2026-09-04 00:39:29 +08:00
AskaEth 7e68313981 fix(fw): M6.33 收编launcher播放页占位符移除+封面空→复原恢复 — 推卡 2026-09-04 00:19:31 +08:00
AskaEth 6eccb17f9b fix(ncm): 歌单切换两bug — ①暂停态跨歌单点歌旧信息(play同曲护栏改按曲目id判同曲而非裸队列下标,切歌单后同下标换歌被吞play→修play_is_same_song) ②切歌单封面残留(playlist_tick补cover_changed/ack+invalidate重画头块);附带占位行cover_request野id修复;宿主P15/covswitch PASS,热更ncm 2026-09-04 00:14:37 +08:00
AskaEth 62ec405dca docs: README 更新至 M6.32/0.8.32(歌词批1-5桌面歌词页双页/封面取色/流媒体客户端功能/蓝牙P0-P2) 2026-09-04 00:09:40 +08:00
AskaEth b988d64653 fix(launcher): ①移除播放页底部歌词占位符(歌词页已做,删NP_LYRIC_Y+占位块) ②封面'空→复原'不恢复修复(meta_changed补artwork/np_cover_load缓存去重改为空也重载/歌词页补载封面);宿主test_np_cover 14项PASS,0warning 2026-09-04 00:07:34 +08:00
AskaEth 18014eb2b9 fix(ncm): 补play_mode持久化定义(appstate_apply_play_mode/ncm_mode_name,同agent-392播放模式) — ncm链通必需,与M6.32上卡二进制一致 2026-09-03 23:48:08 +08:00
AskaEth 76a22a6a91 fix(fw): M6.32 重编meow_ui.a+重链四App(播放页进度条取色+ncm列表gap)+面板透传 — 推卡+热更四App
- meow_ui.a 重编(86af4d29): SDK W_PROGRESS 改读 opts.bg/fg=轨道/填充(agent-408)
- launcher 513d7603: 播放页进度条/按钮局部重绘接取色(np_grad_row/np_page_bg_row/np_theme_widgets)
  + 链新 meow_ui.a + sdk meow_lyric/lyric_view/meow_img_color; 经固件 /usr/bin 刷入
- ncm 05ff05f1 重链新 meow_ui.a(agent-409 列表项 gap 已入源), hello f35495a5 / files 5d70b7b2 重链
- meowpanel.c: audio_fill 补透传 audiod target/volctl/codec(输出目标/音量AVRCP/编解码器)
  前端 meowpanel.js 已前向兼容; 宿主 /api/audio JSON 验证含三键
- fw_content.conf launcher 0cdf940f -> 513d7603(M6.32)
- r3proii_m6_32.upt md5 8ce917ad(0.8.32, rootfs 38707200<=45088768, unpack 0 diff)
2026-09-03 23:42:46 +08:00
AskaEth e8218b8ae9 fix(ncm): SDK圆角矩形列表项加外边距 — 实为ncm_app应用层自绘(row_bg圆角卡),调用处加NCM_ROW_GAP=8px卡间可见外距(卡高48/行距56上下对称+擦SDK行分隔线);三屏playlist/home/queue;宿主test_meow_row_gap PASS,0warning,不涉meow_ui.a无需重链 2026-09-03 23:29:45 +08:00
AskaEth a82f3f9dfa fix(lyric): 全局播放页进度条/按钮带局部重绘回退默认色修复 — 取色主题抽共用(np_grad_row/np_page_bg_row/np_theme_widgets),1s局部重绘也接取色(带底色渐变/进度条fill=accent/track=fg_dim/四钮accent);SDK W_PROGRESS改读opts.bg/fg向后兼容;宿主R1-R3 PASS,0warning;需重编meow_ui.a+四App重链 2026-09-03 23:28:37 +08:00
AskaEth 8971f6067b fix(fw): M6.31 收编桌面歌词页封面修复+alsa.conf DEV自动识别(bt_dev) — 推卡 2026-09-03 22:43:05 +08:00
AskaEth 8c165bb8e6 fix(lyric/bt): ①桌面歌词页封面不显示修复(根因:歌词页从不画封面+scrim逐秒叠涂压黑→新增np_draw_lyric_coverbg封面背景+1s整页重绘;播放页封面宿实证非批5破坏) ②alsa.conf pcm.bluealsa DEV自动识别(bt_dev.c:audiod BT打开时bluealsa-cli检测a2dpsrc设备幂等改写DEV=保留codec/ldac_eqmid等);test_bt_dev 14/14,0warning 2026-09-03 22:36:17 +08:00
AskaEth ef6eca86b1 fix(fw): M6.30 收编批5取色+蓝牙P2(audiod glibc-dynamic)+面板蓝牙卡 — 推卡+panel热更 2026-09-03 21:44:23 +08:00
AskaEth 4ecddb5ea3 fix(bt): 蓝牙P2 mipsel libasound链接 — 改glibc-dynamic(设备实为glibc2.22硬浮点,pcm.bluealsa=独立glibc插件需dlopen,musl-static死路)直链设备libasound.so.2,BT层脱离dlopen(内联9个libasound原型);qemu装载动态库验证+宿主67pass/0warning;audiod改glibc-dynamic(57KB)M6.30后需重验DAC出声 2026-09-03 21:18:19 +08:00
AskaEth 71b2dafecc feat(bt): 蓝牙P2 source实装 — audiod OUT_BT输出目标(libasound直连pcm.bluealsa,BT独立目标不动DAC独占/FPGA/音量反转)+音量AVRCP隔离(不下发DAC反转)+codec生效+MSG_AUDIO_OUTPUT/CLI;宿主67pass/0warning;sink标记P2.5;遗留mipsel静态dlopen不支持libasound(硬集成项) 2026-09-03 20:52:06 +08:00
AskaEth 1f6d390e1c feat(lyric): 批5 封面双色渐变取色 — SDK meow_img_color纯整数分桶主色+次色(副采样≤3.6k零分配)+launcher播放页/歌词页set_colors注入渐变主题+无封面回退全局主题+meow_lyric_view灰阶随覆盖cur修正;宿主C1-C7全过,0warning,未并入meow_ui.a 2026-09-03 20:44:33 +08:00
AskaEth a3679e3688 fix(launcher): M6.29 播放页任意右滑返回桌面(回退出np_gesture) — 推卡 2026-09-03 20:32:23 +08:00
AskaEth 0da68e4972 fix(fw): M6.28 收编桌面NowPlaying双页歌词页+歌词数据源+蓝牙P1(netmgrd/bt_page) — 推卡 2026-09-03 20:06:36 +08:00
AskaEth eb07415f7b feat(bt): 蓝牙P1 — netmgrd BT_CTL权威收口(hcitool con判连接/paired-devices补名/a2dp详情/connect-disconnect-pair-unpair-scan)+launcher蓝牙子页sub_page_t重做(8分组,数据全走netmgrd)+设置键audio.output_bt/bt.codec;修act冲突+sub悬垂;宿主82PASS+bt_ctl/bt_page全过 2026-09-03 20:01:13 +08:00
AskaEth 8c677f127f feat(launcher): 批4 Now Playing双页(播放页⇄歌词页) — 方向感知手势(播放页右→左进歌词/左缘右滑回桌面;歌词页左→右回播放)+歌词页用批3数据+批1/2控件实时滚动+空态;宿主G5/G6全过,0warning;meow_ui.a未重编 2026-09-03 20:00:38 +08:00
AskaEth 2e5b814059 feat(media): 批3歌词数据源框架 — meow_media_session扩实时歌词(lrc/tlrc/seq)+MSG_META_LYRIC上报分片+ncm上报+launcher读meow_media_lyric_* 接批1/2;宿主L1-L7全过/ncm热更27554dd8,launcher待固件收编 2026-09-03 19:49:28 +08:00
AskaEth 7ca3e5bd84 feat(ncm): 播放模式三态(随机/列表循环/单曲循环,播放页三键左)+当前实时播放列表(三键右,歌单滑窗/meow_listview虚拟化分页至5194首,当前曲高亮,点播切歌) — 模式持久化,宿主modetest/queuetest全过 2026-09-03 18:51:28 +08:00
AskaEth 29e13b2b5e fix(netmgrd): M6.27 流量统计收编(pub_bytes/去lo) — 推卡+panel热更 2026-09-03 18:45:12 +08:00
AskaEth 671f0d1835 feat(ncm): 底部mini播放栏(替代顶栏图标) — 异Activity常驻,非播放页+有活动播放时显示:封面缩略+歌名/歌手+播放暂停+进度带+点击进播放页,宿主barhit/plsplay/home/player PASS 2026-09-03 18:07:57 +08:00
AskaEth 26a140e00f feat(netmgrd): 播放器流量统计细化 — A.实时速率去lo(uplink_totals) C1.netlink sock_diag TCP流公网/内网字节(pub_bytes)=status+panel展示+诚实标注;UDP暂不计(tcp_only),netmgrd宿主70/70pass 2026-09-03 18:00:39 +08:00
AskaEth dce12c63a2 feat(sdk): 批2 meow_lyric_view 歌词页渲染控件 — 上收ncm成熟布局(活动行纯白/灰阶/偏上锚定/翻译行不叠行/折行/点行seek),布局命中绘制同源,宿主L1-L7全过,预留批5取色set_colors,暂不并入meow_ui.a 2026-09-03 17:56:27 +08:00
AskaEth 4827cc8d92 fix(launcher): M6.26 播放页三键命中(push哨兵)+媒体卡片体FOCUS(点击开播放页) — 推卡 2026-09-03 14:54:41 +08:00
AskaEth 76eb2f4d1c fix(bootmenu): M6.25 开机菜单/第二屏亮度按用户设置恢复(方案A:显示前读settings.conf写回) — 推卡 2026-09-03 14:40:56 +08:00
AskaEth 42718385cc feat(sdk): 批1 meow_lyric 通用歌词组件 — lrc/tlrc解析+存取+查询+折行辅助,纯libc+pthread零依赖,宿主54断言全过,暂不并入meow_ui.a 2026-09-03 14:40:44 +08:00
AskaEth b5da7d7151 fix(launcher): M6.24 App内按电源键锁屏时序(松开边沿误判唤醒) — 推卡验收 2026-09-03 14:27:00 +08:00
AskaEth b89b91c9c4 fix(ncm): 歌词页两bug — ①app.json单行被fgets(256)截断致keep_awake失效(改多行版部署) ②翻译行块高不含翻译致叠行(lyr_layout s_lyr_bh) 2026-09-03 14:20:14 +08:00
AskaEth ec8e43e1fd fix(watchdog): M6.23 亮屏亮度按用户设置恢复(修 panel ON 固定60跳变) — 推卡 2026-09-03 14:12:21 +08:00
AskaEth e448b9bfda fix(launcher): M6.22 播放页返回根因修复 — np_exit 退出外层主循环(break错层级) — 推卡 2026-09-03 14:00:28 +08:00
AskaEth e802cc678a fix(launcher): M6.21 播放页返回诊断日志+阈值放宽(M6.20基础上) — 推卡 2026-09-03 13:50:40 +08:00
AskaEth cd9d9da3bd fix(launcher): M6.20 播放页右滑返回+顶部下滑通知中心+时钟1s刷新 — 推卡验收 2026-09-03 13:42:32 +08:00
AskaEth 3d70a3dd2c feat: M6.10–M6.19 批量收编 + M6.19 推卡验收
- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘)
- OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复
- ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640
- 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫)
- 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
2026-09-03 12:16:01 +08:00
AskaEth c2ee7626a4 fw_content: M6.9 收编 launcher 94a33b7b(327 M7.0b 试点) 2026-09-02 18:38:25 +08:00
AskaEth d8241cd2d6 launcher(M7.0b): 电源菜单/Now Playing 控件化 + SDK 主题薄壳 + i18n 抽取(试点迁移)
M7 整体 MD3 重写试点(M7.0 定两隔离页,只动 patch_v2/meow_launcherd/):
- 电源菜单 power_menu.c:圆角卡→meow_card;6 圆钮→meow_iconbtn(回HiBy=ARROW_BACK、
  取消=CLOSE;sb_icons 表在位图)+ meow_widget_custom_create(关机/重启/升级/锁定=pm_icons
  专有,圆形命中自定义);命中/选中/按压态全走 SDK(meow_widget_hit/set_state/draw),
  触摸按下置 PRESSED、抬起清(pm_blend 按主题混合状态层);py 手动驱动控件(哨兵 activity
  0x50 push/pop,owner 闸门使命中生效)。
- Now Playing now_playing.c:进度条→meow_progress;播放控制三钮→meow_iconbtn(SKIP_PREV /
  PLAY_ARROW|PAUSE同位双钮按状态只画其一/ SKIP_NEXT);命中走 meow_widget_hit;封面
  (meow_img contain) 与歌名/艺术家 (meow_font) 保留;哨兵 activity 0x51 push/pop。
- 主题薄壳 font_theme.c:s_ui_theme() 改读 SDK meow_theme_export()(单一权威,9 字段映射
  §8.1.5),退役 launcher 自持 4 套静态色板(#if 0 回退位)。
- i18n 抽取:两页硬编码中文案改 meow_str(新增 meow_str_zh 辅助:SDK 内嵌表无 launcher 域串
  时回退中文默认,TF 覆盖表/后续并入 SDK 内嵌后自动换语)。
- 会话骨架不动:物理键语义(单击息屏/3s 菜单/8s 强关/左缘返回/横划 NP)与两页进入退出/事件流
  未改,只替换「怎么画/怎么命中」;旧绘制函数 #if 0 留回退位。
- 验证:新增 test_power_menu_widgets / test_now_playing_widgets 宿主测试(圆形命中/owner+enabled
  闸门/选中按压态像素/主题四态 MD3+MIUIx×明暗 渲染 + meow_theme_export 九字段)全 PASS;
  launcher 全量宿主回归(lock_screen/overlay_gate/utf8_name/media/depends/ams_start/bgplay/
  settings_brightness/netmgr_ipc/vol_ramp_jack/notif_panel/notif_tf/lock_render + SDK theme_export/
  iconbtn)全 PASS;交叉 -Wall -Wextra 0 warning。新 meow_launcherd md5 94a33b7b。
2026-09-02 17:25:21 +08:00
AskaEth a8c51be1fc sdk(M7.0a): 视图事件接线 + IconButton + 主题色板导出/注入(launcher 重写基座前置)
M7 launcher 整体 MD3 重写的 SDK 基座补全(只动 patch_v2/sdk/):
- 缺口#7 meow_view 事件接线:meow_view_dispatch_event 接入 meow_ui_run 主循环
  (§6.4 分流之上,TAP/KEY/SWIPE/LONG_PRESS 先经 view 路由、命中即消费、未命中回落
  on_* —— 无 view 树/未命中完全向后兼容);meow_view_add_child 在父已进入时挂子视图
  立即进入(递归 children 修正);meow_view_sub_push/pop 标注废弃(设计定案 b 不做页内
  子导航栈)。
- 缺口#2 IconButton:meow_iconbtn_create —— sb_icons 位图 + 圆形命中 + 按压/按住态
  (实时,同 meow_btn)+ owner/enabled 闸门;电源菜单/播放控制/锁屏通用件。
- 缺口#6 SDK 半主题收口:meow_theme_export/meow_theme_inject(§8.1.5 色板导出/注入),
  launcher s_ui_theme() 将据此变读 SDK 的薄壳(launcher 侧改动不在此批)。
- 文档:MEOW_APP_SDK.md 补 meow_iconbtn_create + meow_theme_export/inject 说明。
- 验证:新增 test_meow_ui_iconbtn / test_meow_ui_theme_export / test_meow_ui_view_event
  全 PASS;全量 SDK 宿主回归(widgets/dispatch/box/anim/scroll/listvirt/str/ime/pinyin/
  overlay/notif/input_keys/p1p2p3/umbrella/ft)全 PASS;交叉 -Wall -Wextra 0 warning。

meow_ui.a md5: be24214e24faadba63577f3a98c2c9d1 -> 7fc83f7731cadb2f91df1c3ed7d3814f
2026-09-02 16:50:40 +08:00
AskaEth e67cc50afa fw_content: M6.9 候选 netmgrd 20243ea1(期4)+powermgr 1ef2f8af(期3) 2026-09-02 16:29:42 +08:00
AskaEth 5663acfbb9 feat(powermgr): 期3 policy 生效 + 标定工具 + 模型单键 setter + events 说明
- policy:IPC 0xD2/0xD3 GET/SET(SET 动作级鉴权=root+系统身份,SO_PEERCRED→pid→exe→classify,同 svc_power)
  + CLI `meow_power policy show|set`。策略项 profile 四档(performance/balanced/powersave/ultra)
  + 低电阈值(power.low_pct/critical_pct 读 settings.conf) + 亮度上限(power.brightness_cap)
  + 息屏上限(power.screen_timeout_cap)。**默认全关**(profile=balanced 无操作=现状, cap=0 不生效,
  只能用户手动开)。生效方式: powermgr 不直接写系统——只写 settings.conf power.* 键(原子),
  launcher/sysui 期4 消费; 本服务绝不 poke 背光/息屏/wlan0/bt。低电/极低阈值改从 settings.conf
  读, 事件检测即时生效。
- calibrate:`meow_power calibrate start <场景>|stop|list`——守护内 2s 节拍采样 电流(mp2731
  current_now)+亮度+CPU+wifi/bt 状态+电量%变化,写 TF MeowOS/logs/power_calib_<epoch>.csv
  (无卡降级 /tmp,逐行 fflush),用户实验中 start/stop 两命令即可。
- model:`meow_power model set <key> <mw>` 读改写单键+原子写(tmp+rename),守护每拍重载 conf。
- events:App 前后台切换——launcher MSG_ACT_FOCUS 为单向 app→launcher 上报,powermgr 侧无导出点
  可接,诚实标注「期4 接 launcher」扩展,本期不编造该事件(延续期2 口径)。
- 验证:宿主+qemu 全量 76/76 PASS(期1/2 全过+新增 policy/calibrate/model-set 用例),mipsel
  静态交叉 0 warning,二进制 151516B(<200KB 阈值)。md5 1ef2f8afc07acc7aa09dee9f7149aba8。
2026-09-02 16:25:51 +08:00
MeowOS 8a43f17653 meow_netmgrd 期4:SDK 自愿通道 + wifi forget + RSSI signal_poll + AAAA 过滤 + iptables 评估
NETMGR_DESIGN.md §7/§13 期4 落地(只动 patch_v2/meow_netmgrd/ + patch_v2/sdk/ 追加式):
1. SDK 自愿通道(§6.3/§4):meow_ui.h 新增 meow_net_policy_check(host)(0=放行 1=block;
   netmgrd 不在/超时→0 乐观放行)+ meow_net_report(rx,tx)(自愿上报,§4 字节级记账自报通道)。
   meow_ipc.h 追加 0xEE MSG_NET_SDK(op=CHECK/REPORT)/ 0xEF RESULT。netmgrd 侧:CHECK 以
   app_id→实体 app:<pkg> 评估 policy_net_blocked||dns_query_domain_denied;REPORT 记入 per-实体
   g_ent_bytes(status JSON 新增 entity_bytes)。SDK 实现 meow_net_ipc.c(一次 connect+send+read,
   降级不崩);meow_ui.a 重编。
2. wifi forget <id>:wpa_cli remove_network+save_config(MeowOS;HibyOS 只读门控),CLI+IPC 接通
   (286 launcher 录入 UI 缺口)。
3. RSSI:wifi_status 在 status 无 signal= 时回退 wpa_cli signal_poll(真机 rssi=-38,修面板 rssi=null 缺口)。
4. DNS AAAA 过滤:global dns.filter_aaaa=0|1(默认 0 透传;纯 IPv4 设备可置 1 让 AAAA 查询回空答案回落 A)。
5. 静态 iptables 评估(devdocs/plan/NETMGR_IPTABLES_EVAL.md,本机不入库):iptables-legacy 1.8.9 交叉
   编译成功(~578KB,含 owner/cgroup/REDIRECT 等全部 extension),但内核实测 matches 仅
   quota/icmp/udp/tcp、targets 仅 NFQUEUE/NFLOG/ERROR、无 nat 表、全部进程 UID 0 → **per-app 管控
   与 REDIRECT 透明代理判不可行**(与 NETWORK_HW_FACTS 一致);正路为 per-app UID 框架改造(单独立项)。

验证:宿主 test_netmgrd.sh 70/70 + qemu 70/70;test_netmgrd_p2/p3/p5 宿主全过 + 交叉 0 warning;
netmgrd_authtest 全过;sdk meow_net_ipc 交叉 0 warning。真机(固件 netmgrd 共存,独立 socket+DNS off):
wifi status rssi=-38(signal_poll)、wifi forget 0(DISABLED 安全移除,id1 CURRENT 保留、wifi 未断)、
SDK policy check(CHECK_GOOD=0/CHECK_BLOCK=1/REPORT ok)、policy show 正常;resolv.conf 未触碰、
固件 meow_netmgrd(pid) running 未动;测试产物全清。

新 md5:meow_netmgrd 20243ea1a800b29badefebb395603aaf;meow_ui.a be24214e24faadba63577f3a98c2c9d1
(主会话更 fw_content.conf / 热更设备 / 记录新 meow_ui.a)。
2026-09-02 16:19:40 +08:00
AskaEth 4e0e7441fd fw_content: M6.8 收编 launcher 42b6633a(310四轮 锁屏自睡) 2026-09-02 15:31:07 +08:00
AskaEth cd7329d141 feat(ncm): MUSIC_U 临期主动续期 — 启动查龄超阈值后台静默刷新/eapi 会话锚点/cookie 合并+冷却
用户定案:失效前用当前 cookie 换新登录态(用户无感);仅刷新失败才退扫码重登。

- core/ncm_req.c|h:eapi Cookie 头补齐会话锚点 MUSIC_U/MUSIC_A(对齐 request.rs,
  补 §36.5 遗留——登录刷新/song_url 会员档依赖);新增 ncm_req_cookie_merge
  (patch 覆盖 base 同名 cookie,MUSIC_A/__remember_me 等不丢)。
- core/ncm_login.c|h:ncm_login_refresh(eapi /api/login/token/refresh 空体,
  code200+set-cookie→merge;code!=200→NCM_ERR_AUTH 退扫码;网络/TLS→负码退避)。
- app/logic/account.c:account.json 增 last_refresh_ts + 拆分 cvip/svip;
  account_cookie_maybe_refresh(年龄阈值 12h 可配 ncm.cookie_refresh_h,单飞/boot,
  失败冷却 30min 跨 boot 持久化,AUTH 置回登录页标志)+ mark_fresh/need_relogin。
- app/ui/home.c:home_worker 先续期再拉歌单;app/ui/login.c:803 落盘后 mark_fresh;
  app/app.c:UI tick 消费回登录标志 → logout+切登录页。
- 新增测试:test_ncm_core 的 cookie_merge + eapi_musicu_hdr;bench/cookie_refresh_sim
  (端点语义,0 网络);bench/account_cookie_sim(临期/未临期/退避/失效/冷却,0 网络)。

验证:host+qemu 核心测试 PASS;build_ncm_sims.sh 全家桶 PASS;build_ncm.sh 0 warning。
真机只读探针 2 次(设备 10.78.193.50,不写回):refresh 走 eapi 域
interface.music.163.com 在当前网络被 TLS reset(alert 80)→ rc=-5 瞬断退避,
session.enc/deviceId md5 前后一致(登录态零损伤)。新 ncm md5 c960244b。
2026-09-02 14:30:04 +08:00
AskaEth de18cef02c feat(ncm): 缓存分目录(songs//covers/)+ 陈旧 mp3 缓存修复
任务1 缓存分目录(用户定案):\n- ncm_cache_init mkdir songs/ 与 covers/ 子目录 + 一次性迁移\n  (根混放 song_*/cov*/cover_current.jpg 挪进两子目录,幂等,legacy_migrated 计数)\n- ncm_cache_path 按 id 前缀路由(cov*→covers/,其余→songs/)\n- LRU(evict/total_bytes/7天 .part 回收)只扫 songs/;clear 清 songs/+covers/ 的 song_*\n- cover.c save_cover_raw 落 covers/cover_current.jpg\n\n任务2 陈旧 mp3 缓存修复(真机实锤:hires 档键下存 mp3,播放命中旧档):\n- song_local_file 缓存命中且用户档>EXHIGH(无损+)且实体是 .mp3 = 静默降级产物\n  → 迁移重命名到 exhigh 键 song_<id>_exhigh.mp3(选极高仍可用;目标已存在删旧)\n  → 本档视为未命中走下载(311 flac 门禁拿真无损/正确降级)\n\n验证:cache_evict_sim case10(目录迁移一次成型)+P1-9;player_flow_sim P13(迁移+miss)\n+P1-12 零回归;stream/window/search/degrade/cover_chain/test_ncm_core(host+qemu) 全 PASS;\nbuild_ncm.sh -Wall -Wextra 0 warning。0 真实请求。新 ncm md5 a90ba8c3 (text1028458/bss1364484)。
2026-09-02 13:51:36 +08:00
AskaEth 986e3814bc fix(launcher,lock): §310 M6.7 锁屏页自睡——空闲超时自动灭屏(294 遗留补)
背景:锁屏页(run_lock_screen)此前常亮不灭屏——用户停在锁屏页不操作,屏幕一直亮着耗电。
M6.6 上机后用户反馈,补 294 遗留项「锁屏页未做空闲超时自动息屏」。

实现(/usr/bin 只读,待 M6.7 固件打包,fw_content.conf 未动):
- run_lock_screen 循环加空闲跟踪(last_act)+ 自睡状态(asleep):
  ① 阈值 = overlay_lock_idle_off_sec(screen_off_timeout) = min(系统 screen_off_timeout, 30s)。
     系统未设/设 0(常亮)/超 30 → 锁屏页封顶 30s(锁屏页不应常亮耗电);系统设更短 → 尊重更短。
  ② 活跃态空闲超阈值 → meow_panel_off()(与息屏路径一致:panel 模式 FBIOBLANK 4 挂起 goodix /
     black 模式仅背光 0)+ asleep=1 —— 锁态保持,仍留在锁屏循环(=「锁屏页自己也会睡」)。
  ③ asleep 后电源键按下边沿 或 触摸活动 → meow_panel_on() + lk_draw 重画锁屏页(本循环唤醒链
     回锁),输入活动(任意按键 change / 触摸)都重置 last_act。
  ④ 音量键本页不动,仅计入输入活动重置计时;自睡期面板已关,不重绘(唤醒时统一重画)。
- EVIOCGRAB 与灭屏交互(核对结论):灭屏期 grab **保持**(tfd 不关)。panel 模式 FBIOBLANK 4
  挂起 goodix → 灭屏期无触摸事件,唤醒只走电源键;black 模式触摸仍活动且被抓取者(launcher)独占
  → 唤醒第一下触摸进锁屏(不穿透 App、不被吞——被锁屏作为活动消费,可能命中 PIN 键)。唤醒后
  FBIOBLANK 0 + grab 仍在,全程无穿透。
- App 前台叠加锁屏(run_app_lock_overlay)期间 App 的 /tmp/meow_notif_active 标记在整个自睡/
  唤醒周期保持(仅锁屏退出才 unlink),故媒体保活 App 自睡期仍被 present 抑制,不覆盖锁屏。

overlay_gate.h 增 overlay_lock_idle_off_sec / overlay_lock_should_sleep / overlay_lock_wake_now
(纯决策,可单测)。test_overlay_gate.c 扩展 S4(12 断言:阈值 tradeoff / 超时灭屏 / 唤醒回锁屏 /
屏幕关不重睡)。launcher 全量宿主回归 + SDK 叠加/状态栏/输入回归全 PASS;mipsel 交叉 -Wall -Wextra
0 warning。md5 42b6633afde37035585fc36b074a73c6(待 M6.7 固件打包,不部署)。
2026-09-02 13:35:56 +08:00
AskaEth cf8b0da3bd fix(fw_content): 拆分 launcher+watchdog 与 audiod+audio 两个合并行 — 修 watchdog/audio CLI 被吞进 desc 不打包
build_fw_apps.sh 以 `IFS='|' read -r src dst md5 desc` 解析,launcher 与 audiod 的 desc 后误并入了
meow_watchdog / meow_audio(CLI) 条目,导致这两个二进制在固件打包时被当作 desc 文本【不】作为独立
条目安装(M6.0 时本为独立行)。拆回各一行,md5 均已对源文件校验证实一致。
2026-09-02 13:05:29 +08:00
AskaEth a70bcaac95 docs: 仓库文档同步 M6.6(0.8.6)——README 版本行 + M6.5/M6.6 演进;SPEC_REF 补 SDK §10.1 矢量字 meow_ft/audiod 315 孤儿回收/锁屏 310 三期;FIRMWARE_BINARIES 按 fw_content.conf 更新到 M6.6
- README:版本行 M6.4→M6.6(新增 M6.5 audiod 孤儿回收 + M6.6 §310 三期锁屏);新增「M6.5-6.6 新增」演进条目(SDK 矢量字/audiod 孤儿回收/流媒体客户端 314 SIGSEGV 根因+317 封面链分诊+兜底链+318 track/all 否决+319 接口地图+320 风控加固);应用生态更新到 2026-09-02 批次。
- SPEC_REF §3.5:补 §10.1 矢量字 `meow_ft_*`(316,上收阅读器 FreeType 管线)+签名蓝本扩到 meow_ui_ft.h;audiod 段补 315 孤儿会话两段式回收;锁屏 PIN 段补 310 二期 EVIOCGRAB + 三期(M6.6)。
- FIRMWARE_BINARIES:按 conf 更新 meow_launcherd(M6.6)/meow_audiod(M6.5)/meow_amsd(M6.2)/S89(M6.2) + 当前基线标注 M6.6。
2026-09-02 13:04:58 +08:00
AskaEth ea645ce2a6 feat(ncm): 风控加固——接口响应短 TTL 缓存 + 游客 MUSIC_A + track/all 死代码
三(按 NCMAPI_NOTES §2.1/§4/§6):
① 接口级响应缓存(对齐参考 apicache 2 分钟,防风控高频 ip 错误):
   - 新增 core/ncm_apicache.{c,h}——内存 4 槽 LRU(单槽≤128KB,TTL 120s,
     键=md5(path+\x01+json))+ 可选盘缓存 <data_dir>/ncm_api_cache/.json
     (8B 墙钟头+body,超 TTL miss 并删,30s 节流清过期)。自包含模块
     (仅 libc+pthread+clock_gettime),可独立链接无符号冲突。
   - ncm_api.c:api_post 拆「请求+解析」两段,新增 api_post_cached;四个只读
     接口接线:user_playlist/playlist_detail/song_detail/cloudsearch。
     song_url/lyric/vip 不入缓存。
   - 手动刷新绕过:ncm_api_cache_refresh_path(path) 一次性标记;home/歌单
     topbar ↻ 接入;playlist_open/search 不标记(走缓存)。
   - 盘层挂载 appstate_init;登出 appstate_logout 全清(防串账号)。
② 游客 MUSIC_A:ncm_req.c weapi+eapi 无 MUSIC_U 且非 login 路径补
   MUSIC_A=anonymous_token(对齐参考 cookie 伪装)。
③ track/all 死代码清理:移除 ncm_api_playlist_track_all(真实服务器 8 形态全
   404,无调用点);ncm_api_parse_track_all 保留(离线重放/单测),注释标注
   「已验证不存在,勿用于真实请求」。

验证:新 bench/apicache_sim.c(0 网络)①TTL 内不重复请求/②过期重拉/③手动
刷新绕过/④cloudsearch 按键区分/⑤清空重拉/⑥盘缓存写入+clear 全 PASS;stream/
player_flow/window/search/cache_evict(1-9)/pagination/playlist_race sim 全
PASS;test_ncm_core host+mipsel(qemu) PASS;build_ncm.sh -Wall -Wextra 0
warning(text 1026914/data 1664/bss 1363460);0 高频真实请求。新 ncm md5
a69c93538920eba9d42a2ebb42f8fdca(留主会话上卡)。
2026-09-02 12:48:34 +08:00
AskaEth 9d5301f931 fw_content: M6.6 收编 launcher 62c817c2(310三期) 2026-09-02 11:52:06 +08:00
AskaEth a3d6e21db5 fix(launcher,lock): §310 M6.5 追加两修——App 内手动息屏不锁屏 + 锁屏脏区被 App 覆盖
真机 M6.4 反馈(c730333 EVIOCGRAB 之后仍未好全):

P1 App 内电源键手动息屏→亮屏无锁屏(桌面/超时路径都有锁、唯独此路径丢)
  根因:App 会话内电源键单击息屏由 **watchdog** 处理(meow_watchdog.c handle_power →
  wd_panel_off,直接 blank 面板背光 0),**不置** meow_auto_slept;而 launcher app 会话循环的
  唤醒锁屏只在 `if (meow_auto_slept)`(超时路径 set)内触发(app_launcher.c:1996/2029)。
  手动路径唤醒时 meow_auto_slept==0 → 不锁屏(真机实测)。
  修法(app_launcher.c):App 循环每圈以读背光 `==0` 补判 watchdog 手动息屏(app_panel_manual_off),
  唤醒分支改 `meow_auto_slept || app_panel_manual_off`,两类唤醒都走 lck_wake_should_lock 锁屏,
  两条路径对齐。overlay_gate.h 增 overlay_wake_should_lock 判据。

P2 锁屏脏区被 App 覆盖(App 刷新控件盖到锁屏上 / 未刷新区显示锁屏自带脏区)
  根因:媒体保活 App 不冻结仍在绘制;SDK present 抑制(/tmp/meow_notif_active)有 ≤200ms
  标记节流窗(meow_ui_gfx.c MEOW_OVERLAY_CHK_MS)——锁屏首扑后 App 仍可能进 1-2 帧,其
  部分脏区落在锁屏上(App 控件覆盖),未覆盖区才是锁屏。另外锁屏关闭需整帧重绘消残影。
  修法:
   ① lk_draw 首帧已 fill_rect(0,0,xres,yres,th->surface) 全屏不透明底(复核满足);
   ② run_lock_screen 进场 300ms(OVERLAY_LOCK_COVER_MS > 200ms 标记窗)后**重铺一次全屏
      不透明底**盖掉 App 抑制窗内的 1-2 帧残影;App 此后被抑制不再上屏,锁屏稳定遮盖;
   ③ 锁屏关闭(run_app_lock_overlay 关闭路径)已 pm_capture_app_frame+meow_ui_flush+
      app_overlay_statusbar 整帧重绘含状态栏(复核满足)。

新增 test_lock_render.c(6 断言:首帧整帧不透明覆盖断言 + 部分填充漏区对照)+ test_overlay_gate.c
扩展(S1c 手动息屏触发锁屏 4 断言 + S2b 重铺阈值>200ms 2 断言)。launcher 全量宿主回归 +
SDK 叠加/状态栏/输入回归全 PASS;mipsel 交叉 -Wall -Wextra 0 warning。
md5 62c817c204b9d9751a4d8f411d1eb69b(/usr/bin 只读,待 M6.5 固件打包;fw_content.conf 未动)。
2026-09-02 11:49:37 +08:00
AskaEth 986944ff24 fix(ncm): 歌词页封面拉伸铺满→等比居中放大(contain,四周主题底;方图不再变形) 2026-09-02 11:47:49 +08:00
AskaEth 2bfe1ed796 feat(ncm): 封面死链兜底链 —— 原 picUrl 失败→web 端 song/detail 重取活链→歌单封面兜底
用户调研: 官方旧版 GET music.163.com/api/song/detail/?id=<id>&ids=[<id>] 免登录出
songs[0].album.picUrl/blurPicUrl。本批宿主实锤: 登录态 eapi 给的死链封面, 该 web 接口给不同活链
(死链 yR2VGt7.../10995116862531 -> album.picUrl p2.../6y-UleORIT.../...jpg 活 PNG 146KB)。

实现 (patch_v2/ncm_app/):
- cover_request(url, song_id): 新增第二参曲目 id; g_tgt_song 记录; 新目标重置 g_fb_done。
  player.c 三处传 s.id(专辑封面), playlist.c 传 NULL(歌单封面无兜底)。
- cover_song_detail_pic(song_id): web song/detail 重取 album.picUrl(缺 blurPicUrl);
  ids=[<id>] 的 [ ] URL 编码 %5B/%5D(裸[ ] curl 报 bad range); 带 Referer+桌面UA; ncm_http_request
  (头进 resp->headers, 体进 16KB 栈缓冲) + ncm_json 解析; 只读一次。
- cover_try_fallback(url, song_id): 仅原链确定性失败时触发; g_tgt_url 仍==url(未被换走) + 曲目非空 +
  未兜底 + 同曲 5 分钟冷却(ncm_monotonic_ms) 才下探; 拿到不同新图->记冷却/置 g_fb_done/换目标返 1
  (worker 重试新图); 无新图/冷却中/已兜底/目标被换走->返 0 走歌单封面兜底; 每步日志写清级别。
- worker: kind=1(webp)/kind=2(非图)/确定性4xx 死链分支清目标前先
  if(song[0]&&!fb_done&&cover_try_fallback(url,song)) continue; 瞬时错误仍指数退避不触发。
- 风控: 只在原链失败触发(不新增常规请求); 单曲目最多一次(g_fb_done)+5分钟冷却。

验证 (宿主+qemu, 0 新增真实 CDN 业务请求):
- cover_chain_sim T1-T12 ALL PASS(新增 T11 原链死->song/detail活链->出图+恰1次+song-detail fallback日志;
  T12 兜底也死->留歌单封面+同曲再请求 song/detail 0次(冷却)+cooldown日志)。
- cache_evict/player_flow/window/search/stream sim 全 PASS; core host+mipsel(qemu) PASS;
  build_ncm.sh 0 warning(text 1022210/bss 1362564,+64B); ncm_host UI dump exit=0。
  新 ncm md5 e1f4b7ade834b18c7910b9c3e64bc858 (未部署, 留主会话上卡)。
2026-09-02 11:41:54 +08:00
AskaEth 9d93f1b1ef fix(ncm): 封面矩阵实锤修正 —— 200解码失败=死对象返JSON错误体(非webp)/UA改桌面浏览器(CNMPlayer UA被拦403)/非图k不定性不重试
真机(设备恢复)CDN 行为矩阵结论并落地:
- 裸请求(无Referer)→403: 防盗链坐实, 封面请求头加 Referer https://music.163.com/。
- +Referer 仍不返真图; 全部变体只见 403/404/200+JSON错误体(43B {"code, application/json)。
  ③「200解码失败」真因=源站死对象返 200+JSON错误体, 非webp; stbi解不了→旧逻辑盲指数退避8次。
  → download_cover 回填 *out_kind(0可解码/1 webp/2非图), worker 对 kind=1/2 与确定性 4xx 一律
  立即清目标走兜底(保留歌单封面)不重试; kind=2 打 'not an image (origin error body/200)'。
- UA 分表: CNMPlayer口径 UA(Mozilla/5.0 CNMPlayer/0.1)同Referer也返403(CDN拦bot串);
  桌面 Chrome UA(旧cover.c)返200。→ NCM_COVER_UA 改回桌面浏览器 UA。
- ?param=300y300 在死对象上→404(对死链无效); 存活URL未获样本, 保留 USE_PARAM=1(标准缩略)。
- cover_chain_sim 增 T10(200+JSON→非图不重试) + stub 内容加 JPEG 魔数(过 body_is_decodable_img);
  T1-T10 ALL PASS。mipsel 0 warning(text 1020306/bss 1362500 持平)。

新 ncm md5 06896b54f31ddf55980cbf921a00d67a (未部署, 留主会话上卡)
2026-09-02 10:50:07 +08:00
AskaEth aeb9176101 fix(ncm): 封面链分诊与修复 —— CNMPlayer 头(Referer+UA 解403)/webp 魔数内容型先判不解码走兜底/URL 追加 param=300y300 缩略图/403 与 webp 单独分诊日志不盲重试
- cover.c: 请求头对齐 CNMPlayer(Referer https://music.163.com/ + UA Mozilla/5.0 CNMPlayer/0.1);
  下载后先判 content-type 含 webp 或临时文件魔数 RIFF....WEBP(stb_image 不支持),回 webp=1 不解码,
  worker 打 'webp unsupported' 立即清目标走兜底(保留歌单封面),不再当 fetch failed 盲目指数重试;
  403 单独日志(anti-hotlink/forbidden, no retry),确定性 4xx 不重试(沿用 311)。
- cover_request 入口 cover_norm_url 追加 ?param=300y300(已有 param= 尊重原样, 溢出保原 URL),
  缓存键/去重/下载 URL 同参版(省内存+出 jpeg)。
- ncm_app_internal.h: NCM_COVER_USE_PARAM/NCM_COVER_PARAM 共享宏(cover.c 与 sim 同源)。
- bench/cover_chain_sim.c: 补 ncm_http_download/meow_img_load_file/meow_img_downscale 桩对齐现行
  cover.c(旧 ncm_http_request 桩已过时), url_hash 同参数规则; 新增 T8 webp/T9 403 用例。
- 验证: cover_chain_sim T1-T9 ALL PASS + stream/player_flow/window/search/cache_evict/degrade/mem
  sim 全 PASS + core host+mipsel(qemu) PASS + build_ncm.sh 0 warning(text 1019834/bss 1362500 持平)
  + ncm_host UI dump exit=0。0 新增真实 CDN 请求; 设备端 CDN 矩阵因设备不可达未执行(见 REPORT 34.6)。

新 ncm md5 a7c22bd4ee3bcd02fb24046d27a33242 (未部署, 留主会话上卡)
2026-09-02 10:37:24 +08:00
AskaEth c7ef1ebb04 fw_content: M6.5 收编 audiod b122c9b8(315 孤儿会话回收) 2026-09-02 10:10:36 +08:00
AskaEth bcc016e50e fix(ncm): 播放页歌词解析栈溢出(SIGSEGV)修复 —— lyric_parse 栈上 227KB 临时数组超 128KB 线程栈
根因(真机 sig=11,RA 落 lyric_parse / pc 在 lib):
app/logic/lyric.c lyric_parse() 在栈上定义
  long ttmp[NCM_LYRIC_LINES=600]; char txt[600][192]; char tl[600][192]
  = ~227KB 栈帧,远超 ncm_thread_create 统一设的 NCM_THREAD_STACK=128KB。
播放页 enter → player_manager_load_lyric → lyric_worker(128KB 线程栈) → lyric_parse → 栈溢出 SIGSEGV。
崩溃仅在「播放页前台 + 拉取歌词成功 + 解析」时触发,故与播放进度无关、autoplay 仅后台起播不崩。

修法:排序临时数组移到堆(按 s_n 计,≤600 行,峰值 ~230KB 瞬态),栈帧回到 KB 级。

复现/定位(/dev 调试 + 真机):
- 真机通过 amd 恢复路径:/tmp/ncm_autoplay 起播缓存曲,app_on_tick 于 UI 线程调 player_play_song
  打开播放页(复现用户点曲路径;autoplay_ui_consume 承接)。原崩溃:pid sig=11 dur=3683ms;
  修复后:连续两次 play:start 无崩溃、60s+ 存活、无 crash_report。
- app.c 增 async-signal-safe SIGSEGV/SIGBUS/SIGILL 处理器(MIPS mcontext_t.pc + gregs ra/sp/fp
  → stderr + crash_report.txt),供 addr2line/链接布局定位。

验证:stream/player_flow/window/search/cache_evict sim 全 PASS;test_ncm_core host + qemu-mipsel
PASS;build_ncm.sh -Wall -Wextra 0 warning;ncm_host NCM_PAGE=player/home 离屏渲染 exit=0。
2026-09-02 09:53:34 +08:00
AskaEth 54750a63c5 feat(sdk): SDK 矢量字(meow_ft_*,§10.1)——上收阅读器 FreeType 管线
问题:SDK 位图字库(meow_font)只有离散字号档位,全字形 atlas 稳态 17.3MiB
(27MB 整机装不下)→ 大字库/大字号必出豆腐。阅读器 R1/R2 的 FreeType 管线已真机验证
(MiSans OTF),本 commit 将其上收 SDK,让任意 App 用上无级字号 + CJK 全字形。

设计(追加式,与位图字库并存):
- meow_ui_ft.c(独立 .o):meow_ft_load/unload/loaded/text/width/line_height/
  ascender/descender/cache_set_slots/cache_slots/cache_used。底层按 READER_ENGINE_NOTES §5:
  FT_New_Face(path) 按需读文件(零整载 anon);灰阶 FT_LOAD_RENDER|NO_HINTING|TARGET_LIGHT;
  glyph LRU key=(px<<32)|cp,默认 512(~0.46MB),可配 256/1024;测宽不栅格化。
- meow_ui_gfx.c 只加两个薄封装 meow_gfx_ft_blend_px/dirty(写 back_buf + 标脏),
  使 meow_ui_ft.o 不引入新依赖——「不调 meow_ft_* 的 App 零增量」(freetype 不拉入,已验证)。
- 降级:加载失败/无卡 → meow_ft_loaded()==0 → meow_ft_text 自动改走 meow_ui_text(位图字库,
  5x7 兜底),绝不豆腐不崩;meow_ft_width/line_height 降级位图字库度量。
- FreeType 2.14.3 vendor 进 patch_v2/sdk/vendor/(gitignore)+ build_sdk.sh 可复现拉取+裁剪
  构建(-DFT_DISABLE_ZLIB/BZIP2/PNG/HARFBUZZ/BROTLI=TRUE,静态 musl),freetype .o 编入
  meow_ui.a,App 链接命令不变(meow_ui.a libz.a -lpthread)。

验证:
- 宿主 test_meow_ui_ft 5 项全 PASS(加载/渲染像素非空/宽度测量/LRU 淘汰/降级回退)。
- 全量 SDK 回归:widgets/sdk_fixes/dispatch/notif_gesture/overlay_suppress/listvirt/box/anim/
  scroll/ime_fix/pinyin/notif_open/sb_collapse/theme/notify_ipc/audio_host/vol/str/config/
  sdk_input_keys 全 PASS。
- 交叉 -Wall -Wextra 全 SDK TU 0 warning;qemu-mipsel 冒烟 PASS(mipsel FreeType 真光栅化)。
- meow_ui.a 新 md5 6ff5be9455f9e197b35fa120be012041(未重链不部署 App,四 App 免重链)。

MEOW_APP_SDK.md 补 §10.1 矢量字一节 + 公共头索引/§9 构建约定更新;.gitignore 忽略
patch_v2/sdk/vendor/ 与 test_meow_ui_ft 二进制。
2026-09-02 09:51:23 +08:00
AskaEth 121d30ec95 fix(audiod): 孤儿会话回收 — 客户端崩溃/被杀(sig=9)/正常退出后会话完整回收,STATE 回干净无流基线
根因: cli_free(EOF)与 STOP 路径只置 active=0 + 唤醒排空,未清 sid/written/played/underrun/
rms/peak/lvlseq——ncm 被内核 OOM 杀后面板 /api/overview audio 块残留(active=0 但 sid/统计挂着)。
修法: 两段式 session_end_locked(即时置死+清 sid/app_id,保留 rate/ch/fmt 供排空算 frame_size)+
session_clean_locked(播放线程排空完成后彻底归零 rate/ch/fmt/written/played/underrun/drop/stall/
rms/peak/lvlseq)。顺带修 dac_vol_set 一处 -Wmisleading-indentation。
宿主 51/51 PASS(47 既有 + T9 SIGKILL 孤儿回收断言 STATE 全清,模拟子进程崩溃). meow_audiod b122c9b8 交叉0warning
2026-09-02 09:32:48 +08:00
AskaEth 19f7e59927 docs: 仓库文档同步 M6.4(0.8.4)——README 版本行+新能力(SDK 演进批次/锁屏 PIN/面板批2+overview 自校验);SPEC_REF 补 SDK §3.5 扩展模块速查+system.pin 锁屏键+面板 batch2/UTF-8 加固 2026-09-02 09:12:56 +08:00
AskaEth 54e737a4a2 fix(panel): overview net 块括号配平(批2 DNS 并入时多写一个 },浏览器 Extra data 破页)+自校验换严格平衡检查(jv_parse 容忍尾随垃圾,浏览器不容忍) 2026-09-02 09:00:08 +08:00
AskaEth 524e5d6967 feat(panel): 前端 NOT_JSON 自动回传原始响应到 /api/debug/report_bad_json(落 /tmp/overview_client_bad.txt,抓浏览器收到的真实字节) 2026-09-02 08:39:29 +08:00
AskaEth 57676766c0 fw_content: M6.4 收编 launcher 56dac860(310二期 EVIOCGRAB 锁屏+名称截断) 2026-09-02 08:17:37 +08:00
AskaEth bc9ba3693f feat(panel web): NOT_JSON 错误日志补 响应长度+尾部 200 字节(定位截断点) 2026-09-02 08:16:57 +08:00
AskaEth c730333776 fix(launcher): §310 追加两修——未冻结App锁屏输入穿透(独占触控) + 主界面应用名截断
真机 M6.3 反馈(bcef8e0 三连修复未好全,追加两问题):

P1 锁屏 PIN 输入穿透仍在(媒体保活 App 实时读 evdev)
  根因:媒体保活 App(播放中)从不 SIGSTOP(保 PCM),SDK 输入线程实时读 evdev,不走缓冲。
  bcef8e0 的「SIGCONT 排空」只作用于冻结 App;对未冻结 App,隔离全靠 /tmp/meow_notif_active
  让 SDK 排空——若 App 的 meow_ui.a 缺该排空(或节流窗内漏网),锁屏期触摸依旧抵达 App。
  修法:run_lock_screen() 与 lck_input_pin_modal() 打开 DEV_TOUCH 后 EVIOCGRAB 独占触控设备
  (真机 goodix 实测 EVIOCGRAB 返回 0)——抓取期事件只投递抓取者、不为其它 fd 缓冲/回放,
  故**任何**前台进程(冻结或未冻结)锁屏期都收不到触摸(真全模态)。失败时记日志降级
  (仍靠标记+SIGSTOP 隔离)。overlay_gate.h 增 overlay_keepalive_needs_grab 判据。

P2 主界面应用名只显示 5 字(「流媒体客户」式,半字残形)
  根因:app_launcher.c `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节——M6.3 只扩了
  name[49]+json_str 整序列,漏改此二处。UTF-8 下 16B = 5 汉字 + 「端」首字节 + 无 NUL,
  显示成「流媒体客户」+ 残形;main_menu.c menu_name_fit 按字节回退又停在半截字符、加 "..." 溢出。
  修法:新增 meow_utf8.h(meow_utf8_char_len / meow_utf8_copy_bounded,纯决策可单测)——
  app_launcher.c 两处 strncpy(16) 改 app_name_copy(=copy_bounded, 拷满 sizeof name=49);
  menu_name_fit 改整字符前进 + 预留省略号宽度(mfont 用「…」/点阵用 "..."),总宽 ≤ maxw,
  绝不劈开多字节字符。

新增 test_utf8_name.c(14 断言:整字符长/整序列有界拷贝,含 cap=16/17/12 边界不劈字)+
  test_overlay_gate.c 扩展(S1b 未冻结 App 靠 EVIOCGRAB 隔离 4 断言)。launcher 全量宿主回归
  + SDK 叠加/状态栏/输入回归全 PASS;mipsel 交叉 -Wall -Wextra 0 warning。
md5 56dac860841c3884b4322707246e22af(/usr/bin 只读,待固件打包 M6.4;fw_content.conf 未动)。
2026-09-02 08:13:55 +08:00
AskaEth 67aa5fc7a8 feat(panel): overview 自校验加严——内嵌 NUL/非法 UTF-8/strlen≠n 全拦并 dump /tmp/overview_bad.json(与浏览器 JSON.parse 对齐) 2026-09-02 08:12:47 +08:00
AskaEth 7a405866af feat(panel web): 调试日志——api() 每请求计时/失败详录(NOT_JSON 附原始内容头 300B)+route 切换日志,控制台过滤 [mp] 2026-09-02 08:07:16 +08:00
AskaEth 908fc53805 feat(panel): /api/overview 服务端自校验——jv_parse 发出前自查,坏则 dump /tmp/overview_bad.json + 500(追查间歇性非 JSON 破页) 2026-09-02 07:56:04 +08:00
AskaEth 44a790dc1d fix(ncm §33): 流媒专项批六项——流式SIGSEGV防御日志/档位保真(无损级必须flac+显示档按实际)/封面4xx不重试/收藏显隐/缓存命名迁移/topbar位图图标
- §2① ncm_api.c level_attempt: 无损级(hires/lossless及以上)命中必须 type=flac,返回 mp3 视为该档不可用续降
  (修「hires 档静默给 mp3 被当命中」)
- §2② player_manager.c eff_level_for_resp: 显示档按响应实际格式/码率——mp3 绝不显示 Hi-Res(≤128k→标准, 否则极高),
  flac 采信链 eff; br 未知钳 ≤EXHIGH
- §1 流式 SIGSEGV(《十面埋伏》sig=11) 审计+UBSAN 探针未复现 → 落地防御日志(dl start/done/interrupt + play done fin/exp/fail/underrun)
- §3 cover.c: download_cover 回填 rc/status; 确定性 4xx(404等)不重试立即跳下一张, 仅瞬时错误退避(rc=%d status=%d 日志)
- §4 playlist.c: pl_show_fav 我创建(is_collected==0)隐藏收藏钮, 我收藏保留, 搜索歌单保守显示(评论槽并入无空洞)
- §5 ncm_cache.c 双点→单点迁移 + player_manager song_local_file 旧键→当前档位键(不重下)
- §6 home.c: topbar 动作 ⚙↻⌕ 迁移 sb_icons 位图(meow_topappbar_set_item_icon, 修 4k 字库豆腐)
- 测试: test_ncm_core 新 test_grade_flac_gate + test_cache 双点迁移断言; degrade_sim/player_flow_sim 桩 flac 化
- 验证: stream/player_flow/window/search/cache_evict/degrade sim 全 PASS + test_ncm_core host+mipsel(qemu) + mem_sim + build_ncm.sh 0 warning
- 新 ncm md5 cb818786 (text 1017110/bss 1362500), 未部署留主会话; 0 真实 NCM 业务请求
2026-09-02 04:20:46 +08:00
AskaEth 68ce46a5d8 fix(ncm §32): 内存瘦身修 OOM 被杀 —— 封面降采样(8.28MB→0.78MB) + 瞬态 rbuf 缩减 + 线程栈显式
真机 dmesg 实锤:OOM Kill ncm (score 460, anon-rss 26.9MB, 设备 56MB)。
画像:封面全分辨率 RGBA 8.28MB(1448x1448 原生解码驻留) 是最大可控块。

瘦身(按收益排序):
1. sdk meow_ui_img 新增纯增量 meow_img_downscale + cover.c cover_cap(≤480px)
   → 封面 8.28MB→0.78MB,-7.65MB(mem_sim 实测 PASS)。
2. ncm_api.c 瞬态响应 rbuf:song_url→64KB、lyric/song_detail/cloudsearch/user_playlist→256KB
   (detail/track_all 保 1MB)。
3. ncm_app_internal.h 新增 ncm_thread_create(显式 128KB 栈),替换全部 14 处 pthread_create。
4. 滑动窗口/meta 复核零回退(window_sim:608B/首,6000 首 280KB 恒定)。

验证:mem_sim 封面收益 PASS + host home/player 渲染 OK + stream_sim/player_flow_sim/
window_sim/search_sim/cache_evict_sim 全 PASS + test_ncm_core host+mipsel(qemu) PASS
+ build_ncm.sh -Wall -Wextra 0 warning。真机 scp 播放在跑确认(缓存曲,0 播放请求)。

新 ncm md5 44fa16fe(text 1015150/bss 1362500)。正式部署留主会话。
2026-09-02 03:43:42 +08:00
AskaEth 202caaf66b fw_content: M6.3 收编 launcher 136dc67c(309 设置页5s+310 锁屏三连) 2026-09-02 03:43:12 +08:00
AskaEth bcef8e092e fix(launcher,lock): §310 锁屏/叠加三连 bug——输入穿透·状态栏消失·App死亡残影
用户真机 M6.2 实测三症状,全部位于锁屏/通知中心叠加路径。根因与修法:

S1 锁屏输入穿透(PIN 触摸打到下层 App / 解锁后「启动奇怪界面」)
  根因:run_app_lock_overlay / run_app_notif_overlay 关闭路径先 unlink
  /tmp/meow_notif_active 再 SIGCONT App——App 被 SIGSTOP 冻结期间缓冲的 evdev
  触摸事件在解除瞬间被 SDK 输入线程在无隔离态回放成 App 内点击(穿透触发)。
  修法:反序——标记仍在时先 SIGCONT,让 SDK 输入线程排空冻结期缓冲事件 +
  EVIOCGKEY 对齐,等 OVERLAY_DRAIN_WINDOW_MS(300ms > SDK 200ms 节流) 再 unlink。

S2 解锁后状态栏消失(App 无状态栏全屏残留)
  根因:S1 的回放点击常把 App 导航进全屏页(状态栏随 app.json/fullscreen 隐掉);
  叠加关闭路径也未强制状态栏重同步。
  修法:解锁后 g_sb_mode_last=-1 + 立即 app_overlay_statusbar() 重叠加(不等 1s 慢刷)。

S3 App 异常死亡(OOM sig=9)残影——fb0 停 App 死帧、无状态栏、launcher 不重绘主屏
  根因:锁屏叠加(run_app_lock_overlay→run_lock_screen)阻塞于 lock_screen 内,
  App 叠加期崩溃不被感知(notif 叠加有 run_notif_center 的 proc_is_zombie 探测,
  锁屏独缺),launcher 卡在锁屏会话永不回 menu 重绘。
  修法:run_app_lock_overlay 置位 g_lock_overlay_pid,run_lock_screen 每圈
  proc_is_zombie 探测,App 死即中止锁屏(LCK_OVERLAY_RET_DIED)走统一退出路径
  (reap_wait 回收 + ams_status_valid + break → run_menu 重绘主屏含状态栏)。

新增 overlay_gate.h(纯决策,宿主可单测)+ test_overlay_gate.c(10 断言)+
test_lock_screen.c 扩展(4 断言 overlay-abort)。launcher 全量宿主回归 +
SDK 叠加/状态栏回归(overlay_suppress/sb_collapse/notif_open/notif_gesture/
sdk_fixes/sdk_input_keys)全 PASS;mipsel 交叉 -Wall -Wextra 0 warning。
md5 136dc67c086a9d75c0ad9c83d519ff0d(/usr/bin 只读,待固件打包;fw_content.conf 未动)。
2026-09-02 03:41:39 +08:00
AskaEth 3611cbbbde sdk: sb_icons 扩充动作图标 + TopAppBar 位图图标支持
TopAppBar 动作项此前用字体码点(⚙↻⌕)绘制,4k 字库无这些码点→豆腐。改走位图:
- sb_icons.h 追加 15 个 Material Symbols Rounded 动作位图(settings/refresh/search/
  play_arrow/pause/skip_next/skip_previous/arrow_back/close/add/check/lyrics/
  queue_music/download/heart),FILL=1 实心与状态栏一致;sb_icons[] 只追加(0-14 不变),
  动作索引 15-29。
- 生成脚本 tools/gen_action_icons.py(cairosvg svg→32x32 单通道 alpha,SVG 缓存
  icons_src/ 可复现、断网可跑)。
- meow_ui_gfx.c 抽取图标 blit 像素核心 icon_blit_px,新增 meow_gfx_blit_icon_ink
  (控件路径不标顶栏脏区);meow_gfx_blit_icon 状态栏路径行为不变。
- meow_ui_widget.c W_TOPAPPBAR 项支持位图模式:新 API meow_topappbar_set_item_icon
  (w, idx, icon_idx),绘制取主题 onSurface/按压向栏底色淡 12%,文本项行为不变;
  hit 用统一项宽。
- 公共头 widgets.h 加 MEOW_ICON_*(15-29) 枚举与 set_item_icon 声明;internal.h 声明
  meow_gfx_blit_icon_ink。
- 宿主测试(test_meow_ui_widgets)加 topbar 位图图标绘制/命中/按压变淡 + 状态栏 blit
  回归断言;test_umbrella 加 MEOW_ICON_* 编译期锁;MEOW_APP_SDK.md 补说明+索引表。
- 验证:宿主测试 ALL PASS(14项);交叉 zig 构建 0 warning;meow_ui.a 新 md5
  1d69cdae55ab5dde3a98f5e2124f3656。
2026-09-02 03:24:46 +08:00
AskaEth 41427fdcb3 launcher: 进设置页不再同步跑串行网络 popen 与逐套字体 sha256(点设置卡≥5s 修复)
- 设置页入口 s_net_sync_from_sys() 改为 toggle 同步(快)+异步 s_net_refresh_kick();
  wifi/bt 状态与列表由 run_settings_page 主循环逐步骤 s_net_refresh_pump() 填充
  (每步一个带超时 popen、~150ms 一步),draw 用 g_net_* 缓存,数据变化即 need_paint
  → 入口不再被 wpa_cli/bluetoothctl/hcitool 5 条串行 popen(各 1.2~2.5s 有界超时)拖住。
- 进设置页字体重扫 meow_font_scan_presets(0) 跳过逐套 sha256(gbk/full 套数 MB,
  逐套双 sha256 真机耗数秒,入口卡顿主因之一);完整性校验保留给启动/热插拔全量(verify=1)。
- s_net_refresh() 保留同步语义(kick+drain) 供通知中心等调用方;WiFi/BT 子页返回、
  网络下拉后改 kick(由主循环承担异步填充)。
- 设置入口加毫秒级分段日志(font rescan / net toggle-sync+kick / page shown)供真机取证。
- 交叉 -Wall -Wextra 0 warning;launcher 宿主测试全 PASS。launcher md5 fbc78c62
2026-09-02 03:18:36 +08:00
AskaEth 0294553096 reader(R2): EPUB/FB2/MOBI/CBZ 加载器落地
- rdsrc 统一文档源抽象:TXT/FB2/EPUB/MOBI/CBZ 都产出「UTF-8 段序列+目录+元数据+图片」,
  与 R1 rdtxt 段契约一致;book.c 改喂 rd_src(文本)或图页(CBZ)。
- EPUB:miniz 解 ZIP → 自写 rdxml 回调解析 OPF(spine/manifest)+NCX/nav → 正文 XHTML 用
  rdhtml 逐行粗解析(坏标签/未闭合/实体容错,不建 DOM),章节按 RDSRC_SEG_MAX 分片。
- FB2:rdxml 严格解析抽 section 段落 + base64 内嵌图;段题进正文+目录。
- MOBI/AZW3:vendor libmobi 最小调用面(mobi_init→load→parse_kf7/8→parse_rawml_opt→
  flow/resources),HTML 走 rdhtml 同款粗解析;LGPL-3.0 合规见 NOTICE(版权+全文+锁定 commit)。
- CBZ:miniz 解 ZIP → 自然序图片序列,rdimg(stb_image) 解码+降采样,阅读页图页模式。
- 目录显示与跳转(EPUB NCX/FB2 section/MOBI 标题);书架开放四格式;进度按
  (段,字符偏移)(TXT/FB2/EPUB/MOBI)/页号(CBZ)。
- 宿主单测 loaders_smoke(四格式抽取/目录/图片/base64/页序)+ qemu-mipsel
  formats_smoke/book_smoke;R1 test_reader_core 58/0 不回归。
- build_reader.sh 集成 miniz/libmobi/stb/zlib 头;NOTICE 入仓;交叉 -Wall -Wextra 0 warning。

新 reader md5: e29036599300eb277b1c960a75b65dbf
2026-09-02 03:11:55 +08:00
AskaEth 10690bc348 fix(ncm §31): 修两处 UI 数据竞态——返回首页行空白(staging发布) + 进第二个歌单显示上一歌单(换歌单清窗)
A. 返回首页各行 2~3s 空白:appstate_load_playlists 就地 memset(g_st.playlists[])
   与 home_src2 无锁逐行绑定竞态,刷新期间 playlist_count 保留旧值 → UI 读清零行。
   照搬 s_build[] staging 范式:网络落 s_pls[] → 成功后锁内原子发布(拷回+更新count),
   失败保留旧快照(断网离线浏览)。绝不就地清零 UI 可见数组。

B. 进第二个歌单显示上一歌单:playlist_open/refresh 同步路径不调 appstate_playlist_reset,
   pl_apply(1) 绑到上一歌单残留窗口(track_ids_count/songs)。
   新增窗口归属(歌单 id 唯一标识, 对搜索歌单同样成立): worker 首屏发布后
   appstate_playlist_window_set;refresh 目标≠归属(窗口is) → 同步 reset 再 pl_apply。
   同歌单手动刷新(id 同)保留旧内容(stale-while-revalidate)。

宿主回归:playlist_race_sim(新 F 并发读无清零态/G 开A→开B 不串)、window/search/
cache_evict/player_flow/stream_sim、test_ncm_core 宿主+mipsel 全 PASS;
build_ncm.sh -Wall -Wextra 0 warning;0 真实请求;未部署。
2026-09-02 03:03:48 +08:00
AskaEth 97b741cf56 fix(ncm): 播放路径接通 auto_av(300 的降级链接线,修「SQ 被跳过掉标准」真机路径) 2026-09-02 02:48:43 +08:00
AskaEth 33414de7c4 fix(ncm §20.3): 降级链账户/fee感知 auto_av —— 修「SQ 被跳过掉标准」(297)
根因:ncm_api_song_url_auto 无账户感知,依赖调用方按会员钳顶;若账户 VIP 态未就绪/
错钳到 EXHIGH,降级链从 EXHIGH 起步,LOSSLESS(无损 SQ) 被跳过,直接掉「标准」。
修法:新增 ncm_api_song_url_auto_av[_f](§20.3 权威形态):fee∈{1,4} 且非 CVIP →
NCM_ERR_VIP 零请求;按 account_vip 自钳顶(NONE→EXHIGH/CVIP→HIRES/SVIP→SURROUND);
只试账户可访问档(ncm_api_level_gate<=vip);落在最近可用档。原 auto/auto_f 行为零回归。
逐级尝试重构为共享 level_attempt(type/url 接受+静默降档采信)。
测试(bench/degrade_sim.c):fee门禁5例 + auto_av矩阵 vip3×desired9×avail9=243例全PASS
+ 真机复现(CVIP+Hi-Res+有SQ无HiRes→落无损SQ flac);原90例零回归。
验证:test_ncm_core_host+qemu-mipsel 全PASS;build_core/build_ncm -Wall -Wextra 0 warning。
新 ncm md5 1064a1d686390cc159f18296ee2980ee(未部署,留主会话)。
遗留:player_manager.c 仍调 auto(不传vip/fee),应改请 auto_av(account_vip()+fee);
真机若仍掉标准需另查旧缓存 song_<id>_hires.mp3 复用或旧二进制。
2026-09-02 02:41:09 +08:00
AskaEth 55f0dfe91d M6.2 备料: S89 amsd.conf 热更 exec→/usr/bin 迁移; fw_content 收编 launcher 03dfcf76(锁屏+门控+名字)/amsd 026cad52(UTF-8 名字)/S89 a1ef9d65 2026-09-02 02:39:11 +08:00
AskaEth 54857bc0fd sdk(ui): 字符串资源外置(i18n 雏形 §13)—— meow_str_* + 内嵌双语表 + TF 覆盖表 + 控件接入
公开 SDK 前把 SDK/App 硬编码文案外置:新增 meow_str/meow_strf/meow_str_set_locale/
meow_str_locale,按当前 locale 取串(永不 NULL),第三方开发者可做多语言。

- 新增 include/meow_ui_str.h + meow_ui_str.c:取串优先级 = TF 覆盖表 > 内嵌当前语言
  (en_us→en 其余→zh) > 内嵌内置默认(zh_cn) > 原样返回 id。内嵌双语表(common.* 21 条 +
  ime.* 9 条);热路径零 malloc(编译期静态表 + ≤256 静态覆盖数组);纯 libc 宿主可单测。
- locale 存系统设置 settings.conf 的 ui.locale(默认 zh_cn,首次调用惰性读取);
  meow_str_set_locale 即时切换 + 重载覆盖表 + best-effort 持久化(流式改写保留其它行)。
- TF 覆盖表 MeowData/<pkg>/strings/<locale>.txt(id=text 行,UTF-8,'=' 两侧去空白);
  无卡/无文件/无该 id → 回落内嵌。meow_strf 用调用方缓冲(snprintf 语义,重入安全)。
- SDK 控件接入:dialog 默认按钮 OK/CANCEL + INPUT 占位、IME 键面/徽标(空格/确定/拼英/
  ?123/清/拼音/(无候选)/中)改走 meow_str;空态/加载态给 App 提供 common.no_data/loading。
- 测试:新增 test_meow_ui_str.c(内嵌回退/TF 覆盖优先级/未知 id 回退/切 locale 即时生效/
  查找正确性/meow_strf,tmp+env 两模式);全量宿主回归(widgets/dispatch/box/anim/scroll/
  listvirt/theme/ime_fix/pinyin/overlay/notif/sdk_input_keys/p1p2p3/config/vol/sb_collapse/
  notify_*) 0 回归;交叉 zig-mipsel -Wall -Wextra 0 warning。
- 文档:MEOW_APP_SDK.md 新增 §13(字符串资源/i18n,遗留顺延为 §14);docs/APP_DEV_GUIDE.md
  新增 §7.11。各宿主 test_*.c 头注释补 meow_ui_str.c 编译依赖。

边界(§13.7):仅 zh_cn/en_us 整串取用;不做复数规则 / BiDi / RTL / 动态字体回退(雏形)。
2026-09-02 02:37:38 +08:00
AskaEth d7bf62f448 fix(meowpanel §UTF-8): JSON 输出 UTF-8 加固——sA_str 统一消毒非法/截断字节,防任一上游坏字节拖垮整页
事故:amsd 的 am list 把「流媒体客户端」(18B) 截成 16B,腰斩「端」字(E7 AB AF→仅剩 E7) →
面板 ams_fill 把非法 UTF-8 嵌进 /api/overview → 浏览器 JSON.parse 拒收(HTTP 200 非 JSON)。
amsd 侧已修(8588a6b),面板侧自此防住任一上游再产坏字节,一个源的坏数据不拖垮整页。

- meowpanel.c: 新增 utf8_seq_len/utf8_seq_ok/utf8_valid_n/utf8_sanitize(C 无依赖);sA_str 收口为
  sA_str_n:逐字节校验 UTF-8,合法序列透传、非法/截断逐字节回退 '?'(保持长度);纯 ASCII 单遍直通。
- 覆盖点(全部经 sA_str/sA_str_n):ams_fill(pkg/name)、svcd_fill(name/exec/state/...)、audio_fill(app/fmt/route_mode)、
  /api/apps(app.json name/pkg/ver/exec)、字体名、通知字段、崩溃/日志中心字段、设置/主题串等。
- 两处日志内联转义(api_net_log、api_logs_read)改走 sA_str_n,日志行同样消毒。
- 原始 IPC JSON(perf/power/net,含 ssid)先 utf8_valid_n 校验:坏字节时 perf/power/net/status 降级
  available:false / 503,不嵌坏字节(api_overview 首页生命线不受影响;api_system network 段坏字节回退 hostname_local)。
- 宿主自测 `--selftest`:喂「流媒体客户\xE7」给 utf8_sanitize/sA_str/ams_fill 桩,断言 JSON 完整可解析 + 无非法字节,
  且不误改完整合法名「流媒体客户端」;`meowpanel --selftest` 通过。
- 验证:build_meowpanel.sh host / 交叉均 -Werror 0 warning;真实 ams_fill 桩复现事故 → 「流媒体客户?」JSON 合法。
- 后端 meowpanel(mipsel) md5: 820b24541ed69f2e27e4a301aff10c98(README 已校正)
2026-09-02 02:32:25 +08:00
AskaEth 3397a702aa fix(ncm §30): 真机四连症状——JSON unescape + 封面流式 + 歌单豆腐/分隔符 tofu + 首屏诊断日志
症状/根因/修法(0 真实 NCM 业务请求;宿主+qemu 全绿):
- ③ LRC 挤一行/时间戳未解析:ncm_json_get_str_at 不做 JSON 转义还原,
  lrc.lyric 的 \n 转义原样进串 → lrc_parse 无真实换行整段一行。修:ncm_json.c 加
  json_unescape(RFC8259 全转义 + \uXXXX/代理对),get_str_at 返回前还原。
- ⑤ 封面全 failed→give up:cover.c 静态 g_buf 288KB < 封面 1.34MB → NCM_ERR_FULL。
  设备 RAM 仅 56MB。修:cover.c 改流式写文件(ncm_http_download cb→ncm_cache_write +
  meow_img_load_file 临时文件),NCM_COVER_MAX 8MB 改下载上限(非缓冲,bss 未涨)。
- ① 162 首歌单所有条目「三个豆腐」+「加载中」:占位符 ···(U+00B7)在 default 字体缺
  该字形 → 3 豆腐(font.idx glyph 扫实证 U+00B7 MISSING)。修:占位 ···→...、全 App
  · 分隔符→/;playlist_worker 首屏打 first-screen 诊断日志。
- ② 返回首页歌单列表 1 秒方框:字体缺 U+00B7/特殊字形(◢◤/Kannada)tofu;瞬态疑 glyph
  atlas 冷缓存(SDK 内部未深挖)。修:·→/。
验证:test_ncm_core(+test_json_unescape, host+qemu) / listvirt44 / dispatch / box42 /
scroll17 / anim32 / widgets / overlay_suppress / sdk_input_keys / theme / vol / ime_fix /
pinyin / notif_gesture / notif_open / sdk_fixes / sb_collapse / ui_audio_host / notify_ipc /
notify_integration + playlist_race_sim / window_sim / search_sim / cache_evict_sim(1-9) /
player_flow_sim(P1-12) / stream_sim(16+2resume) 全 PASS;mipsel -Wall -Wextra 0 warning。
新 md5:ncm d91cd906、ncm_host b731654a、test_ncm_core_host 7a38194c。

(症状④ VOL_DOWN×3 杀 App 的 launcher 源修复 app_launcher.c 门控 !a->framework 已随
 并发 commit 54a6e82 存入 HEAD;launcher 二进制 03dfcf76 留主会话/固件打包。)
注:build_ncm_host.sh 补 SDK 292/295 的 anim/box/scroll 源(宿主链接)。
2026-09-02 02:25:37 +08:00
AskaEth 54a6e828b7 fix(launcher): app 显示名缓冲 name[17]→[49] + json_str 截断 UTF-8 整序列保护(同 amsd 修复,M6.2 备料 launcher 03dfcf76) 2026-09-02 02:20:06 +08:00
AskaEth 8588a6b453 fix(amsd): app 显示名 name[17]→[49] + UTF-8 整序列截断(utf8_trim_incomplete)——修「流媒体客户端」18 字节被砍成 16 致 am list 输出非法 UTF-8、面板 /api/overview 被浏览器 JSON.parse 拒收(非 JSON 响应) 2026-09-02 02:15:19 +08:00
AskaEth 9d54cf89c5 sdk(ui): W_LISTVIEW 虚拟化层 — 固定 item 池 + 多行富 item + 完全自绘 (§5.5)
在既有 W_LISTVIEW (src2 每帧取行) 之上叠加虚拟化层 (API 追加式, 旧 API 一字不改):
- 数据源 count_fn + bind_fn (index, meow_lv_item*, ctx); count 每帧实时查 (任意长度≥65535,
  不常驻 item 对象); bind 仅在行进入视口时被调一次
- 固定 item 池: 视口行数+2 静态槽 (有界 ≤96), 回滚不重绑 (同 index 命中同槽); 整表扫过后
  wrap 回收合法但每次 draw 有界 (≤视口窗口)
- 多行富 item: 主标题/副标题/左图标·缩略图/右尾件; 行高 set_item_h(px) 或 set_item_tier(1→48/2→56);
  默认 renderer 主题语义色绘制; set_item_draw 完全自绘; section 段头占位行(非粘性); 空态占位
- 集成: tap_at→meow_ui_focus_acquire (§6.4); meow_scroll_wrap(lv,1) track+fling+snap(行边界);
  选中/按压态走主题; enabled=0 变淡不响应
- 兼容: src2==NULL 走既有路径; W_LIST/W_LISTVIEW 单行用法零改动

验证(宿主): 新测试 test_meow_ui_listvirt T1-T5 全 44 PASS (10000 项滚动窗口内容正确/hit 映射/
bind 有界=count/回滚不重绑/多行渲染/截断/图标占位/段头/空态/自绘/焦点/fling 吸附);
全量回归 widgets/dispatch/box/anim/scroll/ime/pinyin/overlay/notif/sdk_input_keys/p1p2p3 全 PASS;
交叉 zig-mipsel -Wall -Wextra 全 SDK TU 0 warning; meow_ui.a 新 md5=2a2d8000b95dd211d09dc736d5538ad0

文档: MEOW_APP_SDK.md §5.5 同步虚拟化层(API 一览/池语义/集成/回归).
2026-09-02 01:36:48 +08:00
AskaEth 85e1d03157 feat(panel): 锁屏一次性解锁端点 POST /api/system/pin-unlock-once(写 /tmp/meow_pin_unlock_once,launcher 锁屏页消费放行一次)+ 设置页 PIN 卡「一次性解锁设备」按钮 2026-09-02 01:01:54 +08:00
AskaEth 54d2791f48 sdk: 事件分发形式化(§6.4)—— 统一命中/捕获/消费/层间隔离/焦点
根治「焦点/事件穿透」一类反复 bug。此前命中/捕获/消费/层间隔离各处 ad-hoc;
现上收为单一仲裁者(meow_ui_input.c),公共 API 只追加、不删改既有签名。

分发模型:
- 触摸沿 z 序自顶向下命中(meow_draw_hit_topdown,owner==active+enable 过滤);
- 命中即消费;模态层永远先审(meow_ui_modal_begin/end/active/depth);
- down 捕获(meow_ui_gesture_begin/owner/end):命中目标捕获手势直到 up,防滑出误触下层;
- 模态存在时下层完全收不到事件——hit_topdown 恒返回模态哨兵(背板消费,不透传);
- meow_ui_run 在 TOUCH_DOWN 自动 begin、TAP/SWIPE/LONG_PRESS/NOTIF_PULL 自动 end。

焦点模型:
- 唯一持有者(meow_ui_focus_acquire/release/owner/tap);
- 点非 focus 区域→释放并通知(meow_ui_focus_tap,list 焦点残留根治);
- 滚动容器(list/listview)tap_at 命中行自动 acquire,点外由 run 的 focus_tap 释放;
- lost 回调(meow_ui_focus_set_lost_cb)通知转移。

迁移(行为修复,正常交互语义不变):
- dialog/sheet → meow_ui_modal_begin/end(模态期下层收不到;叠黑由既有整帧快照+卡片仅重绘保证);
- list/listview tap → meow_ui_focus_acquire;
- 注入/叠加让位(overlay_input_suppressed)与模态层隔离互补,语义不变。

验证:新穿透矩阵 test_meow_ui_dispatch(M1 模态点下层不触发 / M2 弹层叠开两层层隔离 /
M3 捕获手势滑出不误触 / M4 list 焦点点外释放)ALL PASS;
全量回归 widgets/ime_fix/pinyin/overlay_suppress/notif_gesture/sdk_fixes/sb_collapse/
p1p2p3_repro/box/anim/scroll/sdk_input_keys 全 PASS;交叉 -Wall -Wextra 0 warning。

meow_ui.a 新 md5:a9fcfb6eeb07ddbdd48039df4f8f18ce(主会话统一重链,本仓未重链 App)
2026-09-02 00:57:58 +08:00
AskaEth c3abc3fee4 feat(launcher,lock): 锁屏补充三定案—面板一次性解锁恢复口 / 设置页 PIN 管理 / 电源菜单主动锁强化
- 面板一次性解锁恢复口:/tmp/meow_pin_unlock_once 存在即被锁屏页消费删除并放行一次
  (不校验 PIN、不清 system.pin、不重置失败计数;重启标志自然消失;lck_unlock_once_consume,
  launcher 侧只做检查/消费,panel 端点由主会话另实现)。
- 系统设置「安全 → 锁屏 PIN」页(lock_settings.c,subpage.c 脚手架):状态(未设置/已设置/
  锁定中+剩余)/ 设置(4 位双输确认)/ 修改(需旧 PIN 一次)/ 清除(**免旧 PIN**,仅二次确认弹窗)
  / system.pin_lock_on_wake 开关;数字输入用 SDK 按钮键盘(lck_input_pin_modal)。
- 电源菜单「锁定」主动锁强化:立即熄屏 + 写 /tmp/meow_pin_locked(主动锁标志,重启消失,
  唤醒时无论 pin_lock_on_wake 是否开都必锁屏)+ rm /tmp/meow_pin_unlock_once 作废挂起绕过;
  lck_wake_should_lock() 作为唤醒门取代 lck_lock_required(1)(主菜单 2 处 + App 会话 2 处)。
- 测试:test_lock_screen 扩 51 ok——bypass 存在即消费/不存在不影响/消费后删除;清除免旧 PIN;
  主动锁标志唤醒必锁/清除后回退;全回归 settings_brightness/notif_panel/notif_tf/vol_ramp_jack 无碍;
  交叉 -Wall -Wextra 0 warning。meow_launcherd md5 bdf884756ef8a8353aa8e5dfb6e10a0a。
- SYSTEM_SETTINGS.md 同步锁屏行为段。
2026-09-02 00:57:17 +08:00
AskaEth e948a0a44e feat(launcher,lock): MeowOS 锁屏 4 位 PIN(system.pin 同源面板,错5锁5分钟持久化,唤醒/电源菜单手动锁)
- system.pin:4 位数字 PIN,sha256+salt 存 settings.conf(格式与面板 panel.pin 全等,绝无明文)。
  panel.pin 已设且 system.pin 未设时锁屏接受 panel.pin,首次验证成功后并入 system.pin。
- 锁屏时机:息屏唤醒后(默认开,system.pin_lock_on_wake=1 可关)+ 电源菜单「锁定」手动锁;
  开机不锁(boot 进主屏不拦)。
- 锁屏页:launcher 渲染系统级全屏页,App 不可见不可触;SDK 按钮画数字键盘(不调 IME),
  输入 4 位自动校验;● 遮罩不回显数字;错 5 次锁 5 分钟(持久化锁定戳/计数,重启不清)。
- 安全细节:锁屏页电源键=息屏(允许再睡),音量键不动,不响应左缘返回/通知中心手势。
  兜底:SSH root 删 system.pin 键即解锁(无其它后门)。HibyOS 不涉及。
- 宿主测试(test_lock_screen):设置/校验/错5锁定持久化(重载模拟重启)/唤醒触发/开机不锁
  /panel.pin 迁移,全 PASS;既有 settings_brightness/notif_panel/notif_tf/vol_ramp_jack 回归
  无碍;交叉 -Wall -Wextra 0 warning。meow_launcherd md5 670f978e71c1e81cfe86d47c21248c9d。
2026-09-02 00:48:23 +08:00
MeowOS 5a5e995ebd SDK 冲刺批①+②+⑤: meow_box 线性布局 + meow_anim 动画基座 + meow_scroll 滚动物理
- meow_box(include/meow_ui_box.h + meow_ui_box.c):竖直/水平容器;子项=固定|weight|wrap;
  margin/padding/gap;主轴对齐 start/center/end/space-between/around/evenly;交叉轴 start/center/end/stretch;
  嵌套(深度≤16 静态栈,单遍求解,无递归爆炸)。可作 meow_view 布局提供者
  (meow_view_set_layout/relayout,opt-in;未设布局的 view 保持现有绝对坐标行为)。
  新增 meow_widget_set_rect(只写字段不标脏)作为 box 写坐标的通道。
- meow_anim(include/meow_ui_anim.h + meow_ui_anim.c):时间驱动 tween(from/to/duration/easing:
  linear/ease-out/ease-in-out/overshoot)+ update/done 回调;静态池≤16、取消、并行多动画;
  meow_anim_set_region 让每帧只 flush 动画影响区(不整屏)。meow_anim_tick 挂主循环 on_tick,
  无动画零开销(只查计数即返回)。
- meow_scroll(include/meow_ui_scroll.h + meow_ui_scroll.c):速度跟踪(最近 N 采样加权)+ 松手 fling
  (减速度曲线,用 anim 基座驱动)+ 边界过冲 clamp + 简单回弹(不做 spring)+ 可选 snap;
  跟手拖动行为不变(track 内部调既有 set_scroll);每帧只 redraw 列表视口区;不引 libm。
- 追加式:既有 meow_row/column/box 与全部 meow_*_create/hit/scroll 签名一行未改;旧绝对坐标永远有效;
  既有 App 免改源码。
- 验证:宿主新测试 box(42)/anim(32)/scroll(17) 全 PASS;既存 9 回归(widgets/sdk_input_keys/ime_fix/
  pinyin/overlay_suppress/notif_gesture/sdk_fixes/sb_collapse/p1p2p3_repro)全 PASS;
  离屏 demo(demo_box_scroll.c)渲染 box 布局 + 列表 fling 正常;zig-mipsel -Wall -Wextra 全 TU 0 warning;
  meow_ui.a md5 83dd6a2d0d24cc748a8659c29f1de2b5(未重链/部署 hello/files/ncm/reader,由主会话统一处理)。
2026-09-01 23:49:24 +08:00
MeowOS 553a7d5d9e reader R1: os.meow.reader 书架+TXT 垂直链路骨架
- core: 编码检测(BOM→UTF-8严格→GB18030/Big5统计, Big5与GB重叠按GB18030兜底, §3.5风险注明)
  + GB18030双字节码表(uint16[126][190]=47880B, tools/gen_gb18030_table.py 生成, 生成物入仓)
  + 流式排版器(贪心断行+Kinsoku避头尾+分页+重排锚定, rd_meas_fn 测量回调替 FreeType, 宿主可测)
  + TXT 流式加载器(按段读, 不整文件入内存, \n 恒独立换行可安全分段)
- engine: FreeType 封装(FT_New_Face 按需读文件, NO_HINTING/LIGHT 灰阶, LRU 512 key=码点+字号,
  blend565, rdft_advance 只测宽不栅格化)
- app: 书架(扫 Books/ 识别 5 后缀, 最近读置顶, 空态提示) + 阅读页(W_CUSTOM 画布 3 栏翻页
  +中央菜单占位(字号±/行距±/返回) +顶栏进度%/章节名) + 书桥(段表+flat 页索引, 渲染, 重排回锚)
  + 进度持久化(MeowData/os.meow.reader/progress/, path hash→seg/ch/fs/ls/ts)
- 取舍: 正文字体 FreeType 直读 TF MeowOS/fonts/ TTF/OTF; 无字体回退 SDK 位图字库(meow_ui_text,
  未打包字体(许可/体积), 全 glyph 兜底 R2); 页位图缓存当前+前后各1页(3×691KB), 翻页动画默认无
- 内存实测(宿主 --wrap malloc, 73页书): 书结构 130KB + 当前段刮擦~1.6MB(行/页容量按 n 偏高,R2 收紧)
  + 字形缓存~650KB + 3页位图 2.07MB ≈ 4.45MB 动态, 在 ≤8MB 阅读态预算内
- 测试: 宿主 core 58/58(enc/gb18030金标准/kinsoku/page/reflow/txt); qemu-mipsel ft_smoke PASS
  + book_smoke(12页/渲染有墨/翻页/重排回锚) PASS; reader 交叉 -Wall -Wextra 0 warning
- 产物: reader(mipsel 静态1.2MB)+build_reader.sh(vendor FreeType 可复现); vendor/二进制 gitignore
2026-09-01 23:26:42 +08:00
MeowOS 33d3059038 feat(panel, PANEL_ENHANCE_PLAN 批2): 崩溃/日志中心 + 音频写 + 后端补缺口
后端(meowpanel.c 0-warning,-Werror 通过):
- /api/system 补 reboot_reason/upgrade_status(svc_power 读级 POWER_REBOOT_REASON(8)/UPGRADE_STATUS(9))
- /api/overview net 块并入 DNS 统计(首页严格单请求;DNS 不可用省略 dns 键不谎报)
- /api/audio 补 vol_max(读 settings audio.volume_max)
- POST /api/audio/route(仅 0=auto/2=3.5/3=4.4,拒 1/4/5;写级 PIN 已护)
- POST /api/audio/volume(clamp 到 vol_max,防炸耳红线;写级 PIN 已护)
- GET /api/logs/crash(崩溃汇总:每实体崩溃数+最近时间/信号 + dmesg 计数;256KB/64 实体/5 行上限)
- /api/logs 文件列表加 group 来源分组
- 顺带补回 批1-B 前端读取的 boot.mode /tmp/meow_boot_mode(此前遗留未提交)

前端(web/meowpanel.js 双主题):
- 音频页:路由选择器(自动/3.5/4.4)+ 音量滑条(上限 vol_max、平方锥度非线性标注、确认弹窗)
- 日志页:崩溃汇总卡 + 崩溃尾巴一键看 stdout 尾部 + 按来源分组 + 前缀过滤
- 首页:netCard 改读 overview net.dns,去掉单独 /api/net/dns

验证:宿主编译 0 warning + -Werror;自建桩测试 24/24(crash 解析/路由 1|4|5 拒 422/音量 clamp 到 50/
reboot_reason+upgrade_status 值/overview net.dns);headless Chromium 渲染 audio/logs/home/system/
light 主题均正确。md5(meowpanel)=aed2ea17a34614acaa26dcbc8965ebb1。
2026-09-01 23:15:52 +08:00
MeowOS 34f2834704 docs: 仓库文档同步 M6.1 2026-09-01 22:33:58 +08:00
MeowOS 06307dc4b2 M6.1 备料: launcher start audiod→meow_audiod(服务名统一收尾, e6bdd091 含286设置页迁移); fw_content 收编 286 launcher/287 powermgr期2 aedaf519/276 netmgrd期3 62369ebe 2026-09-01 22:18:24 +08:00
MeowOS 0cae95c9cc powermgr 期2: 历史采样环/功耗分解top/电源事件流 + mp2731电流接入 + BT·WiFi分档细化
- history: 30s 节拍内存环形(RING_N=480≈4h)存 pct/est_mw/充电态/屏幕态/亮度,
  不落盘重启清零(板载写磨损不考虑); IPC MSG_PM_HISTORY(0xD4)/D5, CLI history[N]
- top: estimate breakdown 分量实时占比排序视图(2s 刷新, Ctrl-C 退,
  MEOW_PM_TOP_ONCE=1 一次输出供测试); CLI top
- events: 插拔充电器/低电阈值穿越(15%/5%/回20%)/亮灭屏/背光跳变(≥30%);
  App 前后台无可轮询导出点→诚实标注不支持(语言, 期3接 launcher)
  IPC MSG_PM_EVENTS(0xD6)/D7, CLI events[N]
- 模型: 系数路径迁 /data/meow/powermgr/power_model.conf(=/usr/data symlink,
  组件持久化目录定案); WiFi 增未关联(carrier=0→unassoc)分档; BT 细化
  rfkill+hciconfig ACL(up/connected, 有界子进程 900ms 超时, 不可用回退 up)
- mp2731 current_now 接入: 充电态实测 charge_mw=cc_voltage×current/1e9,
  charge_confidence=measured(消费侧无实测节点仍 estimated, 条件写清)
- IPC 0xD4-D7 号段本文件局部定(不动 meow_launcherd/meow_ipc.h, 严守只动本目录)

验证: 宿主 aarch64 54 PASS + qemu-mipsel 54 PASS; 交叉 -Wall -Wextra -Werror 0 warning;
固件阈值 <200KB 达标(133KB); BSS 37.6KB 起见 RSS<1MB 达标
2026-09-01 22:13:42 +08:00
MeowOS 3b6a73ac10 feat(launcher, NETMGR_DESIGN §12): 设置页 WiFi/蓝牙子页迁 netmgr IPC + WiFi 新网络录入 UI
- wifi_page/bt_page 查询/操作改走 netmgrd IPC(netmgr_ipc.c 客户端:
  socket /usr/data/meow_net.sock,HELLO 鉴权 → WIFI_CTL/BT_CTL → RESULT)。
- WiFi 新网络录入:扫描结果点选 →(加密网络)SDK IME 输密码 → connect →
  结果反馈 → 刷新已存网络;开放网络直连;当前连接详情含断开。
- 双轨过渡:netmgrd 不可达(NETMGR_UNREACH)回退 net.c 直连 wpa_cli/hciconfig
  (保功能),页面显示「网络管理服务未运行(直连模式)」降级占位(不崩)。
- BT 仅迁 on/off/status/paired(netmgrd BT_CTL 无连接/配对子命令,其余直连)。
- net.c net_wifi_scan 补 sec 字段(wifi_page.c 补 meow_net_t.sec)。
- 测试:+test_netmgr_ipc.c(假 socket 帧:扫描渲染/连接成功失败两态/降级占位)。
2026-09-01 22:09:31 +08:00
MeowOS cd4e2c247f feat(S89): 服务名统一 meow_*(用户定案)——S89 迁移旧 audiod.conf/mdnsd.conf 为 meow_audiod.conf/meow_mdns.conf(stop+rm+预置);SPEC_REF/meow_svcd.md 同步。launcher main_menu.c 的 svcd start audiod 调用待 286 交还后改(避让同目录) 2026-09-01 22:08:29 +08:00
MeowOS b37dc23cd9 meow_netmgrd 期3:管控层(三实体策略引擎+防自锁+SOCK_DESTROY 实测→仅 DNS 层)
NETMGR_DESIGN.md §6/§13 期3 落地(全在 patch_v2/meow_netmgrd/ + meow_ipc.h 追加):
1. 策略引擎全量:/data/meow/netmgrd/net_policy.conf(/data=/usr/data)读改写单键+原子写
   (temp+rename,绝不 O_TRUNC 整文件重写——watchdog 毁设置教训);per-实体(app:/svc:/proc:)
   net=allow|block + domain 白/黑(接通 DNS 层执行)+ global log_level;默认 allow。
2. 防自锁硬编码:dropbear/wpa_supplicant/udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init 拒绝 block
   (CLI/IPC 拒 + 加载校验把手工改坏的文件里这些实体的 net=block 同样强制复位 allow——§14 红线)。
3. SOCK_DESTROY 真机实测(sockdestroy_probe.c,root @内核4.4):**内核接受 SOCK_DESTROY 请求(rc=0)
   但未真正销毁 socket(cookie=0 与 dump-cookie 两法均 dead=0)→ 判定 no-op/不可用**。
   ⇒ 连接级执行降级为「仅 DNS 层生效」(net=block → dns_query_domain_denied 全量 deny,DNS 层落地),
   policy show 头 enfor=dns_only 明示。SDK 自愿通道/设置页迁移期外。
4. IPC 0x00EA MSG_NET_POLICY(op: show/set net/set domain/del)+ RESULT 0x00EB:读级任意、写级仅系统身份。
   CLI:policy show [entity] | set <entity> allow|block | domain <entity> +host|-host | del <entity>。
5. 两小修:①classify_exe 增认 MeowPanel 精确路径(修面板 dns flush 403;面板已有 PIN 门禁)
   ②连接未归因(inode=0/查不到)一律落 proc:unknown——修 apps 表出现「IP:port」原始端点行。
6. WiFi connect 持久化:MeowOS 模式 connect 后 wpa_cli save_config(HibyOS 只读由服务端门控)。

验证:宿主 test_netmgrd.sh 61/61 + qemu 61/61;test_netmgrd_p2/p3 宿主+qemu 全过;netmgrd_authtest 全过;
交叉 -Wall -Wextra 0 warning。真机只读实测(DNS-on 固件共存,用独立 socket+/tmp 临时 policy.json):
policy set/domain/del 全链 + 防自锁拒绝 proc:dropbear(rc=1) + 空文件 del(size=0) + apps 无原始端点 +
resolv.conf 未触碰(测后核验固件两行形式);Firmware meow_netmgrd(pid 8142) running 未动。

新 md5:meow_netmgrd 62369ebe29e7ef4b55067a0f14e240d0(主会话更 fw_content.conf/热更设备)。
2026-09-01 22:08:10 +08:00
MeowOS 7ba0fde3e6 feat(panel, PANEL_ENHANCE_PLAN 批1-B): 前端五页——首页仪表盘/音频电平/电源/进程服务/系统补强/网络健壮性/PIN 卡
- 新增 #/home 默认路由:单请求 /api/overview + 5s 节拍(电平 1s 子节拍独立),
  卡片:设备信息(boot/默认启动)/电量/音频(rms·peak dBFS 双条电平表 -60..0)/WiFi+DNS/
  在跑App与服务健康/存储/性能小条;任一源 available:false 该卡显示「—」。
- 音频页:rms(青绿)+peak(黄) 双条电平表 + deci-dB 数值 + 近30采样 sparkline,轮询提速 1s;无流占位。
- 新增 #/power:电池卡 + 功耗估算卡(est_mw + confidence 徽标 estimated/derived/measured) +
  子系统分解堆叠条(screen/wifi/bt/audio/cpu/baseline mW+占比)。
- 新增 #/proc:svcd 服务表(restart 计數>=3 高亮/来源 board|tf) + ams App 表(crashes/launches/last_kind),
  行点开经 /api/files 列 MeowOS/logs/<pkg>/ 读最新 stdout_*.log 尾部(崩溃尾巴);全只读,无 restart/kill。
- 系统页补强:电池行、当前运行模式(后端已有 boot.mode/current 则显示) vs 默认启动并列(不一致高亮)、
  重启原因/升级状态(后端未暴露时显示「—」)。
- 网络页健壮性(281 缺陷):status 瞬态失败不再整页踢「不可用」占位,改顶部内联错误条 + 5s 自动重试,
  数据区保留上次成功内容。
- 设置页 PIN 卡:显示已设/未设/锁定状态,「修改 PIN」(旧+新双输入)与「清空 PIN」(旧 PIN 确认),
  走 /api/auth/login 校验旧 PIN + PUT /api/settings/panel.pin。
- 首页置顶导航新增「首页」,默认路由 #/ 或 #/home;侧栏新增「电源」「进程·服务」入口。

验证:headless Chromium --dump-dom 静态样例数据(参照 281),9 页 JS 0 错误,
双主题(md3/miuix × dark/light)通过;proc stdout 尾部支持 5s 重渲染后重开。
仅改 patch_v2/meowpanel/web/。
2026-09-01 21:52:25 +08:00
MeowOS d12164305d feat(panel, PANEL_ENHANCE_PLAN 批1-A): 访问级 4 位 PIN 鉴权 + 后端只读端点 power/overview/ams/svcd + audio 电平键
鉴权(用户定案:访问面板即需登录):
- PANEL.pin 存 sha256+salt(salt:hex,绝不含明文);与未来锁屏 PIN 同源预留 system.pin(g_sets 注记)。
- 未设 PIN → /api/auth/setup 首次设置流程;未设 PIN 前读/写端点全拦(require_auth 无条件,登录/设置流程除外)。
- token 128bit(16B→32hex) 只存进程内存(重启自然失效),3 天固定不滑动续期,多会话并存。
- 连错 5 次 PIN 锁 5 分钟(429 LOCKED,status 透出 lock_remaining_ms)。
- 旧密码机制 panel.auth_enabled/panel.auth_pass 兼容迁移:未设 PIN 时可暂用旧密码登录(status=legacy),设 PIN 后以 PIN 为准。

新只读端点(均需 PIN 会话):
- /api/power: meow_powermgr MSG_PM_QUERY(0xD0)→RESULT(0xD1) battery/estimate/breakdown,confidence 原样透传。
- /api/audio: 补 rms/peak/lvlseq(追加键,ABI 兼容),抽 audio_fill() 供 overview 复用。
- /api/ams: am list 解析 App 表(pkg/name/state/pid/launches/crashes/last_kind/last_code)。
- /api/svcd: meow_svcd status all 解析服务表(name/state/pid/restart/backoff_ms/trigger/src/exit/exec)。
- /api/overview: 单线程后端只暴露聚合——perf/power/audio/net/ams/svcd 六源顺序 fan-out,任一源失败该子块 available:false,绝不假数据。

前端最小支撑:登录页(PIN 输入+首次设置双态)+ 401 自动跳登录 + token 存 localStorage 随请求带 X-MeowToken(后端亦认 Authorization: Bearer)。
红线:仅新增读级端点,未加任何写端点(音量/路由/策略等);本任务不含批1-B 页面。

md5: meowpanel 3696e85b5eaf111f530c5c39fa68886e(mipsel 静态)/ meowpanel_host 3180e4f87e77db8717597619097805eb。
验证:host+mips -Werror 0 warning;假 IPC/am/svcd 桩六源聚合(含 power 宕机降级)PASS;PIN 全流程(设置→登录→连错5次锁→token 过期)PASS;qemu-mipsel 冒烟 PASS。
2026-09-01 21:30:41 +08:00
MeowOS 30e2ec901f meow_netmgrd 期2.1 修复:status JSON 漏闭 bt 对象(/api/net/status res.json() 必炸)
根因(patch_v2/meow_netmgrd.c build_status_json):bt 对象开 '{' 后,dns 数组之后漏收 '}',
导致 rfkill/rate 被吞进 bt、bt 与 root 均不闭合 → 外层 data 对象收尾只剩 3 个 '}',JSON 非法,
面板 /api/net/status 纯透传 MSG_NET_STATUS_RESULT(0xE0) 后 res.json() 失败。

修法:bt 的 dns 是末字段,收尾改为 ']},'(数组闭 + bt 闭 + 逗号)——不应含尾逗号
(首版 '],},' 产生 "dns":[...],} 尾逗号 JSON 非法,一并修正)。

顺手核查:build_iface_json(0xE2 单/全接口)括号配对正确、JSON 合法;conns/dns 为文本表非 JSON,不受影响。

加固:test_netmgrd.sh 增 T13b-e 端到端断言 STATUS(0xE0)/IFACE_ALL(0xE2)/IFACE_WLAN(0xE2) 的原始 JSON
可被 json.loads 完整解析(括号配对/无尾逗号/唯一有效对象)。

验证:宿主 test_netmgrd.sh 52/52、qemu-mipsel 52/52、test_netmgrd_p2(宿主+qemu) 全过、netmgrd_authtest 全过;
交叉 -Wall -Wextra 0 warning。新 meow_netmgrd md5 abfb97680b4f576f3669a3e4b3b29134(主会话热更设备)。
2026-09-01 21:24:35 +08:00
MeowOS 8e54b2c150 docs(repo): 公开 SDK 仓 MeowOS-SDK 拆仓说明 — README 加链接 + scripts/publish_sdk.sh(subtree split 命令 + 增量同步流程) 2026-09-01 21:15:35 +08:00
MeowOS f0b73e119d 280 歌单链路三修:开A→开B 竞态防串(5194 串数据) + 92/200 批边界 Load Failed + 歌单列表盘缓存 + 孤儿 .part 回收
ncm_app(REPORT §26, ncm 158ec367 未部署留主会话):
- 打开时快照 plid+代次:appstate_playlist_begin/open_gen/open_idx/reset +
  appstate_load_playlist_songs_ctx(快照 pl 不读 g_st.playlists[],detail 结果落
  staging 锁内原子发布,代次/目标不匹配判 NCM_ERR_STALE 不写全局);playlist_tick
  过期丢弃 + appstate_playlist_reset 清窗口防串。修 5194「我喜欢的音乐」3 首/串数据。
- appstate_song_ensure_range fill-and-continue:bn<m 部分返回/单批网络失败非致命
  (记 NCM_ERR_NOTFOUND/r2),已物化保留+占位行;worker 首屏 track_ids_count>0
  降级非致命(snack)。修 92/200 首歌单点开整页 Load Failed。
- 歌单列表盘缓存 <data_dir>/playlists.bin(magic+ver+ts+数组):home_reload 先缓存
  秒渲 + 后台静默刷新,刷新失败有数据不置错态。修返回首页卡 LOADING(275 遗留)。
- 孤儿 .part/.tmp:evict/total_bytes LRU 扫描顺续删 mtime>7 天(in-flight mtime 新鲜安全)。
验证:新 bench/playlist_race_sim(竞态防串/批边界/非致命/盘缓存/reset)+cache_evict_sim
case9(7 天回收)+window/pagination/search/stream_sim 零回归+test_ncm_core host+mipsel
PASS;build_ncm.sh -Wall -Wextra 0 warning(text 978354/bss 1496484);0 真实请求。
遗留:appstate_logout 未联动删 playlists.bin;g_st.playlists[] 快照拷贝仍有极小交错窗。
2026-09-01 21:06:57 +08:00
MeowOS d8eeef5c57 feat(panel, netmgr §11): MeowPanel 网络管理页—netmgrd 期2 面板侧
后端(patch_v2/meowpanel/meowpanel.c,/api/audio 同款透传模式):
- GET /api/net/status    → 0xE0 概览 JSON(接口表 + WiFi/BT 摘要 + 速率)
- GET /api/net/conns?entity= → 0xE8 连接表(解析 entity|本端|远端|状态|寿命|域名 → JSON,transit 截断标记)
- GET /api/net/dns       → 0xEC status(mode/enabled/upstream/缓存命中/查询/deny/resolv_modified)
- POST /api/net/dns/flush → 0xEC flush(写级:HTTP 层走 require_auth;IPC 身份由 netmgrd 判定)
- GET /api/net/log       → TF MeowOS/logs/ 最新 net_*.log tail(256KB 上限,1000 行)
- netmgrd 不在/超时/应答异常 → 503 降级 JSON(NET_UNAVAILABLE),面板其它页不受影响
- 新增 --sock-net / env MEOW_NET_SOCK(宿主/qemu 测试用)

前端(web/,MD3/MIUIx 双主题沿用 270 落地件):
- 侧栏新增「网络」入口(#/net);接口卡 + WiFi 摘要 + 蓝牙摘要 + 每实体连接表
  (entity 前缀筛选 + 5s 轮询)+ DNS 代理卡(统计 + 清缓存按钮)+ 日志查看器
- netmgrd 不在 → 显示「网络管理服务不可用」占位 + 重试,不白屏不报错栈

验证:宿主 cc -Wall -Wextra 0 warning;mipsel 交叉编译 0 warning;假 netmgrd IPC 桩
(Python 帧级桩)联调五端点含 entity 过滤/flush 成功与 403/503 降级全通过;headless
Chromium 渲染 #/net(服务在线/离线两态)与 #/perf #/audio 无回归。
后端起二进制 md5: 21c70046476d3bf63e76f3d63e9eca2c(mipsel 静态)。

遗留(主会话部署时决策):netmgrd flush 写级依 SO_PEERCRED 判系统身份,面板 exe 位于
/usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/(非 /usr/bin/meow_* 亦非 os.meow.* 包)→ 真机
上 flush 会被 netmgrd 拒(403)。需在 netmgrd classify_exe 增认面板路径,或接受 403 只读。
不部署到设备(留主会话热更)。
2026-09-01 21:01:00 +08:00
MeowOS 52edf0c9e4 meow_netmgrd 期2:观测层(PID→实体归因+连接普查)+DNS 代理+事件日志+连接/DNS IPC 与 CLI
NETMGR_DESIGN.md 期2范围落地(全在 patch_v2/meow_netmgrd/ + meow_ipc.h 追加):
- 连接普查(§4):每 2s 解析 /proc/net/{tcp,udp}(IPv4) → socket inode→pid(由 inode 集驱动扫
  /proc/<pid>/fd,不逐 pid 盲扫)→ 三实体归因 app:<pkg>(amsd status)/svc:<svc>(svcd 落盘状态
  /tmp/meow_svcd.status,socket 查询兜底)/proc:<comm>(无则 proc:unknown)。连接表静态 ≤512:
  entity/五元组/状态/寿命;远端 IP 由 DNS 缓存反查域名。
- DNS 代理(§5):监听 127.0.0.1:53 UDP;启动备份 resolv.conf 写双行(127.0.0.1+原上游,崩溃零断流);
  透传转发+缓存(TTL 尊重;≤256 条);按源端口(port→entity)归因;域名规则 per-entity+global allow/deny
  (deny→NXDOMAIN+日志);atexit/SIGTERM/SIGHUP 恢复;5s 自检被第三方改回即重写;HibyOS 绝不启用/绝不改 resolv。
  DNS wire(deny NXDOMAIN / forward→A+cache / NXDOMAIN id 回显) 宿主+qemu 实测 PASS。
- 事件日志(§8):新连接/关闭/dns deny/接口状态变化;同 entity 同 dst 1 分钟去重(首条+计数)。
- IPC meow_ipc.h 追加 0x00E8 CONNS_REQ/RESULT、0x00EC DNS_CTL/RESULT(纯追加不改既有行;
  0x00EA 策略(期3)/0x00EE 事件推送(预留) 保留)。
- CLI meow_net 补 apps / conns [entity] / top(2s 刷新) / dns status|stats|flush。
- 内存:静态表禁 malloc,mipsel bss 228KB → RSS≈374KB,≤600KB 目标 / ≤1MB 预算。

宿主编译 -Wall -Wextra 0 warning;test_netmgrd.sh 48/48(宿主+qemu)、test_netmgrd_p2 全过(宿主+qemu)、
netmgrd_authtest 全过;真机(M6.0/0.8.0)只读实测 apps/conns/dns status 正确
(svc:meowpanel/svc:mdnsd/proc:dropbear 真实三实体归因),resolv.conf 测后核验原值未触碰。
2026-09-01 20:40:46 +08:00
MeowOS Dev 239345bfd2 merge: SDK 公共头按模块拆分(meow_ui.h 922行→67行伞头+16 子头 include/;API 零改动,meow_ui.a md5 不变 4786158d;agent-278 隔离分支验证) 2026-09-01 20:36:05 +08:00
MeowOS a5f675750b docs(ncm §25): REPORT 根因/修法/测试证据/新ncm md5(commit a01e5ca 配套)
§25 记录切音质后重播同曲恒「缓冲中」:根因(player_manager.c 同 idx 新 play intent
不被解码主循环识别)、修法(seq 化 intent 护栏 still_intent + 解码主循环 seq 断 +
外层自然播完收紧 state==PLAYING)、测试证据(player_flow_sim 新 P12 + P1-P11 零回归 +
stream_sim 全矩阵)、新 ncm md5 28698dd0e。用户所问「歌单离线缓存/手动刷新」顺带核实:
离线缓存未落地(appstate_load_playlists 每次网络拉取),手动刷新已落地。
2026-09-01 20:31:49 +08:00
MeowOS a01e5ca18a fix(ncm §25): 切音质后重播同一曲被卡「缓冲中」——play intent 以 seq 标识(同 idx 再 play 重启换档)
真机反馈:切音质后再播同一首歌,声音已出但播放页恒显示「缓冲中」。

根因(app/play/player_manager.c):同 idx 的新播放意图不被解码主循环识别。
切音质后重播同曲 = player_manager_play(idx),而 idx 与在播曲相同——cur_song 不变
(仍是 idx)、state 由 PLAYING 置 BUFFER、seq++。旧仍_current(cur==idx && state==BUFFER
恒真)与解码主循环(只认 cur!=idx / STOPPED)均不放过,于是:
  ①旧曲(旧档位)继续出声、state 停在 BUFFER → UI 恒「缓冲中…」(dl_done=0 无百分比);
  ②新档位下载/缓存条目从未发起;
  ③旧曲收尾后外层把 BUFFER 当「自然播完」顺延下一首——新档位永不生效。
三条取数路径(缓存命中/边下边播/.part续传)统一受影响(根因在 intent 层)。

修法:play intent 以 seq 标识,层层判别「我的 intent 是否仍有效」:
  - 新增 still_intent(idx, my_seq)(cur==idx && state==BUFFER && seq==my_seq),取代
    仍_current(同 idx 重播时 seq 变即判过期),贯穿 预缓冲/重试退避/物化/解码 open 各护栏;
  - run_song 入口捕获 my_seq,沿 song_local_file/song_download_attempt 下传;
  - 解码主循环 seq != my_seq → break 回外层重跑 run_song(换档重播);
  - 外层「自然播完」收紧为 state==PLAYING,不把 BUFFER(挂起新 intent)当播完顺延下一首。

验证:player_flow_sim 新增 P12(同 idx 重播换档重启)——修前恒 PLAYING 不了/键仍旧档/
downloads=0/eff=-1;修后 state 回 PLAYING、下载新档位、缓存键 1000_lossless、eff_level
按非会员钳顶回填 EXHIGH。P1-P11 零回归(宿主+qemu-mipsel ALL PASS)。build_ncm.sh
自有代码 -Wall -Wextra 0 warning。0 真实 NCM 请求。未部署真机(留主会话)。
REPORT.md 另记(docs commit)。
2026-09-01 20:30:59 +08:00
MeowOS 4534b840c2 sdk: meow_ui.h 公共头按模块拆分(伞头聚合,API 零改动)
- meow_ui.h(922 行)改为伞头:只做 #include 聚合 + 模块索引,不定义新 API。
- 新增 patch_v2/sdk/include/ 16 个子头(types/draw/image/theme/ime/widgets/input/
  activity/zorder/statusbar/dialog/log/notify/config/font/audio),各自自洽可单独 include,
  依赖最小化(不透明句柄前向声明 + 必要模块互引)。
- 验证:meow_ui.a md5 与主树完全一致(4786158d…,头重组 .o 逐字节等价);
  SDK .c 用 mipsel -Wall -Wextra 0 warning;hello/files 全量重编链接通过
  (-Wall -Wextra 0 warning);宿主 test_p1p2p3_repro(P1c/P2/P3 ALL PASS)
  + 15 个 SDK/宿主测试 PASS;16 子头自洽编译 0 warning;伞头完整性回归
  test_umbrella.c 0 warning;ncm 5 个含 meow_ui.h TÜ mipsel 交叉编译到 .o 0 warning。
- meow_ui_internal.h 及 view/nav/media/sys/vol 子头未动;build_sdk.sh 无需改。
- MEOW_APP_SDK.md 头部新增「公共头模块索引」。分支 sdk-header-split。
2026-09-01 20:30:16 +08:00
MeowOS 51281e609f docs(ncm §24): 对外名称按约定改「网易云音乐」→「流媒体客户端」(app.json) + REPORT 补注
task 约束:对外文档称「流媒体客户端」,不写「网易云/NetEase」。仅改显示名,package/exec 不变。
2026-09-01 20:01:13 +08:00
MeowOS 19e8fc93ed feat(ncm §24): 流媒体客户端边下边播阶段3——.part断点续传(Range/206/200/416)+LRU防淘汰(.tmp/.part跳过)+失败重试1次退避
按 devdocs/plan/NCM_STREAM_PLAY.md §8 阶段3:
- .part 断续传:中断留 .part,续传带 Range,206 追加 / 200 丢头段 / 416 删残留整首重下;URL 每次新拉
- LRU 防淘汰:evict/total_bytes 跳过 .tmp/.part(在播文件不被动过)
- 失败重试:预缓冲前网络失败重试 1 次,退避 800ms,重试拿新签名 URL(风控:0 真实请求,未部署)
- 顺手修:gb_to_bytes 返回 int 溢出致默认 10GB 上限失效 -> uint64_t
- 验证:stream_sim(续传 PCM 金标准+全矩阵)/player_flow_sim(P9/P10/P11)/cache_evict_sim(case7/8)/test_ncm_core 宿主+qemu-mipsel 全 PASS;build_ncm.sh 0 warning;ncm_host 离屏正常

md5 ncm f8f316a1e6f3bdd8f6b5921fdbb3f3a2
2026-09-01 20:00:14 +08:00
MeowOS fcff0b0ac1 fw_content: M6.0 收编 meow_netmgrd/meow_net 双形态条目(8f9303b5, agent-276 期1) 2026-09-01 19:59:02 +08:00
MeowOS 79a0fed336 meow_netmgrd 期1:网络管理服务骨架+接口统一层+CLI+IPC+日志框架
NETMGR_DESIGN.md 期1范围(→ M6.0)落地:
- meow_netmgrd 守护:svcd 托管 trigger=boot/restart=on-fail,进程内 HibyOS 只读降级
  (/tmp/meow_boot_mode != meow → 读可查/写拒+日志,热切换)。
- 接口统一层(§3):iface 枚举(/sys/class/net+/proc/net/dev,lo/wlan0/usb-eth 热插拔节拍检测);
  WiFi 状态/扫描/网络列表/连接/断开(wpa_cli popen 有界收割防挂死,257 教训);IP/路由/DNS 只读
  (ip 优先 /proc/net/route+resolv.conf);BT 状态/开关/已配对(hciconfig/bluetoothctl);rfkill 读写;udhcpc 续租/释放(MeowOS)。
- IPC 0x00E0-0x00E7(STATUS/IFACE/WIFI_CTL/BT_CTL+各自 RESULT):读级任意、写级仅系统身份(SO_PEERCRED
  同 svc_power),0xE8-0xEF 期2/3 预留(仅注释)。
- CLI 双形态 meow_net(status/iface/wifi/bt/rfkill/help):IPC 薄壳,help 本地必可用。
- 日志框架:TF MeowOS/logs/net_<bootts>.log(无卡降级 /tmp),接口状态变化/写操作必记。
- 单线程 poll、静态表、热路径禁 malloc;mipsel 静态(0 warning),宿主+qemu-mipsel 全过,
  真机只读验证 status/wifi scan/bt status 正确。

meow_ipc.h 仅追加 0x00E0-0x00E7 段(不改既有行)。
2026-09-01 19:55:26 +08:00
MeowOS cfeb9b775b chore(fw_content): 待打包标注口径 M2.98→M6.0(版本定案:M3-M5 系列号被历史里程碑占用,M2.97 后跳 M6.0) 2026-09-01 19:48:47 +08:00
MeowOS 4a49901bc1 feat(S89): M6.0 预置 meow_netmgrd.conf + 存量热更 exec(/usr/data→/usr/bin) 迁移(同 powermgr 模式; NETMGR_DESIGN); fw_content.conf S89 行同步 f3bb197f 2026-09-01 19:47:44 +08:00
MeowOS 980d161972 fix(launcher/amsd): amsd MSG_AMS_START 拉起缺口——pending-start 抽 meow_ams_start 模块复用启动链 + 负错误码符修复
根因(真机日志实证,M2.92 遗留):meow_ams_pending_start_set 误返回正错误码
(-MEOW_ERR_BUSY=+8/-MEOW_ERR_INVAL=+1),ipc_dispatch 的 rc<0?-rc:MEOW_OK 把「有前台 App
拒绝/空包名非法」误判为 OK(0) → amsd 报 OK 但 pending 未置位 → App 永不拉起(am start 报 OK
但界面没出现/进程未起)。

launcher:
- 新增自包含模块 meow_ams_start.c/.h(ops 注入 launch=launch_app/launch_activity、
  resolve=resolve_chain、log=logmsg):pending 状态 + 拉起决策 + picker 上下文。
- set 统一返回负 MEOW_ERR_*(根因修复);consume 复用既有启动链路真正拉起目标 App,
  列表未就绪(state!=0)保留 pending 待 2s 重扫重试,不再静默丢弃。
- main_menu run_menu 改经 meow_ams_start_consume 消费(保留 launch_fail_toast/菜单收尾);
  app_launcher 移除旧 g_ams_start/g_ams_pick_* 迁入模块;util meow_ipc_process 顶部同步 busy、
  meow_ipc_init 末尾 meow_ams_launcher_init()。
- build_launcher.sh 编入 meow_ams_start.c;meow_launcher_internal.h 更新挂点声明。

amsd(协议层归属说明):cmd_start 的 busy 判定对照 launcher ACK 正幅度(busy=8),
原 (uint32_t)MEOW_ERR_BUSY=0xFFFFFFF8 永不匹配 → busy 落入 launcher offline,改 (uint32_t)(-MEOW_ERR_BUSY)。

验证:宿主 test_ams_start.c 33 断言全 PASS(合法->拉起链、activity chain 透传、空包名/非法/busy
不拉起不崩、列表未就绪保留 pending、无路由 NO ROUTE、启动失败 idx 回报、picker 取回);
launcher 目录全量回归 test_media/bgplay/depends/notif_panel/notif_tf/settings_brightness/
vol_ramp_jack/host_icon + test_font_tier/weight 全 PASS;mipsel -Wall -Wextra 0 warning。
新 md5:meow_launcherd a8200ae1b0aa68d7262749db87377d49、meow_amsd 7ddc677ce7938bc9b95c83c17bc78295。(M2.98 待打包,未刷机)
2026-09-01 19:40:29 +08:00
MeowOS 7e7e45ae26 docs(APP_DEV_GUIDE): 第三方开发者上手指南 + 校订 SDK 头状态行 + README 索引
新增 docs/APP_DEV_GUIDE.md(从零到上架动线:环境→最小App→工程结构→
构建打包→签名安装→部署调试→SDK能力速查→红线→FAQ),全部以仓库现状为准;
修正 hello app.json 全字段表(framework/immersive/background_audio/depends/
icon/version+version_code)、meow_audio_*/通知/媒体会话/字体档位等事实。
同步:MEOW_APP_SDK.md 头部「设计稿→实现版」状态行(只动状态行,正文不动);
README.md 文档索引加该指南链接。

说明:hello_app 实际无独立 app.json(包装到 MeowAPPs/<pkg>/ 根目录),指南按真实
打包形态描述;nyai 无独立 pack 命令,出包用 nyai export 或 tar czf;.mpk 须在 tar 根。
2026-09-01 19:38:52 +08:00
MeowOS 1e97dbf54a ncm search: 历史+单曲/歌单分类+分页+误触修复+不覆写歌单上下文 (§23)
- 搜索历史:进页自动弹键盘;meow_config 持久化 ≤12 去重最新在前;空态=历史列表(点搜/左滑删/清空)
- 结果分类单曲/歌单页签:独立结果集+独立滚动;歌单行进详情(playlist_open_pl);单曲行播放
- 触底分页:offset 累加 limit=30/页 cloudsearch 同参数;每类各记 offset
- 误触抑制:g_press_row + g_press_delta>10(对齐 home/playlist §13.3)
- 结果改独立数组(不写 g_st.songs[]);播放搜索单曲建搜索队列(queue_src=SEARCH),不弄丢歌单窗口
- core cloudsearch 扩展 type/offset/limit/total + 歌单解析
- bench/search_sim.c 离线回归(历史/独立集/翻页 offset/队列上下文);host searchtap 误触自检

验证:mipsel -Wall -Wextra 0 warning;core host/mipsel PASS;search_sim PASS;
宿主离屏 search(单曲)/searchpl(歌单)/searchhist(空态历史)/searchtap(误触) 均正常;0 真实 NCM 请求。
2026-09-01 19:21:16 +08:00
MeowOS d4928224a5 feat(S89): M2.97 预置 meow_powermgr.conf + 存量热更 exec(/usr/data→/usr/bin) 迁移; fw_content.conf 同步 M2.97(launcher 899ac22c M4+depends / audiod 176c840d 电平监控+锥度 / watchdog 553840cd 锥度 / powermgr 469dddb7 新入固件 / nyai a7d24553 depends) 2026-09-01 19:11:42 +08:00
MeowOS 7bd6962ccb feat(ncm §22): 播放页重做——封面页/歌词页双页(NCM_CACHE_DESIGN §6 定案)
- 去标题栏(不再用 ncm_topbar);返回=左缘手势/系统返回钮语义
- 双页结构:封面页/歌词页,屏幕正中左/右滑切换(与左缘≤24px返回、顶部通知分流),
  页面指示点显示当前页;中部横滑=on_touch_move 锁存 page-drag + on_tap/on_swipe 判向
- 封面页:方形大图+圆角(无旋转动画)+进度条(点 seek)+MD3 矢量控制钮(prev/play/pause/next,
  新增 app/ui/ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)+元信息+实际音质档
- 歌词页:LRC 滚动(当前行居中高亮、上下渐隐)+点行跳转(新增 lyric_line_time);
  默认=封面模糊置底(封面铺底+主题 scrim),省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode)
- 日夜间:主题配色只映射到半透明叠加层/scrim/accent(红),不动沉浸底(NCM_PLAYER_BG 恒定)
- 分带刷新沿用:封面页=封面静态带+动态带(互不重叠 bbox,tick 只刷动态带);歌词页换行(~5s)才重画
- 性能:宿主 4 页 PPM 核验+手势自检 PASS;mipsel -Wall -Wextra 0 warning;qemu core 回归 PASS;
  0 真实请求(封面/歌词用本地样本注入)。未部署真机(主会话统一)

md5: ncm 730e0b6e / ncm_host 500ac85 / player.c 00934de / ncm_icons.h 20f0bba
2026-09-01 18:52:14 +08:00
MeowOS ed2ae10320 app.json 二进制依赖 depends:启动前检查(launcher)+ 安装提示(nyai)
需求(用户定案):App 复杂功能若依赖现成 Linux 二进制(板载 /usr/bin 或卡上
MeowOS/opt/bin)能效更好,声明后系统启动 App 前检查依赖是否存在。

- depends 行式声明:depends=curl,jq:optional,ffmpeg:required(无标注默认
  required;也接受 :opt/:req;未知修饰符按 required 处理并 WARN;空项/超量丢弃)
- launch_app_chain 冷启动时按序查 /usr/bin/<name> 与 <卡>/MeowOS/opt/bin/<name>
  (X_OK,两类命中即满足);缺 required → 不启动(toast「缺依赖: <name>」+ 日志,
  沿用 CANNOT START 风格);optional 缺失 → 仅 WARN 放行;只在启动时查,不做周期复查
- nyai install 对包内 app.json depends 同查并提示(不阻断安装,缺 required 标注
  「安装后启动将失败」)
- 新增 meow_dep.c(独立 TU,解析/检查/块原因),宿主测试 test_depends.c
  解析矩阵 12 用例 + 假根检查路径 12 用例全 PASS;launcher/nyai mipsel 交叉
  -Wall -Wextra 0 warning;launcher 现有宿主测试回归全过
- 文档:APPS.md §2.1 字段表 + docs/MEOWOS_SPEC_REF.md §5.5 同步;hello app.json
  加 depends:curl:optional 示例

launcher md5 899ac22c10f596909d7f35ea685744de(fw_content.conf 已更新;/usr/bin
只读,留给主会话打包固件)
2026-09-01 18:28:07 +08:00
MeowOS 9e25db77cc feat(ncm §21): 边下边播阶段1+2——stream_src流式源+decoder双路open_cb+下载线程预缓冲/stall联动+P2 HTTP空闲超时+cache fd写者+stream_sim全矩阵PCM金标准(0真实请求,未部署) 2026-09-01 18:23:17 +08:00
MeowOS dbee37e1d6 chore(ncm): app.json 补 icon 字段(与卡上对齐) 2026-09-01 18:13:28 +08:00
MeowOS 8f8ffa605b fix(test): test_settings_brightness 期望值随亮度默认 50 同步(M2.97 陈旧) + ncm app.json 声明 background_audio=1(接 M4 后台播放) 2026-09-01 18:12:46 +08:00
MeowOS 5aabede0cd feat(launcher): M4 后台服务保活+后台播放(FRAMEWORK_DESIGN §5/§9 落地)
- app.json 新字段 background/background_audio 解析入 struct app/meow_acct 注册表
- meow_ipc.h:MSG_REGISTER(m_register_t)/SERVICE_OK(m_ack_t.status=pid)/SERVICE_STOP/
  MSG_SYS(m_sys_t, SHUTDOWN=1/SVC_DEAD=2) 负载结构落地
- 新模块 meow_bgplay.c/h(自包含+ops 注入,仿 meow_media,宿主可单测):
  · 后台播放保活——声明 background_audio 且媒体会话 PLAYING 的 App nav-exit 不终止,
    进程+音频流保留;UI 冻结走 §10.5 /tmp/meow_notif_active SDK present 自抑(非字面
    SIGSTOP:冻结同进程播放线程断供 PCM,§10.5 真机实证);点图标/媒体卡片回前台=
    原进程恢复(chain/picker 忽略),停播后再退出=真退 SIGTERM
  · 无头后台服务——MSG_REGISTER spawn/stop/reap + 退避重启 1/2/4/10s cap +
    max_restart 熔断(清该 App 通知 + 注册连接广播 MSG_SYS{SVC_DEAD},重注册复位)
  · 卡拔例外(卡上二进制后台 App/服务仍 SIGTERM 防 SIGBUS)、崩溃照收、
    关机前 meow_bgplay_kill_all 有界回收(含 MSG_SYS{SHUTDOWN} 广播)
- util.c:MSG_REGISTER/MSG_SERVICE_STOP 分发(权限/声明校验链)+ ipc_conn_alive 连接
  存活查询 + meow_ipc_process 末尾挂 meow_bg_housekeep(drain/reap/退避 tick)
- app_launcher.c:launch_app_chain 入口 resume(bg_take 原进程回前台)+ nav-exit
  保活分支 + 转后台出口(杀看门狗/写自抑标记/复位顶条,跳过退出尾)+ 冷启动自动
  拉起静态 keepalive(mode=0) 服务(exec 路径约定=App 同目录 <svc> 文件)
- main_menu.c:应用列表「·后台」标注(宫格/列表,1s tick 按 change_seq 刷新)+
  两条退出尾 kill_all + unlink NOTIF_ACTIVE
- 宿主单测 test_bgplay.c 50 项全过(真 fork 子进程走状态机 + 假时钟退避熔断);
  test_media/test_notif_panel/test_notif_tf/test_vol_ramp_jack 回归全过;
  zig-mipsel-cc -Wall -Wextra 0 warning。launcher md5=c07d011c0769fca14d3bfb752d735463
2026-09-01 18:09:09 +08:00
MeowOS 7db8197f9c fix(settings): display.brightness 默认值改 50(100 太亮; 存量 settings.conf 用户值优先不受影响) 2026-09-01 17:37:26 +08:00
MeowOS bce6a9a1f4 docs: 新增 docs/FIRMWARE_BINARIES.md 固件内置组件清单(源自 fw_content.conf) 2026-09-01 16:28:53 +08:00
MeowOS 0eb866d716 fix(settings): display.brightness 默认/上限 101→100 — 背光量程 0-101 且 101 满量程 PWM 闪屏(用户实测); clamp 上限 100 已有(M2.39),本次把 g_defs 默认与滑条上限一并收齐 2026-09-01 16:28:13 +08:00
MeowOS 677c0c47ac feat(meowpanel): 面板 MD3/MIUIx 双主题 × 明暗四套变量表 + 主题切换器(跟随系统/浏览器/明/暗)
- CSS:色值全量抽为 :root 变量,[data-style]×[data-mode] 四套主题表;
  MIUIx 色值对齐设备 meow_ui_theme.c(light F7F3EE/F57200、dark 181310/FF8C4A、radius 20/28、spacing 12、细描边);
  根属性 data-theme 更名 data-mode
- JS:默认联动设备 settings.conf(GET /api/theme 的 ui.style/display.theme);
  面板覆盖存 localStorage(meowpanel.style / meowpanel.mode),不写设备;
  模式 system/browser/light/dark,browser 走 prefers-color-scheme 并监听日夜切换实时生效;
  旧键 meowpanel.theme 自动迁移;顶栏切换器保留原行为(本地生效 + PUT /api/theme 同步设备)
- 设置页新增「主题(面板外观)」卡:风格 MD3/MIUIx(可一键回跟随设备)+ 明暗四选一
- 后端未改:/api/theme 已透传 ui.style/display.theme,无需新增
2026-09-01 16:26:02 +08:00
MeowOS 250c9ae8b1 feat(powermgr): meow_powermgr 期0调研+期1骨架——功耗管理专项服务(POWER_MANAGER_DESIGN)
期0: POWER_HW_FACTS.md 真机只读取证——battery/无current_now(实时功率=derived+estimated
双轨)、无cpufreq(省电档无降频点)、背光量程0-101(非255)、rfkill可用、axp_battery为
幻影设备、无thermal zones、audiod STATE字段完备。

期1: patch_v2/meow_powermgr/ 单二进制双形态(meow_powermgr守护+meow_power CLI):
- svcd托管 trigger=boot/restart=on-fail;HibyOS只读降级(/tmp/meow_boot_mode!=meow
  只采样电池,不碰wlan0/bt/声卡,热切换)
- 权威充电链0-diff照抄svc_power(mp2731→fusb302→battery兜底)
- 电池深加工 state/cap/mV/health/续航估算(容量差分速率环 30s×240)
- 经验功耗模型(/usr/data/meow/power_model.conf 可调,内置默认全部标注待标定)
- IPC /usr/data/meow_pm.sock MSG_PM_QUERY(0x00D0)→MSG_PM_RESULT(0x00D1) JSON,
  subset位;meow_ipc.h 0x00D0-0x00D7 号段回填
- CLI status/estimate/model 落地(IPC优先+直读降级),history/top/events/policy 期2/3桩
- 宿主桩测试36+1例全过(MEOW_PS_DIR等假节点)+qemu-mipsel全过+真机对照svc_power
  battery口径一致(93%/4270mV);mipsel静态117KB(<200KB);-Wall -Wextra 0 warning
2026-09-01 16:21:12 +08:00
MeowOS 6a4d330a26 feat(audio): CS43198 音量按 DS1039 数据手册定档 — 感知平方锥度映射(dB衰减=(1-pct)²×96dB, pct=0→真静音255)四路统一; SPEC_REF 补数据手册事实(PCM_VOLUME_A/B=0x0B/0x0C,DSD独立音量); DSD 模式记入 AUDIOD_DESIGN §13 后期规划。audiod 47/47 PASS 2026-09-01 16:08:01 +08:00
MeowOS bdd9e898f4 feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ
- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
  + level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
  + song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
  + vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
  刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
  fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
  生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
  test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
  mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
2026-09-01 15:55:42 +08:00
MeowOS ab4dfb4f9c docs: 文档一致性整理 (agent-265) — 对齐 M2.91-96/0.7.86-0.7.91 + 261/262/264
- README: 版本头 M2.90→M2.96;M2.91-96 批次( audiod FPGA 开机武装根治 PCM 死锁 / 充电权威链 mp2731 / amsd 注册自愈 / NP 死循环修复+充电闪电 / 插孔消抖三路 / 长流护栏+几何 period=1024×4 / wd_vol_save 毁设置修复 / 音量反转四路统一 dac=255-pct*255/100 / 炸耳修复 dac_vol_mute=255 );流媒体客户端三期续(254 滑窗/256 封面缓冲 UX/260 FLAC dr_flac/262 播放键 toggle);已知问题 PCM 挂死条目移除(已根治)
- MEOW_APP_SDK §5.9: 后续批次全量(FPGA 武装/长流护栏+几何/音量反转+炸耳修复/261 电平监控 rms/peak+monitor CLI, ABI 兼容);IME 节后注拼音 IME 已落地
- FRAMEWORK_DESIGN: 头部里程碑 M2.15-M2.96;附录 B PLAYCTL 补 cmd=6 toggle
- SPEC_REF: §4 audiod 条目补 M2.91-97 加固批;音频硬件事实补衰减量语义+端口 5=audiod 强制重烧约定;现行版本 0.7.91(261 待 M2.97);252 面板 /api/audio+字体页;OPEN_QUESTIONS 待定批更新(PCM 挂死已清结)
- SYSTEM_SETTINGS: audio.volume 衰减量注记 + 音量存盘读改写单键纪律
- KEY_CONTROL §9: SDK 插孔消抖已落地(56c569f) + 音量反转/存盘修复 + 262 播放键 cmd=6 toggle
- APPS §3.7: audiod 加固批摘要 + 262 toggle
- DEVELOPMENT_PLAN: 头部截至 M2.96
- 口径: 入库文档应用名统一中性表述「流媒体客户端(dev.player.ncm)」
2026-09-01 15:41:33 +08:00
MeowOS ad9778476a fix(§10.5): 通知中心叠加媒体保活——播放中 App 不 SIGSTOP(修播放中滑通知中心音乐停)
- launcher: run_app_notif_overlay 入口 meow_media_app_playing() 判定(当前源+PLAYING+app_id 匹配)
  → 不 SIGSTOP/不 SIGCONT,App 续跑保 PCM;非播放中维持 SIGSTOP 模板;关闭时 unlink 标记挪到
  恢复帧上屏后(防未冻结 App 抢 present 一帧回闪)。
- SDK gfx: present() 见 /tmp/meow_notif_active(env MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏,
  仅 App 进程(系统 UI/launcher 不抑制),200ms 节流 stat,脏区累积解除后一次上屏;
  meow_gfx_overlay_suppressed_count 供单测断言。
- SDK input: 叠加期排空丢弃本进程 evdev 副本 + EVIOCGKEY 位图静默对齐(防面板点击被 App
  双重消费/解除后边沿误判),进入沿复位触摸状态机。
- 宿主:新 test_overlay_suppress T1-T4 + test_media §8(8 用例)全 PASS;test_notif_*/
  test_sdk_*/test_vol_ramp_jack 等回归全过;zig-mipsel -Wall -Wextra 全 TU 0 warning。
- meow_launcherd fbb82036(/usr/bin 只读待固件,fw_content.conf 已更)/ meow_ui.a d6242fc2 /
  hello a073535a / files 2b799d07(已热更到卡);ncm 未重链(ncm_app 他 agent 在改)。
2026-09-01 15:23:32 +08:00
MeowOS 820d1e52bf fix(media): 物理播放/暂停键(164)改发 PLAYCTL cmd=6 toggle + App 前台状态栏音量%实时刷新
Bug1: SDK meow_ui_media 按 g_last_state 本地判 play/pause,但 ncm 走自有 ipc_media
线程不经 SDK meta 上报 → g_last_state 恒 0 → 恒发 cmd=1(播放),播放中按键无效。
修:SDK 164 恒发 cmd=6 toggle(删 g_last_state 判定),ncm apply_playctl case 6
按 player_manager_state() 当前态翻转(源是状态权威);launcher meow_media/util
本就透传 cmd 不动结构,仅 meow_ipc.h/SPEC_REF 注释补 6=toggle。媒体卡片/NP 页
按钮(按 launcher 侧 s->state 判,来自 META 是准的)维持现状。

Bug2: App 前台时状态栏为 launcher 叠加层,App 内 SDK 改音量/亮度只写
settings.conf+DAC,launcher meow_settings 内存表不感知 → g_sys.vol 停旧值。
修:app_overlay_statusbar 前插 app_overlay_settings_poll——1s 节流 stat
settings.conf,mtime 变才 meow_settings_init 重读+sync 缓存+g_sys.vol/brightness
(meow_settings 新增 meow_settings_file_path() 访问器防硬编码分叉)。

验证:宿主 test_sdk_input_keys 19/19(K4 改 toggle 断言)、test_media +cmd=6
透传用例 ALL PASS、ncm bench/toggle_sim 10/10(新)、player_flow/cache_evict
sim PASS、ncm core 宿主+qemu-mipsel PASS、test_settings_brightness PASS;
mipsel 交叉 -Wall -Wextra 0 warning(改动文件单独验+整构建)。
真机(0.7.91):ncm 热更(.new+mv)后 autoplay(Docu) 起播缓存曲 → meow_ipc
playctl 6 二连发 → paused(pos=139691)/resumed 日志实证 toggle 全链路。
md5:ncm 8f10eee5 / meow_ui.a 7b359ec9(gitignore)/ launcher eba4f2b2
(/usr/bin 只读,fw_content.conf 已更新待固件)。物理键 164 实体按压留用户终验。
2026-09-01 14:44:55 +08:00
MeowOS 9a4ea10c64 feat(audiod): DAC 实时监控 — 数字域电平表(100ms窗口整数RMS/peak,deci-dBFS)+1s监控日志(TF audio_monitor_<bootts>.log,流停即停)+CLI monitor 电平条;STATE 补 rms/peak/lvlseq 键(ABI兼容)。宿主47/47,qemu+真机0.7.91 tone验证(1kHz 0.3幅:rms=-13.9 peak=-10.8dBFS,hwrate~44.1k健康)。audiod ab193a2b meow_audio 891ab264 2026-09-01 14:25:51 +08:00
MeowOS 70aec677ce ncm: FLAC 解码落地(dr_flac 0.13.4 单头入库) + 缓存键=歌曲ID+音质档位
- 解码分发 decoder.c:magic(fLaC→dr_flac / 其它→minimp3);flac ≤16bit→S16、
  >16bit→S32 满幅送 audiod(AUDIO_FMT_S32),rate/ch/bits 按实际流,零 SRC 零裁剪
- song_url_auto 接受 flac(type 权威/url 扩展名兜底),降级语义不变;flac 下载限幅 1GB
- 缓存键 song_<id>_<level>.<ext>:同歌同档命中不重下,换档新条目旧档留 LRU;
  ext 前导点正名(修双点名),lookup 回退历史双点名+旧无档键温和兼容
- audio_out S32 通路(AUDIOD/ALSA/WAV)+ 流死重开按 bits 还原格式
- 验证(0 真实请求):44.1k/16 与 96k/24 自制样本宿主+qemu 帧数/时长精确、
  逐样本零误差;mp3 回归逐帧一致;player_flow_sim P1-P6(新 P6 档位键/flac ext)、
  cache_evict/cover/pagination/window/audiod_chain sim 全 PASS;mipsel 0 warning
- REPORT §19 / VENDOR_NOTES dr_flac 条目;未部署真机(留主会话)
2026-09-01 14:14:38 +08:00
MeowOS 80bc191593 build: fw_content audiod 772cc3d8(静音安全修复) 2026-09-01 13:51:31 +08:00
MeowOS c1353ce28f fix(audiod): 安全修复 — dac_vol_mute 写255(真静音),旧版写0=CS43198最大响度,重配/回收瞬间满幅炸耳(真机事故)。772cc3d8 已热跑验证,38/38 PASS 2026-09-01 13:50:26 +08:00
MeowOS 109d48747f fix(audio): CS43198 音量寄存器=衰减量(0最响255静音),四路(launcher/audio.c+audiod+watchdog+SDK vol)统一反转映射 dac=255-pct*255/100 — 修「0档炸50档最低」。真机 audiod 热修版实测 L/R 值方向正确 2026-09-01 13:34:58 +08:00
MeowOS 84fd0a87aa fix(audiod): ALSA 几何收窄 period=1024×4(tiny_alsa惯例) — 根治长流停摆(驱动默认巨buffer致hbc3000桥周期中断稀缺wedge; tonepcm2矩阵实测60s零stall)。宿主38/38 PASS 2026-09-01 11:06:01 +08:00
MeowOS 805fcf179a chore(ncm): 随 meow_ui.a(6a82b97e, sdk meow_ui_audio 257护栏) 重链 6491ea07 并热更到卡 2026-09-01 10:36:29 +08:00
MeowOS e1e1331e44 fix(audiod): 长流卡死根治 — 防卡死护栏全量落地(AUDIOD_DESIGN §12)
根因(级联死锁): 内核驱动 wedge(hbc3000/cs43198 时钟域族, write/PREPARE/amixer ioctl
卡死在内核) → 播放线程永不返回 → ring 填满 → IO 线程在 ring_write 无超时 cond_wait
永久睡眠 → accept 停摆(backlog 满后 CLI ECONNREFUSED) → ncm 堵 socket 写 → 全链冻结。
同效路径: popen_read_tmo 无界 waitpid 随挂死的 amixer 子进程拖死持锁线程。

修法:
- ring 写有界等待(MEOW_AUDIO_RING_WAIT_MS 缺省 5s)超时丢弃+计数(dropped=), IO 永生;
  等待期 sid 代际守卫(抢占中等待的旧流 PCM 不再注入新流 ring)
- 全部 cond 等待 timedwait 化(播放 1s 周期自愈, g_cond 改 CLOCK_MONOTONIC);
  信号处理器只置 g_stop(不再调非 async-signal-safe 的 cond_signal)
- popen 收割有界: WNOHANG 300ms → SIGKILL 300ms → D 态放弃收割(泄漏换永生);
  路由线程 amixer 调用不持 g_mu; QUERY 改报 g_route_cur 缓存(IO 热路径不再 popen)
- ALSA 改 O_NONBLOCK + poll(POLLOUT,1s): 连续 3 次停摆判 wedge → PREPARE 自救无效即
  关卡进丢弃模式(stall=), 下一路 OPEN 重开重试
- server→client 下行帧单发 write 写不全即整帧丢弃(txdrop=), 杜绝半帧错位/慢客户端堵 IO
- 主线程 watchdog: IO/播放心跳 10s 静默告警; io 线程 poll 出错不退出(listener 不丢);
  backlog 4→8; RESUME/PAUSE 补 cond 唤醒
- SDK meow_ui_audio.c + CLI: connect 非阻塞+poll 2s, SO_SNDTIMEO/RCVTIMEO 2s
  (audiod 真死时 App 报错重开, 不再永久阻塞)

验证: 宿主 38/38 PASS(旧 19 回归 + 新增 T5 长流 3min 等效 31.7MB 持续灌 played==written
underrun/dropped=0 喂流中 listener 探活 15/15; T6 wedge 故障注入生存 — 旧代码同款注入
挂死(90s timeout 强制 kill), 新代码 IO/accept/preempt/SIGTERM 全活; T7 慢客户端 tx 丢弃);
qemu-mipsel 静态二进制 status/tone 全通(written=played=13230 underrun=0);
-Wall -Wextra 0 warning(host+mipsel+SDK 双端编译)。
meow_audiod md5 86a8b6793d516c90bbd12c2997b30d8f / meow_audio c23b96490842f54324b855fd9c2467a5
(fw_content.conf 已更)。暂不部署真机(主会话统一); meow_ui.a/App 重链留主会话。
2026-09-01 10:32:15 +08:00
MeowOS 243f328499 fix(watchdog): build_watchdog.sh ROOT 路径修正(../..) 2026-09-01 10:29:22 +08:00
MeowOS c8cd6c3a54 chore(watchdog): 补 build_watchdog.sh(交叉+宿主测试), 对齐各组件 build_xxx.sh 惯例; 固件打包唯一入口仍是 scripts/build/build_fw_apps.sh 2026-09-01 10:27:53 +08:00
MeowOS 104bfe2d21 fix(watchdog): wd_vol_save 不再 O_TRUNC 毁 settings.conf(改读改写单键+去重,与SDK同款语义) — 真机实证整文件被抹/重复行/丢max+step。test_wd_fw 17/17 PASS; 52f2dadd 已热更 /usr/data/meow_watchdog 2026-09-01 10:25:18 +08:00
MeowOS 56c569f3a9 fix(sdk/input): 框架App侧插孔消抖 — 插孔变化后800ms内忽略音量键边沿(与launcher/watchdog 255同款,补齐SDK侧缺口); meow_ui.a 12cdd5d7; hello 34b01e76/files 2f44e27e/ncm 52412137 重链热更到卡。test_sdk_input_keys PASS 2026-09-01 09:28:09 +08:00
MeowOS 6200a41b71 fix(ncm): 三连修 — 封面切歌不更新/缓冲无反馈失败无声/暂停切歌边界 (REPORT §18)
- cover worker 忙等死循环(成功后不清目标,每圈解码+换入+gen++,CPU 100%+每帧整盘重绘)
  → 成功清目标转怠速; 失败指数退避 0.5-4s 连 8 次放弃+WARN 日志; 去重加在拉目标;
  cover_get 持锁+cover_put 修绘制期 UAF; g_host_mock 防真网守卫
- 封面兜底污染修复: 不再把单曲 al_pic 写进 g_st.cover_url(否则 al_pic 缺失曲回落成
  上一首封面且撞去重 → 封面永停); plr_page_enter 强制重估自愈; pl_page_enter 回填歌单封面
- 缓冲 UI: BUFFER 态「缓冲中… N%」(g_play.dl_done/total, 下载回调累加); 播放错误中文
  错误条+snack(旧版 err 无 UI 消费者=无声死等); STOPPED 态播放键=重试; play/next 清 err
- 写失败 set_err 修连环跳歌(旧版静默 break 被外层当自然播完,流死时跳完歌单+狂发 song_url);
  still_current 护栏修下载途中切歌的状态/快照过期覆盖
- 验证(0 真实请求): cover_chain_sim 7 组+player_flow_sim P1-P5 全 PASS, 旧代码对照
  T2/P2/P4 必败坐实根因; audiod_chain_sim 14/14; pagination/window/core+qemu 回归 PASS;
  宿主+mipsel 自有代码 0 warning; 9 页离屏渲染 rc=0 (含新 mock 页 playerbuf/playererr)
- ncm md5 50d41bbf4a379e925699ba013b997ffd (未部署真机, 留主会话)
2026-09-01 05:29:09 +08:00
MeowOS 614694ad4a fix(launcher): Now Playing 死循环修复(空态不再每帧整页重画,修100%CPU/卡死) + NP页接管电源键(单击息屏/亮屏) + NP页自动息屏 + 状态栏充电闪电图标(sb_icons 新增 bolt, charging 时电量图标左邻显示)。宿主 test_sb_statusbar PASS; launcher 6de97628 待入 M2.92 2026-09-01 04:40:34 +08:00
MeowOS 4bafdb5d69 fix(keys): 插孔消抖 + 音量 ramp 3s 上限 + 亮度写日志 — 耳机插拔 ADC 幽灵音量键修复
真机实证(meow_20260901_034946.log): 4.4 插入瞬间电气瞬态在 jz_adc_aux(event2)
产生 VOL_UP 幽灵按住, vol_ramp 24%→上限50 (225391 ramp 起→227213 爬满,
紧随 audiod jack changed). 修(限 meow_launcherd/+meow_watchdog/):
① launcher audio.c + watchdog 各自读 /sys/class/switch/{headset,balance}/state
  (节流缓存 1s/500ms, 热路径不每圈读文件), 变化后 800ms 内忽略音量键边沿
  (vol_ramp_begin 不起/在爬 ramp 中止/wd_vol_adj 单步忽略)
② ramp 总时长上限 VOL_RAMP_MAX_MS=3000 兜底防一次幽灵打满(正常长按 24→50 ~1.4s 不伤)
③ write_brightness(val,src) 全 10 调用点带源+逐写 logmsg, 亮度跳上限下次可定位
  (grep 全库: 写 backlight 仅 launcher/watchdog/svc_power/bootmenu, 无确定 255 路径)
宿主: 新 test_vol_ramp_jack 15/15 PASS(单击/长按回归+3s cap+消抖忽略/中止/恢复);
test_wd_fw 17/17 + test_settings_brightness + test_notif_tf PASS; MIPS -Wall -Wextra 0 warning
(顺带修 watchdog 既有 sys/poll.h→poll.h 重定向 warning).
watchdog 已热更设备 /usr/data(baa997ea); launcher 7be1d4eb 待固件. fw_content.conf 已更新.
遗留: SDK meow_ui_input.c 音量键路径需同款消抖(sdk/ 有主未动)
2026-09-01 04:28:10 +08:00
MeowOS 44d170413d build: M2.91(0.7.86) fw_content — audiod a6e4c295(FPGA武装)/svc_power 39ae3aa2(充电权威链)/amsd da9a5004(注册自愈)/launcher 2aa61780(MIUIx+充电态轻刷) 2026-09-01 04:04:51 +08:00
MeowOS 997f89f879 ncm 三期三批: 歌曲对象瘦身(1324→608B)+滑动窗口(NCM_SONG_WINDOW=256, 取消1000截断支撑任意长度歌单)+播放快照解耦 — agent-251
- core/ncm_api.h: ncm_song_t 瘦身 id24/name128/art96/al96/pic256/dt int32 (1324B→608B)
- appstate.c 滑动窗口: song_ids 全量 trackIds 索引(16384) + songs[256] 窗口感知
  (song_valid 位图/window_base/滑窗 memmove 保留重叠), ensure_range 批网格(100)对齐
  预取, songmeta.c 盘缓存 <cache_dir>/meta/<id>.bin (回滚/二次进入 0 网络请求)
- playlist.c 触底分页退役→视口 ensure(±24 行), 占位行可直点(播放线程 fetch 物化),
  footer 改视口区间/总数
- 播放兼容: g_play.play_song 快照(mu), run_song 起播物化后全链路(下载/歌词/IPC META/
  播放页 UI)读快照——窗口滑出当前播放曲不崩; 边界 song_count→track_ids_count
- 验证(0 真实请求): pagination_sim 全 PASS(5191 无截断/52 批); window_sim 6000 首
  全程下滚 750 步视口全物化/song_detail 恰 60 次/回滚+0 请求/播放 fetch 不滑窗;
  core 宿主+qemu 全 PASS; mipsel -Wall -Wextra 0 warning, bss 2052068→1080900(-47%)
- 未部署真机(设备音频线排查中, 部署留主会话)
2026-09-01 04:03:01 +08:00
MeowOS d8898afa09 fix(audiod): 开机武装 hbc3000 FPGA — 启动时无条件写 Output Port Switch(4.4→3/3.5→2/无插孔→4, 同值先写5强制重烧), 根治 PCM 启动内核死锁(未烧 FPGA 时第二次 hbc3000_start 挂死); port_name 补 4/5。真机验证: arm 日志正确+tone played=88200 underrun=0 不挂 2026-09-01 04:02:33 +08:00
MeowOS ca2290e691 fix(amsd): 注册表扫空自愈 (agent-250) — 根因: svcd trigger=boot 拉起 amsd 时 TF 卡未挂载, 启动首扫打空挂载点且此后永不重扫 → am start 报 pkg not registered (M2.90 真机日志实证). 修: registry_scan 改合并式重扫(只刷静态字段, 运行时状态/DECLARE路由保留, 源不可达绝不清表) + 主循环自愈重扫(源未通 3s 快重试/常态 30s 合并重扫) + am rescan 手动触发 + 兼容 M2.90 固件 launcher 旧 5 段 EV LAUNCH(nt==5 无 chain, epoch 不再错当 pid). 宿主+qemu+真机全链 PASS: 空目录启动 4s 自愈出 3 App, 真机 am start dev.player.ncm→OK(真pid)→kill 全通. meow_amsd da9a5004 / am 9e323faf (fw_content.conf 已更新, 待 M2.91 入固件闭环冷启动) 2026-09-01 03:41:19 +08:00
MeowOS 7cb1bdace4 meowpanel: 新增 /api/audio(audiod MSG_AUDIO_QUERY→STATE 透传, 不在时 available:false)+ 前端「音频」页(5s轮询只读)与「字体」页(列表/上传进度态/导出/409冲突确认force重试); 后端 md5 015f9cf1 2026-09-01 03:40:28 +08:00
MeowOS f149e24f28 充电态权威源 mp2731-charger/status(修 battery/status 撒谎恒 Discharging)
真机实测:设备充电中但 battery/status 恒 Discharging;权威源为
mp2731-charger/status(Charging/Full),回退链 fusb302-typec-source/online==1
→ 旧逻辑 battery/status==Charging || ac/online==1。电量仍读 battery/capacity。

- meow_svc_power: battery/status 命令充电判定走权威链(MEOW_PS_DIR 可重定向供宿主自测);
  svc_power_authtest 补 6 用例(mp2731 Charging/Full/Discharging + 两级回退)全 PASS
- SDK meow_ui_sys: 状态栏充电态同权威链
- launcher util.c: 抽 batt_is_charging() 公共判定;meow_sys_light 1s 轻刷补充电态(插拔实时)
- bootmenu 无充电判定(仅读 capacity)/MeowPanel 透传 power 服务:均无需改
- 真机验证(充电窗口):新二进制 battery=88%(Charging),对照固件内旧版 88%(Discharging)
- hello/files/ncm 随 meow_ui.a(af6703f2) 重链已热更到卡(.new+mv,md5 对核一致)

md5: meow_svc_power 39ae3aa2 / meow_launcherd 2aa61780 / hello 2444c151 /
files 691db88b / ncm 89da63ed(仅重链,源码未动)
2026-09-01 03:25:01 +08:00
MeowOS 793a807ce3 tools: tonepcm2 支持设备参数(测 D1p) + PROBE_ONLY 模式(只探测不播放, 防挂死) 2026-09-01 03:16:52 +08:00
MeowOS 57509cd582 docs: 文档一致性整理 (agent-249) — 对齐 M2.90/0.7.85 + ncm A2: SDK 新增 §5.9 meow_audio_* 客户端 API + IME 九键数字盘注记; README 网易云三期+A2 audiod+PCM 挂死已知问题; APPS §3.7/§5 直开 hw:0,0 旧案标注被 meow_audiod 取代; SETTINGS audio.output/音量键 audiod 收口注记; FRAMEWORK §10.1 supervisor 树补 meow_audiod + 头部里程碑 M2.90; PLAN 头部截至 M2.90; SPEC_REF OPEN_QUESTIONS 摘要同步 (#4/#12/#17 清结) 2026-09-01 03:11:10 +08:00
MeowOS 697fd6ba9b chore: gitignore audiod_chain_sim 构建产物 2026-09-01 01:21:09 +08:00
MeowOS e0136213fe ncm A2: 音频输出迁移 meow_audiod(SINK_AUDIOD 默认后端)— 修无声根因
- audio_out.c: 新增 SINK_AUDIOD 默认(SDK meow_audio_* 连 audiod);NCM_AUDIO_SINK=file/null/alsa
  与 MEOW_AUDIO_DIRECT=1 保留调试后端;ALSA 保底修两大坑(hw_params 全放行初始化 + mask 清零
  再单点)——旧 memset0 interval=「只能取0」→ HW_PARAMS 恒 EINVAL 即播放无声根因
- 新增 audio_out_pause:PAUSED 沿向 audiod 发 pause(停时钟不丢流);resume 流死自动重开
- player_manager: 注册 meow_audio_on_preempt → g_play 命令通道暂停;播放事件全量 meow_log
  (open 参数/grant_*/write 错误节流/切歌/暂停/seek/done)
- appstate/app: /tmp/ncm_autoplay 真机联调 hook(仅播已缓存曲目,绝不发 song_url)
- bench/audiod_chain_sim.c: 真实 audiod_host 假 socket 全链离线回归 14 项全 PASS
- 验证: host 0 warning / WAV 回归 / qemu core PASS / mipsel 0 warning;真机 OPEN→grant 已通,
  持续写帧被 M2.90 设备 PCM 播放内核硬挂死阻断(REPORT §16.3,4/4 复现,留主会话)
- ncm md5 db5b78ad9e8f40e6306484efe2a08eb3(已热更设备)
2026-09-01 01:17:21 +08:00
MeowOS 9bbc3a1467 chore: gitignore 诊断/bench 构建产物 (tools 二进制+ncm sim) 2026-08-31 23:03:06 +08:00
MeowOS 284c6ae6fd docs(SPEC_REF): §4 新增 meow_audiod 权威记录(socket/号段/CLI/门控/抢占) + 音频硬件实测事实(cs43198-hifi-0 能力/ALSA UAPI 两坑/音量死镜像/混音器清单) 2026-08-31 23:01:56 +08:00
MeowOS 213ac28c95 docs(README): 同步 M2.89/M2.90 — meow_audiod 独占音频守护(含 supervisor 树/系统组件/目录清单)、meow_route 并入退役、MIUIx 主题、选单回归修复、面板字体 tar.gz、网易云三期; 版本头 0.7.85 2026-08-31 22:59:20 +08:00
MeowOS 942bdece78 build+tools: fw_content.conf 填 audiod/audio CLI md5; 新增诊断工具 sndprobe(PCM设备探测)/tonepcm2(HW_REFINE能力探测+写计时时钟验证) — 无声定位用。M2.90(0.7.85) 已打包 504bc74b 回验0diff 2026-08-31 22:45:43 +08:00
MeowOS ade0a57353 A1: meow_audiod 独占音频服务(agent-244 骨架 + agent-246 真机收尾)
- meow_audiod:启动门控(/tmp/meow_boot_mode 非 meow exit 0)、IO/播放/路由三线程、
  ring ~200ms、ALSA UAPI 独占直开、切率 mute-重配-恢复、抢占(后开赢+PREEMPTED)、
  socket EOF 回收、xrun PREPARE 恢复;meow_route 轮询逻辑并入路由线程
- meow_audio CLI:status/tone/route/vol;services.d/audiod.conf(trigger=manual,on-fail)
- SDK meow_ui_audio.c 编进 meow_ui.a:懒连接/读线程分发 PREEMPTED 回调/EPIPE 标死/
  无 audiod 返回 MEOW_ERR_NOENT 不隐式直开(MEOW_AUDIO_DIRECT=1 仅调试)
- launcher:确认 MeowOS 模式后 meow_svcd start audiod(double-fork best-effort 不阻塞)
- meow_route 退役:S89 预置换 audiod.conf + 清理旧 meow_route.conf(防拉起已删二进制
  重启循环);fw_content.conf 删 meow_route、加 meow_audiod+meow_audio(md5 打包时填)
- 真机定位并修复 ALSA UAPI 两大坑(即 ncm 播放无声根因):
  ① hw_params 须全放行初始化(mask 全 1/interval [0,UINT_MAX]),memset 0 interval
     等价只能取 0 → refine 必空 → HW_PARAMS 恒 EINVAL
  ② access/format mask 先清零再单点(pmask 只 OR),全 1 被 choose 成 MMAP_INTERLEAVED
     → write() 恒 EINVAL
- 附带修复:dev_write fd<0 防日志风暴丢 PCM 限频日志;SDK state 解析 app_id 截空白;
  meow_ui.h 文档负错误码表述修正
- 验证:宿主 FILE sink 全 IPC 链+抢占+tone WAV 20/20,SDK 客户端 13/13;
  qemu-mipsel 同过;真机 tone 44.1k/96k 切率 written=played、underrun=0、日志完整
- meow_ui.a 53a3edd2 → ncm 重链 d2031829(已热更到卡);launcher a2f932a1
2026-08-31 22:40:08 +08:00
MeowOS 94874107b6 fix(ncm/ui): 文案中文化+事实性错误修正 — 音质档位对齐官方口径(Hi-Res/无损/极高/较高/标准)、设置页topbar/sheet/确认框中文、页脚会员提示改写准确; 播放页页脚「左右滑切歌」改「左缘右滑返回 ・ ⟳ 切横屏」、空态/歌词态/顶栏中文; 歌单/搜索空态与登录页状态全中文。二进制随 246 重链后热更 2026-08-31 22:24:49 +08:00
MeowOS 33b4ae703c README: 校正 meowpanel 二进制 md5 为最终构建 51ffc183(tmpdir 修复后重建) 2026-08-31 20:46:30 +08:00
MeowOS d8b95c0aab MeowPanel: 字体库 tar.gz 导入/导出 + 文件管理自然排序修复 + multipart 二进制边界修复
- 字体库导入/导出(用户 2026-08-31 定案):放弃 zip,与 .mpk 同构走 busybox 'gzip -dc | tar' 管道
  (设备必带 tar/gzip,零新增依赖)。导入 POST /api/fonts/import(multipart file=<.tar.gz>,上限32MB,
  ?force=1 覆盖):tar -t 成员预校验(sanitize_member 拒绝对/../shell 危险字符,恰为三件套平铺)→
  解压到临时目录 → opendir/lstat 锁(拒软链/目录/设备/多出条目/解压后超大)→ font.json 解析 family/weight
  (safe_dir_segment 消毒)→ 覆盖冲突 409 → sha256_png/idx 仅 WARN → rename 到 MeowOS/fonts/<family>/<weight>/。
  导出 GET /api/fonts/export?family=&weight=:tar -cf - | gzip -c 打包 <family>-<weight>.font.tar.gz 流式下载。
  列表 GET /api/fonts:扫描三件套齐。前端设置页加「字体库」卡(导入 zip/tar.gz + 409 confirm 后 force=1 重试 + 导出按钮)。
- 文件管理排序修复:/api/files 排序改为目录优先 + 名称自然排序(数字感知 track2<track10,中文按无符号
  字节序),替换原 strcasecmp 词法排序(用户报「很离谱」)。
- multipart 二进制边界修复(latent bug):strstr 遇数据内 NUL 提前停止 → memmem 语义 mem_find 字节安全查找,
  修复字体 tar.gz/.mpk/二进制上传可能误吞后段的问题。
- 文档同步:docs/MEOWOS_SPEC_REF.md §5.2/§5.7.6、patch_v2/meowpanel/README.md 改 tar.gz 定案;
  FONT_SYSTEM.md 无 zip 表述无需改。meow_font_import_zip/export_zip 语义注为 tar.gz(_tgz 可更名)。
- 宿主编译 0 warning(-Werror 亦过);宿主端到端:导入→导出→逐字节比对、损坏/缺件/坏json/穿越/软链/非gzip全拒、
  自然排序样本全过;真机已部署(meowpanel md5 51ffc183,服务 running)并 curl 验证 font 端点 + 排序。
2026-08-31 20:45:52 +08:00
MeowOS 4e3f41a5a3 M2.90: MIUIx 风格包落地 + ui.style 一键切换闭环
- meow_ui_theme.c: 为 MEOW_STYLE_MIUIX 实现整套 light/dark 配色(暖基面+小米橙 primary
  0xF57200/0xFF8C4A+蓝 tertiary;圆角 20/卡片 28、间距 12、细描边 1),与 MD3 可感知差异
- display.background 随 style×mode 派生 MIUIx 色板(bg_surfaces 增 style 参;图片背景保持各 style 默认面)
- meow_settings.c g_defs: ui.style 值域 'md3,miui'→'md3,miuix'(label Material You,MIUIx);
  apply_value SET_ENUM 对存量 'miui' 兼容映射为 miuix
- settings_page s_commit: ui.style 分支 → meow_ui_set_style + paint_sb_right + meow_dirty_full 即时换肤
- font_theme s_ui_theme() 接 ui.style + MIUIx dark/light 表;main_menu 背景签名含 ui.style + 纯色底随 style 换族
- 宿主编译 -Wall -Wextra 0 warning;test_meow_ui_theme 四套 s×m 渲染全 PASS;
  test_meow_ui_widgets/test_sdk_fixes 全 PASS(MD3 行为一致)
- 产物 md5: meow_ui.a a4231296 / launcher 9f97af1b(未部署走固件)/ hello e5527578 /
  files 993e41ca / ncm 6b9eeb0f(三 App 已 .new+mv 热更到卡, 设备 md5 已核一致)
- 待后续: 状态栏图标集仍 MD3 一套(sb_icons.h 无 per-style 换集接口, MIUIx 仅换配色);
  App 进程 SDK 主题缓存 g_loaded 不随 settings 外部改写重读, 运行中 App 下次启动才换肤
2026-08-31 20:17:51 +08:00
MeowOS f23720f934 feat(ncm): 三期第二批 — 中文搜索接拼音IME+设置页做好(音质全档/缓存上限可设数字+清理缓存/关于)+缓存LRU淘汰接生产路径(写前写后+手动调低上限)
- 搜索页输入框 MEOW_IME_ASCII→MEOW_IME_PINYIN: 候选上屏UTF-8经 on_change 实时回写 g_st.search_query, 键盘「拼/英」「中」切换, 英文零回归; 搜索仍仅在点「搜索」时发 cloudsearch(风控)
- 设置页 5 行: 音质(Hi-Res 默认,Lossless/Exhigh/Higher/Standard 全档,core 既有降级链)/缓存上限(1~100GB 预设+「自定义」NUM IME 数字输入,默认10上限100)/清理缓存(显示当前占用,ncm_cache_clear)/退出登录/关于行
- 缓存淘汰接线: ncm_cache_evict 进生产路径 — player_manager(下载写前/写后)+cover(写前/写后)+appstate_save_settings(手动调低上限立即淘汰); 封面与 mp3 同池; 上限读设置值
- core 新增 ncm_cache_clear; 设置持久化走 meow_config_* → MeowData/dev.player.ncm/data
- bench/cache_evict_sim.c 离线回归(0网络) 6 组 PASS; pagination_sim/core(host+mipsel/qemu)/6页宿主渲染 全过; mipsel -Wall -Wextra 0 warning
(未动 sdk/、meow_launcherd/、vendor/;0 真实 NCM 请求;真机部署留用户)
2026-08-31 20:15:31 +08:00
MeowOS 1a239c5eab docs: APPS.md §4.5 过期注记修正 — bootmenu 日志已在 MeowOS/logs(meow_boot_<bootts>.log),「仍用 MeowAPPs/logs 待统一」作废 2026-08-31 20:11:27 +08:00
MeowOS 673f85e4ab docs: APPS.md §3.5 看门狗表对齐现状(虚拟返回键M2.21已移除/SIGUSR1仅裸沉浸App+SDK护栏) + KEY_CONTROL_DESIGN 偏差3/4标已落地(M2.81 NowPlaying横划+媒体键路由) 2026-08-31 20:08:27 +08:00
MeowOS 4e3edd7952 build: fw_content.conf M2.89 — launcher 3c1bf9c0(选单抢屏回归修复)/watchdog 6373d8e5(SIGUSR1不误杀框架App)/nyai 2858c3bd(导出exec相对化)/amsd c6c2eda2(陈旧revents丢连接修复) 2026-08-31 19:40:30 +08:00
MeowOS 6d1189eadc fix(amsd): launcher 重启后 amsd 失联三修 — am 命令连接被陈旧 revents 提前关闭/ipc_xchg 无超时冻结/无自动重连
launcher 重启后 am start/kill (MSG_AMS_START/KILL 0x00A0/0x00A1) 不再转发到 launcher。

根因(均在 meow_amsd.c,launcher 无需改):
① am 命令连接被提前关闭(真根因):poll 循环 pfds[] 每圈未清零,上一连接关闭残留的
   POLLHUP/POLLERR 落在 pfds[2+i];accept 后同圈用陈旧 revents 处理新连接 → 误判 HUP/ERR
   提前 close(am 收 SIGPIPE/141)。launcher 重启后 EV 事件连接密集、每条约关闭 → 必触发。
   修:每圈 memset(pfds,0,sizeof pfds),新连接当轮 revents=0、下轮才处理。
② ipc_xchg 无收发超时 → 阻塞 read 冻结整个单线程 amsd(launcher 接受但不回包窗口)。
   修:设 SO_SNDTIMEO/SO_RCVTIMEO(tv_usec<1000000,否则 setsockopt 返 EINVAL 静默失效)。
③ ams_request_ctl 加指数短退避重试(2 次,250ms 基数),launcher 数秒内回来即自动恢复。

验证(宿主端到端):假 launcher 杀→重启 am start 恢复 OK;注入多条 EV 后 am list×10 全
exit=0(无 141);哑 launcher(接受不回包)am start ~2s 有界返回 ERR 且 amsd 存活可继续
am list。meow_amsd mipsel md5 c6c2eda2d63ac73c4b6fac29008e0c9b(am 不变 d687d8bc)。
不动 launcher/sdk/根目录文档。
2026-08-31 19:38:33 +08:00
MeowOS 21ff5e82ea feat(sdk/ime): 数字键盘改居中九键布局 (1-9 自上而下 3x3,0 垫底居中,底行 ESC|0|退格+OK) + 全链重链 (meow_ui.a ee71b94a; hello 8cb81964/files 8b2b2373/ncm 4e975d6d 已热更到卡)。宿主 test_ime_fix/test_pinyin 全 PASS 2026-08-31 19:38:21 +08:00
MeowOS 1cfd124211 fix(launcher): M2.83 回归 — 开机选单期不抢屏。sysui S70 拉起 launcher 时 mode 文件未写+播放器未起,is_meow_mode()=find_hiby_pid<=0 恒真 → 误入 MeowOS 主屏,与选单/HibyOS 播放器抢 fb0+触摸(开机选 HibyOS 黑屏根因)。修: mode 文件缺失且 bootmenu 存活→怠速等写(不设上限,bootmenu 消失回退旧判定); hiby→怠速守护不碰 fb,轮询 mode 变 meow(热键切换)再落地主屏; 等待期被 sysui stop→直走退出尾 2026-08-31 19:33:26 +08:00
MeowOS ee104f1b18 feat(ncm): 三期第一批 — 歌单真分页(取消256截断)+专辑封面ncm_cache+歌词后台加载补锁
[agent-239] 仅改 patch_v2/ncm_app/。

1. 歌单分页(REPORT §14.1): track/all 全 404(§13.1) 弃用, 回 detail(n=1)全量 trackIds
   + song_detail 分批(≤NCM_SONG_BATCH=100) 触底逐页加载。appstate_load_playlist_songs_page
   offset=0 重建 song_ids[]/track_ids_count 载第0页, offset>0 追加(失败保留已载);
   playlist.c g_pl_more=(song_count<track_ids_count && <NCM_SONG_MAX), 触底 g_ploffset=song_count
   起下一页(保留滚动), 首屏失败整页空态+点按重试, 追加失败 snack+保留已载。NCM_SONG_MAX
   256→1000(单页渲染上限, bss +~1MB), NCM_SONG_BATCH 64→100, 删 NCM_PLAYLIST_PAGE(track/all 遗留)。

2. 专辑封面(§14.2): cover.c 封面改走 ncm_cache(LRU, 键=djb2(url)→song_cov<h>.jpg 同池),
   缓存命中免下载/未命中下载→open/write/commit 落盘, 仍写 cover_current.jpg 供 launcher;
   顺带修真 bug: save_cover_raw 在已持 g_cm 临界区内再 lock(非递归死锁)→封面永不刷新, 改调用方持锁。
   歌单列表/详情头块封面保持现状。

3. 歌词后台加载(§14.3): lyric_worker 已异步(§11)未改线程模型; 补 lyric.c g_lyric_mu 数据竞争锁,
   写侧 lyric_parse 整段持锁(清空用 lyric_clear_nolock 免二次取锁), 读侧 lyric_count/line_at/
   line_text/line_tl 逐次取锁, 消除 UI/IPC 与 worker 间数据竞争(原 §10.9 FM4 良性竞态)。

验证(0 真实 NCM 请求): bench/pagination_sim.c 离线 stub 驱动真实 appstate 分页函数全 PASS
(130首读全程/5191首封顶1000/空/越界无操作); mipsel build_ncm.sh 0 warning(text 842820/bss 2052068);
宿主 build_ncm_host.sh+MEOW_UI_DUMP home/playlist/player/lands 渲染正常无崩溃; host SDK_SRC 补
meow_pinyin(拼音IME py_predict 漏编致链接断)。ncm md5 b34bc1ed, ncm_host md5 1a2327e0。
未动 sdk/(主会话改 meow_ui_ime.c)/meow_launcherd/meow_amsd/vendor/core; 未 commit 它们。
2026-08-31 19:33:10 +08:00
MeowOS 5fac5e380d fix(nyai): 导出 exec 绝对路径相对化 + gzip stderr 重定向 (agent-签名产签链路)
- cmd_export 原先对 app.json 的 exec(绝对路径,如 /usr/data/mnt/sd_0/MeowAPPs/<pkg>/<bin>)
  误用 sanitize_member 剥前导 '/' 拼到包目录下,导致 access(<src>/usr/data/...) 必然失败,
  所有真实 app 导出被拒(exec 文件不存在或不可执行),卡死「导出→产签」链路。
  新增 exec_rel_to_pkg(): 按 ①src/ 前缀 ②/MeowAPPs/<pkg>/ 段(根路径不一致的沙箱/CI)
  ③相对路径 ④绝对但定位不到包目录→拒, 正确相对化 exec。
- install/upgrade 解包命令把 2>/dev/null 只绑到 tar,gzip stderr(footer trailing garbage /
  损坏 crc error)会漏到用户输出;改 gzip 也重定向。
- 同链验证: 宿主 meow_sign/meow_verify_host/qemu-mipsel 验签矩阵通过; 真机(10.78.193.50)
  无签 os.meow.*拒装/已签安装OK+.mpk.src/篡改拒装/降级拒装 全过; 装后已卸并恢复原状。
  (devdocs 本地不入库; 产签实走记录+README 见 patch_v2/meow_sign/README.md §8)
2026-08-31 19:30:32 +08:00
MeowOS 92086129c5 fix(sdk/watchdog): 236 收尾 — sigusr1_guard 无日志目录时落 stderr 可观测; test_wd_fw 补 fw-app/裸沉浸/裸非沉浸三用例; hello/files/ncm 随 meow_ui.a(4020227c) 重链 (已热更到卡, watchdog 6373d8e5 待入 M2.89) 2026-08-31 19:12:51 +08:00
MeowOS 49c31f1901 docs: 全项目文档一致性审计同步 (agent-237) — README/FRAMEWORK_DESIGN/SDK/APPS/PLAN/SETTINGS/SPEC_REF 对齐 M2.88 现状(amsd/picker/签名2B/拼音IME/面板阶段C/ncm改名dev.player.ncm等),过期「未实现」表述按落地实况修正 2026-08-31 19:11:25 +08:00
576 changed files with 122369 additions and 4726 deletions

No files matched your search

+20 -1
View File
@@ -75,6 +75,25 @@ font/src/**/*.ttc
patch_v2/sdk/test_p1p2p3_repro
# ncm_app vendor 源码与构建产物(体积大;版本/获取方式见 VENDOR_NOTES.md,构建脚本可复现)
patch_v2/ncm_app/vendor/
# 例外:dr_flac 单头文件入库(§19 FLAC 解码依赖,532KB 单文件可入库)
patch_v2/ncm_app/vendor/*
!patch_v2/ncm_app/vendor/dr_flac/
patch_v2/ncm_app/out/
patch_v2/sdk/test_sdk_fixes
# SDK 矢量字(§10.1):FreeType vendor 源码/构建产物(体积大;版本 2.14.3,build_sdk.sh 可复现
# 拉取+裁剪构建 -DFT_DISABLE_* 静态 musl)与宿主测试二进制
patch_v2/sdk/vendor/
patch_v2/sdk/test_meow_ui_ft
# 诊断工具构建产物(源码入库,二进制免)
patch_v2/tools/sndprobe
patch_v2/tools/tonepcm
patch_v2/tools/tonepcm2
patch_v2/ncm_app/cache_evict_sim
patch_v2/ncm_app/pagination_sim
patch_v2/ncm_app/audiod_chain_sim
patch_v2/ncm_app/window_sim
patch_v2/ncm_app/cover_chain_sim
patch_v2/ncm_app/player_flow_sim
patch_v2/ncm_app/degrade_sim
+14 -10
View File
@@ -74,10 +74,11 @@
| `author` | — | 保留字段 |
| `fullscreen` | — | app 级全屏:`1`=进入全屏(状态栏**收合至提示线** + **保留左缘返回/顶部提示线/下滑通知中心**,见 MEOWOS_SPEC_REF §5.1);activity 级用它 `route.<id>.fullscreen`。**app.json `fullscreen` 字段此前未解析/已定未做 → 本次实现**(launcher 启动/切 activity 据声明预设 SDK 全屏,env `MEOW_FULLSCREEN`)[源:用户 2026-08-30] |
| `fonts` | — | 字库档位声明:`"full"` / `"gbk"` / `"common"`(app 需大字库时可声明 `"full"`,默认小字库 `common`≈4k;launcher 据此为该 app 加载大字库 `gbk`≈20k,从已扫描的 `MeowOS/fonts/<family>/<weight>/` 里按同 family(其次同 weight)选大字库,M2.6)。**注意**:与 `"fonts":["<handle>"]`(应用自带字库句柄数组)**同名二义**,见 MEOWOS_SPEC_REF §7 A-14 [源:用户 2026-08-30] |
| `depends` | — | **二进制依赖声明**(launcher/nyai 启动前检查):`"curl,jq:optional,ffmpeg:required"`(JSON 值串)或行式 `depends=curl,jq:optional,ffmpeg:required`。逗号分隔多项,每项 `<name>[:<mod>]`;**无标注默认 required**,也接受 `:opt`/`:req` 缩写(=optional/required),未知修饰符按 required 处理并 WARN。App 某些复杂功能若依赖现成 Linux 二进制(**板载 `/usr/bin/<name>` 或卡上 `/usr/data/mnt/sd_0/MeowOS/opt/bin/<name>`,均按 X_OK 查**,两类任命中即满足)能效更好时声明。**launcher 启动前检查(仅启动时,不做周期复查)**:required 缺失 → **不启动**(toast「缺依赖: <name>」+ 日志,沿用 CANNOT START 风格);optional 缺失 → 仅 WARN 放行。[源:用户 2026-09-01] |
- 目录扫描深度 1 级:只枚举 `MeowAPPs/` 下的直接子目录
- 应用计数上限 **8**(超出忽略;单屏 8 行)
- app.json 解析器极简(launcher 内实现):仅匹配 `"name":`、`"exec":`、`"package":` 后的引号字符串(及扩展 icon/framework/immersive/route./`awake.<id>`(keep_awake)/service.),**`fullscreen`(本次实现)与 `fonts`(档位串) 已解析**,不做 JSON 校验(多行/单行均可)[源:审计 2026-08-30][源:用户 2026-08-30]
- app.json 解析器极简(launcher 内实现):仅匹配 `"name":`、`"exec":`、`"package":` 后的引号字符串(及扩展 icon/framework/immersive/route./`awake.<id>`(keep_awake)/service./**`depends`**(二进制依赖,line 解析)),**`fullscreen`(本次实现)与 `fonts`(档位串) 已解析**,不做 JSON 校验(多行/单行均可)[源:审计 2026-08-30][源:用户 2026-08-30]
- 列表刷新时机:每次菜单打开时重新扫描(TF 卡热插拔后立即可见)
### 2.2 边界情况行为(launcher 实现,全部真机验证)
@@ -162,7 +163,7 @@
**约定(MeowOS 界面 = launcher 主菜单/选单/电源菜单,是唯一允许映射物理键的地方)**:
> ⚠️ **适用范围**:**除了电源键菜单(`show_power_menu`,允许"电源=确认、音量=导航")以外,所有按键信号由 MeowOS 全局接管**——**任何界面(含 MeowOS 自身的设置页 / 应用列表 / 选单、以及 MeowAPP)都不得私自定义按键行为**。物理键的语义只有一份:电源=息屏/亮屏/长按菜单/8s强关,音量=调系统音量,播放/上下曲=媒体控制(未落地前「抓住」)。任何界面(如系统设置页)**不得**把电源键/播放键映射成"返回/确认"这类自定义行为;界面导航一律走触摸(返回按钮/左缘左滑),按键语义统一由系统判。
> ⚠️ **适用范围**:**除了电源键菜单(`show_power_menu`,允许"电源=确认、音量=导航")以外,所有按键信号由 MeowOS 全局接管**——**任何界面(含 MeowOS 自身的设置页 / 应用列表 / 选单、以及 MeowAPP)都不得私自定义按键行为**。物理键的语义只有一份:电源=息屏/亮屏/长按菜单/8s强关,音量=调系统音量,播放/上下曲=媒体控制(**已落地**,M2.81 起路由最近活跃媒体会话;无会话时「抓住」)。任何界面(如系统设置页)**不得**把电源键/播放键映射成"返回/确认"这类自定义行为;界面导航一律走触摸(返回按钮/左缘左滑),按键语义统一由系统判。
| 键 | 系统语义(APP 收到的是语义事件,不是裸 keycode) |
|---|---|
@@ -184,16 +185,16 @@
| 特性 | 行为 |
|---|---|
| 虚拟返回键 | 看门狗独立 mmap `/dev/fb0`(RGB565 自适应),每 100ms 重画左上角 48×48 返回按钮(深色底+白箭头);读 `/dev/input/event1` 触摸,按下落在按钮区 → `SIGTERM` 应用 → 1s 后 `SIGKILL`(应用卡死也能退出)。**v6.4 防抖**(针对"启动误杀"):`last_x/y=-1` 起始(无坐标的按下丢弃)、BTN_TOUCH 按实际值匹配边沿(消除失同步翻转)、仅短按 ≤600ms 有效、启动后先 drain 一次、**冷却期 2.5s**(实测幽灵触摸窗口 1.2–2.25s;此窗内返回键不响应,改 `COOLDOWN_MS` 可调) |
| 沉浸模式 | 应用写 `/tmp/meowapp_state` 内容为 `1` → 看门狗不画按钮、只识别「左缘 24px 内起点向右滑 >50px」手势 → 发 `SIGUSR1`;写 `0` 或删除 → 恢复虚拟键 |
| 虚拟返回键 | **已移除(M2.21 起)**:全局左缘返回手势取代左上角返回键,看门狗不再画按钮。历史行为:独立 mmap fb0 画 48×48 返回键 + 命中 SIGTERM(保留备查) |
| 沉浸模式 | 应用写 `/tmp/meowapp_state` 内容为 `1` → 看门狗识别「左缘 24px 内起点向右滑 >50px」手势。**M2.89 起 SIGUSR1 只对「裸沉浸 App」(非 framework=1 且 immersive)发送**(236:SDK 框架 App 走 `on_swipe` 处理返回,发 SIGUSR1 会误杀——ncm signal=16 崩溃的根因);SDK 侧另有 SIGUSR1 护栏(`sigusr1_guard`,记日志+吞掉)双保险 |
| 看门狗自退出 | 应用进程消失(`kill(pid,0)` ESRCH)或 launcher SIGTERM → 退出 |
| 强制退出兜底 | 应用运行期(launcher 处于 waitpid):**音量减连按 3 次**(间隔 < `hotkey_seq_gap`,与主序列同阈值)→ SIGTERM → 0.5s → SIGKILL → 恢复。⚠️ 响应音量键的应用(如示例 hello)会先自行退出(`exit:key`),强杀序列是**不响应音量键/卡死应用**的兜底 |
| 长按电源 | 硬件级强制关机(设备固有),最终兜底 |
| 测试模式 | `meow_watchdog <pid> --sim-tap N` / `--sim-swipe N`,无触摸设备也可验证 |
**应用协议约定**:
- 默认(非沉浸)由看门狗画返回键,应用**不必**处理返回手势
- **⚠️ 返回键信号 = `SIGTERM`**(看门狗 `handle_touch` 按钮命中 → `kill(app_pid, SIGTERM)`→1s→`SIGKILL`);`SIGUSR1` **仅**是沉浸左缘滑动手势,不是返回键。com 与 SIGTERM 默认终止会**不画 BYE** 直接死,故示例 hello 安装 `SIGTERM` 处理器:画 BYE + `exit(0)`,使返回键呈现确定性的「bye → 立即退出」(v6.8,Bug 2)。
- 默认(非沉浸)SDK 框架 App 由 SDK `on_swipe` 处理左缘返回;**看门狗虚拟返回键已移除(M2.21)**,返回语义全走手势
- **⚠️ `SIGUSR1` 仅**是裸沉浸 App 的左缘滑动手势(M2.89 起看门狗不对 framework=1 的 SDK App 发),不是返回键;框架 App 另有 SDK `sigusr1_guard` 护栏吞掉漏网 SIGUSR1。裸 App 的 SIGTERM 默认终止会**不画 BYE** 直接死,故示例 hello 安装 `SIGTERM` 处理器:画 BYE + `exit(0)`(v6.8,Bug 2)。
- **⚠️ 应用启动后须防 goodix 幽灵触摸**(v6.8,Bug 2 根因):goodix 在启动触摸的尾部发出一段幽灵触摸(实测启动后 1.2-2.25s,§18.16),无防护时应用会**误把幽灵当下按而立即退出画 BYE**,且 launcher 菜单会把同一幽灵重放成行点击而**重开应用**(用户看到「bye 后又回 hello 主界面、需再点一次」)。hello v6.8 增加 `COOLDOWN_MS=2500`:启动后前 2.5s 忽略一切按下边沿,只有用户**真正**的下按才触发退出。launcher 侧同步在应用退出后 drain + 冷却(见 §4.7)。
- 沉浸应用写 `/tmp/meowapp_state`,处理 `SIGUSR1` 作为「返回事件」
- 应用异常卡死 → 虚拟返回键(若可见)→ SIGKILL;沉浸态卡死 → 音量减连按 3 次(强杀序列)
@@ -278,7 +279,7 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
### 4.5 统一日志系统(MeowAPPs 组件共用)
- **默认位置**:TF 卡 `/usr/data/mnt/sd_0/MeowOS/logs/meow_<bootts>.log`(如 `meow_20260828_020123.log`;`bootts` = 当前墙钟 − /proc/uptime,同一 boot 会话内多次启动共用同一文件)。**系统日志**(launcher/watchdog 等 MeowOS 组件)写此文件;**应用日志**另按 `logs/<app_id>/<ts>_<pid>.log` 分文件(`<app_id>` 即该应用目录;见本 §4.5「组件环境变量」)。⚠️ `meow_bootmenu` 仍用 `MeowAPPs/logs`(代码侧不一致;bootmenu 与 launcher 日志目录**待统一为 `MeowOS/logs`**,[源:审计 2026-08-30])。旧布局 `logs/apps/<app_id>/logs/` 已废弃:launcher 启动时一次性迁移到 `logs/<app_id>/` 后删除
- **默认位置**:TF 卡 `/usr/data/mnt/sd_0/MeowOS/logs/meow_<bootts>.log`(如 `meow_20260828_020123.log`;`bootts` = 当前墙钟 − /proc/uptime,同一 boot 会话内多次启动共用同一文件)。**系统日志**(launcher/watchdog 等 MeowOS 组件)写此文件;**应用日志**另按 `logs/<app_id>/<ts>_<pid>.log` 分文件(`<app_id>` 即该应用目录;见本 §4.5「组件环境变量」)。`meow_bootmenu` 用 `logs/meow_boot_<bootts>.log`(同目录,文件名前缀区分——早期「仍用 MeowAPPs/logs」注记已过期,现统一在 `MeowOS/logs`)。旧布局 `logs/apps/<app_id>/logs/` 已废弃:launcher 启动时一次性迁移到 `logs/<app_id>/` 后删除
- **无 TF 卡 / 卡晚挂载(S90 早于卡挂载)**:**开不了卡日志**——日志落 **/tmp 启动缓冲**(`/tmp/meow_boot.log`,上限 256KB 滚动),**绝不写板载存储**(/usr/data 只剩 ~10MB);**卡出现后自动迁移**(主循环 ~1s 周期重试 + 输入设备重试循环内也检查;无需开菜单)——`log_poll_card()` 打开卡日志并把 /tmp 缓冲整段搬入后删除 /tmp 文件
- **容量上限**:logs/ 目录总量 1GB,开机时超限按 mtime 删最旧
- **组件环境变量**:
@@ -312,6 +313,8 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
## 5. 音频通路(未来应用的出声方案)
> **2026-09-01 更新**:常驻模式(MeowOS 主控、播放器未运行)下的出声方案**已由 `meow_audiod` 独占音频守护落地(M2.90)**——守护独占 hw:0,0、App 经 SDK `meow_audio_*` 喂 PCM(MEOW_APP_SDK §5.9 / `devdocs/plan/AUDIOD_DESIGN.md`)。本节下述 v3 候选为「会话覆盖模式(HibyOS 热键唤起、SIGSTOP 冻结播放器)」的历史推演,仅在该模式下仍有参考意义;常驻模式直开 hw:0,0 的旧结论见 §3.7。
参考 ANALYSIS.md 第 11 节结论:
- 声卡 = 内部唯一 card0(CS43198 双 DAC),**无混音**,谁打开谁独占;hiby_player 常驻持有 `hw:0,0`。
@@ -456,10 +459,11 @@ Description: A simulated app
**与 launcher 一致**:install/export 复用 M2.12 `meow_pkg_install/export` 同套语义(tar.gz、gzip 管道、包名=文件名茎、同名先备份、校验 app.json、数据 init);exFAT 无符号链接,数据目录由 MeowOS 经 `MEOW_DATA_DIR/CACHE_DIR` 注入,CLI 只建目录不建链接。
### 3.7 音频通路与媒体键(2026-08-31 实测定案,覆盖 §5 旧推论)
- **MeowOS 常驻模式(boot 直进,hiby_player 未运行)声卡空闲**:真机实测 `fuser /dev/snd/pcmC0D0p /dev/snd/controlC0` 无任何占用、`pidof hiby_player` 不存在——**MeowAPP 可直接 open `hw:0,0` 出声**(audio.output 设置键即此);无混音,后开者失败,音频焦点=先占先得(媒体会话/MPRIS 落地后再规范抢占语义)。
### 3.7 音频通路与媒体键(2026-08-31 实测定案,覆盖 §5 旧推论;**M2.90 起被 meow_audiod 收口,见末段**)
- **MeowOS 常驻模式(boot 直进,hiby_player 未运行)声卡空闲**:真机实测 `fuser /dev/snd/pcmC0D0p /dev/snd/controlC0` 无任何占用、`pidof hiby_player` 不存在。~~MeowAPP 可直接 open `hw:0,0` 出声~~ → **M2.90 起改走 `meow_audiod` 独占音频守护**:守护独占 hw:0,0,App 经 SDK `meow_audio_*` 客户端喂 PCM(见 MEOW_APP_SDK §5.9 / `devdocs/plan/AUDIOD_DESIGN.md`);独占+抢占(后开流赢,旧流收 PREEMPTED)、DAC 原生率跟随无 SRC、路由自动跟随插孔(meow_route 并入退役);App 直开仅 `MEOW_AUDIO_DIRECT=1` 调试用。媒体键路由最近活跃媒体会话不变。
- **会话覆盖模式(HibyOS 运行中热键唤起)**:SIGSTOP 冻结播放器,声卡仍被其持有,App 无声(§5 旧结论,仅适用该模式)。
- **物理媒体键路由**:MeowOS 与 HibyOS 不同时运行,不存在「转发给 hiby」问题;MeowOS 界面下播放/上下曲 = 全局媒体控制,路由给**最近活跃媒体会话**(**媒体会话已落地** M2.81:`MSG_META`(0x72)/`MSG_PLAYCTL`(0x73) + meow_media 最近活跃源(active_seq,30s 超时回收)+ 通知中心媒体卡片 + Now Playing 大页;OS.meow.ncm 是首真实媒体源),无会话时「抓住」不动作。
- **物理媒体键路由**:MeowOS 与 HibyOS 不同时运行,不存在「转发给 hiby」问题;MeowOS 界面下播放/上下曲 = 全局媒体控制,路由给**最近活跃媒体会话**(**媒体会话已落地** M2.81:`MSG_META`(0x72)/`MSG_PLAYCTL`(0x73) + meow_media 最近活跃源(active_seq,30s 超时回收)+ 通知中心媒体卡片 + Now Playing 大页;**dev.player.ncm**(原 os.meow.ncm,agent-221 改名)是首真实媒体源),无会话时「抓住」不动作。**262 起物理播放/暂停键(164) 恒发 PLAYCTL `cmd=6`(toggle)**,由源侧按当前态翻转(修 SDK 本地态失真恒发「播放」)。
- **audiod 加固批(M2.91-96,详见 MEOW_APP_SDK §5.9 / docs/MEOWOS_SPEC_REF §4)**:开机武装 hbc3000 FPGA(根治 PCM 启动内核死锁);长流护栏 + ALSA 几何收窄 period=1024×4(根治长流 ~12s 停摆);音量反转四路统一(CS43198 寄存器=衰减量,`dac=255-pct*255/100`);`dac_vol_mute`=255 真静音(炸耳修复)。
### 3.8 版本号规则(2026-08-31 定案,对齐 Android)
- `app.json` 增加 **`version_code`**(纯数字,单调递增,**安装/升级比较的唯一依据**);`version` 仅作显示字符串(versionName 角色,可为 `1.2.3-beta` 等任意形态)。
+8 -8
View File
@@ -2,14 +2,14 @@
> 目标:把 R3 Pro II 打造成可扩展的"第二系统"。**MeowOS 作为主控层优先启动、拥有屏幕与输入;HibyOS 播放器降级为 MeowOS 启动的一个内置系统应用**(Android home/launcher 模型)。MeowLauncher(即 MeowOS 核心)、MeowAPPs、MeowPanel 均为其组成部分。
> 本文档是路线图与分阶段计划。当前技术档案见 `ANALYSIS.md`,应用规范见 `APPS.md`,框架协议见 `FRAMEWORK_DESIGN.md`。
> **⚠️ 本文档为公开路线图,自身已过时**:里程碑/阶段划分以**实际进度为准**(权威现状见 `devdocs/WORK_STATE.md`,本仓库 gitignore 不入库)。截至 **M2.83(0.7.78)** 已达成(M2.69→M2.83 全链路:launcher 拆分/控件库/系统框架专项服务/主界面 MD3 双布局/M3 导航栈+M5 通知跳转/媒体会话+Now Playing/SDK nav+media 封装/字库启动竞态修复);下方「当前主线」为实际推进序列,**按依赖因果排列,非平行**;旧「阶段拓扑序」仅作背景参考,勿按它安排开发。
> **⚠️ 本文档为公开路线图,自身已过时**:里程碑/阶段划分以**实际进度为准**(权威现状见 `devdocs/WORK_STATE.md`,本仓库 gitignore 不入库)。截至 **M2.96(0.7.91)** 已达成(M2.69→M2.96 全链路:launcher 拆分/控件库/系统框架专项服务/主界面 MD3 双布局/M3 导航栈+M5 通知跳转/媒体会话+Now Playing/SDK nav+media 封装/字库启动竞态修复/**amsd 一期+二期(picker 契约)/签名鉴权 2B/流媒体客户端三期/MeowPanel 实时画面·遥屏/中文拼音 IME/开机选单抢屏回归修复/MIUIx 顶级主题/meow_audiod 独占音频守护(meow_route 并入退役)/流媒体客户端 A2 迁 audiod/M2.91-96 音频加固批(FPGA 开机武装根治 PCM 死锁·长流护栏+几何收窄·音量反转四路统一·炸耳修复)/充电态权威链/插孔消抖/amsd 注册自愈**);下方「当前主线」为实际推进序列,**按依赖因果排列,非平行**;旧「阶段拓扑序」仅作背景参考,勿按它安排开发。
---
## 当前主线(按依赖因果 [源:2026-08-30])
> 每项必须在其依赖落地后才推进(不在依赖未满足时抢先实现上层)。✅=已达成。
> **进度注(2026-08-31)**:主线 1-7 已基本达成——阶段 B 通知中心 UI(M2.65+39)、启动器 SDK 改造(M2.69 拆分 + M2.79 主界面 MD3 双布局 + 视图框架)、系统基建线(M2.67+ svcd 子进程化 + sysui 总监 + 专项服务)、M3 导航栈+M5 通知整合+媒体会话(M2.80/M2.81)、SDK 缺失件(meow_ui_nav/media/config)、蓝牙/WiFi 子页(M2.76)、MeowPanel(M2.72+)。当前新主线见 `devdocs/WORK_STATE.md`(**在跑 agent-212 amsd 一期**)。
> **进度注(2026-08-31)**:主线 1-7 已基本达成——阶段 B 通知中心 UI(M2.65+39)、启动器 SDK 改造(M2.69 拆分 + M2.79 主界面 MD3 双布局 + 视图框架)、系统基建线(M2.67+ svcd 子进程化 + sysui 总监 + 专项服务)、M3 导航栈+M5 通知整合+媒体会话(M2.80/M2.81)、SDK 缺失件(meow_ui_nav/media/config)、蓝牙/WiFi 子页(M2.76)、MeowPanel(M2.72+)。当前新主线见 `devdocs/WORK_STATE.md`(**在跑 agent-235 SDK 绘制性能回退 hello demo 滑动卡 / agent-236 顶部下滑手势分流+watchdog SIGUSR1 误杀框架 App**;amsd agent-212 一期+二期已落地)。
1. **阶段 B 通知中心 UI(进行中)** —— 依赖 M2 通知存储 + SDK 控件基座 + B2a 后端。顶部快捷按钮/亮度滑条走 `meow_settings_*` 零旁路;通知列表读 launcher `meow_notif`;手势(状态栏下拉开、上滑/点外关、左缘返回关)。
2. **启动器 SDK 改造** —— 依赖上游 SDK 控件基座(阶段 B 沉淀的 MD3 基座)。状态栏收敛单一 SDK 全局(launcher 退役 `draw_statusbar`);主界面/设置页/电源菜单迁到 SDK 控件;MD3 化、日/夜主题、全局 UI 统一。(见 FRAMEWORK_DESIGN §10.4)
@@ -91,7 +91,7 @@
### 依赖链 B【UI 基建】
```
[渲染地基 M2.16 ✅]
└─ [SDK/控件层 meow_ui, B1] ← 几乎全部 UI 的底座(设置页/通知中心/媒体播放界面/网易云都依赖它)
└─ [SDK/控件层 meow_ui, B1] ← 几乎全部 UI 的底座(设置页/通知中心/媒体播放界面/流媒体客户端都依赖它)
└─ [B2a 设置后端 meow_settings_* + settings.conf + listener] ← 设置的单一数据源
└─ [B2b 实体设置页 UI] ──┐
└─ [C MeowPanel Web 面板] ─┘(B2b 与 C 都消费同一 B2a 后端,天然镜像)
@@ -110,7 +110,7 @@
### 依赖链 D【应用主体】
```
[E 网易云客户端] ← 跑在链C 全链路(M3/M4/媒体会话)+ 链B(SDK)之上
[E 流媒体客户端(dev.player.ncm)] ← 跑在链C 全链路(M3/M4/媒体会话)+ 链B(SDK)之上
```
### 关键因果修正(相对旧平行表)
@@ -132,8 +132,8 @@
### 阶段 D(Activity/通知框架)—— 即链 C 的集合
> 通知中心/MeowPanel/应用生态的前置。协议设计已就绪(FRAMEWORK_DESIGN.md)。实现:M0 IPC 骨架 → M1 路由表 → M2 通知存储 → M3 导航/后台服务 → M4 媒体会话。M0/M1 已完成(0.7.10,M2.15)。
### 阶段 E:网易云客户端(链接 C + 链 B 之后,跑在框架上)
- 播放器「背光时间」需在应用内管理;应用退出按阶段 D 的导航语义。
### 阶段 E:流媒体客户端(dev.player.ncm;链接 C + 链 B 之后,跑在框架上)
- 播放器「背光时间」需在应用内管理;应用退出按阶段 D 的导航语义。**(已落地并迭代至三期续:core 库/复古 UI/歌单分页/封面/拼音搜索/设置页/缓存 LRU/滑动窗口/FLAC 无损;音频输出经 meow_audiod)**
### 阶段 F(可选项/远期)
- HiByLink 协议逆向 → 播放器级无线遥控;音乐库数据库管理(sqlite3 CLI 已具备);更多应用(播客/TTS/时钟等)。
@@ -169,7 +169,7 @@
2. 阶段 B:状态栏升级 + 通知中心 + 快捷按钮 + 亮度滑条 + MD3 化
3. 阶段 C:MeowPanel(含实时画面 + 远程控制菜单)
4. 阶段 D:Activity/通知 IPC 框架(先协议文档 → FRAMEWORK_DESIGN.md 已就绪)
5. 阶段 E:网易云客户端
5. 阶段 E:流媒体客户端
6. 阶段 F:HiByLink 逆向、更多应用
> 用户优先级:系统框架(阶段 0 + D)优先,MeowPanel 与网易云随后。
> 用户优先级:系统框架(阶段 0 + D)优先,MeowPanel 与流媒体客户端随后。
+12 -7
View File
@@ -1,6 +1,6 @@
# MeowOS 阶段 D:Activity / 通知 IPC 框架设计文档
> 状态:设计稿 v1(2026-08-28)。先出协议与架构,本阶段只设计、不实现。
> 状态:协议/架构参考稿(2026-08-28 起草;**以下协议已随 M2.15-M2.96 各里程碑落地**,消息号以 `patch_v2/meow_launcherd/meow_ipc.h` 为准)。本阶段的「只设计、不实现」为起草期口径;M0-M3/M5/§10.5/媒体会话/amsd/meow_audiod(独占音频守护,规范 `devdocs/plan/AUDIOD_DESIGN.md`)已实现,**M4 后台服务保活+后台播放已落地(2026-09-01,见 §9 进度)**,M6 加固待排期。
> 前置:ANALYSIS.md §18(Launcher 全架构)、§11(播放器 unix socket 先例)、APPS.md、DEVELOPMENT_PLAN.md 阶段 0/D。
> 实现语言:启动器 C(musl 静态 MIPS32r2,zig-mipsel-cc);应用同为 C 静态 MIPS。
> 补充:本文件由 plan 子代理产出设计,主会话落盘,并附「后台运行/后台播放权限」(用户追加)。
@@ -58,8 +58,10 @@ struct ipc_hdr { /* LE */
| 0x0032 | MSG_ACT_RESULT | ↓ | 返回链路 |
| 0x0040 | MSG_NAV | ↑ | 上报导航/请求退出(**M3 导航栈已落地**) |
| 0x0041 | MSG_SHOW_ACTIVITY | ↓ | 令前端应用显示某屏 |
| 0x0070 | MSG_PICKER_CALL | ↑ | 请求跨 App picker 调用(发起方 A → 持有方 B;**仅预留协议位**,本期不实现) |
| 0x0071 | MSG_PICKER_RESULT | ↓ | picker 返回值(候选结果回 A;仅预留协议位) |
| 0x0070 | MSG_PICKER_CALL | ↑ | 请求跨 App picker 调用(发起方 A → 持有方 B;**已实现**,amsd 二期 agent-225:`am picker`/`picker-status`,files_app 注册 `pick_file`) |
| 0x0071 | MSG_PICKER_RESULT | ↓ | picker 返回值(候选结果回 A;**已实现**,amsd 二期) |
| 0x00A0 | MSG_AMS_START | → | amsd→launcher:请求拉起某 App(pkg + 可选 activity,含 picker 上下文 `MEOW_PICKER_*`;**已实现** agent-212/225) |
| 0x00A1 | MSG_AMS_KILL | → | amsd→launcher:请求终止某 App(SIGTERM 语义;**已实现**) |
| 0x0072 | MSG_META | ↑ | 媒体会话上报元数据/进度/状态(**附录 B 已落地**,最近活跃源回收) |
| 0x0073 | MSG_PLAYCTL | ↓ | 播放控制下发 / ↑ 控制器转发到最近活跃媒体会话(**已落地**) |
| 0x00F0 | MSG_SYS | ↓ | 系统事件(守护关闭/服务死亡/播放器状态) |
@@ -115,8 +117,10 @@ M0 IPC 骨架(含 meow_ipc 测试钩子)→ M1 路由表+解析 → M2 通
### 进度(阶段 D)
- ✅ **M0 IPC 骨架**(0.7.10,M2.15):`meow_launcherd` 加 IPC 线程 + AF_UNIX `/usr/data/meow_ipc.sock`(unlink+bind+chmod0666+listen4)+ 帧解析(16B 头 magic=0x4D4F5701/version=1/type/req_id/len,LE)+ `MSG_HELLO/ACK/ERR` + eventfd 唤醒主循环 + 无锁 SPSC 内存环(`__sync`)+ 日志埋点。遵守 §1/§6.2:IPC 线程只读帧+解析+入队,主循环 `meow_ipc_process()` 单写者改状态,跨线程 eventfd,主循环 10ms 热键轮询每圈非阻塞排空(eventfd 失败时 10ms 轮询兜底)。测试钩子:`meow_ipc`(`patch_v2/meow_launcherd/meow_ipc.c`,静态 MIPS,与 evtool 同定位)+ launcher `--ipc-only N [--ipc-sock <path>]`。验证:`meow_ipc hello`→ACK、播放器不动(MeowOS 模式下无播放器即满足)。
- ✅ **M1 路由表 + 解析**(0.7.10,M2.15):`app.json` 扁平行 `route.<id>=path` / `framework=true` / `service.<svc>=mode:restart:max:window_ms` / `icon` 解析入 `g_accts` 注册表(主循环单写者);`MSG_ACT_DECLARE`(运行时并集覆盖静态)/`MSG_ACT_QUERY`/`MSG_ACT_RESULT`(深度+链路);`resolve_chain(app,activity)` 返回 `[root..target]` 链路与深度——`route` 缺失(app 未声明路由)→ 深度 1 兜底 `chain=activity`;声明了路由但 activity 非 route 键 → `MEOW_ERR_NOENT`(activity 未注册)。验证:`meow_ipc query <app> <activity>` 返回链路(qemu + 真机 MeowOS 模式均通)。
- ✅ **M2 通知存储+分发**、**M3 导航+前台会话改造+看门狗返回**、**M5 通知中心 UI 整合**(详见下,M2.65-M2.82 已落地);⬜ **M4 后台服务保活**(`MSG_REGISTER` 后台服务登记)、**M6 加固**:待排期。
- **M3 已落地(M2.80,agent-202 · 85b4aea)**:`nav_stack`(≤16 深)+ `MSG_NAV` REPORT/EXIT + `launch_activity` 链路注入(env `MEOW_ACTIVITY`/`MEOW_CHAIN`)+ 栈根退出回主屏;通知点击按 `route_status`(0 拉起/1 NO ROUTE/2 SOURCE NOT FOUND/-1 NONE 关面板不动作);picker 协议位预留(0x0070/0x0071);裸 App 兼容。
- ✅ **M2 通知存储+分发**、**M3 导航+前台会话改造+看门狗返回**、**M5 通知中心 UI 整合**(详见下,M2.65-M2.82 已落地);✅ **M4 后台服务保活**(2026-09-01 落地,见下);⬜ **M6 加固**:待排期。
- **M4 已落地(2026-09-01)**:`app.json` 新增 `background`/`background_audio` 解析入注册表(`struct app`/`meow_acct` 同名字段);`MSG_REGISTER`/`MSG_SERVICE_OK`/`MSG_SERVICE_STOP`/`MSG_SYS` 负载结构落地(`m_register_t`/`m_service_stop_t`/`m_sys_t`,MEOW_SYS_SHUTDOWN=1/SVC_DEAD=2);新模块 `meow_bgplay.c/h`(自包含+ops 注入,宿主可单测):**后台播放保活**——声明 `background_audio` 且媒体会话 PLAYING 的 App nav-exit 时**不终止**,进程+音频流保留(UI 冻结走 §10.5 `/tmp/meow_notif_active` SDK present 自抑,**非字面 SIGSTOP**:冻结同进程播放线程会断供 PCM,§10.5 真机实证),点图标/媒体卡片回前台=原进程恢复(chain/picker 忽略),停播后再退出=真退 SIGTERM;**无头后台服务**——MSG_REGISTER spawn/stop/reap + 退避重启 1/2/4/10s cap + `max_restart` 熔断(清该 App 通知 + 注册连接广播 MSG_SYS{SVC_DEAD},重注册复位熔断);卡拔例外(二进制在卡上的后台 App/服务仍 SIGTERM 防 SIGBUS)、崩溃照收、关机前 `meow_bgplay_kill_all` 有界回收(含 MSG_SYS{SHUTDOWN} 广播);主界面应用列表「·后台」标注(1s tick 按 change_seq 刷新);tick 挂载点=`meow_ipc_process` 末尾(主循环单写者排空点)。宿主单测 `test_bgplay.c` 50 项全过;amsd 侧 BG 状态展示=§6.3 顺延。
- **M3 已落地(M2.80,agent-202 · 85b4aea)**:`nav_stack`(≤16 深)+ `MSG_NAV` REPORT/EXIT + `launch_activity` 链路注入(env `MEOW_ACTIVITY`/`MEOW_CHAIN`)+ 栈根退出回主屏;通知点击按 `route_status`(0 拉起/1 NO ROUTE/2 SOURCE NOT FOUND/-1 NONE 关面板不动作);裸 App 兼容。
- **amsd(一期 agent-212 · 5fbf307 + 二期 agent-225)已落地**:活动管理器守护(svcd 托管 `trigger=boot`)+ `am` CLI(status/list/history/start/kill/`resolve`/`picker`)。二期将路由表迁 amsd(amsd 路由权威)+ **picker 契约**(`MSG_PICKER_CALL=0x70`/`MSG_PICKER_RESULT=0x71`,`am picker`/`picker-status`;files_app 注册 `picker.pick_file` 选文件返回路径)+ `MSG_AMS_START/KILL=0x00A0/0x00A1`(amsd→launcher 决策执行)。参见 `devdocs/plan/AMSD_DESIGN.md`。
- **M5 通知中心 UI 整合已落地(M2.65-M2.81)**:通知中心 + 媒体卡片 + Now Playing(媒体会话见附录 B,已落地:`meow_media.c` 最近活跃源、30s 超时回收、`MSG_META`/`MSG_PLAYCTL`)。
- **§10.5 App 前台唤起通知中心已落地(M2.82,agent-206 · 9b41957)**:`MSG_NOTIF_OPEN=0x28`/`CLOSE=0x29` + SDK `meow_ui_nav`/`meow_ui_media` 封装。
- ✅ **渲染地基(0.7.11,M2.16,前置基座)**:所有 UI 统一走 **RAM 离屏后备缓冲 `back_buf`(480×720×2=691,200B) + `meow_present`(dirty)** = `FBIO_WAITFORVSYNC` 等垂直同步 + 单次原子 memcpy(脏区或全帧)到 fb0(buffer0,yoffset=0)——无渐进/无中间态/无撕裂。脏区 `meow_dirty_add`(矩形合并集、相交吸 bbox、>12 转全屏、裁可视区);状态栏时钟/右侧格按需局部标脏。整合铺垫:框架后续 LAUNCH/NOTIFY/前台会话的绘制复用该机制(而非旧「全屏 clear→重绘到 buffer0」路径)。详情见 ANALYSIS.md §18.43。刷新率调度(display.refresh_rate 60/30/15 + refresh_always_max + 自适应降档 governor)归口 PERF_SCHEDULER.md。
@@ -144,6 +148,7 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最
├─ meow_launcherd 系统 UI 渲染宿主(主界面/设置页/电源菜单/全局状态栏/通知中心/全局手势)
├─ meow_svcd 服务管理器(服务=独立子进程;逐个子进程崩溃隔离)
│ ├─ meow_mdns mDNS 应答器(板载 /usr/data/meow/services.d/mdnsd.conf,trigger=boot)
│ ├─ meow_audiod 独占音频守护(M2.90;独占 hw:0,0+抢占+路由并入;HibyOS 模式门控 exit(0) 不启动——规范 devdocs/plan/AUDIOD_DESIGN.md)
│ └─ 未来媒体服务 / 其它 services.d 服务(TF MeowOS/services.d/…)
├─ 未来媒体服务 / MeowPanel(后置)
```
@@ -253,7 +258,7 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最
## 附录 B(用户追加):默认播放控制界面 / 正在播放(Media Session)
**需求**:MeowOS 启动器内置一个**默认播放控制界面**(Now Playing)。其他播放类应用(如网易云)在后台播放时,通过接口提供**实时播放数据**(元数据、歌词、封面等),供启动器的播放界面显示;启动器界面也能向应用下发**播放控制指令**(播放/暂停/上一首/下一首/进度)。
**需求**:MeowOS 启动器内置一个**默认播放控制界面**(Now Playing)。其他播放类应用(如流媒体客户端 dev.player.ncm)在后台播放时,通过接口提供**实时播放数据**(元数据、歌词、封面等),供启动器的播放界面显示;启动器界面也能向应用下发**播放控制指令**(播放/暂停/上一首/下一首/进度)。
**设计**(在 §1 IPC 基础上新增"媒体会话"消息):
- **上报(应用→启动器)`MSG_META`**:实时元数据。负载:
@@ -274,7 +279,7 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最
- 流式更新(播放位置/进度)用高频 `MSG_META`(或拆出轻量 `MSG_PROGRESS`,由应用按 ~1s 推送);元数据变化低频。
- **指令(启动器→应用)`MSG_PLAYCTL`**:
```c
struct m_playctl { uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek */ int64_t seek_ms; };
struct m_playctl { uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停toggle(物理键164恒发6,由源侧按当前态翻转;262) */ int64_t seek_ms; };
```
- 启动器播放界面上的按钮/滑条 → 通过该应用的**服务连接**下发。
- **注册**:播放应用声明 `MF_PLAYBACK` 并注册**媒体会话**(`MSG_REGISTER` 带 `svc=media_session` 或 `HELLO.flags` 含 PLAYBACK)→ 成为当前"正在播放"提供的源。
+5 -2
View File
@@ -95,7 +95,10 @@ watchdog 主循环(`for(;;)`)加按键轮询(`read /dev/input/event0/event
偏差(实现以稳妥为准,记录如下):
1. **8s 强关拆分**:设计给 watchdog,但为避免与菜单内 `pm_interacted` 冲突(用户正点选时被强关抢先),落地为「launcher 菜单内 8s 强关(pm_interacted 保护)+ watchdog 8s 兜底(仅当菜单未接管,即 `/tmp/meow_power_menu_active` 未出现)」。
2. **音量跳步**:watchdog 每按下边沿按 `audio.volume_step` 一步(不做 launcher 的线性渐变);短按与 launcher 菜单一致,长按不连续爬(媒体落地后可补)。launcher 应用返回时 `settings_load()` 同步。
3. **左右滑切屏**:菜单触摸段已留 `menu swipe (dx,dy) — no launch` 判定,未实现切换逻辑。
4. **媒体键**:仅抓消费(日志 `media key captured (code N) -> consumed, no route`),路由待阶段 D。
3. **左右滑切屏**:**已落地(M2.81 Now Playing 横划页)**——主界面左滑进 Now Playing 大页;左缘返回手势保留。此处旧注「未实现切换逻辑」作废。
4. **媒体键**:**已落地(M2.81 起路由最近活跃媒体会话)**——播放/上下曲路由到最近活跃媒体会话(MSG_PLAYCTL),无会话时「抓住」不动作。此处旧注「路由待阶段 D」作废。
5. **插孔消抖(2026-09-01,真机日志实证)**:4.4/3.5 插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生 VOL_UP 幽灵按住,launcher vol_ramp 曾从 24% 一路爬到上限 50(日志 225391 ramp 起 → 227213 爬满 → 紧随 audiod「jack changed」)。修:launcher 与 watchdog 音量键处理统一读 `/sys/class/switch/{headset,balance}/state`(节流读缓存:launcher 1s / watchdog 500ms,绝不在热路径每圈读文件),插孔状态变化后 **800ms 内忽略音量键边沿**(vol_ramp_begin 不起、在爬 ramp 中止、watchdog 单步忽略);另有 **ramp 总时长上限 VOL_RAMP_MAX_MS=3000** 兜底,防任何「持续按住」假象一次打满。~~SDK 输入层需同款消抖~~ → **SDK 侧已补齐(56c569f,meow_ui.a 12cdd5d7,hello/files/流媒体客户端重链热更)**:框架 App 前台时的音量键路径同款 800ms 消抖窗。
6. **音量反转统一(M2.95,2026-09-01)**:CS43198 音量寄存器 = **衰减量**(0=最响 255=静音),旧映射方向反(0 档炸耳、50 档最低)。launcher/audio.c + audiod + **watchdog `wd_vol_adj`** + SDK vol 四路统一 `dac = 255 - pct*255/100`。**watchdog 存盘修复(M2.93)**:`wd_vol_save` 旧 O_TRUNC 整文件重写 settings.conf 曾毁配置(整文件被抹/重复行/丢 max+step)→ 改读改写单键+去重(与 SDK 同款语义)。
7. **播放/暂停键(164) toggle(262,2026-09-01)**:SDK `meow_ui_media` 旧按本地 `g_last_state` 判 play/pause,流媒体客户端走自有 ipc_media 线程不经 SDK meta → 恒发 cmd=1 播放、播放中按键无效。修:164 **恒发 `MSG_PLAYCTL cmd=6`(toggle)**,由源侧按当前态翻转(launcher 透传不动结构;媒体卡片/NP 页按钮按 launcher 侧 META state 判,本就准,维持现状)。同批修:App 前台状态栏音量%/亮度实时刷新(launcher 叠加层 1s 节流 stat settings.conf,mtime 变才重读同步 g_sys)。
真机验收要点(§7):应用内电源单击/长按菜单/8s 强关、音量±调音量、播放/上下曲无反应(被抓住)、launcher 菜单同样一致、电源菜单任何界面可弹(含应用内)、息屏后任意键唤醒。
+885 -19
View File
File diff suppressed because it is too large. Load diff
+46 -18
View File
@@ -3,9 +3,18 @@
> 把 HiBy R3 Pro II 便携播放器(君正 X1600,MIPS32r2 / mipsel / musl 静态软浮点,480×720,无 GPU 软渲染)变成一个可扩展的「第二系统 + 应用框架」。
> MeowOS 与官方 HibyOS 共存于同一台设备:开机由引导选单二选一;启动后 MeowOS 作为主控层,提供自己的桌面 UI、应用框架、服务管理与系统组件。
>
> 版本:**M2.83(0.7.78)**(M2.69→M2.83 全链路自刷验收:控件库/主界面 MD3 双布局/M3 导航栈/M5 通知跳转/媒体会话/Now Playing/SDK nav+media 封装/字库启动竞态修复)。版本号单点 = 板上 `/etc/meowos_version`,见「构建」。本仓库是一个**自带 SDK、包管理、媒体会话、网页面板、网易云客户端的完整第三方系统**。
> 版本:**M6.35(0.8.35,已上卡)**。M6.19→M6.35 连续十七版推卡自刷验收——**M6.19**:ncm 后台化 UI 叠层/点击穿透+再进 ncm 桌面叠层+播放页新旧 UI 叠层抢刷脏区(软自抑帧移交不完整,恢复边沿强制整帧重画)+PIN 亮屏亮度跳+通知中心封面显歌单封面;**M6.20**:播放页右滑返回+顶部下滑通知中心+状态栏时钟 1s;**M6.21-22**:播放页返回修复(`np_exit` 退出外层主循环)+方向感知手势;**M6.23**:watchdog 亮屏亮度按用户设置恢复;**M6.24**:App 内按电源键锁屏时序;**M6.25**:bootmenu 开机菜单亮度方案 A;**M6.26**:播放页三键命中+媒体卡片体 FOCUS;**M6.27**:netmgrd 流量统计收编(pub_bytes/去 lo)+面板公网流量卡;**M6.28**:收编桌面 Now Playing 双页歌词页+歌词数据源+蓝牙 P1(netmgrd BT_CTL/bt_page);**M6.29**:播放页任意右滑返回桌面;**M6.30**:收编批5 取色+蓝牙 P2(audiod glibc-dynamic)+面板蓝牙卡;**M6.31**:桌面歌词页封面修复+alsa.conf DEV 自动识别;**M6.32**:重编 meow_ui.a+重链四 App(播放页进度条取色+ncm 列表 gap)+面板透传;**M6.33**:收编 launcher 播放页占位符移除(已有独立歌词页)+封面「空→复原」恢复修复(test_np_cover 14 项 PASS);**M6.34**:收编**批①后台常驻+宽限期**(全 App 入后台表+30min `MEOW_BG_GRACE_MS` 宽限超时回收+声明常驻/媒体活跃无限期保活+槽满 LRU+app.json `background_keepalive` 三权限位+amsd `EV BG/FG` 上报)+**audiod A3/DSD**(24/32bit 高清 byte-exact+独立 `AUDIO_FMT_DOP` DoP DSD 透传+音量收编 v2 双组写)+**meow_img_pixels 访问器上收**+**面板拆分+屏幕·遥控合并页**(前端三层+左屏右遥控 MD3 SVG 矢量按钮);**M6.35**:收编**批③后台管理页**(桌面顶栏「后台」入口+`bg_page` 后台列表+每行「杀」按钮+顶栏「一键清」,`!framework` 门控不杀框架)+**amsd FG/BG 收编**(`57e19cde`)+**i18n settings/lock 补抽**。M6.35 之后工作区已提交(暂未打包,并入 M6.36):批①叠加/穿透根治(`/tmp/meow_notif_active` 内容=前台归属 app_id+SDK present/input 共享判定+launcher 7 处转场)、主界面顶栏后台按钮图标美化、主界面列表圆角卡 8px 外距、后台管理页行 MD3 圆角卡;**MD3 对齐在跑**(agent-435,参考 `third_party/md-material-web` 最新 M3)。M6.10→M6.18:M7 launcher 重写第 1-5 期(M6.10-14:虚拟化宫格/设置页卡顿修复/OOM 通用化/自动切歌根治)+(M6.15)meowpanel jv_free 泄漏修复+(M6.16)Hello demo 滚动卡顿修复+(M6.17)歌词 UI+(M6.18)媒体通知封面+封面分辨率。M6.1→M6.9 全链自刷验收——**M6.1**:netmgrd 期3/powermgr 期2/面板 PIN 鉴权+首页仪表盘/WiFi 录入 UI/服务名统一 meow_*;**M6.2**:锁屏 PIN 全量(`system.pin` 同源面板)+VOL_DOWN×3 框架门控+显示名 name[49](amsd/launcher UTF-8 整序列截断);**M6.3**:设置页 5s 修复(309 网络异步步骤机+跳过字体 sha 校验)+310 锁屏三连(输入穿透/解锁状态栏消失/App 死亡残影);**M6.4**:310 二期 **EVIOCGRAB 锁屏独占触控**(未冻结 App 锁屏输入隔离)+主界面应用名截断;**M6.5**:audiod 315 孤儿会话两段式回收(客户端崩溃/OOM 杀/正常退出后会话完整回收,STATE 回干净无流基线);**M6.6**:锁屏 310 三期——App 内手动息屏也触发锁屏 + 锁屏脏区全屏不透明底 + 300ms 重铺;**M6.7**:锁屏页自睡(空闲超时自动灭屏,294 遗留补;0.8.7 补刷吸收 fw_content 事故修复——拆分 watchdog+meow_audio 两行);**M6.8**:310 四轮 锁屏自睡 min(系统息屏,30s);**M6.9**:netmgrd 期4(SDK 自愿通道/forget/RSSI/AAAA 过滤/iptables 评估)+powermgr 期3(policy/标定工具)固化 + 327 M7.0b launcher 试点(电源菜单/Now Playing 控件化+主题薄壳+i18n 抽串);历史:M2.69→M2.96 全链路自刷验收:控件库/主界面 MD3 双布局/M3 导航栈/M5 通知跳转/媒体会话/Now Playing/SDK nav+media 封装/字库启动竞态修复**/M2.84-88**:installed 图标修复+残帧pan加固/物理键EVIOCGKEY双源/签名鉴权 2B/meow_route 音频路由/amsd 二期(picker 契约)/流媒体客户端二期(封面·歌词后台·大歌单分页)/MeowPanel 实时画面+遥控+通知实时化/中文拼音 IME**/M2.89**:开机选单抢屏回归修复+watchdog SIGUSR1 不误杀框架 App+amsd 失联修复+顶部下滑手势分流**/M2.90**:**meow_audiod 独占音频守护**(AUDIOD_DESIGN,meow_route 并入退役)+MIUIx 顶级主题+流媒体客户端文案中文化+面板字体库 tar.gz 导入导出**/M2.91-96**:audiod 开机武装 hbc3000 FPGA(根治 PCM 启动内核死锁)+svc_power 充电态权威链(mp2731)+amsd 注册自愈(`am rescan`)/Now Line truncated
>
> 许可证:Apache-2.0(见 [`LICENSE`](LICENSE))。本仓库为非官方第三方项目。
> 主仓**公开可克隆**:`https://git.yeij.top/AskaEth/MeowOS-For-R3P2.git`(维护者推送走
> `ssh://git@git.yeij.top:2222/AskaEth/MeowOS-For-R3P2.git`)。
> **第三方组件声明(LGPL)**:阅读器 `os.meow.reader` 静态链接 **libmobi(LGPL-3.0)**。
> 其 NOTICE + GPL-3.0 + LGPL-3.0 全文随 `.mpk` 的 `resources/licenses/` 分发(见
> `patch_v2/reader_app/NOTICE`、`patch_v2/reader_app/resources/licenses/`);libmobi 在
> `build_reader.sh` 锁定 commit `906274205c11944b628da1c553b255acb1af7c55`;替换/重链指引见
> `patch_v2/reader_app/README_LGPL_RELINK.md`。阅读器自有代码仍为 Apache-2.0(LGPL §1 豁免)。
>
> **公开 SDK 仓(面向第三方开发者)**:`ssh://git@git.yeij.top:2222/AskaEth/MeowOS-SDK.git`(MIT;只含 `sdk/`(meow_ui)+ `examples/hello/` + `docs/`,自包含可构建,不含系统组件/密钥)。由本仓经 `git subtree split` 拆出,增量同步见 `scripts/publish_sdk.sh`。
---
@@ -15,9 +24,10 @@
- **双系统共存**:官方 HibyOS 原样保留;开机引导选单决定本次进入哪个系统(MeowOS / HibyOS),两者互斥运行,任一时刻只跑一个。
- **应用框架**:一套 C 应用 SDK(`meow_ui`),第三方 App 用统一控件 / 输入 / 主题 / 字体 / 通知 API 开发,无需自读帧缓冲、自读输入、自管返回——把每个 App 都要踩一遍的坑收口到框架里。
- **系统组件**:状态栏、通知中心、电源菜单、全局左缘返回手势、主界面(应用列表,**MD3 卡片宫格/列表双布局**)、设置页、服务管理器、mDNS 广播;系统进程总监 `meow_sysui`;专项子服务 `meow_svc_perf`(性能/系统只读聚合,含真实 network)与 `meow_svc_power`(关机/重启/升级/回 HiBy,含升级写保护 + dac 真实音量 + 读级鉴权放宽);系统级崩溃捕获 `meow_crash` + App 崩溃直报;.mpk **Ed25519 签名验签** `meow_verify`(见 §2.9);MeowPanel 外部管理面板(HTTP/JSON 后端 + Web 前端,支持可选密码鉴权,见 §2.10)。
- **媒体会话 / Now Playing**:`MSG_META`/`MSG_PLAYCTL`(0x72/0x73)媒体会话(最近活跃源,30s 超时回收)+ 通知中心**媒体卡片** + 主界面左滑 **Now Playing 大页**;媒体键路由到最近活跃媒体会话。
- **应用生态**:**网易云 os.meow.ncm**(`core/`:mbedtls+jsmn+qrcodegen+minimp3 ≈438KB;v1 复古 UI:登录二维码/我创建的·我收藏的双页签/搜索/歌单详情/黑胶播放页/设置,ALSA 直开 hw:0,0 播放,登录 803 已通);外置**系统文件管理器 os.meow.files**;`meow_ui` SDK 配套结构化列表 `W_LISTVIEW`、通用长按、SDK nav/media 封装、视图框架(可嵌套 view + 事件路由 + 绘制期 clip);App 通知查询/清除 IPC(0x25-27)与**点击通知跳转路由**(route_status)。
- **系统组件**:状态栏、通知中心、电源菜单、全局左缘返回手势、主界面(应用列表,**MD3/MIUIx 顶级主题×明暗**,卡片宫格/列表双布局)、设置页、服务管理器、mDNS 广播;系统进程总监 `meow_sysui`;专项子服务 `meow_svc_perf`(性能/系统只读聚合,含真实 network)与 `meow_svc_power`(关机/重启/升级/回 HiBy,含升级写保护 + dac 真实音量 + 读级鉴权放宽);**独占音频守护 `meow_audiod`**(M2.90:App 经 SDK `meow_audio_*` 喂 PCM,守护独占 `hw:0,0`——DAC 原生率跟随无 SRC、格式归一、4.4/3.5 路由自动跟随插孔、抢占+EOF 回收、CLI status/tone/route/vol,规范 `devdocs/plan/AUDIOD_DESIGN.md`);系统级崩溃捕获 `meow_crash` + App 崩溃直报;.mpk **Ed25519 签名验签** `meow_verify`(见 §2.9);**网络管理服务 `meow_netmgrd`**(M6.0 期1 / M6.1 期3:接口统一层/连接普查三实体/DNS 代理/策略引擎防自锁,HibyOS 只读)与**功耗管理服务 `meow_powermgr`**(M6.0 期1 / M6.1 期2:功耗查看/分解/历史/事件,HibyOS 只读);MeowPanel 外部管理面板(HTTP/JSON 后端 + Web 前端,支持可选密码鉴权 + 4 位 PIN 访问级鉴权 + 首页仪表盘 + 字体库 tar.gz 导入导出,见 §2.10)。
- **媒体会话 / Now Playing**:`MSG_META`/`MSG_PLAYCTL`(0x72/0x73)媒体会话(最近活跃源,30s 超时回收)+ 通知中心**媒体卡片** + 主界面左滑 **Now Playing 大页**(播放页⇄歌词页双页,方向感知手势:播放页右→左进歌词/左缘右滑回桌面、歌词页左→右回播放);媒体键路由到最近活跃媒体会话。
- **App 后台常驻框架(M6.34 起,`devdocs/plan/APP_BACKGROUND_FRAMEWORK.md`)**:App 返回桌面**不立即杀**——`meow_media_app_resident` 按 App 自身会话判活跃 + `meow_bg_should_keep` 三参(keepalive/媒体活跃/声明)+ app.json **`background_keepalive`** 声明 + amsd `EV BG/FG` 上报;**宽限期**`MEOW_BG_GRACE_MS`(30min)全 App 入后台表,未声明超时回收(SIGTERM→SIGKILL 真退序列),声明常驻/媒体活跃**无限期保活**,槽满 LRU;**批③后台管理页**(M6.35,桌面顶栏「后台」入口):列出每个后台常驻 App(名字+CPU/内存+状态)+ 每行「杀」按钮 + 顶栏「一键清」(`!framework` 门控不杀框架 App),数据源=amsd FG/BG + svc_perf 同口径 /proc 采样。批②底部上滑回桌面+home 指示条待做、批④面板后台列表排队。
- **应用生态**:**流媒体客户端 dev.player.ncm**(`core/`:mbedtls+jsmn+qrcodegen+minimp3 ≈438KB;复古 UI:登录二维码/我创建的·我收藏的双页签/搜索/歌单详情/黑胶播放页/设置,登录 803 已通;二期:专辑封面/歌词后台加载/大歌单分页;三期:歌单真分页/真实专辑封面/歌词锁修复/搜索接拼音 IME 中文搜索/设置页(音质 5 档+缓存上限+清理缓存)/缓存 LRU 淘汰接生产路径/文案全中文;三期续:歌曲对象瘦身 1324B→608B+256 首滑动窗口(任意长度歌单)/封面与缓冲 UX 修复(消除 worker 忙等 100% CPU)/FLAC 无损(dr_flac)+缓存键=歌曲 ID+音质档位/物理播放键 toggle 全链路;2026-09-02 批次:真机四连症状(豆腐/歌词一行/封面加载/音量键 SIGKILL)+返回首页竞态 staging+内存瘦身(封面 8.28MB→0.78MB)+流媒体专项六项(档位保真/封面 4xx 不重试/收藏显隐/缓存命名迁移/topbar 位图图标)+三死因闭环(OOM/误杀/歌词栈溢出)+封面链分诊+风控加固(接口 120s 缓存+游客令牌)+**M6.10 批次**(批B UI/批C 播放后台/338 VIP3 档+多歌手/336 根因——每次 audio_out_open 建短连接致 audiod fd 风暴已修;卡上热更 `bef4b0e1`,内存 40→49.2MB OOM 疑 page cache 待真机取证);**2026-09-03 桌面歌词/取色/蓝牙批**:桌面 Now Playing「播放页⇄歌词页」双页+歌词链路批1-5(SDK `meow_lyric` 通用歌词组件+`meow_lyric_view` 歌词页渲染控件+media 会话歌词数据源 `meow_media_lyric_*`/MSG_META_LYRIC+方向感知手势)+封面双色渐变取色(`meow_img_color`)+ncm 底部 mini 播放栏/播放模式三态+实时播放列表/列表项 8px 外距;**蓝牙 P0/P1/P2**(`aplay -D bluealsa` 真机出声打通/netmgrd BT_CTL 权威收口+launcher 蓝牙子页重做/audiod OUT_BT 输出目标+音量 AVRCP 隔离+alsa.conf DEV 自动识别))。**A2 起音频输出走 `meow_audiod`**(SINK_AUDIOD 默认后端,SDK `meow_audio_*` 客户端,被抢占回调→暂停;旧 ALSA 直开仅 `MEOW_AUDIO_DIRECT=1` 调试);外置**系统文件管理器 os.meow.files**;`meow_ui` SDK 配套结构化列表 `W_LISTVIEW`、通用长按、SDK nav/media 封装、视图框架(可嵌套 view + 事件路由 + 绘制期 clip);App 通知查询/清除 IPC(0x25-27)与**点击通知跳转路由**(route_status)。
- **进程隔离**:系统 UI 渲染进程与 App 进程相互独立,App 卡死不会冻结整个 UI。
- **包管理**:`nyai`(固件内置 `/usr/bin/nyai`)支持 .mpk 安装/升级/导出/列表/索引,走 **version_code 安卓式版本规则**(缺 code 拒装、防降级)+ 签名分级(有签必验、`os.meow.*` 无签拒装、第三方直装告警)+ 导出过滤与体检。
@@ -43,18 +53,21 @@
```
init (rcS)
├─ /etc/init.d/S70meow_sysui 系统进程总监(meow_sysui,S70,M2.71 引入;监管 svcd,launcher 由 bootmenu 拥有)
├─ /etc/init.d/S70meow_sysui 系统进程总监(meow_sysui,S70,M2.71 引入;监管 svcd + launcher,M2.83 起 launcher 移交 sysui)
├─ /etc/init.d/S89meow_svcd 开机自启 meow_svcd(服务管理器)
│ ├─ meow_mdns 及 services.d 服务 = 独立子进程
│ └─ 专项子服务(一期)= meow_svc_perf / meow_svc_power
│ ├─ meow_audiod 独占音频守护(M2.90,含路由/格式归一;HibyOS 模式不启动)
│ ├─ 专项子服务 = meow_svc_perf / meow_svc_power
│ ├─ meow_netmgrd 网络管理服务(M6.0 期1 / M6.1 期3:接口统一层+连接普查三实体+DNS 代理+策略引擎;HibyOS 只读)
│ └─ meow_powermgr 功耗管理服务(M6.0 期1 / M6.1 期2:功耗查看/分解 top/历史/事件;HibyOS 只读)
└─ /etc/init.d/S90meow_bootmenu Supervisor / 开机引导选单
├─(选 MeowOS)execv meow_launcherd ← 系统 UI 渲染宿主
│ └─ 用户点 App → fork/exec MeowAPP + meow_watchdog(每 App 一个)
└─(选 HiByOS)exec hiby_player
```
- **进程总监 `meow_sysui`(M2.71 已落地,`/usr/bin/meow_sysui` + `S70meow_sysui`)**:监管系统进程,**一个崩→只拉起它,绝不级联、绝不拖垮自身**。自身最小依赖、不渲染、不承载重逻辑 → 最稳。当前默认只监护 `meow_svcd`(launcher 仍由 bootmenu 的 `execv` 拥有,`enabled=0` 防双重拉起,移交见后端让位);`meow_mdns` 是 svcd 的服务子进程(不双重管理)。配置 `sysui.d/` 目录式(复用 services.d 语义),见 `patch_v2/meow_sysui/meow_sysui.md` 与 `docs/MEOWOS_SPEC_REF.md` §4。
- **专项子服务(M2.73 已落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:新增两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能 / 系统只读聚合,含 network)、`meow_svc_power`(关机 / 重启 / 升级 / 回 HiBy 系统动作,含升级写保护 + dac 真实音量)。launcher 电源菜单与 MeowPanel 改经这些服务 IPC 触发,不再直连 `sys_do_reboot`/爬 `/proc`。电源 CLI `meow_svc_power <cmd>`(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot)。**二期**补关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签。详见 §2.9。
- **进程总监 `meow_sysui`(M2.71 已落地,`/usr/bin/meow_sysui` + `S70meow_sysui`)**:监管系统进程,**一个崩→只拉起它,绝不级联、绝不拖垮自身**。自身最小依赖、不渲染、不承载重逻辑 → 最稳。默认监护 `meow_svcd` + `meow_launcherd`(**M2.83 起 launcher 生命周期移交 sysui**:`sysui.d/launcher.conf` `enabled=1`(restart=always/backoff=3000/max_restart=5);bootmenu 在 MeowOS 模式让位——sysui 在则不 exec launcher);`meow_mdns` 是 svcd 的服务子进程(不双重管理)。配置 `sysui.d/` 目录式(复用 services.d 语义),见 `patch_v2/meow_sysui/meow_sysui.md` 与 `docs/MEOWOS_SPEC_REF.md` §4。
- **专项子服务(M2.73 已落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:新增两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能 / 系统只读聚合,含 network)、`meow_svc_power`(关机 / 重启 / 升级 / 回 HiBy 系统动作,含升级写保护 + dac 真实音量)。launcher 电源菜单与 MeowPanel 改经这些服务 IPC 触发,不再直连 `sys_do_reboot`/爬 `/proc`。电源 CLI `meow_svc_power <cmd>`(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot)。**二期已落地**:关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签(agent-185 2A / agent-221 2B)。详见 §2.9。
- **隔离保证(现已落地)**:系统组件各自独立进程;App 进程与系统 UI 渲染进程独立;svcd 的**每个服务是独立子进程**(服务崩只重启该服务,不拖垮 svcd)。任何 App / 服务崩溃都不冻结系统 UI。
### 2.2 双系统引导(bootmenu)
@@ -67,14 +80,16 @@ init (rcS)
### 2.3 launcher —— 系统 UI 渲染宿主
`/usr/bin/meow_launcherd`是 MeowOS 的核心进程,负责渲染**系统级 UI**。**二进制名不变**(`/usr/bin/meow_launcherd`,bootmenu `LAUNCHER_BIN` 指向它),源码 M2.69 起由单文件 `patch_v2/meow_launcherd/meow_launcherd.c`(9778 行)**拆分为多模块**(`patch_v2/meow_launcherd/`,当前含 `main_menu/statusbar/settings_page/power_menu/notif_center/app_launcher/fb_compositor/audio/backlight/net/about/font_theme/keys/util` + 后期新增 `subpage/wifi_page/bt_page`(蓝牙/WiFi 独立子页)、`now_playing`(Now Playing 大页)、`meow_media`(媒体会话,最近活跃源 + 30s 超时回收)+ 共享头 `meow_launcher_internal.h`;`build_launcher.sh` 列模块)——逻辑更清晰、便于并行开发,行为不变。
`/usr/bin/meow_launcherd`是 MeowOS 的核心进程,负责渲染**系统级 UI**。**二进制名不变**(`/usr/bin/meow_launcherd`,bootmenu `LAUNCHER_BIN` 指向它),源码 M2.69 起由单文件 `patch_v2/meow_launcherd/meow_launcherd.c`(9778 行)**拆分为多模块**(`patch_v2/meow_launcherd/`,当前含 `main_menu/statusbar/settings_page/power_menu/notif_center/app_launcher/fb_compositor/audio/backlight/net/about/font_theme/keys/util` + 后期新增 `subpage/wifi_page/bt_page`(蓝牙/WiFi 独立子页)、`now_playing`(Now Playing 大页)、`meow_media`(媒体会话,最近活跃源 + 30s 超时回收)、`bg_page`(批③后台管理页)+ `meow_bgplay`/`meow_bgmgr`(批①后台常驻保活/管理逻辑)+ 共享头 `meow_launcher_internal.h`;`build_launcher.sh` 列模块)——逻辑更清晰、便于并行开发,行为不变。
负责渲染:
- **状态栏(统一系统进程渲染)**:由 launcher 作为**唯一渲染主体**(主界面与 App 前台用**同一个**状态栏实例,App 不画、不做「样式一样的两套实现」);右侧 **MD3 图标 + 变量值**(时钟/电量/WiFi/蓝牙/音量,数据源唯一由 launcher 统一喂/取,两边必然一致);**全屏收合**:状态栏向上收出屏顶、只留提示线,**顶部保留 28px 热区**(仍可下滑通知中心)+ 左缘指示条;随主题 `ui.style` 换图标集。
- 通知中心(顶部下滑展开,launcher 渲染,功能见 §2.6);
- 电源菜单、全局左缘返回手势、全屏提示线;
- 主界面(应用列表)、设置页。
- 主界面(应用列表)、设置页、**后台管理页**(`bg_page`,M6.35:桌面顶栏「后台」入口,列出后台常驻 App + 每行杀 + 一键清);
- **后台常驻框架**(M6.34):App 返回桌面保活 + 宽限期回收(`meow_bgplay`),amsd `EV BG/FG` 上报数据源;
- **锁屏(M6.2 起)**:`lock_screen.c` 4 位 PIN(`system.pin` 同源面板 sha256+salt)+ 息屏唤醒锁 `system.pin_lock_on_wake` + 错 5 锁 5 分钟持久化 + 电源菜单「锁定」(作废一次性绕过标志)+ 设置「安全→锁屏 PIN」页(设/改需旧、清免旧)+ `/tmp/meow_pin_unlock_once` 一次性解锁恢复口;M6.4 起锁屏用 **EVIOCGRAB 独占触控**杜绝输入穿透下层 App。
**渲染与 App 进程隔离**:launcher 是系统进程,App 是独立进程。App 卡死/崩溃只影响该 App,系统 UI(状态栏/返回/通知中心)始终可用。
@@ -106,6 +121,13 @@ init (rcS)
- **M2.73 控件库修复(`meow_ui.a` md5 `e8341975`,见 `MEOW_APP_SDK.md` §5.8)**:`W_LIST`(含 LazyColumn/LazyRow)**视口裁剪**——滚动时只绘制落在视口内的行/分隔线/文本,杜绝「行滚出视口把内容画到视口外并标脏上屏」导致的标题栏/底部脏区(SDK 全局生效,非 App 侧)。IME(`meow_ui_ime.c`):键盘区背景快照/恢复(无残影)、配色走主题、shift 大小写、阻塞 `meow_ui_text_input` 喂 `on_tick`(避免进度条卡死)、OK(回车)= 提交并关闭 / ESC = 取消并关闭、回显条=实时输入回显(非候选/联想,候选框留中文输入法)。渲染:局部刷新不再清底重画单个控件(改 z 栈重画),消除重叠区残留。
- **M2.76+ 新增**:`W_LISTVIEW` **结构化列表控件**(§5.5 A②落地:图标/标题/副标题/右侧操作 arrow·switch·check·more、单选/多选/禁态、行右 1/3 操作区命中、视口裁剪、全语义色);**通用长按事件** `MEOW_EV_LONG_PRESS`(坐标式 + `meow_ui_set_long_press_cb/ms` opt-in,默认 500ms,移动>12px 取消、长按后抬手抑制 TAP/SWIPE);**视图框架**(`meow_ui_view.c`:可嵌套容器 + 六生命周期 + `page_push/pop/replace` 包装 activity 栈 + 广播注册表 z 序 + `meow_view_dispatch_event` z 降序子>父命中即消费 + **绘制期通用 clip**`g_clip_stack[16]` 根除越界脏区);**SDK 级 z 序已并入视图框架**(自动注册 + 子>父 + `set_z(999)` 置顶 + 收敛 live_btn),hello App 层 `d_paint_region` 临时方案退役。
- **M2.82 新增**:SDK **nav/media 封装**——`meow_ui_nav.c`(env 栈初始化 + report/request_exit)、`meow_ui_media.c`(懒连断线重连,meta 上报 + playctl 接收);SDK 顶部下滑手势→IPC 请求 launcher 展开通知中心(§10.5,App 进程不渲染系统 UI)。
- **M2.84-88 新增**:**amsd 活动管理器**(一期 agent-212 生命周期注册表+`am` CLI,二期 agent-225 路由决策化+**picker 契约** `MSG_PICKER_CALL/RESULT 0x70/71` + `MSG_AMS_START/KILL`);**签名鉴权 2B**(agent-221 多公钥数组 + v2 签名块);**meow_route 音频输出路由**自动跟随插孔(agent-220);**中文拼音 IME**(agent-229 `meow_pinyin.c` 静态表 403 音节+329 词,组合条+候选栏+翻页+中英切换+九键数字盘);**App 内物理音量/媒体键**(agent-226 EVIOCGKEY 位图轮询+120ms 去重);**开机亮度 stop100 竞态**(agent-230);**流媒体客户端二期**(agent-222 专辑封面/歌词后台/大歌单分页)+ 歌单详情回退(agent-227);**MeowPanel 阶段 C**(agent-224 实时画面 `/api/screen.jpeg` + 遥控 `/api/input/tap`)+ 通知实时化+屏幕增量帧(agent-234)。
- **M2.89/90 新增**:**开机选单抢屏回归修复**(sysui S70 早拉 launcher 误入主屏→改怠速等 mode 文件,HibyOS 选单/热键链路恢复);**watchdog SIGUSR1 仅裸沉浸 App** + SDK sigusr1 护栏(修框架 App 误杀);**amsd 失联三修**(陈旧 revents 丢连接/收发超时/自动重连);**MIUIx 顶级主题**(ui.style 一键切换,暖底橙调大圆角);**meow_audiod 独占音频守护**(A1 全量,含 SDK 客户端与 `meow_audio` CLI;meow_route 并入退役);**MeowPanel 字体库 tar.gz 导入/导出** + 文件管理自然排序;**流媒体客户端**:搜索接拼音 IME、设置页(音质/缓存上限/清理缓存)、缓存 LRU 淘汰接线、文案全中文。
- **M2.91-96 新增**:**audiod 开机武装 hbc3000 FPGA**(路由线程启动无条件写 Output Port Switch,根治「未烧 FPGA 时 PCM 第二次 start 内核死锁」);**svc_power 充电态权威链**(mp2731-charger/status,修 battery/status 恒 Discharging)+ 状态栏充电闪电图标;**amsd 注册自愈**(开机早于卡挂载扫空→3s 快重试+30s 合并重扫+`am rescan`);**Now Playing 死循环修复**(空态 100% CPU/卡死)+ NP 页电源键/自动息屏;**插孔消抖**(4.4/3.5 插拔 ADC 幽灵音量键:launcher/watchdog/SDK 三路 800ms 消抖窗 + ramp 3s 上限);**audiod 长流护栏 + 几何收窄 period=1024×4**(根治长流 ~12s 停摆:ring 有界等待/timedwait/popen 有界收割/O_NONBLOCK+poll 写/wedge 自救/心跳 watchdog);**watchdog `wd_vol_save` O_TRUNC 毁 settings.conf 修复**(读改写单键+去重);**音量反转四路统一**(CS43198 音量寄存器=衰减量:0 最响/255 静音,launcher/audiod/watchdog/SDK 统一 `dac=255-pct*255/100`);**炸耳安全修复**(`dac_vol_mute` 旧写 0=最大响度 → 改 255 真静音);**SDK 插孔消抖补齐**(meow_ui.a 12cdd5d7)+ **物理播放/暂停键(164) 改发 PLAYCTL cmd=6 toggle** + App 前台状态栏音量%实时刷新。
- **M6.0-6.1 新增**:**网络管理服务 `meow_netmgrd`**(期1 接口统一层 WiFi/BT/rfkill/IP/DNS 只读 + 期2 连接普查三实体(app:/svc:/proc:)+DNS 代理 127.0.0.1:53 双行 resolv 零断流 + 期3 策略引擎防自锁(dropbear/wpa_supplicant/udhcpc 等拒 block)/SOCK_DESTROY 真机 no-op→仅 DNS 层生效/面板白名单/WiFi save_config;CLI `meow_net`,M6.0 期1/M6.1 期3,设计见 `devdocs/plan/NETMGR_DESIGN.md`);**功耗管理服务 `meow_powermgr`**(期0 调研 current_now/cpufreq 不存在/背光 0-101 + 期1 权威充电链复用/经验模型 14 系数 + 期2 history 30s 环形/top 功耗分解/events/mp2731 current_now 实测充电功率(放电仍 estimated);CLI `meow_power`,M6.0 期1/M6.1 期2,设计见 `devdocs/plan/POWER_MANAGER_DESIGN.md`);**设置页迁 netmgr IPC + WiFi 新网络录入 UI**(扫 SSID→IME 输密码→connect,286);**服务名统一 meow_***(S89 迁移 audiod.conf/mdnsd.conf→meow_audiod.conf/meow_mdns.conf,用户定案 cd4e2c2);**MeowPanel 增强**(4 位 PIN 访问级鉴权 + 首页仪表盘 `/api/overview` + 电源/进程服务页 + 系统页补强,PANEL_ENHANCE_PLAN 批1)。
- **M6.2-6.4 新增(SDK 演进批次,meow_ui.a 逐批重链四 App,API 全部追加式零改动)**:**公共头拆分**(278:`meow_ui.h` 922→67 行伞头 + `include/` 16 子头,API 零改动、`meow_ui.a` md5 不变、三 App 免重链);**布局/滚动/动画基座**(292:`meow_box` 线性布局纯计算器(固定/weight/wrap/对齐/嵌套,opt-in `meow_view_set_layout`)+ `meow_scroll` 滚动物理(速度采样+fling+边界回弹+snap)+ `meow_anim` 动画基座(静态池≤16+缓动+脏区联动,挂 on_tick 零开销));**事件分发形式化**(293 §6.4:单一仲裁者——z 序自顶向下命中+模态哨兵+down 捕获+唯一焦点,dialog/sheet/list/scroll 已迁,穿透矩阵 M1-M4 全 PASS);**ListView 虚拟化**(295 §5.5:vsource count/bind 数据源+固定 item 池(视口+2)+多行富 item+完全自绘钩子);**字符串资源 i18n**(299 §13:`meow_str`/`meow_strf`/`meow_str_set_locale`,内嵌 zh_cn/en_us 双语+TF 覆盖表+locale 持久化);**sb_icons 动作图标 + topbar 位图**(307:15 个 MD3 动作图标 `MEOW_ICON_*` + `meow_topappbar_set_item_icon`);**`meow_img_downscale`**(308:图片降采样 ≤maxdim,流媒体客户端封面 8.28MB→0.78MB)。详见 `MEOW_APP_SDK.md` §5.5/§6.4/§12/§13 与 `docs/MEOWOS_SPEC_REF.md` §3。
- **M6.5-6.6 新增**:**SDK 矢量字**(316:`meow_ft_*` §10.1——把阅读器 R1/R2 已验证的 FreeType 管线整体上收到 SDK,按需读文件零整载 + glyph LRU 512 + 灰阶 blend565,与位图字库**并存**、App 显式调 `meow_ft_*` 才用,加载失败/无卡自动降级位图(不豆腐不崩);不调 ft 的 App **链接零增量**(独立 .o + vendor 裁剪构建);**SDK 演进主体收官**);**audiod 孤儿会话两段式回收**(315,M6.5:客户端崩溃/OOM 杀(sig=9)/正常退出后会话完整回收,STATE 回干净无流基线);**流媒体客户端 2026-09-02 批次**:**SIGSEGV 根因**(314:歌词解析栈上 227KB 临时数组 vs 128KB 线程栈→挪堆,真机复现修前 3.7s/修后连播两次过);**封面链分诊与修复**(317:Referer+桌面 UA 解 403 + webp 魔数内容型先判不解码 + 200 解码失败=源站死对象返 JSON 错误体实锤 + 死链/403/webp 幂等快跳不重试 + 缩略图 `param=300y300`)+**封面死链兜底链**(原 picUrl 失败→web 端 song/detail 免登录重取活链→歌单封面兜底)+**歌词页封面等比居中**(contain,四周主题底,方图不再变形);**`track/all` 接口验证否决**(318:该接口在服务端不存在,8 形态全业务码 404,维持 detail+song_detail 现状);**外部接口地图**(319:流媒体客户端外部 API 调研笔记落盘 `devdocs/plan/`);**风控加固**(320:接口响应 120s 短 TTL 缓存 + 游客 MUSIC_A 匿名令牌 + 死代码清理)。详见 `MEOW_APP_SDK.md` §10.1 与 `docs/MEOWOS_SPEC_REF.md` §3.5。
- **M2.80-83 已落地**:M3 导航栈+M5 通知跳转(M2.80)、媒体会话+Now Playing(M2.81)、SDK nav/media 封装(M2.82)、字库启动竞态修复(M2.83)。
- 既有 `meow_btn/slider/switch/list_create` 签名一字不改(向后兼容),Options/状态机走新增挂载器。
### 2.5 服务
@@ -115,12 +137,14 @@ init (rcS)
- 目录式 `key=value`:`exec`(必填)/`args`/`env`/`enabled`/`restart`/`backoff_ms`/`max_restart`/`trigger`(`boot`/`card-mount`/`wifi-up`,默认 `card-mount`)。
- 状态机 `DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;CLI(SSH 直呼):`status` / `reload` / `start|stop|restart <name|all>` / `pid`。
- **M2.73 悬垂指针修复(md5 `da6ebc66`)**:`scan_and_reconcile` 用 `*s=tmp` 整体拷贝后 `s->argv[]` 曾指向已销毁栈帧 → `execv`/`/proc/pid/cmdline` 错乱;新增 `svc_rebuild_argv` 重定位到服务自身 `exec`/`argbuf`(删除服务后同 tick 拉起不再错位)。
- **meow_mdns(mDNS 应答器,自研)**:`/usr/bin/meow_mdns`,**IPv4-only**(该设备内核无 IPv6,完整 mDNSResponder/SAResponder 均无法运行),应答 `<host>.local` 的 A 记录与 `_ssh._tcp` 的 PTR/SRV/TXT。由 `meow_svcd` 托管(板载 `services.d/mdnsd.conf`,`trigger=boot`)。
- **meow_mdns(mDNS 应答器,自研)**:`/usr/bin/meow_mdns`,**IPv4-only**(该设备内核无 IPv6,完整 mDNSResponder/SAResponder 均无法运行),应答 `<host>.local` 的 A 记录与 `_ssh._tcp` 的 PTR/SRV/TXT。由 `meow_svcd` 托管(板载 `services.d/meow_mdns.conf`,`trigger=boot`)。
- **hostname = `meowos`**(设置键 `system.hostname`,缺省 `meowos`;`S89meow_svcd` 开机 `sethostname`)→ 即 `meowos.local`。
- **M2.78 竞态修复**:无可用 IPv4 不再 `exit(2)`(改内部 2s 等待重试,SIGTERM 可打断);5s 周期检测掉线/换 IP → 重新 join 组播 + A 记录实时刷新(修 IP 变更还带旧地址);接口 down 时零应答守卫;`MEOW_MDNS_IF` 落地。
- 注:手机热点会丢弃设备→主机的组播应答,`avahi-resolve meowos.local` 在上网热点下会超时(网络层热点隔离,非应答器缺陷),主机可用 DNS 兜底。
- **meow_crash(系统级崩溃捕获)**:svcd 服务异常退出上报(AF_UNIX best-effort,不崩 svcd)+ dmesg 增量扫 segfault/oops;落盘 TF `MeowOS/logs/crash_<bootts>.log`(无卡回退 /tmp + 迁移);CLI `dump`/`tail`;`S89` 预置 `crash.conf`。launcher 另在 `launch_app` waitpid 回收处 **notify_app_crash**(信号杀/非0退出上报,干净 exit 不报)。
- **meow_verify / meow_sign(包签名,二期 2A 最小闭环)**:`meow_verify`(Ed25519 验签 CLI,公钥固化固件,TweetNaCl+SHA256 单文件);`meow_sign`(宿主 keygen/sign,尾部 180B footer 签名块,私钥仅 `.keys/` 不入库);nyai 与 launcher `meow_pkg_install` **验签分级**(有签必验 / `os.meow.*` 无签拒装 / 第三方直装告警 / 三重绑定 / 原签存 `.mpk.src` 导出保留);svc_power IPC 鉴权(SO_PEERCRED→/proc/pid/exe→包名/系统身份 allowlist)。
- **meow_netmgrd(网络管理服务,M6.0 期1 / M6.1 期3)**:`/usr/bin/meow_netmgrd`(svcd 托管板载 `services.d/meow_netmgrd.conf`,`trigger=boot`;HibyOS 只读降级,RSS ≤1MB)。**接口统一层**(iface 枚举+热插拔 / WiFi `wpa_cli` 有界收割 / BT `hciconfig`+`bluetoothctl` / rfkill / IP 路由 DNS 只读 / udhcpc 续租释放仅 MeowOS);**连接普查**(2s `/proc/net/{tcp,udp}`,inode→pid→**三实体** `app:<包名>`/`svc:<服务名>`/`proc:<comm>` 归因);**DNS 代理**(127.0.0.1:53,双行 resolv.conf+atexit 恢复+5s 自检,崩溃零断流);**策略引擎**(`/data/meow/netmgrd/net_policy.conf`,防自锁双层:dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init 拒 block+加载校验强制复位;**SOCK_DESTROY 真机 no-op(内核 4.4 接受不销毁)→ 仅 DNS 层生效,`meow_net policy show` 明示 `enforce=dns_only`**)。双形态 CLI **`meow_net`**(`status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help`)。设计见 `devdocs/plan/NETMGR_DESIGN.md`。
- **meow_powermgr(功耗管理服务,M6.0 期1 / M6.1 期2)**:`/usr/bin/meow_powermgr`(svcd 托管板载 `services.d/meow_powermgr.conf`,`trigger=boot`;HibyOS 只读降级)。**期0 事实**:`battery/current_now`/`cpufreq` **不存在**→实时功率只能 derived+estimated 双轨、背光量程 **0-101**(非 255)、mp2731 有 current_now(充电功率可测)、rfkill×4、无 thermal。**期1**:权威充电态链复用 + 经验模型 14 系数(`/data/meow/powermgr/power_model.conf`)。**期2**:`history`(30s 环形 4h 不落盘)+`top`(功耗分解)+`events`(充放电/低电/亮灭屏/背光跳变)+模型细化(WiFi 未关联档/BT rfkill·ACL 档)+ **mp2731 current_now 实测充电功率**(charge_confidence=measured,放电仍 estimated)。双形态 CLI **`meow_power`**(`status/estimate/model/history/top/events`)。设计见 `devdocs/plan/POWER_MANAGER_DESIGN.md`。
### 2.6 通知系统
@@ -155,12 +179,12 @@ init (rcS)
### 2.9 系统框架 / 专项服务
把**系统复用功能收敛到系统框架**(`devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`,一期已落地):meow_sysui 派生 → meow_svcd 托管 → IPC。新增两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务:
把**系统复用功能收敛到系统框架**(`devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`,一期已落地):meow_sysui 派生 → meow_svcd 托管 → IPC。新增 **svcd 托管、板载 `trigger=boot`** 的专项子服务(perf/power 一期;**netmgrd/powermgr** 二期——网络管理/功耗管理,见 §2.5 服务清单):
- **`meow_svc_perf`**(性能 / 系统只读聚合,IPC `/usr/data/meow_perf.sock`,`MSG_PERF_QUERY(0x0050)→MSG_PERF_RESULT(0x0051)`);**`meow_svc_power`**(关机 / 重启 / 升级 / 回 HiBy 系统动作,IPC `/usr/data/meow_power.sock`,`MSG_POWER_ACTION(0x0060)→MSG_POWER_STATUS(0x0061)`,服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73 已落地**:`meow_svc_perf` 增 **network** 子集(真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`),并入 SYSTEM 子集输出;`meow_svc_power` **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启——**只完整性、不签名**,海贝官方固件无签名,签名会挡自刷)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(弃 amixer dummy)。**M2.78 读级鉴权放宽(agent-189)**:`STATUS`/`BATTERY`/`REBOOT_REASON`/`UPGRADE_STATUS`/`dac status` 读级不再查 uid(任意调用者);动作级维持 `os.meow.*`/系统身份白名单。当前 md5:perf `7ea196de`、power `c6ddf3f8`。
- **`meow_svc_perf`**(性能 / 系统只读聚合,IPC `/usr/data/meow_perf.sock`,`MSG_PERF_QUERY(0x0050)→MSG_PERF_RESULT(0x0051)`);**`meow_svc_power`**(关机 / 重启 / 升级 / 回 HiBy 系统动作,IPC `/usr/data/meow_power.sock`,`MSG_POWER_ACTION(0x0060)→MSG_POWER_STATUS(0x0061)`,服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73 已落地**:`meow_svc_perf` 增 **network** 子集(真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`),并入 SYSTEM 子集输出;`meow_svc_power` **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启——**只完整性、不签名**,海贝官方固件无签名,签名会挡自刷)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(弃 amixer dummy)。**M2.78 读级鉴权放宽(agent-189)**:`STATUS`/`BATTERY`/`REBOOT_REASON`/`UPGRADE_STATUS`/`dac status` 读级不再查 uid(任意调用者);动作级维持 `os.meow.*`/系统身份白名单。**M2.88 签名鉴权 2B(agent-221)**:svc_power 面板身份精确路径绑定(删启发式)。当前 md5:perf `7ea196de`、power `306df962`。
- **电源 CLI `meow_svc_power <cmd>`**(root 免鉴权、单一命令、**不覆盖**系统 shutdown/reboot):`shutdown` / `reboot[--hiby]`(强制回 MeowOS)/ `upgrade`(写 mtd5,含升级写保护)/ `set-default-meow` / `set-default-hiby` + `status` / `battery` / `reboot-reason` / `upgrade-status` / `screen on|off|timeout<秒>` / `dac on|off|status`。
- launcher 电源菜单与 MeowPanel 均**改为经这些服务 IPC 触发**,不再直连 `sys_do_reboot` / 爬 `/proc`。
- **二期**:关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签(防伪造包名,解包验签 / 导出保留签名)。
- **二期望签鉴权已落地(agent-185 2A / agent-221 2B)**:关键 IPC 只接受 `os.meow.*` 鉴权 + `.mpk` 打包签名验签(防伪造包名,解包验签 / 导出保留签名);`meow_verify` 多公钥数组 + v2 签名块(含 ts,兼容 v1)+ svc_power 面板身份精确路径绑定(删启发式)。
- **开机默认**:权威标记 `/data/meow_boot_default`(单值 O_TRUNC:`0`=MeowOS / `1`=HiByOS)。
### 2.10 MeowPanel(外部管理面板)
@@ -169,9 +193,11 @@ init (rcS)
- **后端**:`patch_v2/meowpanel/meowpanel.c` 单进程零第三方依赖 C 静态二进制,内嵌 HTTP/JSON 服务器(8080)+ 静态托管(`/`→index.html、`/assets/*`)+ 框架专项服务 IPC 客户端 + `settings.conf` 设置镜像。**读侧透传** `meow_svc_perf` IPC(`/api/{status,perf,system}`,单一权威源,不各自爬 /proc)、**动作侧**经 `meow_svc_power` IPC(`/api/system/{reboot,shutdown,ota}`)。
- **能力**:设置镜像(`settings.conf` 解析 `key = value` 带空格 + 精确匹配,防 `audio.volume` 误配 `audio.volume_max`)、文件管理(白名单 `MeowOS/Music/MeowData/MeowAPPs/book`,`..` 逃逸→403;**目录优先+名称不区分大小写排序、条目上限 1024**;上传 8MB 上限、文件名净化、双重白名单、`read>1MB` 截断)、**通知中心后端**(`/api/notifications` 读 TF 归档环形 200 条 + 单删 + dismiss_all)、应用(`nyai install/export/remove`,包名=`app.json.package`;`nyai` 路径固件内置 `/usr/bin/nyai` 优先+卡上回退)、按键注入(`/dev/input/event*` evdev,只注入音量/电源/播放/上下曲,**禁返回(158)/主页(172)/确认(28)/退出(96)**)、日志(`MeowOS/logs`)。
- **OTA 检查升级 bug 已修**:改读 `GET /api/system` 返回的 `ota` 字段,不再误触 `POST /api/system/ota` 触发升级。
- **M2.78 可选密码鉴权(agent-196,默认关)**:`panel.auth_enabled`(默认0) + `panel.auth_pass`(带盐 sha256 不明文);token 会话(`/dev/urandom`,TTL 12h,内存+卡持久化,静态屏蔽 session 文件);未启用零回归,启用后 `/api/*` 全要会话;前端登录页 + 401 回跳 + 设置卡片。
- **M2.78 可选密码鉴权(agent-196,默认关)**:`panel.auth_enabled`(默认0) + `panel.auth_pass`(带盐 sha256 不明文);token 会话(`/dev/urandom`,TTL 12h,内存+卡持久化,静态屏蔽 session 文件);未启用零回归,启用后 `/api/*` 全要会话;前端登录页 + 401 回跳 + 设置卡片。**面板增强(PANEL_ENHANCE_PLAN 批1,2026-09-01 用户定案)**:4 位 **PIN 访问级鉴权**(面板访问即需登录,与未来锁屏同源 `system.pin` 预留,未设 PIN 首访引导设置、未设前写端点拒绝;token 会话 3 天或重启失效,进程内存);前端五页:**首页仪表盘**(`/api/overview` 聚合)+ 音频电平(rms/peak 只读)+ 电源页只读 + 进程/服务页(svcd/amsd)+ 系统页补强 + 网络健壮性 + PIN 卡;后端只读端点 `power/overview/ams/svcd` + audio 电平键;音量 clamp `vol_max`+锥度提示 / 防自锁禁停 / 确认弹窗三红线。详见 `devdocs/plan/PANEL_ENHANCE_PLAN.md`)。
- **面板批2 + 加固(2026-09-02)**:崩溃/日志中心 + 音频路由页 + 音量写经 clamp `vol_max` + 后端补 `reboot_reason`;**JSON 输出 UTF-8 加固**(`sA_str_n` 单遍校验、非法序列换 `?`、IPC 原始 JSON 门禁降级,防任一上游坏字节拖垮整页);**overview 自校验纪律**:`/api/overview` 服务端发出前先自查/严格平衡检查(坏则 dump `/tmp/overview_bad.json` + 500),前端非 JSON 自动回传原始字节到 `/api/debug/report_bad_json`(落 `/tmp/overview_client_bad.txt`);**教训**:后端 `jv_parse` **容忍尾随垃圾**,而浏览器 `JSON.parse` **不容忍**——net 块多写一个 `}` 即「Extra data」破页(batch2 DNS 并入事故),故自校验必须做与浏览器口径对齐的**严格括号/闭合平衡检查**。
- **⚠️ 鉴权默认关**:内网信任模型(`reboot/shutdown/ota/apps/删除/上传` 等危险端点默认无鉴权,**勿把 8080 暴露到不可信网络**);`meow_svc_power` 服务端做 `SO_PEERCRED uid==0` 基础校验。
- **前端**:`patch_v2/meowpanel/web/`(MD3/MIUIx 双主题 + 宝塔式 + 浏览日夜);`/usr/bin/nyai` 装 `.mpk`;launcher 的 `meow_pkg_export` / `nyai export` 导出 tar.gz(busybox tar + gzip,不需 zip)。详见 `patch_v2/meowpanel/README.md`。
- **M6.34 前后端拆分 + 屏幕·遥控合并页(2dc4cad)**:后端 `meowpanel.c`(6372 行)拆为 `src/*.c`(20 个,按 API 域)+ `mp.h`(235 函数原型、20 端点 output byte-identical、0 warning);前端拆 `mp_shared.js`/`mp_pages.js`/`meowpanel.js` 三层;新增**屏幕·遥控合并页**(左=设备实时画面、右=MD3 SVG 矢量按钮无 emoji),替代旧的分离屏幕/遥控页;重编后热更 TF 卡。面板后端热更只写白名单 `MeowOS/opt/MeowPanel/`。
---
@@ -184,15 +210,16 @@ init (rcS)
- `meow_launcherd/` launcher(系统 UI 宿主,**多模块**:`main_menu/statusbar/settings_page/power_menu/notif_center/app_launcher/fb_compositor/audio/backlight/net/about/font_theme/keys/util` + `subpage/wifi_page/bt_page`(蓝牙/WiFi 子页)`now_playing`(Now Playing)`meow_media`(媒体会话)`meow_ipc`/`meow_settings`),+ `meow_launcher_internal.h` + `build_launcher.sh`
- `sdk/` meow_ui SDK 源码 + `build_sdk.sh`(→ `meow_ui.a`;含**统一控件库**:widget/theme/activity/global/dialog/ime/img/input/log/notify/sys/vol/view(视图框架)draw(绘制 z 序/注册表)nav/media(导航/媒体会话封装))
- `hello_app/` 示例 App(`app.c` + `ui/{home,full,demo}.c` + `ui/common/chrome.c` + `logic/{notify,state}.c` + `build_hello.sh`)
- `ncm_app/` 网易云客户端源码(`core/`:mbedtls 裁剪+jsmn+qrcodegen+minimp3;UI v1 + 复古配色;vendor/ 与 out/ gitignore 不入库)
- `ncm_app/` 流媒体客户端源码(dev.player.ncm;`core/`:mbedtls 裁剪+jsmn+qrcodegen+minimp3+dr_flac;UI 复古配色;vendor/ 与 out/ gitignore 不入库)
- `files_app/` 外置系统文件管理器 os.meow.files(app.c + ui/{browser,viewer}.c + ui/common/chrome.c + logic/fsx.c)
- `meow_sysui/` 系统进程总监(`meow_sysui` + `S70meow_sysui` + `meow_sysui.md`)
- `meow_svcd/` 服务管理器;`meow_watchdog/` 应用看门狗;`meowpkg/` 包管理(`nyai.c`);`uinput_tap/`
- `meow_crash/` 系统级崩溃捕获;`meow_verify/` `.mpk` Ed25519 验签;`meow_sign/` 宿主签名工具;`meow_svc_perf/`、`meow_svc_power/` 专项子服务;`meowpanel/` MeowPanel 外置面板后端 + `web/` 前端;`meow_amsd/` amsd 活动管理器(agent-212 规划,设计见 `devdocs/plan/AMSD_DESIGN.md`)
- `meow_crash/` 系统级崩溃捕获;`meow_verify/` `.mpk` Ed25519 验签;`meow_sign/` 宿主签名工具;`meow_svc_perf/`、`meow_svc_power/`、`meow_netmgrd/`、`meow_powermgr/` 专项子服务(性能/电源动作/网络管理/功耗管理);`meowpanel/` MeowPanel 外置面板后端 + `web/` 前端;`meow_amsd/` amsd 活动管理器(一期 agent-212 + 二期路由决策/picker 契约 agent-225,设计见 `devdocs/plan/AMSD_DESIGN.md`);`meow_audiod/` **独占音频守护 + `meow_audio` CLI**(M2.90,规范见 `devdocs/plan/AUDIOD_DESIGN.md`;`meow_route/` 已并入退役,源码保留归档);`tools/` 诊断小工具(sndprobe/tonepcm 等)
- **`mips-repo/`(入库)**—— mipsel 交叉编译软件仓库:`bins/`(`meow_mdns` / `mdnsd` 等二进制工具 + `build.sh` + `manifest.md`)、`mpk/`(`.mpk` 应用包)。
- **`docs/`(入库)**—— `MEOWOS_SPEC_REF.md` 权威速查总索引。
- **`font/`(入库)**—— 字形系统:`gen/gen_font.py` 生成器、`meow_font.c/.h` 渲染库、`demo/`、`src/`(字体源文件)。
- **`bootlogos/`(入库)**—— 启动 / 关机 logo。
- **`third_party/`(入库)**—— 第三方参考/依赖归拢目录(与 MeowOS 源码 `patch_v2/` 分离、不混入构建):`CNMPlayer`(流媒体客户端功能对照)、`md-material-web`(**最新 Material 3 参考**,MD3 对齐依据,agent-435 按其对所有组件细化)。
- **`LICENSE`** —— Apache-2.0。
- **顶层文档(入库)**—— `APPS.md`、`MEOW_APP_SDK.md`、`FRAMEWORK_DESIGN.md`、`SYSTEM_SETTINGS.md`、`NOTIFICATION_SYSTEM.md`、`TF_CARD_LAYOUT.md`、`DEVELOPMENT_PLAN.md`、`GESTURE_BACK_DESIGN.md`、`KEY_CONTROL_DESIGN.md`、`PERF_SCHEDULER.md`、`MEOWOS_API.md`。
- **本地构建资产(根目录 gitignore;但 `scripts/build/` 的 `README.md`/`build_fw_apps.sh`/`fw_content.conf` 已 `git add -f` 入库)**—— 其余(`fw_apps/` 固件组装产物、官方固件基线 rootfs、交叉工具链、本地开发文档、设备密钥等)不入库。详见「构建」与「部署」。
@@ -286,7 +313,7 @@ patch_v2/hello_app/build_hello.sh # 链 meow_ui.a + libz.a + -lpthread → hel
| `fullscreen` | — | app 级全屏:状态栏**收合至提示线** + 保留左缘返回 / 顶部提示线 / 下滑通知中心(activity 级用 `route.<id>.fullscreen`) |
| `fonts` | — | 字库档位声明 `"full" | "gbk" | "common"`(默认 `common`≈4k) |
其他扩展(launcher 极简解析、不校验 JSON):`icon` / `framework`(导航栈承载)/ `immersive` / `route.<id>`(路由表,`framework=1` 时看门狗返回发 SIGUSR1)/ `awake.<id>`(keep_awake)/ `service.<svc>`。应用数上限 8,扫描深度 1 级。
其他扩展(launcher 极简解析、不校验 JSON):`icon` / `framework`(导航栈承载)/ `immersive` / `route.<id>`(路由表,`framework=1` 时看门狗返回发 SIGUSR1)/ `awake.<id>`(keep_awake)/ `service.<svc>` / **`background_keepalive`**(后台常驻声明,M6.34:`1`=返回桌面**无限期保活**不清;缺省/`0` 走 30min 宽限期回收;播放类 App 若带 `background_audio` 且媒体活跃也无限期保活)。应用数上限 8,扫描深度 1 级。
### 6.2 链接与入口
@@ -336,6 +363,7 @@ patch_v2/hello_app/build_hello.sh # 链 meow_ui.a + libz.a + -lpthread → hel
| `docs/MEOWOS_SPEC_REF.md` | **权威速查总索引**(首选入口):路径 / 常量 / 接口 / 约定 + 「已知矛盾 / 待确认」清单 |
| `FRAMEWORK_DESIGN.md` | Activity / 通知 IPC 框架协议(AF_UNIX socket、路由表、导航栈、后台权限、媒体会话、freedesktop/MPRIS/XDG 对齐);§10 进程模型 / 系统 UI 归属 / 专项子服务 |
| `MEOW_APP_SDK.md` | SDK 设计:架构分层、绘制/控件/图片/输入/主题/字体/通知 API、三层控件模型、Activity 栈、动画与性能;**§5.8 控件层架构升级(统一控件/Options/状态机/布局 + Material 子集)、§8.1 主题增强(MD3/MIUIx × 明暗 + 背景源)** |
| `docs/APP_DEV_GUIDE.md` | **面向第三方开发者的上手指南**:环境→最小 App→工程结构→构建打包→签名安装→部署调试→SDK 能力速查→红线→FAQ |
| `APPS.md` | MeowAPPs 应用规范:目录、`app.json` 字段、生命周期、边界、返回键、`.mpk` 包与 `nyai` |
| `NOTIFICATION_SYSTEM.md` | 通知系统:freedesktop 标准、配额、TTL/转储、存储分层 |
| `SYSTEM_SETTINGS.md` | 设置子系统:统一存储 + `meow_settings_*` C API + 变更通知 + 导入导出 |
+21 -5
View File
@@ -23,7 +23,7 @@
| | `display.theme_mode` | enum | `manual` | 主题判定方式:`manual`(=读 `display.theme`)/`auto`(按深色时段)。**B2b 新增** |
| | `display.theme_dark_start` | int 分钟 0-1439 | `0` | 深色时段开始(如 19:00=1140);`start==end` 视为未配置 → auto 回退 manual。**B2b 新增** |
| | `display.theme_dark_end` | int 分钟 0-1439 | `0` | 深色时段结束(如 07:00=420);`start>end`=跨午夜。**B2b 新增** |
| | `display.background` | enum/图片 | `black` | **由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度;可选集+默认随 style/mode 一起变):**MD3×dark**=黑/深蓝/墨绿(默认黑)、**MD3×light**=白/浅蓝/浅绿(默认白)、**MIUIx×dark/light**各一套(草案)。每种配色定义**深/浅两套 surface/bg**,随 `display.theme` 切换(修复"背景仅对主页生效/仅深蓝/浅色不搭")。亦可为图片路径 `MeowAPPs/backgrounds/xx.png`(图片本身即观感,深浅作用于文字/角色色,待定案)。见 MEOW_APP_SDK §8.1 |
| | `display.background` | enum/图片 | `black` | **由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度;可选集+默认随 style/mode 一起变):**MD3×dark**=黑/深蓝/墨绿(默认黑)、**MD3×light**=白/浅蓝/浅绿(默认白)、**MIUIx×dark/light**各一套(草案)。每种配色定义**深/浅两套 surface/bg**,随 `display.theme` 切换(修复"背景仅对主页生效/仅深蓝/浅色不搭")。亦可为图片路径 `MeowAPPs/backgrounds/xx.png`(**定案**:图片 **cover 居中裁剪**,深浅作用于文字/角色色、背景图不随深浅换,叠加层固定跟明暗主题)。见 MEOW_APP_SDK §8.1 |
| **电源** | `power.longpress_menu_ms` | int | 3000 | 长按电源弹电源菜单阈值 |
| | `power.forceoff_ms` | int | 8000 | 长按电源强关阈值 |
| | `power.defeat_hotkey` | string | `power_voldown` | 切系统/切 MeowOS 热键组合 |
@@ -44,7 +44,7 @@
| | `font.class_other` | path | *(空=跟全局)* | 多字体:其他字符 所用字体 |
| | `font.load_full` | int 0/1 | 0 | **加载大字库(M2.5x)**:1=**强制全系统用大字库(`gbk`≈20k 档)**,覆盖 app 未声明/覆盖默认 `font.file`;0=默认小字库(`common`≈4k),仅 app 声明 `fonts:"full"` 才大字库 [源:用户 2026-08-30] |
| | `font.size.title` / `font.size.body` | int | 28/20 | 标题/正文字号 |
| | `ui.style` | enum | `md3` | `md3`(默认,Material You)/`miuix`(MIUIx,**未实现**)——**顶级主题风格族**(定义整套配色风格/控件形态/圆角/间距/描边),对应 SDK `MEOW_STYLE_MD3`/`MEOW_STYLE_MIUIX`(MEOW_APP_SDK §8.1) |
| | `ui.style` | enum | `md3` | `md3`(默认,Material You)/`miuix`(MIUIx,**M2.90 已实现**:完整 light/dark 配色+控件形态参数,见 MEOW_APP_SDK §8.1.3)——**顶级主题风格族**(定义整套配色风格/控件形态/圆角/间距/描边),对应 SDK `MEOW_STYLE_MD3`/`MEOW_STYLE_MIUIX`(MEOW_APP_SDK §8.1)。`g_defs` 值域 `"md3,miuix"`(label "Material You,MIUIx");存量 `"miui"` 兼容映射为 `miuix`。**状态栏图标集仍 MD3 一套**(无 per-style 换集接口,MIUIx 仅换配色) |
| **通知** | `notify.center_enabled` | bool | true | 下拉通知中心开关 |
| | `notify.app_quota` | int | 16 | 每应用通知配额 |
| | `notify.categories` | int | 4 | 通知类型数上限(每 App,M2.53)[源:审计 2026-08-30]|
@@ -53,19 +53,35 @@
| | `notify.dnd` | bool | false | 通知中心「勿扰」面板内开关(通知列表上方小 toggle;开=只显示 `urgency>=2`(HIGH/CRITICAL),关=全部;面板内读写 `meow_settings_*`,零旁路)[源:用户 2026-08-30] |
| **日志** | `log.level` | enum | `info` | `error/warn/info/debug` |
| | `log.to_card` | bool | true | 写 TF 卡(无卡回退内存环) |
| **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(MeowOS 直写 CS43198 DAC;读回为追踪值) |
| **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(audiod/launcher/watchdog/SDK 四路统一写 CS43198 DAC;读回为追踪值)。⚠️ **寄存器=衰减量**(0=最响/255=静音,M2.95 四路统一映射 `dac=255-pct*255/100`;非线性 0.5dB/步,感知 dB taper 待做) |
| | `audio.volume_max` | int 0-100 | 100 | MeowOS 内部音量上限(clamp;**不与 HibyOS user.ini 上限同步**——其带 CRC 有风险,已定案不做;用户可自设 MeowOS 上限) |
| | `audio.volume_step` | int 1-5 | **1** | 音量键跳步(默认 1%,可配 1-5,最大 5) |
| | `audio.output` | string | `hw:0,0` | MeowAPPs 音频输出设备 |
| | `audio.output` | string | `hw:0,0` | 音频输出设备。**M2.90 起声卡由 `meow_audiod` 独占**,App 不再直开(经 SDK `meow_audio_*`);本键仅作 audiod 开卡目标/调试直开(`MEOW_AUDIO_DIRECT=1`)参考 |
> **音频子系统(M2.90 起)**:`meow_audiod` 独占音频守护收口全部声卡交互——路由自动跟随插孔(4.4 优先,`meow_audio route` 可手动)、音量 `MSG_AUDIO_VOLUME` v1 透传写 DAC numid=1/2 + 本 `audio.volume` 键(与 watchdog/launcher 同源,不抢主权;v2 收编另定)。规范 `devdocs/plan/AUDIOD_DESIGN.md`。**M2.91-96 加固**:开机武装 hbc3000 FPGA(根治 PCM 启动内核死锁)/长流护栏+period=1024×4 几何(根治长流停摆)/音量反转四路统一/炸耳修复(`dac_vol_mute`=255 真静音)。**⚠️ 音量存盘纪律(M2.93)**:写 `audio.volume` 一律**读改写单键**——watchdog `wd_vol_save` 旧用 O_TRUNC 整文件重写曾毁掉 settings.conf(真机实证整文件被抹/重复行/丢 max+step),已修;任何组件不得整文件重写本配置。
| **网络** | `network.wifi_enabled` | bool | true | WiFi 开关(写 wlan0;设置页开关经 `wifi_up.sh`/`ip link set wlan0`,`ensure_wifi()` 尊重此值——关=0 则不再自动连网) |
| | `network.bluetooth_enabled` | bool | false | 蓝牙开关(写 `hciconfig hci0 up/down`;默认关=少耗电) |
| **系统** | `system.hostname` | string | `meowos` | 主机名(RFC1035 首标签;bootmenu 的 S89meow_svcd 开机读它 `sethostname`,缺省 `meowos`;供自研 mDNS 应答器 `meow_mdns` 公布 `<host>.local`)[源:M2.54] |
| | `system.pin` | string | 空 | **锁屏 4 位 PIN**:`<salt_hex>:<sha256_hex(salt||pin)>`(sha256+salt,格式与面板 `panel.pin` 全等,绝无明文)。空=未设(锁屏关闭)。**panel.pin 迁移**:`panel.pin` 已设而本键未设时锁屏接受 `panel.pin`,首次验证成功后并入本键;此后以本键为准。**兜底**:SSH root 删本键即解锁(无其它后门)[源:用户 2026-09-02 锁屏定案] |
| | `system.pin_lock_on_wake` | int 0/1 | **1** | 息屏唤醒后锁屏(默认开);0=关(唤醒直进主屏,电源菜单「锁定」手动锁仍可用)。开机不锁(boot 进主屏不拦) |
| | `system.pin_lock_until` | int(epoch 秒) | 0 | 错 PIN 锁定截止戳。**持久化到 settings**(防 launcher 重启清零;面板进程内存方案重启失效,此处落盘);截止前锁屏页禁输入并倒计 |
| | `system.pin_fails` | int | 0 | 连续错 PIN 计数(0-4;达 5 触发锁定并清零)。持久化防「关机重启重试」 |
> **锁屏行为(2026-09-02 用户定案,实现权威见 `lock_screen.c`/`lock_screen_ui.c`/`lock_settings.c`)**:
> - **触发**:息屏唤醒后(默认开,`system.pin_lock_on_wake` 可关)+ 电源菜单「锁定」主动锁;**开机不锁**。
> - **主动锁**(电源菜单「锁定」):立即熄屏 + 写 `/tmp/meow_pin_locked`(主动锁标志,重启消失)→
> 无论 `system.pin_lock_on_wake` 是否开,唤醒必走锁屏;解锁(PIN 或绕过)时清除。同时 `rm /tmp/meow_pin_unlock_once` 作废挂起的绕过。
> - **面板一次性解锁恢复口**:`/tmp/meow_pin_unlock_once`(tmpfs 标志,panel 端点创建)存在即被锁屏页**消费删除并放行进入系统一次**
> (不校验 PIN、不清除 `system.pin` 本身、不重置失败计数;重启标志自然消失)。launcher 只做检查/消费侧。
> - **系统设置「安全 → 锁屏 PIN」页**:状态(未设置/已设置/锁定中+剩余)/ 设置(4 位双输确认)/
> 修改(需旧 PIN 一次)/ 清除(**免旧 PIN**,物理持机即授权,仅二次确认弹窗)/ `system.pin_lock_on_wake` 开关。
> - **清除兜底**:SSH root 删 `system.pin` 键即解锁;`lck_pin_clear()` 同义(settings API)。
> **字体尺寸标记(M2.5x,随「加载大字库」一起落地)**:字体/主题选择器**每个字体选项标注库大小标记**,如 `MiSans Regular(common≈4k)` / `gbk≈20k` / `full`,直观标注码位/体积量级,让用户知道所选字库大小(M2.6 含字重:`<name> <Weight>(<marker>)`)。对应 `MeowOS/fonts/<family>/<weight>/font.json` 的 `charset`(`common|gbk|full`)+ `glyph_count`;档位枚举 `MEOW_FONT_TIER_COMMON`(默认/≈4k)/`GBK`(大/≈20k)/`FULL`(全/可选);`gen_font.py --charset common|gbk|full [--weight <name>]` 按档生成。定案见 MEOWOS_SPEC_REF §5.2「字体档位/大字库」与 §5.7.8 [源:用户 2026-08-30]。
> 新增设置项时:在清单加一行 + 存储 schema + API 常量/枚举 + 实体 UI 控件 + web 面板控件(四端同步)。
> **主题三键语义(2026-08-30 定案,实现权威见 MEOW_APP_SDK §8.1)**:`ui.style`(MD3/MIUIx)=**顶级主题风格族**(定义整套配色/控件形态/圆角/描边);`display.theme`(dark/light)=该 style 下的**明暗**;`display.background` =**由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度,可选集+默认随 style/mode 一起变)。三者关系:**切明暗 → 同 style 浅/深背景自动切换**(修复"浅色不搭");**切 style → 整套配色+背景换族**(修复"背景仅对主页生效/只有深色")——同时把 `display.background` 从"仅 launcher 主页/仅深色"改为"**全局背景源**"(状态栏/未声明背景 App/主页同步;App 显式声明背景则覆盖,App 声明优先)。`meow_ui_theme` 旧字段 `bg/fg/accent/dim` 保留映射到 MD3 语义色(§5.8.4)。**注**:`ui.style` 值域现状 `meow_settings.c g_defs` 为 `"md3,miui"`(label "Material You,MIUI");本定案将第二风格命名为 **`miuix`**(对应 `MEOW_STYLE_MIUIX`),实现时把 `g_defs` 的 `miui` 对齐为 `miuix`(待定案——MIUIx 未实现,当前仅 md3 生效)。
> **主题三键语义(2026-08-30 定案,实现权威见 MEOW_APP_SDK §8.1)**:`ui.style`(MD3/MIUIx)=**顶级主题风格族**(定义整套配色/控件形态/圆角/描边);`display.theme`(dark/light)=该 style 下的**明暗**;`display.background` =**由「当前 style × 当前 mode」决定的背景底选项集**(非独立全局维度,可选集+默认随 style/mode 一起变)。三者关系:**切明暗 → 同 style 浅/深背景自动切换**(修复"浅色不搭");**切 style → 整套配色+背景换族**(修复"背景仅对主页生效/只有深色")——同时把 `display.background` 从"仅 launcher 主页/仅深色"改为"**全局背景源**"(状态栏/未声明背景 App/主页同步;App 显式声明背景则覆盖,App 声明优先)。`meow_ui_theme` 旧字段 `bg/fg/accent/dim` 保留映射到 MD3 语义色(§5.8.4)。**注**:`ui.style` 值域已由 `"md3,miui"` 对齐为 **`"md3,miuix"`**(label "Material You,MIUIx",对应 `MEOW_STYLE_MIUIX`);存量 `settings.conf`/备份里的 `"miui"` 在 `meow_settings.c apply_value`(SET_ENUM)里兼容映射为 `miuix`。**MIUIx 已实现**(M2.90):`meow_ui_theme.c` 为 `MEOW_STYLE_MIUIX` 提供整套 light/dark 配色+控件形态参数,`display.background` 按 style×mode 派生 MIUIx 色板;状态栏图标集仍为 MD3 一套(无 per-style 换集接口)。
## 2. 统一存储(单一数据源)
+101
View File
@@ -0,0 +1,101 @@
# POWER_HW_FACTS — meow_powermgr 期 0 真机只读调研结论
> 调研时间:2026-09-01 15:43(设备 10.78.193.50,uptime 1:48,`/tmp/meow_boot_mode`=meow,固件 0.7.89+)。
> 方式:全部只读(ls/cat/sysfs 读),未写任何节点、未碰进程、未重启。
> 对照基准:`meow_svc_power battery` = `battery=94%(Discharging) 4275mV`。
## 0. 两个决定形态的关键结论(必读)
| 问题 | 结论 | 影响 |
|---|---|---|
| `battery/current_now` 存在吗? | **不存在**。battery/ 全集无 current_now/charge_now/charge_full/charge_full_design/cycle_count/temp | 实时功率**不可能 measured**;走「容量差分 derived」+「经验模型 estimated」双轨(设计 §3.3 第三档)。健康度(charge_full/design)**不可算**,只能透传 `health` 字符串 |
| `cpufreq` 存在吗? | **不存在**(`/sys/devices/system/cpu/cpu0/cpufreq/` No such file;单核 cpu0) | 省电档**无降频控制点**;CPU 功耗项只能按 util×满载系数估算,无可控项(设计 §5.1 CPU 行降级为「仅观测」) |
## 1. §7 清单逐项结论
### 项 1:battery/ 全集
```
capacity=94 health=Good present=1 status=Discharging(会撒谎,勿用)
technology=Li-ion voltage_now=4275000(µV) time_to_empty_now=8191
```
- `time_to_empty_now=8191`(=0x1FFF)为**恒定哨兵值**,不随负载变化 → 不可用,勿报。
- 无 temp 节点 → 电池温度报 null(充电器温度见项 2 可替代参考)。
### 项 2:mp2731-charger/(权威充电态 + 充电功率可算)
```
status=Discharging(权威源,设计 §3.1 已实测) online=0 temp=300(=30.0°C)
current_now=0(µA,放电时恒 0;充电时=充电电流) constant_charge_current=0/_max=4520000
constant_charge_voltage=4200000/_max=4670000 charge_term_current=120000
input_current_now=100000 input_current_limited=2000000 input_suspend=0
input_voltage_settled=2280000 charge_control_limit=3000000 charge_full=0(无效)
real_type=USB typec_power_role=sink step_charging_enabled=1
```
- **充电功率可估**:`constant_charge_voltage × current_now / 1e6`(充电中 current_now 非 0,待插电实测确认非 0 时刻的值域)。
- `charge_type` 节点不存在(清单原问的 charge_type 无)。
### 项 3:fusb302 / ac / usb / axp_battery
- `fusb302-typec-source/`:online✓(插线检测兜底)、voltage_now、current_max(PD 档位相关,本次=0)。
- `ac/`:online=0;voltage_now=4600/current_now=1200 为**静态默认值**(online=0 时不变),仅 online 位可用。
- `usb/`:online=0;current_now=780 静态默认。仅 online/present 位可用。
- **`axp_battery/`:幻影设备(不可用)**——capacity=0、voltage_now=0、current_now=200000 **三次采样(间隔 5s)恒定不变** → 非实时电量计,是 AXP 遗留驱动的死数据。唯一有用信息:`energy_full_design=1600` + `voltage_max_design=4350000/min=3400000`——可作**标称容量参考**(1600mAh 级,与 R3 Pro II 官方 1600mAh 电池吻合;诚实标注:单位/口径未经证实,仅作续航估算的默认标称容量,可经 power_model.conf `nominal_mah` 覆盖)。
### 项 4:cpufreq —— **不存在**(见 §0)
### 项 5:rfkill —— **存在且可用**
```
rfkill0: type=bluetooth state=1 soft=0 hard=0
rfkill1: type=wlan state=1 soft=0 hard=0
rfkill2: type=wlan state=1 soft=0 hard=0
rfkill3: type=bluetooth state=1 soft=0 hard=0
```
- 4 个 rfkill(2×wlan + 2×bluetooth,两个同类型并存——WiFi/BT combo 芯片双接口特征)。
- **软关断控制点存在**(写 soft=1,本次未写只读 state)→ 期 3 策略层可用「rfkill soft block」替代 `ip link down`/`hciconfig down`(更省电)。**红线不变:策略层不直接 poke sysfs,需经 svc_power 类动作通道或新增授权写点,期 3 定案。**
### 项 6:背光节点全集
```
max_brightness=101 brightness=0 actual_brightness=0 bl_power=0 type=raw
```
- **⚠️ 量程是 0-101,不是设计 §3.2 假定的 0-255** → 功耗表背光分档按 0-101 归一(亮度%=brightness/101×100 再分档)。POWER_MANAGER_DESIGN §3.2 表「0-255」需修正为「0-101(R3 Pro II 实测)」。
- 调查时刻 brightness=0(设备息屏中)→ **息屏态可用 actual_brightness==0 判定**(fb0/blank 读取为空串——驱动未实现 read,不可用)。
### 项 7/8:CPU 满载/背光/WiFi 标定实验
- 本期 0 未执行:容量差分法需分钟级窗口 + 逐项对照(亮屏定档/stress 满载/WiFi 传输),且设备无 current_now 真值可借 → **留待用户在场配合的标定窗口**(每次实验 ≥10min)。
- 方法已备:capacity 差分(94%→x%)÷ Δt × nominal_mah × 平均电压 = 平均功率;基线=息屏 WiFi 关联空闲。
- 在标定完成前,一切 mW 数字均为**占位经验值**(设计 §10 诚实标注不变)。
### 项 9:WiFi 状态/计数器
- `wpa_cli status` ✓:`wpa_state=COMPLETED`、bssid/freq/ssid/ip_address 全有(关联态可判)。
- `/proc/net/dev` wlan0 收发计数器 ✓(bytes/packets 递增)→ 传输态由增量判,可行。
- wpa_cli 无直接 scan 计数;扫网风暴可用 `wpa_cli scan_results` 时间戳或 wpa 日志——期 2 再定,标记**待验**。
### 项 10:蓝牙
- `hciconfig hci0` ✓(Type: Primary Bus: UART,当前 DOWN,RX/TX 计数器有)。
- `/sys/class/bluetooth/hci0/` ✓ → up/down + 连接数可判(连接数走 `hciconfig -a` 或 /sys 下 device 枚举,期 2 细化)。
### 项 11:audiod MSG_AUDIO_STATE
- `meow_audio status` 实测:`active=0 app=- sid=0 rate=44100 ch=2 fmt=- written=... played=... underrun=1 dropped=0 stall=0 txdrop=0 paused=0 route_mode=auto port=4 vol=44`。
- **字段完备**:active(流活跃)/route/port/vol 全有 → 音频链功耗项输入够用(无流≈静态 / 播放≈DAC+FPGA 档)。
### 项 12:thermal zones —— **不存在**
- `/sys/class/thermal/` 目录本身不存在;`/sys/class/hwmon/` 也不存在。
- 唯一温度源:`mp2731-charger/temp`=300(30.0°C,充电器芯片温度,非电池/SoC)。
- → 「异常发热」辅助信号**无热源可用**,期 2 异常检测规则不含温度维度(或仅用充电器温度粗参考)。
## 2. 对设计文档的回填修正
1. **§3.2 背光量程 0-255 → 0-101**(max_brightness 实测)。
2. **§3.3 实时功率**:current_now 路径(measured)**整层不可达**——v1 只有 derived(容量差分)+ estimated(经验模型)。UI 必须带 confidence 标注(设计已要求,此处确认强制执行)。
3. **§3.1 健康度**:charge_full/design 不存在 → health 输出透传 `health` 字符串(Good),不输出百分比。
4. **§3.1 续航估算**:以容量差分平均速率(%/min)为准;nominal_mah 默认 1600(axp energy_full_design 旁证,待标定)。
5. **§5.1 CPU 行**:无 cpufreq → 「降频」行为项删除,CPU 仅观测。
6. **§5.1 WiFi/BT 行**:rfkill soft block 可用作控制点候选(期 3 定案,v1 不动)。
7. **息屏判定**:fb0/blank 不可读 → 用 `backlight_pwm0/actual_brightness==0` 代理(期 1 采纳)。
8. **充电功率**:mp2731 `current_now`(µA) + `constant_charge_voltage`(µV) 可估充电功率(插电态待实测值域)。
## 3. 遗留 / 待验
- [ ] 插电态复测 mp2731 current_now/constant_charge_current 非零值域(充电功率估算校准)。
- [ ] 标定实验(项 7/8):需用户在场,≥10min/项 × 约 5 项。
- [ ] axp energy_full_design=1600 的单位口径(mAh? mWh?)——与官方 1600mAh 规格吻合但未经驱动源码证实。
- [ ] wpa 扫网频率计数法(期 2)。
- [ ] 时间校准对照:无 current_now,derived 功率的时间分辨率受 capacity 1% 粒度限制(1600mAh×1%=16mAh,~5-15min 才跳 1%)→ 续航估算 v1 精度受限,UI 需标注。
+476
View File
@@ -0,0 +1,476 @@
# MeowOS 第三方 App 开发指南(APP_DEV_GUIDE)
> 受众:想在 R3 Pro II / MeowOS 上发布自己 App 的第三方开发者(非 MeowOS 系统组)。
> 从零到上架动线:**环境 → 最小 App → 工程结构 → 构建打包 → 签名安装 → 部署调试 → SDK 能力 → 红线 → FAQ**。
> 权威口径:本文所有 API 以 `patch_v2/sdk/meow_ui.h` 为准;规范以 `MEOW_APP_SDK.md` / `APPS.md` / `docs/MEOWOS_SPEC_REF.md` 为准。凡与 `meow_ui.h` 不一致,以头文件为准。
> 配套文档:`MEOW_APP_SDK.md`(SDK 设计全量)、`APPS.md`(加载器/生命周期)、`docs/MEOWOS_SPEC_REF.md`(速查)、`NOTIFICATION_SYSTEM.md`(通知)、`SYSTEM_SETTINGS.md`(设置)。
---
## 0. 一图看懂:App 是什么
一个 MeowOS App = **一个静态 MIPS32r2 ELF 二进制** + 一份 `app.json` 描述 + 若干资源,打包成 `.mpk`(= tar.gz),装到 TF 卡 `MeowAPPs/<package>/` 下。二进制**链接 `meow_ui.a` 静态库**,通过 `meow_ui.h` 声明的一套 API 拿屏幕、输入、控件、主题、字体、通知、音频——**绝不**自己 `open("/dev/fb0")` / `mmap` / `ioctl` / `open("/dev/input/event*")`。
```
┌──────────────────────────────┐
│ MeowAPP(你的业务,只调 meow_ui API)│
├──────────────────────────────┤
│ meow_ui.a(静态库,含 meow_font) │
│ 绘制/控件/图片/输入/主题/字体/通知/音频│
├──────────────────────────────┤
│ MeowOS 系统(launcher/amsd/audiod)│
└──────────────────────────────┘
```
设备:480×720、RGB565(代码里颜色统一 24-bit `0xRRGGBB`,SDK 内部转 RGB565);文本字号约定 **28**(标题/状态栏)/ **20**(正文/按钮)。
---
## 1. 开发环境准备
### 1.1 交叉工具链
仓库自带了 mipsel musl 静态交叉工具链,直接用,不用自己装:
```sh
$HOME/Project/R3ProII/.tools/zig-mipsel-cc # zig cc 包装(--target=mipsel-linux-musleabi, soft-float)
$HOME/Project/R3ProII/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf/ # sysroot(含 libz.a 等)
```
编译一个静态 MIPS 二进制(参考 `patch_v2/hello_app/build_hello.sh`):
```sh
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
"$CC" -Os -static -Wall -Wextra -I"$ROOT/patch_v2/sdk" -I. \
app.c ui/*.c logic/*.c \
"$ROOT/patch_v2/sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o myapp
```
要点:
- 必须 `-static`(加载器只跑静态 MIPS32r2 ELF,软/硬浮点均可,见 `APPS.md §2.1`)。
- 链 `meow_ui.a`(`build_sdk.sh` 已把 `meow_font.o` 打进库里)+ `libz.a`(meow_font 的 inflate 依赖)+ `-lpthread`。
- `-I"$ROOT/patch_v2/sdk"` 引 `meow_ui.h`/`meow_ui_view.h` 等公共头;`-I.` 引你自己工程的头。
- 期望 `-Wall -Wextra` 零警告(参照 hello)。
### 1.2 宿主编译(离屏逻辑测试)
SDK 依赖板载 `/dev/fb0` 与字库,宿主机没有——但你可以**链真实 SDK 目标文件 + 打桩**来跑逻辑单测(参考 `patch_v2/sdk/test_host_stub.c`):
- `test_host_stub.c` 提供了 `meow_font_*` / `meow_ui_global_render` 等宿主桩(无 fb → 5×7 点阵回退;无字库 → `meow_font_load` 返回 -1)。
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的,宿主宿主验证全 PASS)。
- 注意:`logic/` 层要写**纯 C**(不依赖 `meow_ui.h`),才能裸宿主单测;`ui/` 层才碰 SDK。
### 1.3 qemu-mipsel 全系统仿真
需要跑全链路(含内核/fb)时可用 `qemu_env/scripts/run_halley6.sh`(Ingenic X1600E 全系统仿真):
```sh
qemu_env/scripts/run_halley6.sh # 引导内核到串口控制台(默认)
qemu_env/scripts/run_halley6.sh -usbrootfs # 附加 USB rootfs 镜像
```
注意:必须带 `mem=64M`(脚本已内置,去掉会 early hang);`-usbrootfs` 的 USB 数据通路在 QEMU 里仍不完整(设备未枚举),rootfs 引导会失败——多数情况下它在宿主编译 + 真机验证之间用不上,**不是常规提交流水线**。
### 1.4 真机前置
- 出站靠 TF 卡;确保卡上至少有 `MeowAPPs/`(放 App)与 `MeowData/`(App 数据,首启自动建)。
- 设备跑 MeowOS 模式(默认)。看 `MEOWOS_SPEC_REF.md §5.4` 确认固件版本单点 `/etc/meowos_version`。
---
## 2. 五分钟最小 App(以 hello 为模板)
最快路径:拷贝 `patch_v2/hello_app/` 改包名与文案。逐文件讲。
### 2.1 app.json 全字段表
`app.json` **必填**,放 `MeowAPPs/<package>/` 根目录。参考示例(`patch_v2/apps_assets/apps/os.meow.hello/app.json`):
```json
{"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,"depends":"curl:optional"}
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `name` | ✓ | 列表显示名。**当前仅 ASCII**(UTF-8 中文留待 v3,需完整 UTF-8→UTF-16 转换) |
| `exec` | ✓ | 可执行文件**绝对路径**,必须是**静态** MIPS32r2 ELF(指向 `MeowAPPs/<package>/` 内二进制) |
| `package` | — | 反域名权威标识(= app_id = 目录名 = `.mpk` 文件名茎,三者必须一致)。**`os.meow.*` 为系统保留前缀**(见 §5),第三方用自有前缀(如 `dev.*`)。缺失回退目录名;两者存在且不同 → WARN |
| `version` | — | 显示字符串(versionName 角色,如 `1.2.3-beta`,**不参与比较**) |
| `version_code` | — | **纯数字,安装/升级比较的唯一依据**;缺失 → 拒装;新版 < 已装 → 拒装(防降级),相等 → 允许重装。见 §5 |
| `author` | — | 保留字段 |
| `framework` | — | `1`=SDK 框架 App(用 `meow_ui.a`;**推荐**)。`0`/不写=裸自绘(不推荐,见红线) |
| `immersive` | — | `1`=沉浸。**注意(M2.89+)**:看门狗 `SIGUSR1` 只发给「**裸沉浸**」App(非 `framework=1`);SDK 框架 App 的返回走 `on_swipe`,别把 `SIGUSR1` 当返回键 |
| `background` | — | M4:无头后台服务注册权限(`service.<svc>` 服务二进制 = App 同目录 `<svc>` 文件) |
| `background_audio` | — | M4:后台播放保活(进程不冻结 + UI 冻结复用 §10.5 自抑);播放类 App 声明。如 `dev.player.ncm` 已声明 `background_audio=1` |
| `fullscreen` | — | app 级全屏:状态栏**收合至提示线** + 保留左缘返回/顶部提示线/下滑通知中心。activity 级用 `route.<id>.fullscreen` |
| `fonts` | — | 字库档位声明:`"full"` / `"gbk"` / `"common"`(默认 `common`≈4k;需大字库声明 `"full"`)。**注意**与 `"fonts":["<handle>"]`(应用自带字库句柄)同名二义,见 `MEOWOS_SPEC_REF §7 A-14` |
| `depends` | — | **二进制依赖声明**:`"curl,jq:optional,ffmpeg:required"`(JSON 值串)或行式 `depends=curl,jq:optional,ffmpeg:required`。逗号分项,每项 `<name>[:<mod>]`;无标注默认 required,也接受 `:opt`/`:req`;未知修饰符按 required 并 WARN。启动前查板载 `/usr/bin/<name>` 或卡上 `/usr/data/mnt/sd_0/MeowOS/opt/bin/<name>`(X_OK,命一即可)。required 缺失 → **不启动**(toast「缺依赖: <name>」);optional 缺失 → 仅 WARN 放行 |
| `icon` | — | 预留:应用列表图标 |
> ⚠️ 解析器极简(launcher/nyai 内实现):只匹配 `"name":`/`"exec":`/`"package":`/`framework`/`immersive`/`route.*`/`awake.*`/`service.*`/`background`/`background_audio`/`fullscreen`/`fonts`/`depends`,**不做 JSON 校验**(多行/单行均可),但字段名和引号写法要规范。
### 2.2 main 入口:SDK 初始化 + 事件循环 + 退出约定
看 `patch_v2/hello_app/app.c`,核心骨架如下:
```c
#include "meow_ui.h"
#include "meow_ui_nav.h"
#include "meow_ui_media.h"
#include <signal.h>
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); } /* 干净退出:不画 BYE 直接退 */
int main(void)
{
meow_log(MEOW_LOG_INFO, "start");
/* ① SDk 初始化:开 fb / 读几何 / 载字库 / 建 back_buf。opts 宽高 0=自动读,宿主导跑用 480x720 */
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { meow_log(MEOW_LOG_ERROR, "init failed"); return 1; }
/* ② 主题监听 + M3 导航上报 + 媒体会话(可选,但推荐接线) */
meow_theme_add_listener(NULL);
meow_ui_nav_init(); /* 读 MEOW_ACTIVITY / MEOW_CHAIN 初始化导航栈 */
meow_ui_media_init(NULL); /* app_id = env MEOW_LOG_APP */
meow_ui_media_on_playctl(demo_media_playctl);
signal(SIGTERM, on_sigterm); /* 退出兜底:SIGTERM → request_exit */
/* ③ 建各页 view(on_create 建控件/状态)+ 弹层实例(dialog/sheet 建一次,复用)*/
meow_view *home = home_view_create();
meow_view *full = full_view_create();
meow_view *demo = demo_view_create();
/* ④ 进入根页:activity_push + home on_enter → home_redraw */
meow_view_page_push(ACT_HOME);
/* ⑤ 事件循环回调表(on_key 物理键是系统语义,SDK 不下发业务确认/exit)*/
meow_ui_events ev = { app_on_key, app_on_tap, app_on_swipe, app_on_tick,
app_on_swipe_progress, app_on_touch_down, app_on_touch_move };
meow_ui_run(&ev); /* 阻塞,直到 meow_ui_request_exit() */
/* ⑥ 清理:释放 view(触发各页 on_destroy)+ 弹层 + shutdown */
meow_view_free(full); meow_view_free(demo); meow_view_free(home);
meow_dialog_free(g_dlg); meow_sheet_free(g_sheet);
meow_ui_shutdown();
meow_log(MEOW_LOG_INFO, "exit");
return 0;
}
```
**退出约定**(`APPS.md §3.3`):
- 任意退出码均可,`exit(0/1)` 即可,**不要 `abort()` 留僵尸**——守护进程 `waitpid` 立即返回并恢复播放器。
- 用 `meow_ui_request_exit()` 或**根页左缘左滑**退出;**绝不监听物理键映射 exit**(见 §8 红线)。
- 不需要「恢复/清屏」——应用退出后系统恢复原界面,你清屏反而闪一下。
### 2.3 事件分发
`meow_ui_run` 给的平铺回调,在 `app.c` 里路由到当前页(见 `app.c` 的 `app_on_tap`/`app_on_swipe`/`app_on_tick`/`app_on_touch_*`)。典型分法:
- `on_tap` → 命中当前页控件(`meow_widget_hit` / `meow_listview_hit` / `meow_topappbar_hit` 等)。
- `on_swipe` → `LEFT`=返回(`meow_view_page_pop`,根页则 `meow_ui_request_exit`);`UP/DOWN`=滚动。
- `on_touch_down`/`on_touch_move` → 跟手拖动(slider / 列表 / 页面滚动)。
- `on_tick` → 动画/时钟(如进度条步进)。
- **`on_key` 仅供 SDK 内部状态,不含电源/音量/播放的确认/exit 语义**——别把它当输入。
- 左缘返回手势:边沿上报 `on_swipe_progress`,SDK 自动画顶部进度条(`meow_ui_swipe_hint`)。
---
## 3. 工程结构约定
hello 的分层(`hello_internal.h` 里写得很清楚),建议照抄:
```
myapp/
├── app.c # main + 装配 + 事件分发(把 meow_ui_run 回调路由到各页)
├── myapp_internal.h # 跨 TU 共享声明(只 extern 声明,不定义)
├── ui/
│ ├── home/home.c # 每页一个 view 文件(持本页视图态)
│ ├── home/home.h
│ ├── demo/demo.c
│ ├── list/list.c # 页面内 CONTAINER 子视图(列表区块)
│ ├── full.c
│ └── common/chrome.c # 共享 chrome(TopAppBar / NavigationBar / FAB)
└── logic/
├── logic.h # 纯 C 接口(不依赖 meow_ui.h)
├── state.c # 计数/状态业务 + getter/setter
└── notify.c # 通知业务(此处才 include meow_ui.h 用 SDK 通知 API)
```
**分层契约**(`logic/logic.h`):
- ① UI→业务:只调 `logic_*` / `notify_*` 纯函数命令/getter,业务**不回调 UI**。
- ② 业务→UI:业务态变化经 `logic_set_listener(cb, ctx)` 的**可选 push 监听器**通知 UI 刷新(`home_state_listener` 示例)。
- ③ 每页 `meow_view` 的 `ctx` = **视图控制器**(biz-UI 绑定点),页面后续可把这层状态吸进控制器。
**视图框架 `meow_view`**(`patch_v2/sdk/meow_ui_view.h`):
- 生命周期钩子:`on_create → on_enter → (事件循环) → on_leave → on_destroy`,另有 `on_layout`/`on_draw`/`on_event`。均可 NULL。
- **页面即文件**:每页一个 `meow_view_create(id, MEOW_VIEW_ACTIVITY, &hooks, &ctrl)`,`id` 是你命名的稳定值(`ACT_HOME=0`/`ACT_FULL=1`/`ACT_DEMO=2`)。
- 页面切换统一走 `meow_view_page_push/pop/replace`(包装 activity 栈,**on_enter/on_leave 唯一入口**),App 不再分别调 `activity_push/pop`。
- 子视图嵌套:`meow_view_add_child` 挂 CONTAINER 子 view(如 demo 页的列表区块 `list.c`),嵌套生命周期由 SDK 递归驱动(随父页进出),无需 App 显式接管。
- `on_draw` 为回调、view 不缓存像素(非保留);重绘由 `meow_ui_invalidate` 触发。
- `meow_view_dispatch_event` 是「把语义事件注入当前页谓词子树路由」的入口(先子>父按 z 降序,命中即消费);未接线于 `meow_ui_run`,你可在 `on_tap`/`on_swipe` 里改用它做 SDK 原生路由。
---
## 4. 构建与打包
### 4.1 链接 meow_ui.a
见 §1.1 命令。`meow_ui.a` 由 `patch_v2/sdk/build_sdk.sh` 构建(内含 `meow_font.o`),改 SDK 源码后**重编 meow_ui.a 必须同步重编你的 App**(结构变化即 ABI 变化,见 `MEOWOS_SPEC_REF §3.4`)。
### 4.2 `.mpk` = tar.gz
`.mpk` 内容 = `app.json` + 二进制 + 资源(**不含 data/cache**,运行时按设备生成)。包名 = `.mpk` 文件名茎 = `app.json.package`(如 `os.meow.hello` → `os.meow.hello.mpk`)。
> **nyai 无独立 `pack` 命令**。两种出包:
> 1. **手工打包**(从 staging 目录,`meow_sign/README §2.2` 的口径):
> ```sh
> mkdir -p build/os.meow.xxx
> # 放 app.json + meow 二进制 + 资源;app.json.package == 目录名
> tar czf os.meow.xxx.mpk os.meow.xxx # .mpk 须在 tar 根(无 <pkg>/ 包裹 → 换包时注意)
> ```
> 2. **导出已装应用**:设备上 `nyai export <pkg> [out.mpk]`(打 `MeowAPPs/<pkg>/`,排掉 `*.pre_*`/`*.bak_*`/`logs/` 等运行残留)。
`.mpk` 内部结构:
```
os.meow.xxx.mpk (tar.gz)
├── app.json # package/exec/version(+version_code)/framework/immersive/background_audio/...
├── meow # 静态 MIPS32r2 ELF(exec 指向)
├── <资源文件> # 只读程序资源
└── <signature footer> # 仅签名过的包有(Ed25519 v2 184B;尾部追加)
```
---
## 5. 签名与安装分级
### 5.1 分级规则(`APPS.md 附录 §3` / 签名设计)
| 包 | 规则 |
|---|---|
| **`os.meow.*`(系统保留前缀)** | **必须有有效签名**;无签 / 验签失败 → 一律拒装(防伪造系统包名) |
| 第三方(自有前缀如 `dev.*`) | 无签可**直接装**(安装时告警);**有签必须验签**(失败拒装) |
| 所有包 | 验签 + **三重绑定** + **防降级**(见下) |
**防降级**:只看 `version_code`(纯数字)。新版 code < 已装 → 拒装;相等 → 允许重装;**code 缺失 → 拒装**(提示缺 version_code)。`version` 仅显示,不比较。
**三重绑定**(三方各司其职):
1. 签名 footer 的 `package` == `.mpk` 文件名茎(`meow_verify` 校验)——防改名。
2. 签名 footer 的 `package` == 解包 `app.json.package`(nyai / launcher 安装流校验)——防改 app.json。
3. `os.meow.*` 前缀 + 必须验签通过(nyai / launcher 安装流)——防无签冒充系统包。
### 5.2 开发者自签名流程(不泄露任何密钥材料)
> 工具:`patch_v2/meow_sign/meow_sign`(**宿主端、离线维护机**,不落设备、设备只持公钥)。私钥**永不入库、永不落设备**。设备侧把关的是固件 `/usr/bin/meow_verify`(内嵌公钥数组,任一公钥通过即放行)。
```sh
# ① 首次:生成/备份密钥(仅一次;若 .keys/signing.key 已存在,直接复用,勿覆盖——覆盖会令旧包全部失效)
./patch_v2/meow_sign/meow_sign keygen .keys/signing.key
# 私钥 .keys/signing.key(0600) 离线备份;公钥 .keys/signing.key.pub 可入库/固化进固件
# ② 打包(不含签名,纯 tar.gz;app.json.package 必须 == 目录名 == .mpk 文件名茎;version_code 单调递增)
tar czf os.meow.xxx.mpk os.meow.xxx
# ③ 签名(默认 v2 含被签时间戳)
./patch_v2/meow_sign/meow_sign sign os.meow.xxx.mpk .keys/signing.key
# ④ 产签后自测(推荐)
./patch_v2/meow_sign/meow_sign verify os.meow.xxx.mpk .keys/signing.key.pub
./patch_v2/meow_verify/meow_verify_host os.meow.xxx.mpk # 用固件内嵌公钥,与设备同口径
# ⑤ 装到设备:把 .mpk 放卡上 → nyai install
```
要点:
- `sign` 的 `.mpk` 文件名茎**必须等于包名**(否则 `meow_verify` 二次校验拒)。
- v2 块(184B,含时间戳)需 **2B+ 固件**;`--v1` 产物(180B)兼容旧 2A 固件——固件与工具链同版本发布即可避免兼容坑。
- 签名是**防误装 / 防伪造包名 / 防沿途篡改**,不是安全边界(见 `meow_sign/README §0`,诚实标注)。
- **本仓库 `.keys/signing.key` 是系统主钥**——第三方开发者**不要用它**,用自己的密钥对(否则无法区分来源,且泄露私钥等于放弃系统包信任)。你自己的私钥离线备份,公钥按需提交给设备厂商固化进 `meow_verify` 公钥数组(或走公开渠道分发)。
### 5.3 安装
- 设备端:`nyai install <pkg或文件.mpk>`(固件内置 `/usr/bin/nyai` 优先,或卡上 `MeowOS/opt/bin/nyai`)。同名 = 更新(旧目录备份 `.bak_<pid>`,`MeowData/<pkg>/{data,cache}` 保留,成功自清 `.bak`)。
- `nyai list` / `nyai info <pkg>` / `nyai remove <pkg>`(数据默认保留,`--purge` 连 MeowData 删)。
---
## 6. 部署与调试
### 6.1 TF 卡目录
```
/usr/data/mnt/sd_0/
├── MeowAPPs/<package>/ # app.json + meow 二进制 + 资源(目录名 = app.json.package)
├── MeowData/<package>/{data,cache} # App 数据(首启自动建;exFAT 无软链,用 env 注入)
└── MeowOS/logs/<package>/ # App 日志(每次启动新文件 <ts>_<pid>.log)
```
启动时注入 env(SDK `meow_config_*`/`meow_ui_nav_init`/日志都读它):
`MEOW_DATA_DIR` / `MEOW_CACHE_DIR` / `MEOW_LOG_DIR` / `MEOW_LOG_APP`(=package) / `MEOW_LOG_LEVEL` / `MEOW_ACTIVITY` / `MEOW_CHAIN`(+ `MEOW_FONT_PNG`/`MEOW_FONT_IDX` 当前生效字体、`MEOW_FULLSCREEN`)。
### 6.2 面板上传
MeowPanel(外置 TF 卡 Web 面板,8080)可 `nyai install/export/remove`。**⚠️ 面板鉴权默认关**(内网信任模型)——`reboot/shutdown/ota/apps/删除/上传` 危险端点勿暴露公网。
### 6.3 am CLI(活动管理器)
`am` 是 amsd 的用户侧 CLI(固件 `/usr/bin/am`),连 `/usr/data/meow_ams.sock`(`--sock` / `env MEOW_AMS_SOCK` 覆盖):
```
am status # 在跑 App 表(pkg/状态/pid/运行时长/activity)
am list # 注册表 + 状态(pkg/name/state/pid/launches/crashes)
am history [pkg] # 事件历史(每 App 最近 8 条)
am start <pkg>[/<act>] # 拉起(root)
am kill <pkg> # 终止(SIGTERM 语义,root)
am rescan # 手动触发注册表重扫(卡刚挂载/热装后立即可见)
am resolve <pkg> <activity> # 路由决策(返回 CHAIN)
```
写操作(start/kill)**仅 root**。**已知坑(agent-250)**:开机早于卡挂载 → amsd 注册表扫空 + 永不重扫 → `am start` 报 `not registered`——用 `am rescan` 或等 3s 自愈/30s 合并重扫。
### 6.4 日志
- **App 日志**:`meow_log(lvl, fmt, ...)` 一行;SDK 写 `MeowOS/logs/<package>/<ts>_<pid>.log`(每次启动新文件,单文件 ≥2MB 自动续新,每 App 总量上限,无卡降级 stderr 不崩)。级别:`MEOW_LOG_DEBUG/INFO/WARN/ERROR`;`meow_log_set_level` 可选,默认 `INFO`。
- **崩溃日志**:系统级 `meow_crash` 捕获 + App 崩溃直报。真机看 `/usr/data/meow_*` 与 `MeowOS/logs/`。
- **离线跑**:`meow_launcherd --scan` 列应用;宿主跑逻辑单测看 stdout。
---
## 7. SDK 能力速查表
(完整签名以 `patch_v2/sdk/meow_ui.h` 为准——下面每行都可交叉印证。)
### 7.1 控件库清单(一句话)
| 控件 | 一句 |
|---|---|
| `meow_btn_create`(Filled)/ `meow_btn_create_variant`(五型:FILLED/TONAL/OUTLINED/TEXT/ELEVATED) | 按钮;命中用 `meow_widget_hit`,选中/禁用走状态机 |
| `meow_slider_create` / `meow_switch_create` | 滑条/开关;`set/get` 取值 |
| `meow_list_create`(label-only)/ `meow_lazycolumn_create`/`meow_lazyrow_create` | 数据源回调 + 可见区虚拟化;`on_swipe` 里 `meow_list_scroll_by` 驱动滚动 |
| `meow_listview_create`(`W_LISTVIEW` 结构化列表) | 行=图标+标题+副标题+右侧操作+选中;`meow_listview_hit`/`meow_listview_tap_at` |
| `meow_textfield_create`(接 IME)+ `meow_textfield_activate` | 单行文本输入(阻塞 IME) |
| `meow_card_create` + `meow_card_set_content` / `meow_divider_create` / `meow_badge_create` | 卡片/分隔线/角标 |
| `meow_checkbox_create` / `meow_radio_create` | 勾选/单选(走 set_selected) |
| `meow_progress_create`(线性)/ `meow_progress_circular_create`(圆形) | 进度条(0-100,`-1`=不定) |
| `meow_fab_create` | 悬浮按钮 |
| `meow_topappbar_create`(标题+返回+动作)/ `meow_navigationbar_create`(底部导航)/ `meow_tabbar_create`(顶部分页) | 标题栏/导航栏/页签 |
| `meow_dialog_create`(CONFIRM/SELECT/INPUT/INFO)/ `meow_sheet_create`(BottomSheet) | 阻塞模态;点外/左缘返回=取消 |
| `meow_row_create`/`meow_column_create`/`meow_box_create` + `meow_layout_add`/`arrange` + `meow_layout_spacer` | 组合布局(固定尺寸子项) |
| `meow_widget_custom_create` | 自定义画布:App 给 render/hit 回调(同 `meow_widget`) |
| 通用 | `meow_widget_set_opts`(Modifier 式 Options)、状态机 `meow_widget_set_state`(ENABLED/SELECTED/FOCUSED/PRESSED)、`meow_widget_set_z(999)` 置顶、`meow_draw_custom_register` 注册非控件直绘 |
### 7.2 主题(MD3 / MIUIx × 明暗——适配义务)
- 主题 = **顶级风格族 `ui.style`(`MEOW_STYLE_MD3`/`MEOW_STYLE_MIUIx`)× 明暗 `display.theme`(light/dark/auto)**。
- **适配义务**:用 `meow_theme_get()` 取的**语义色**(`primary`/`surface`/`surfaceContainer`/`outline`/`bg`/`fg`/`accent`/`dim`…),**不要硬编码颜色**;明暗都要能看(`AUTO` 会来回切)。当前 MIUIx 仅换配色(状态栏图标未 per-style 换表,是已知缺口)。
- 字号约定:`font_title=28` / `font_body=20`(`meow_theme` 给默认)。
- 界面底 `meow_ui_set_page_bg(th->bg)`;状态栏高度 `meow_ui_statusbar_height()`(非全屏=28,全屏=0)用于内容 inset。
### 7.3 字体档位声明 + 应用字库接口
- 档位:`MEOW_FONT_TIER_COMMON`(默认 ≈4k)/ `GBK`(≈20k+)/ `FULL`(全字符集)。App 需大字库时 `app.json "fonts":"full"`,launcher 为你加载大字库。
- SDK:`meow_font_set_tier` / `meow_font_tier`;`meow_font_get_available_count` / `meow_font_enumerate`(读 `MeowOS/config/fonts/index`)/ `meow_font_select_by_id`(App 会话内固定用某套;丢失降级默认 + WARN)。
- **判缺字**:`meow_font_has_glyph(cp)` 预判当前字库是否含该码位——缺字自行回退/提示。
- 注意:App **未自带字库**则跟随系统 `font.file`(launcher 以 `MEOW_FONT_PNG`/`MEOW_FONT_IDX` env 注入);应用自带字库(`load_handle`)为**草案/未实现**。
### 7.4 IME(输入法)
- 模式:`MEOW_IME_ASCII` / `MEOW_IME_NUM` / `MEOW_IME_PINYIN`(中文拼音)。
- 阻塞:`meow_ui_text_input(ctx)`(回车=提交+关闭、ESC/点背景=取消);`meow_textfield_activate` 聚焦并打开阻塞 IME。
- 非阻塞:`meow_ui_text_input_async` + `meow_ime_active()`/`meow_ime_text()`/`meow_ime_composition()`(App 在 `on_tick` 里轮询)。
- **坑**:阻塞 IME 的内层轮询必须喂 `meow_ui_activity_ping`(SDK 已做)否则进度条/息屏卡死;键盘区背景快照/恢复(防残影);回显条是**实时输入回显**非候选,候选框留中文。
### 7.5 音频(meow_audio_*,独占 audiod)
- 链路:`meow_audio_open(rate, ch, fmt)` → 返回 `stream_id > 0` → `meow_audio_write(pcm, frames)`(**阻塞**,socket 背压)→ `meow_audio_pause(sid, paused)` → `meow_audio_close(sid)`。
- audiod 独占 `hw:0,0`;**后开流赢**,旧流收 `MSG_AUDIO_PREEMPTED` → 你的 `on_preempt` 回调 → 之后 `write` 返回负值,应重新 `open`。
- **audiod 不在 → `open` 返回 `MEOW_ERR_NOENT`(负值),不做隐式直开回退**;`MEOW_AUDIO_DIRECT=1` 仅调试用别依赖。
- `meow_audio_query(&state)` 读快照(活跃流/采样率/underrun/路由口/音量);`meow_audio_on_preempt(cb, ctx)` 注册被抢占回调。
- DAC 原生率跟随、无 SRC;格式归一(S16 直通 / S32 升位 / mono 复制);4.4/3.5 路由随插孔。
- 播放类 App 记得 `background_audio=1` + 媒体会话上报(见 §7.7)。
### 7.6 通知(meow_notify_* / MSG_NOTIFY)
- 声明:`meow_notify_register_type(cat)`(每 App ≤4 类型);发:`meow_notify_send(&n)`(返回 id);消:`meow_notify_cancel(id)`;进度:`meow_notify_update_progress(id, p)`。
- 配额:**每 App ≤4 类型;每类型活动 ≤4(满丢最旧);媒体单例(每 App 1 条,重发替换)**。
- `timeout_ms`:`>0`=过期毫秒数;`0`=不过期(直到 cancel);`-1`(ONGOING)=常驻。
- 负载 `meow_notify{n}`:`category`/`urgency`/`title`/`body`/`icon`/`timeout_ms`/`target_activity`(点击跳转 Activity 名,M3 导航预留)/`progress`。字段对齐 freedesktop `Notify()`。
- 走 IPC `MSG_NOTIFY`;10s 后自动转储到 TF `MeowOS/notifications/`(ring 10MB)。当前 SDK 内部维护活动表 + 日志落点;真实投递到通知中心 UI(launcher 渲染)为已落地链路。
### 7.7 媒体会话(meow_ui_media_*)
- `meow_ui_media_init(app_id)` / `meow_ui_media_on_playctl(cb)`(注册播放控制并启动后台接收线程)/ `meow_ui_media_meta(state, pos_ms, dur_ms, title, artist, album, artwork, lyric)`(上报元数据)/ `meow_ui_media_stop()`。
- 控制回调 `playctl(cmd, seek_ms)`:cmd `1`=播放 `2`=暂停 `3`=上一首 `4`=下一首 `5`=seek `6`=播放/暂停 toggle(物理播放键 164 恒发 6,由你对当前态翻转——**源是状态权威**)。物理上下曲:`meow_ui_media_sys_key(code)`。
- 声明 `MF_PLAYBACK` 的 App 首次上报 → 注册为媒体源;通知中心媒体卡片 / 主界面 Now Playing 据此显示。
- best-effort:无 launcher/socket 静默降级,绝不崩。
### 7.8 后台播放声明
- `app.json "background_audio":1`(M4 已落地):进程**不解冻** + **UI 冻结**复用 `§10.5 自抑`(比任务书 SIGSTOP 方案优)。launcher 有 `meow_bgplay` 保活表(退避 1/2/4/10s + 熔断 + 广播);应用列表标注「·后台」。依赖 `background` 字段 + `service.<svc>`。
- 声明后台播放后:无头时仍喂媒体会话 + 可继续 `meow_audio_*` 出声;退出时停线程/关流。
### 7.9 应用配置(meow_config_*)
- App 自有键值偏好,落 TF 数据目录(`MEOW_DATA_DIR`,无卡回退 `/tmp/meowcfg_<pid>`),**不占板载、无 schema**(与系统 `meow_settings_*` 区分)。
- `meow_config_set/get/int/bool/remove`;写 `<data>/<sanitized_key>.cfg`(key 只允许 `[A-Za-z0-9._-]`,上限 64)。返回码 `MEOW_CFG_OK(0)` / `NOENT(-2)` 等。
### 7.10 网络注意事项
- App 一般不需要联网;确需联网时**克制请求**(见 §8 风控式节制)。
- 系统层面(`netmgrd` 期 0/1):无 iptables 用户态 / 无 cgroup / 无 conntrack → **内核级每应用管控不可行**;主杠杆 = DNS 代理(域名级)+ 连接普查(inet_diag)+ **SDK 自愿通道**。App 走 SDK 自愿通道配合管控是后续方向;当前 App 如联网,请自律、尊重断网降级。
### 7.11 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
- **任何 UI 文案都不要硬编码**,用 `meow_str("域.名")` 按当前语言取串(示例:`meow_str("common.ok")`)。`meow_str` 永不返回 `NULL`:未知 id 原样返回 `id`,方便你打点/兜底展示。
- **`meow_strf(out, outsz, id, ...)`**:取串后按 `snprintf` 语义格式化进调用方缓冲(自带 NUL;`out==NULL`/`outsz<=0` 返回 `-1`)。
- **切换语言**:`meow_str_set_locale("zh_cn"|"en_us")` 即时生效;locale 存系统设置 `settings.conf` 的 `ui.locale`(默认 `zh_cn`,SDK 启动惰性读取)。App 里做语言切换页时调用它即可。
- **覆盖/追加**:可不改二进制在 TF 卡放 `MeowData/<pkg>/strings/<locale>.txt`(每行 `id=text`,UTF-8)覆盖内嵌或新增自己的串(如 `player.pause=暂停`);无卡/无文件/无该 id → 回落内嵌双语默认表(`common.*` + `ime.*` 内置,见 `include/meow_ui_str.h`)。
- **id 规范**:`域.名`;SDK 自带 `common.*`/`ime.*`,你自建 `域`(如 `player.*`)避免撞名。
- **边界**:当前为 i18n 雏形,仅 `zh_cn`/`en_us` 双语**整串**取用;**不做复数规则 / BiDi / RTL / 动态字体回退**(详见 `MEOW_APP_SDK.md §13.7`)。
---
## 8. 规范与红线
> 详见 `APPS.md §1.3 / §3.3 / §3.4.1`、`MEOWOS_SPEC_REF §2.4 / §5.1`。以下为最要紧的违约原因。
1. **物理键禁映射返回/确认/OK/翻页**。物理键是系统资源:电源=息屏/亮屏/长按菜单/8s 强关,音量=调音量,播放/上下曲=媒体控制,返回=**触屏左滑**(`MEOW_SWIPE_LEFT`)/虚拟返回(非物理键)。**绝不用 `on_key(POWER)→exit` 这类映射**;`on_key` 仅供 SDK 内部状态。界面导航一律走触摸(返回钮/左缘左滑)。
2. **局部脏区刷新,别整屏重画**。控件/区域变化用 `meow_ui_invalidate(x,y,w,h)`(清底 + 按 z 重画相交绘制物 + 原子上屏),别 `meow_ui_flush`/整帧重画。**非控件直绘内容**(`meow_ui_text`/`meow_ui_fill_rect` 等)**必须 `meow_draw_custom_register` 注册**(声明 bbox + 绘制回调 + z),否则区域重画清底后会漏画(hello demo 的分区标题/slider 值文本就是典型注册示例)。滚动列表的视口裁剪由 SDK 全局负责(`W_LIST` 只画视口内行),别在 App 侧再做整屏。
3. **内存预算**。设备内存很紧(QEMU `mem=64M` 参考,rootfs ≤45MB)。避免大块分配、避免大字库双驻留(大字库按档装载、卸载旧 atlas)、别把卡上大文件 mmap 当运行必需——**运行期别依赖卡上资源**(见 §9 卡移除)。
4. **板载存储别乱写**。板载 `/usr/data` 只剩 ~10MB,是稳定框架 + KB 级设置。**App 数据一律写 `MeowData/<pkg>/`(TF,经 `MEOW_DATA_DIR`/`MEOW_CACHE_DIR` env)**;绝**不碰 NAND 分区 / mtd / `/data/recovery_all`**(恢复出厂,红线)。
5. **系统动作经专项服务**。App/组件不直连 `sys_do_reboot`、不写 NAND/mtd5;关机/重启/升级/回 HiBy 一律经 `meow_svc_power`(IPC)。App 一般无需做,但别绕过。
6. **风控式网络请求节制**。避免高频轮询/无限重试/后台自灌;尊重断网/弱网(优雅降级而非死循环/崩溃);与系统 netmgrd 管控共存(域名级,非内核 per-app)。
7. **别 destroy 别占用播放器/声卡**。不要 `killall hiby_player`(触发看门狗整机重启);不要 fork 长驻后台进程(退出要「真正退出」,否则 SIGCONT 后仍占屏抢输入)。
8. **`app.json` 别缺失 `version_code`**(拒装);别用 `os.meow.*` 前缀(除非你是系统签名 App)。
---
## 9. FAQ / 常见坑(真踩过的)
1. **脏区透传:局部刷新抹掉上层 chrome**。症状:拖动/点某个控件后,上层 FAB/标题栏被抹掉。根因:App 侧旧的「清底 + 只画单个控件」刷新。**修法**:改走 SDK `meow_ui_invalidate`(z 栈重画下层+上层),非控件直绘**注册 CUSTOM**(`meow_draw_custom_register`),否则清底后漏画。`meow_ui_paint_full()` 整帧按 z 重画兜底。
2. **IME 阻塞不喂 on_tick → 进度条/息屏卡死**。阻塞 `meow_ui_text_input`/`meow_textfield_activate` 的内层轮询必须喂 `meow_ui_activity_ping`(SDK 已做);键盘区用背景快照/恢复防残影;回显条是实时输入回显非候选。
3. **字体声明丢失 → 降级小字库**。`app.json "fonts":"full"` 丢了 / 大写不符 / 该档没生成 → 按 `common` 装载,生僻字缺字(`meow_font_has_glyph` 预判,回退或提示)。字体三件套(`font.png`+`font.idx`+`font.json`)缺一件 → 该套不进列表。
4. **热更新 `.new` + `mv` / 覆盖运行中程序的坑**。程序主体在卡上、运行中被覆盖/替换 → 访问旧代码页可能 **SIGBUS**。**修法**:升级用 `nyai install`(同包名 = 更新:旧目录备份 `.bak_<pid>` → 重启后替换 → 数据保留 → 成功自清 `.bak`);别自己 rm/mv 运行中的可执行文件。导出时 `*.pre_*`(热更备份)、`*.bak_*` 会被过滤。
5. **开机早于卡挂载 → amsd 注册表扫空**。`am start/list` 报 `not registered`(agent-250)。**修法**:`am rescan` 手动触发,或等 3s 快重试 / 30s 合并重扫自愈。
6. **`SIGUSR1` 误当返回键**。M2.89 起看门狗只对「**裸沉浸**」App(非 `framework=1`)发 `SIGUSR1`;SDK 框架 App 收到会被 SDK 的 `sigusr1_guard` 护栏记日志+吞掉。**别**为「返回」装 `SIGUSR1` handler;SDK 框架 App 返回走 `on_swipe`。
7. **goodix 幽灵触摸**。真机启动触摸尾部会发一段幽灵触摸(启动后 1.2–2.25s,见 `APPS.md §3.5`)。框架 App 走 `on_tap`/`on_swipe` 语义事件,launcher/SDK 已做 drain + 冷却;裸自绘 App 要自己加启动冷却(如 `COOLDOWN_MS`),否则会把幽灵当下按立即退出。
8. **音频:audiod 不在 / 被抢占**。`meow_audio_open` 返回负值(`MEOW_ERR_NOENT`)→ audiod 不在,**别隐式直开**(`MEOW_AUDIO_DIRECT=1` 仅调试)。被抢占后 `write` 返回负值 → 重新 `open`。长流卡死(~12s 停摆)是早期驱动/ring 问题,已由 audiod 加固(`period=1024×4` + 长流护栏)修掉;App 侧 `write` 阻塞背压,别一 flood 灌。
9. **物理键映射 exit**。旧 hello 曾 `on_key(KEY_POWER)→request_exit()`,违反红线——已要求删除。规范:电力键=息屏/亮屏,播放键=媒体控制,返回=左缘左滑。你的 App 别重蹈。
10. **`meow_ui.a` ABI 变化**。改 SDK(`meow_ui_theme`/`meow_widget` 结构)即 ABI 变化 → **必须同步重编 App**(`MEOWOS_SPEC_REF §3.4`)。SDK 编译期依赖 launcher 头(`-I../meow_launcherd` 引共享 `meow_ipc.h`)是**已知设计**,不是耦合错误——运行时只链 `meow_ui.a` 不会链入 launcher 符号。
---
## 10. 上架检查单
- [ ] `app.json`:`package`(自有前缀)/`exec`(静态路径)/`version`+**`version_code`(单调递增)**/`framework:1`;需大字库加 `fonts`;播放类加 `background_audio`;依赖外部二进制加 `depends`。
- [ ] 二进制是**静态** MIPS32r2 ELF;链 `meow_ui.a` + `libz.a` + `-lpthread`;`-Wall -Wextra` 零警告。
- [ ] 颜色用主题语义色不硬编码;明暗都能看。
- [ ] 控件/区域刷新走 `meow_ui_invalidate`;直绘内容已 `meow_draw_custom_register`。
- [ ] 数据写 `MeowData/<pkg>/`,不写板载/NAND;退出干净 `exit(0)`;根页左滑 `meow_ui_request_exit`。
- [ ] `os.meow.*`(系统签名)vs 第三方(自有前缀,建议也签名防篡改)。打包 `tar czf`(或 `nyai export`,文件名茎=包名)→ `meow_sign sign` → 自测 `verify`/`meow_verify_host`。
- [ ] 真机冒烟:`am start`/`am status`/日志 `MeowOS/logs/<pkg>/`,卡移除/无网/断流降级不崩。
+36
View File
@@ -0,0 +1,36 @@
# MeowOS 固件内置(板载)二进制/组件清单
> **当前基线:M6.35(0.8.35,commit `b23c6d5`,已上卡)**。**本文件由 `scripts/build/fw_content.conf` 生成/手工同步——改固件内容改 conf,别改本文件。**
> 基线 = 官方 squashfs 解包 + 下表组件。打包入口 `scripts/build/build_fw_apps.sh`(见 scripts/build/README.md)。
| 固件内路径 | 仓库源 | 说明 |
|---|---|---|
| `/usr/bin/meow_bootmenu` | `patch_v2/bootmenu/meow_bootmenu` | 引导选单 M2.85(sysui让位 agent-216) |
| `/usr/bin/meow_launcherd` | `patch_v2/meow_launcherd/meow_launcherd` | 主屏 launcher M6.35(批①后台常驻+宽限期 `f3ae06d/45c6338` + 批③后台管理页按钮化 `211c5a5/5c17422` + i18n settings/lock `c69fb92` + 310 三期锁屏/播放页双页歌词/取色;md5 `9515c9d8`) |
| `/usr/bin/meow_watchdog` | `patch_v2/meow_watchdog/meow_watchdog` | 应用看门狗 M2.97(音量反转 + DS1039 平方锥度 + 插孔消抖) |
| `/usr/bin/meow_svcd` | `patch_v2/meow_svcd/meow_svcd` | 服务管理器 M2.77(+崩溃上报 meow_crash agent-187) |
| `/usr/bin/meow_mdns` | `mips-repo/bins/meow_mdns/bin/meow_mdns` | mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194) |
| `/usr/bin/meow_sysui` | `patch_v2/meow_sysui/meow_sysui` | 系统 UI(sysui) |
| `/usr/bin/nyai` | `patch_v2/meowpkg/nyai` | nyai M2.97(M2.89 导出 exec 相对化 + 271 app.json depends 安装提示 agent-240/271) |
| `/usr/bin/meow_svc_power` | `patch_v2/meow_svc_power/meow_svc_power` | 电源专项服务(充电态权威源 mp2731-charger/status,修 battery/status 撒谎恒 Discharging) |
| `/usr/bin/meow_verify` | `patch_v2/meow_verify/meow_verify` | II期验签 2B(多公钥+v2签名块 agent-221) |
| `/usr/bin/meow_crash` | `patch_v2/meow_crash/meow_crash` | 系统级崩溃捕获 (agent-187) |
| `/usr/bin/meow_svc_perf` | `patch_v2/meow_svc_perf/meow_svc_perf` | 性能专项服务 M2.75(net_json 补 network 闭合括号,修 /api/system JSON 解析失败) |
| `/usr/bin/meow_audiod` | `patch_v2/meow_audiod/meow_audiod` | 独占音频守护 M6.35(M6.0 全量+315 孤儿会话两段式回收+M6.34 A3/DSD:24/32bit 高清 byte-exact+DoP DSD 透传+音量收编 v2 双组写;改 glibc-dynamic 链 libasound;md5 `9f47c8fd`) |
| `/usr/bin/meow_audio` | `patch_v2/meow_audiod/meow_audio` | audiod 用户侧 CLI M2.97(+monitor 实时电平表/文本电平条;status 补 rms/peak) |
| `/usr/bin/meow_powermgr` | `patch_v2/meow_powermgr/meow_powermgr` | 功耗管理专项服务期2 M6.1(期1+history/top/events+mp2731 current_now 实测充电功率+WiFi 未关联档/BT rfkill 细化+系数路径 /data/meow/powermgr/;IPC 0xD0-D7) |
| `/usr/bin/meow_power` | `patch_v2/meow_powermgr/meow_powermgr` | meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩) |
| `/usr/bin/meow_netmgrd` | `patch_v2/meow_netmgrd/meow_netmgrd` | 网络管理服务期3 M6.1(期1 接口层+期2 连接普查/DNS 代理+期3 策略引擎防自锁/SOCK_DESTROY 实测 no-op 记档/面板白名单/WiFi save_config;IPC 0xE0-EB;agent-276) |
| `/usr/bin/meow_net` | `patch_v2/meow_netmgrd/meow_netmgrd` | meow_netmgrd 的 CLI 形态(同二进制双形态,argv[0]=meow_net 即 CLI:status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help) |
| `/etc/init.d/S70meow_sysui` | `patch_v2/meow_sysui/S70meow_sysui` | sysui 自启 M2.85(launcher.conf enabled=1+迁移) |
| `/etc/init.d/S89meow_svcd` | `patch_v2/meow_svcd/S89meow_svcd` | svcd 自启 M6.2(+amsd.conf 热更 exec→/usr/bin 迁移) |
| `/usr/bin/meow_amsd` | `patch_v2/meow_amsd/meow_amsd` | 活动管理器 M6.35(注册表自愈 3s 快重试+30s 合并重扫+am rescan + busy 识别修复 + 显示名 UTF-8 整序列 name[49] + **批① FG/BG 上报 `EV BG/FG`/handle_bg/handle_fg**(md5 `57e19cde`,替代 M6.34 旧 `026cad52`;amsd FG/BG 供批③后台管理页/`am status` 数据源) |
| `/usr/bin/am` | `patch_v2/meow_amsd/am` | amsd 用户侧CLI(+resolve/picker/rescan) |
| `/etc/init.d/S90meow_bootmenu` | `patch_v2/bootmenu/S90meow_bootmenu` | bootmenu 自启 M2.85(注释同步) |
| `/etc/init.d/S92_03_start_music_player` | `patch_v2/bootmenu/S92_03_start_music_player` | §4.3 兜底 patch(rcS 正常路径被 S90 阻塞不执行) |
| `/etc/init.d/S95shutdown_logo` | `patch_v2/bootmenu/S95shutdown_logo` | rcK 关机时显示 /etc/shutdown_logo.jpeg |
| `/etc/profile.d/meow_card_path.sh` | `patch_v2/meow_launcherd/meow_card_path.sh` | SSH 会话 PATH(M2.12 起):指向 TF 卡 MeowOS/opt/bin |
## 官方基线自带(非本表管理)
官方 rootfs 自带 busybox 全套、wpa_supplicant/wpa_cli、BlueZ(bluetoothd/bluealsa 等)、hiby_player、sys_server、thttpd、strace 等(详见网络/系统盘点记录)。本表只管 MeowOS 装入/替换的组件。
+60 -30
View File
@@ -8,11 +8,13 @@
### 1.1 板载(NAND rootfs / /data)
- 设置持久化:**板载** `/usr/data/meow/settings.conf`(UBIFS,小文件 <10KB,跨换卡保留)`[SYSTEM_SETTINGS §2][MEOWOS_API §3]`
- **板载服务目录**:`/usr/data/meow/services.d/<name>.conf`(M2.54,Data 区,恒定存在、权威优先;首启由 `S89meow_svcd` 预置 `mdnsd.conf`)。与 TF `MeowOS/services.d/` 双源,同名板载优先 + WARN;板载 `trigger=boot` 服务不依赖卡(拔卡保留)`[meow_svcd.md][meow_svcd.c]`
- **组件持久化目录(2026-09-01 用户定案)**:`/data` = `/usr/data` 的符号链接(UBIFS rw,**不随固件**);组件的可变动持久化文件(模型系数/策略/状态缓存等)一律放 **`/data/meow/<组件名>/`**(即 `/usr/data/meow/<组件名>/`),不再平铺在 `/usr/data/meow/` 根。例:`/data/meow/powermgr/power_model.conf`、`/data/meow/netmgrd/net_policy.conf`。存量平铺件(settings.conf、services.d/、sysui.d/、meow_boot_default)保持原位不动。`[WORK_STATE 2026-09-01]`
- **板载服务目录**:`/usr/data/meow/services.d/<name>.conf`(M2.54,Data 区,恒定存在、权威优先;首启由 `S89meow_svcd` 预置 `meow_audiod.conf`/`meow_mdns.conf`(**M6.1 服务名统一 meow_\***:旧 `audiod.conf`/`mdnsd.conf` 自动迁移(stop+rm+预置),cd4e2c2 用户定案)。与 TF `MeowOS/services.d/` 双源,同名板载优先 + WARN;板载 `trigger=boot` 服务不依赖卡(拔卡保留)`[meow_svcd.md][meow_svcd.c]`
- **主机名**:设置键 `system.hostname`(SET_STR,默认 `meowos`;RFC1035 首标签),`S89meow_svcd` 开机读 settings `sethostname`;供自研 mDNS 应答器公布 `<host>.local` `[SYSTEM_SETTINGS §1][mips-repo/bins/meow_mdns]`
- `/etc/meowos_version`:版本唯一来源(launcher 运行时 `load_meowos_version` 读;`MEOWOS_VERSION` 宏仅作 dev 占位兜底)`[DEV_NOTES #4]`
- **`/data/meow_boot_default`:开机默认权威标记(O_TRUNC 单值:`0`=MeowOS / `1`=HiByOS)**。由 launcher `meow_settings_save()` 在保存 `boot.default` 时同步写;bootmenu `set_boot_default_meow()`(写 0)/ launcher `set_boot_default_hiby()`(写 1)同文件同语义;bootmenu 开机读它定默认;init 读入不触发、不反向覆盖;`meow.conf` 不再承担 boot_default `[M2.61 同步修复][README][meow_settings.c][meow_bootmenu.c]`
- `/tmp`:tmpfs,开机清空;开机模式标记 `/tmp/meow_boot_mode`、电源菜单 `/tmp/meow_power_menu{,_active}`、app 沉浸 `/tmp/meowapp_state` `[BOOT_SELECTOR §1][KEY_CONTROL §9][APPS §3.5]`
- `/tmp`:tmpfs,开机清空;开机模式标记 `/tmp/meow_boot_mode`、电源菜单 `/tmp/meow_power_menu{,_active}`、app 沉浸 `/tmp/meowapp_state`、**锁屏** `/tmp/meow_pin_unlock_once`(一次性解锁恢复口,launcher 锁屏页消费放行一次、重启自消)/`/tmp/meow_pin_locked`(电源菜单「主动锁」标志,重启消失)`[BOOT_SELECTOR §1][KEY_CONTROL §9][APPS §3.5][WORK_STATE 2026-09-02]`
- **锁屏 PIN(M6.2 起,与面板同源 `system.pin`,规范 `SYSTEM_SETTINGS` 锁屏小节)**:设置键 `system.pin`=`<salt_hex>:<sha256_hex(salt||pin)>`(sha256+salt,格式与面板 `panel.pin` 全等,**绝无明文**;空=未设=锁屏关;`panel.pin` 已设时锁屏接受并首次验证成功后并入本键)+`system.pin_lock_on_wake`(息屏唤醒后锁屏,int 0/1,默认 1=开;开机不锁)+`system.pin_lock_until`(错 PIN 锁定的 epoch 截止戳,持久化防 launcher 重启清零)+`system.pin_fails`(连续错 PIN 计数 0-4,达 5 触发锁定并清零,持久化)。清/改:设置「安全→锁屏 PIN」页——设/改需旧 PIN 一次、**清除免旧 PIN**(物理持机即授权,仅二次确认)、`system.pin_lock_on_wake` 开关;**兜底**:SSH root 删 `system.pin` 键即解锁(无其它后门)。**锁屏模态演进**:M6.4 §310 二期 **EVIOCGRAB 独占触控**(锁屏层独占 grab,下层 App 完全收不到锁屏触摸)+主界面应用名截断;M6.6 §310 三期——**App 内手动息屏也触发锁屏** + **锁屏脏区全屏不透明底** + **300ms 重铺**;M6.7 锁屏页自睡(空闲超时自动灭屏);M6.8 310 四轮锁屏自睡 min(系统息屏,30s);M6.9 批A **锁屏期音量/播放/上下曲键转发**(EVIOCGRAB 独占真模态下下层收不到,锁屏本层显式边沿处理:音量 `settings_volume_adj`+`vol_ramp_tick`、播放/上下曲 `meow_media_playctl`)。`[SYSTEM_SETTINGS §1][WORK_STATE 2026-09-02]`
- 日志启动缓冲 `/tmp/meow_boot.log`(256KB 滚动,卡出现后自动迁卡)`[APPS §4.5]`
- IPC socket:`/usr/data/meow_ipc.sock`(AF_UNIX SOCK_STREAM,0666,backlog 4)`[FRAMEWORK_DESIGN §1][WORK_STATE]`
- 专项服务 IPC socket:`/usr/data/meow_perf.sock`(meow_svc_perf,收 `MSG_PERF_QUERY→RESULT`)与 `/usr/data/meow_power.sock`(meow_svc_power,收 `MSG_POWER_ACTION→STATUS`),均 0666、可 `--sock`/`MEOW_IPC_{PERF,POWER}_SOCK` 覆盖;power 服务端以 `SO_PEERCRED uid==0` 基础鉴权 `[meow_svc_perf.c][meow_svc_power.c][meow_ipc.h]`
@@ -57,8 +59,9 @@
### 2.2 meow_ipc 消息协议(AF_UNIX,全小端 LE)
- 帧头 16B:`magic=0x4D4F5701`('MEOW'|1)/`version=1`/`type`/`req_id`/`len`;`len` 上限 **MEOW_IPC_MAX=4096**;列表编码 `[u16 count]`+NUL 串 `[FRAMEWORK_DESIGN §1]`
- 消息类型:`MSG_HELLO 0x0001↑ / ACK 0x0002↓ / ERR 0x0003↓ / REGISTER 0x0010↑ / SERVICE_OK 0x0011↓ / SERVICE_STOP 0x0012↑ / MSG_NOTIFY 0x0020↑ / NOTIFY_ACK 0x0021↓ / NOTIFY_DISMISS 0x0022↑ / NOTIFY_LIST 0x0023↓ / NOTIFY_PROGRESS 0x0024↑ / ACT_DECLARE 0x0030↑ / ACT_QUERY 0x0031↑ / ACT_RESULT 0x0032↓ / ACT_FOCUS 0x0033↑ / ACT_BACK 0x0034↑ / NAV 0x0040↑ / SHOW_ACTIVITY 0x0041↓ / PERF_QUERY 0x0050↑ / PERF_RESULT 0x0051↓ / POWER_ACTION 0x0060↑ / POWER_STATUS 0x0061↓ / SYS 0x00F0↓` `[FRAMEWORK_DESIGN §1][meow_ipc.h]`
- 媒体:`MSG_META`(元数据)、`MSG_PLAYCTL`(cmd 1=播放2=暂停3=上一首4=下一首5=seek)、`MSG_PROGRESS` `[附录 B]`
- 消息类型:`MSG_HELLO 0x0001↑ / ACK 0x0002↓ / ERR 0x0003↓ / REGISTER 0x0010↑(M4 已落地:无头后台服务注册,m_register_t,保持连接)/ SERVICE_OK 0x0011↓(M4:m_ack_t status=服务 pid)/ SERVICE_STOP 0x0012↑(M4:SIGTERM 不重启)/ MSG_NOTIFY 0x0020↑ / NOTIFY_ACK 0x0021↓ / NOTIFY_DISMISS 0x0022↑ / NOTIFY_LIST 0x0023↓ / NOTIFY_PROGRESS 0x0024↑ / ACT_DECLARE 0x0030↑ / ACT_QUERY 0x0031↑ / ACT_RESULT 0x0032↓ / ACT_FOCUS 0x0033↑ / ACT_BACK 0x0034↑ / NAV 0x0040↑ / SHOW_ACTIVITY 0x0041↓ / PERF_QUERY 0x0050↑ / PERF_RESULT 0x0051↓ / POWER_ACTION 0x0060↑ / POWER_STATUS 0x0061↓ / SYS 0x00F0↓(M4:m_sys_t,SHUTDOWN=1/SVC_DEAD=2 熔断广播)` `[FRAMEWORK_DESIGN §1][meow_ipc.h]`
- 媒体:`MSG_META`(元数据)、`MSG_PLAYCTL`(cmd 1=播放2=暂停3=上一首4=下一首5=seek6=播放/暂停toggle——物理键164恒发6由源侧按当前态翻转,launcher 透传)、`MSG_PROGRESS` `[附录 B]`
- 专项服务消息号(见 §4):**电源管理 `meow_powermgr`** 0x00D0 QUERY/0x00D1 RESULT/0x00D2 POLICY_GET/0x00D3 POLICY_SET(期1;SET=动作级),期2 0x00D4 HISTORY/0x00D5 HISTORY_RESULT/0x00D6 EVENTS/0x00D7 EVENTS_RESULT;**网络管理 `meow_netmgrd`** 0x00E0 STATUS_REQ/RESULT、0x00E2 IFACE_REQ/RESULT、0x00E4 WIFI_CTL(→0xE5 RESULT)、0x00E6 BT_CTL(→0xE7 RESULT)、0x00E8 CONNS_REQ/RESULT、0x00EA POLICY_GET/SET、0x00EC DNS_CTL(0x00E0-0x00EB 已用,预留 0xEE 事件订阅)。**读级任意调用者、写级(CTL/POLICY SET)仅系统身份**(沿用 svc_power SO_PEERCRED 鉴权模式)`[meow_ipc.h]`
- IPC 线程**只读帧+解析+入队**;状态变更主循环单写者(eventfd 唤醒,10ms 热键轮询非阻塞排空)`[FRAMEWORK_DESIGN §0][§6]`
### 2.3 watchdog / supervisor 信号
@@ -120,23 +123,38 @@
> 定位:把 §5(最小控件集)与设计模板(§5.2-§5.7)收敛到**统一控件架构**;实现分两批(§5.8.8)。
- **第一批 = 通用基座(M2.70/0.7.65)**:`meow_widget` 基座统一标准控件/自定义画布;`meow_widget_opts`(Modifier 式 Options,`meow_widget_set_opts`/`meow_widget_opts_get`,0/-1 用默认);统一状态机 `MEOW_WS_ENABLED/SELECTED/FOCUSED/PRESSED`(`meow_widget_set_state`/`meow_widget_state`/`meow_widget_enabled`/`selected`);`meow_widget_custom_create`(自定义画布,App 给 render/hit 回调,`W_CUSTOM`);布局 `meow_row/column/box`+`meow_layout_add/arrange`+`spacer`;MD3 语义色板(§5.8.4)+ 主题接口 `meow_ui_set_style`(`MEOW_STYLE_MD3/MIUIX`)`/meow_theme_get`。
- **第二批 = Material 组件子集(M2.71/0.7.66)**:`meow_btn_create_variant`(五型)/`meow_textfield_create`(IME)/`meow_card_create`/`meow_divider_create`/`meow_badge_create`/`meow_checkbox_create`/`meow_radio_create`/`meow_progress_create`/`meow_progress_circular_create`/`meow_fab_create`/`meow_topappbar_create`/`meow_navigationbar_create`/`meow_tabbar_create`/`meow_lazycolumn_create`/`meow_lazyrow_create`/`meow_dialog_create`/`meow_sheet_create`。
- **M2.73 控件库修复(`meow_ui.a` md5 `e8341975`,`[meow_ui_widget.c][meow_ui_ime.c]`)**:① `W_LIST`(含 LazyColumn/LazyRow,`meow_widget_draw` case W_LIST)**视口裁剪**——按 `vx0/vx1/vy0/vy1` 只绘制落在视口内的行/分隔线/文本,杜绝「行滚出视口仍画/标脏上屏」的标题栏/底部脏区(SDK 全局,非 App 侧);② **z 序**:App 侧 `hello_app/hello.c` 的 `d_paint_region` 局部刷新改按 z 栈重画(内容→标题→slider 值→chrome),根治「层动态控件实时重绘盖住/抹掉上层 FAB」;**SDK 级全局 z 序方案在评估,暂未落定**(未正式纳入 SDK 公共 API 前不要写成正式规范);③ 渲染残影/脏区:局部刷新不再「清底+只画单个控件」(改 z 栈重画),消除重叠区残留;④ **IME 行为**(`meow_ui_ime.c`):键盘区背景快照/恢复(`ime_capture`/`ime_restore`,无残影)、配色走主题(不再硬编码黑底白键)、shift 大小写、阻塞 `meow_ui_text_input` 在打字循环喂 `meow_ui_activity_ping`(重置息屏计时,进度条不卡死)、OK(回车)=提交并关闭 / ESC(X)/背景点=取消;回显条=**实时输入回显**(`"%s_"` 拼接,非候选/联想),**候选框留中文输入法再做**。
- **M2.73 控件库修复(`meow_ui.a` md5 `e8341975`,`[meow_ui_widget.c][meow_ui_ime.c]`)**:① `W_LIST`(含 LazyColumn/LazyRow,`meow_widget_draw` case W_LIST)**视口裁剪**——按 `vx0/vx1/vy0/vy1` 只绘制落在视口内的行/分隔线/文本,杜绝「行滚出视口仍画/标脏上屏」的标题栏/底部脏区(SDK 全局,非 App 侧);② **z 序**:App 侧 `hello_app/hello.c` 的 `d_paint_region` 局部刷新改按 z 栈重画(内容→标题→slider 值→chrome),根治「层动态控件实时重绘盖住/抹掉上层 FAB」;**SDK 级全局 z 序已落地并并入视图框架**(`meow_ui_view.c` 广播注册表 z 序:自动注册 + 子>父 + `set_z(999)` 置顶 + 收敛 live_btn,agent-176 · M2.76),hello App 层 `d_paint_region` 临时方案退役(agent-195 绘制期通用 clip `g_clip_stack[16]` 根除越界脏区);③ 渲染残影/脏区:局部刷新不再「清底+只画单个控件」(改 z 栈重画),消除重叠区残留;④ **IME 行为**(`meow_ui_ime.c`):键盘区背景快照/恢复(`ime_capture`/`ime_restore`,无残影)、配色走主题(不再硬编码黑底白键)、shift 大小写、阻塞 `meow_ui_text_input` 在打字循环喂 `meow_ui_activity_ping`(重置息屏计时,进度条不卡死)、OK(回车)=提交并关闭 / ESC(X)/背景点=取消;回显条=**实时输入回显**(`"%s_"` 拼接,非候选/联想),**候选框留中文输入法再做**。
- **兼容**:既有 `meow_btn/slider/switch/list_create` 签名一字不改(Options/状态机走新增挂载器);`meow_ui_theme`/`meow_widget` 结构变化 = **`meow_ui.a` ABI 变化** → 需同步重编 hello(§5.8.8)。
- **主题增强(§8.1)**:主题 = **顶级风格族 `ui.style`(md3/miuix)× 明暗 `display.theme`(dark/light)** 共同决定「整套配色+控件形态+背景底」;`display.background` 为「style×mode」派生的**可选背景底选项集**(非独立维度);状态栏图标随 `ui.style` 换集。⚠️ **`miui`/`miuix` 命名待定案(保留,勿拍板)**:`MEOW_APP_SDK §8.1` 用 `miuix`;`SYSTEM_SETTINGS §1` 注明 `meow_settings.c g_defs` 现值 `"md3,miui"`(label "Material You,MIUI"),实现时把 `g_defs` 的 `miui` 对齐为 `miuix`(**MIUIx 未实现,当前仅 md3 生效**)。
### 3.5 SDK 扩展模块速查(构造/滚动/动画/i18n/图标库/矢量字/事件分发/ListView 虚拟化/图片降采样)
> 权威签名 = `patch_v2/sdk/include/meow_ui_{box,anim,scroll,str,input,widgets,image,ft}.h`;蓝本 `MEOW_APP_SDK.md` §5.5/§6.4/§10.1/§12/§13。**本批全部追加式 API**——既有 App/签名零改动,`meow_ui.a` 变化需重链四 App(hello/files/reader/流媒体客户端)。
- **§12.1 线性布局 `meow_box`(292,纯布局计算器非控件,opt-in `meow_view_set_layout`)**:`meow_box_new(dir)`(`MEOW_BOX_H/V`)/`_free`/`_set_rect`/`_set_padding`/`_set_gap`/`_set_align(main_align,cross_align)`;子项 `meow_box_add(b,w,size,weight)`/`_add_ex`(WRAP 用 min/preferred+measure 回调)/`_add_sub`(嵌套);`size`=`MEOW_BOX_FIXED/WEIGHT/WRAP`;`align`=`START/CENTER/END/SPACE_BETWEEN/AROUND/EVENLY`、`cross`=`START/CENTER/END/STRETCH`;求解 `meow_box_arrange(b)` 把子控件 x/y/w/h 写进 `meow_widget`(`meow_widget_set_rect`)。定长 items(≤`MEOW_BOX_MAX_CHILD=24`)+嵌套深度 ≤16 静态栈,arrange 零 malloc。
- **§12.2 动画基座 `meow_anim`(292)**:`meow_anim_start(const meow_anim_desc*)`(desc:`from/to/duration_ms/ease/update/done/ctx`;start 即 value=from+回调一次 update(0))/`_cancel`(回调 done(canceled=1))/`_active`/`_value`/`_set_region(x,y,w,h)`(动画每 tick 只 flush 影响区,不整屏);`meow_ease`=`LINEAR/OUT/IN_OUT/OVERSHOOT`+`meow_ease_apply(e,t)`;节拍 `meow_anim_tick()`(挂 on_tick / `meow_ui_feed_tick`,无动画零开销)+`meow_anim_count()`;静态池 ≤`MEOW_ANIM_MAX=16`。
- **§12.3 滚动物理 `meow_scroll`(292)**:`meow_scroll_create(set,max,get,redraw,ctx)`/`meow_scroll_wrap(list,is_listview)`(is_listview:1=W_LISTVIEW /0=W_LIST|LazyColumn|LazyRow,redraw 只 flush 控件视口区)/`_free`/`_track(dy,t_ms)`(跟手+速度采样,内部 set_scroll clamp)+`_fling()`(加权初速+ease-out 推进+边界回弹)/`_stop`/`_velocity`/`_active`/`_offset`/`_set_snap(step)`/`_set_friction(tau_ms,默认180)`。拖动态保持现状不变,fling 由 §12.2 驱动。
- **§13 字符串资源 / i18n `meow_str_*`(299)**:`meow_str(id)` 按当前 locale 取串(返回静态、**永不 NULL**、未知 id 原样返回 id)/`meow_strf(out,outsz,id,...)`(snprintf 语义)/`meow_str_set_locale(locale)`(切 `"zh_cn"|"en_us"`,即时生效+重载 TF 覆盖表+best-effort 持久化 `ui.locale`)/`meow_str_locale()`。四层取值:TF 覆盖表(`<pkg>/strings/<locale>.txt`)> 内嵌当前语言 > 内嵌内置默认(zh_cn) > 原样 id。热路径零 malloc;id 形如 `域.名`(common.ok / player.pause)。不做复数/BiDi/RTL/动态字回退(当前为「按 locale 取整串」雏形)。
- **§6.4 事件分发形式化(293,单一仲裁者)**:模态作用域 `meow_ui_modal_begin/end/active/depth`(作用域存在时下层收不到触摸,未命中面板的事件由背板消费恒返回哨兵);自上而下命中 `meow_ui_hit_topdown(x,y)`/`meow_ui_widget_hit`;手势捕获 `meow_ui_gesture_begin/owner/end`(down 命中即捕获到 up,move/up 只发捕获目标);唯一焦点 `meow_ui_focus_acquire/release/owner/tap/set_lost_cb`(点外释放+回调+滚动容器转移)。传播矩阵:模态>z 序自顶向下;命中即消费;向下穿透仅当未命中。
- **§5.5 ListView 虚拟化(295,W_LISTVIEW 扩展层,API 追加式)**:`meow_listview_set_vsource(lv,count_fn,bind_fn,ctx)`(count 每帧实时查、bind 仅在行进视口时调一次;count_fn/bind_fn 均非 NULL 才切 pool 模式,否则回退 src2);`meow_lv_item{title,subtitle,icon,icon_kind,right,on,enabled,section,user}`;`meow_listview_set_item_renderer(lv,lines,show_icon,show_right)`/`_set_item_tier`(单行 48/双行 56)/`_set_item_draw`(完全自绘覆盖默认 renderer,state=`MEOW_LISTVIEW_ST_{SELECTED,PRESSED,DISABLED}`);固定池=视口+2 槽,行离开视口不销毁、同 index 命中同槽不重绑;段头 `item->section=1`。
- **图标库 `sb_icons`(307,Material Symbols Rounded / MD3,单通道 alpha 32×32,生成 `tools/gen_action_icons.py`)**:索引只追加——状态栏 `MEOW_SB_ICON_WIFI=0..BATT_CHG=14` 原样保留;**动作** `MEOW_ICON_SETTINGS=15..MEOW_ICON_HEART=29`(设置/刷新/搜索/播放/暂停/上下曲/返回/关闭/添加/勾选/歌词/列表/下载/收藏),经 `meow_topappbar_set_item_icon(w,idx,MEOW_ICON_*)` 使 W_TOPAPPBAR 动作项画位图(`layout 见 meow_ui_widgets.h`)。
- **图片降采样 `meow_img_downscale(img,maxdim)`(308)**:把解码图降到 ≤maxdim 边长以省内存(流媒体客户端封面 1448² RGBA 8.28MB→cap≤480px→0.78MB,-7.65MB),返回新 `meow_img*`。
- **§10.1 矢量字 `meow_ft_*`(316,上收阅读器已验证的 FreeType 管线,`patch_v2/sdk/meow_ui_ft.c`)**:`meow_ft_new_face(path)`(`FT_New_Face` 按需读文件零整载 anon + glyph LRU 512(key=`(px<<32)|cp`,可配 256/1024)+ 灰阶 blend565);`meow_ft_draw_*`/`meas`/`free`(App **显式**调才用,`meow_ui_text` 位图路径零改动);加载失败/无卡自动降级 `meow_font`(绝不豆腐不崩)。与位图字库**并存**;FreeType 2.14.3 vendor 进 `patch_v2/sdk/vendor/`(gitignore)+ `build_sdk.sh` 裁剪构建(`-DFT_DISABLE_ZLIB/BZIP2/PNG/HARFBUZZ/BROTLI=TRUE`,静态 musl);**不调 `meow_ft_*` 的 App 零增量**(`meow_ui_ft.o`/freetype 链接器不拉取,无任何 FT 符号)。**SDK 演进主体收官**。
- **M7.0a SDK 基座(326,`meow_ui.a 7fc83f77`,launcher 重写前置)**:**`meow_view` 事件接线**(`meow_view_dispatch_event` 接 `meow_ui_run` 主循环 + `on_enter/on_leave` 递归 children + 动态挂子修正);**`W_ICONBTN`**(圆形命中 + 按压态,`MEOW_ICON_*` 位图/自定义画布按钮,电源菜单/播放控制用);**主题色板导出/注入** `meow_theme_export/inject`(launcher `s_ui_theme` 薄壳读 SDK,为 M7.4 主界面/各页统一 `meow_theme` 权威铺路)。
- **SDK 新增批次(lazygrid/select/swipe/cachegov/net,`meow_ui.a c3d6371b`)**:**§12.4 2 维虚拟化宫格 `meow_lazygrid`**(M7.4 主界面前置:count+cols+视口虚拟化,item 池有界 ≤`MEOW_LAZYGRID_POOL_MAX`=96,fling/snap 复用 §12.3 meow_scroll,`set_vsource/set_item_draw/set_empty_text`+`hit/tap_at`);**§5.8.9 枚举选择器 `meow_select`**(M7.2 设置页缺口 #4:BottomSheet+`meow_listview(SINGLE)` 单选,`create/add_item/set_on_change/open(阻塞返回 idx/-1)/free` + 一次性 `meow_select_show`,选项 ≤`MEOW_SELECT_MAX_ITEMS`=32);**§5.5 左滑删除 swipe-delete**(M7.3 通知中心缺口 #9:`meow_listview_set_swipe_delete(lv,enabled,on_delete,ctx)` + `swipe_begin/move/end/active/offset`,横滑删/纵向滚/点按 tap 三分);**§14.2 磁盘缓存治理 `meow_cachegov`**(OOM page-cache 治理 A 侧:L0 曲终整文件 DONTNEED / L1 丢已读前缀 / **L2 禁止**,10s 滚窗逐级回退,`cfg{enabled,keep_hot_bytes,min_hot_ahead,drop_mode,refault_backoff}` + `register/unregister/tick/drop_finished/set_cfg/note_refault/backoff_level/download_paused/prefetch_paused`);**§14.1 网络自愿通道 `meow_ui_net`**(netmgrd 期4:`meow_net_policy_check(host)` 0=allow/1=block + `meow_net_report(rx,tx)` best-effort)。权威签名 = `patch_v2/sdk/include/meow_{lazygrid,select,cachegov}.h` / `meow_ui_net.h`;蓝本 `MEOW_APP_SDK.md` §5.5/§5.8.9/§12.4/§14。
---
## 4. SDK 之外系统组件
- **系统进程 supervisor 树(源:2026-08-30 定案;M2.71 已落地 `meow_sysui`)**:`meow_sysui`(`patch_v2/meow_sysui/meow_sysui.c`,~113KB;`etc/init.d/S70meow_sysui` 开机拉起,早于 S89)监管系统进程,**一个崩→只拉起它,绝不级联**;`meow_launcherd`=系统 UI 渲染宿主(状态栏/通知中心/电源菜单/全局手势,与 APP 进程独立);`meow_svcd`=服务管理器(服务全子进程化,svcd 自身不死);APP 进程只渲染自己内容,崩不连累系统 UI。**系统组件渲染归系统进程(launcher),APP 进程不渲染系统 UI**(SDK 系统组件 API 仅系统进程可链,App 不可自建悬浮层);通知中心 UI 渲染主体=系统进程,数据经 IPC 从通知存储读。现网(bootmenu execv launcher + S89 svcd 自启)→ 定案(sysui 统一监管)为渐进迁移,属系统基建线。**sysui 当前默认只监护 `meow_svcd`**(`/usr/data/meow/sysui.d/<name>.conf`,目录式复用 services.d 语义;launcher 由 S90meow_bootmenu 的 execv 拥有,登记但 `enabled=0` 防双重拉起;mdns 是 svcd 服务子进程,不双重管理)。`[FRAMEWORK_DESIGN §10][MEOW_APP_SDK §6.1][NOTIFICATION_SYSTEM §9][patch_v2/meow_sysui/meow_sysui.md]`
- **专项子服务(一期在跑 + M2.73 落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能/系统只读聚合)、`meow_svc_power`(关机/重启/升级/回 HiBy 系统动作)。launcher 电源菜单与 MeowPanel 均改经这些服务 IPC 触发(不再直连 `sys_do_reboot`/爬 `/proc`)。**IPC**:perf 走 `/usr/data/meow_perf.sock` `MSG_PERF_QUERY→RESULT`;power 走 `/usr/data/meow_power.sock` `MSG_POWER_ACTION→STATUS`(服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73**:perf 增 **network** 子集(SYSTEM 子集输出:真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`,单点权威源);power **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启;**只完整性不签名**,海贝官方固件无签名,期望 md5/hash 二期)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(`settings_get_int_trim`,弃 amixer dummy;`dac on/off` 因 cs43198 `pwr_gpio=-1` 无控制点→`MEOW_ERR_NODEV`)。新 md5:perf `fa073bf9`、power `f3e653d5`。**电源 CLI `meow_svc_power <cmd>`**(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot):`shutdown`/`reboot[--hiby]`(强制回 MeowOS)/`upgrade`(写 mtd5=`ota:kernel2`,含写保护)/`set-default-meow`/`set-default-hiby` + `status`/`battery`/`reboot-reason`/`upgrade-status`/`screen on|off|timeout<秒>`/`dac on|off|status`。**二期**:关键 IPC 只接受 **`os.meow.*`** 鉴权 + `.mpk` 打包签名验签(解包验签/导出保留签名)。`[SYSTEM_FRAMEWORK_SERVICES][APPS 附录 §3][meow_svc_perf.c][meow_svc_power.c]`
- **系统进程 supervisor 树(源:2026-08-30 定案;M2.71 已落地 `meow_sysui`)**:`meow_sysui`(`patch_v2/meow_sysui/meow_sysui.c`,~113KB;`etc/init.d/S70meow_sysui` 开机拉起,早于 S89)监管系统进程,**一个崩→只拉起它,绝不级联**;`meow_launcherd`=系统 UI 渲染宿主(状态栏/通知中心/电源菜单/全局手势,与 APP 进程独立);`meow_svcd`=服务管理器(服务全子进程化,svcd 自身不死);APP 进程只渲染自己内容,崩不连累系统 UI。**系统组件渲染归系统进程(launcher),APP 进程不渲染系统 UI**(SDK 系统组件 API 仅系统进程可链,App 不可自建悬浮层);通知中心 UI 渲染主体=系统进程,数据经 IPC 从通知存储读。现网(bootmenu execv launcher + S89 svcd 自启)→ 定案(sysui 统一监管)为渐进迁移,属系统基建线。**已迁移(M2.83)**:sysui 默认监护 `meow_svcd` + `meow_launcherd`(`/usr/data/meow/sysui.d/<name>.conf`,目录式复用 services.d 语义;`sysui.d/launcher.conf` `enabled=1`(restart=always/backoff=3000/max_restart=5),bootmenu 在 MeowOS 模式让位——sysui 在则不 exec launcher;mdns 是 svcd 服务子进程,不双重管理)。`[FRAMEWORK_DESIGN §10][MEOW_APP_SDK §6.1][NOTIFICATION_SYSTEM §9][patch_v2/meow_sysui/meow_sysui.md]`
- **专项子服务(一期 M2.73 已落地,规划见 `devdocs/plan/SYSTEM_FRAMEWORK_SERVICES.md`)**:两个 **svcd 托管、板载 `trigger=boot`** 的专项子服务——`meow_svc_perf`(性能/系统只读聚合)、`meow_svc_power`(关机/重启/升级/回 HiBy 系统动作)。launcher 电源菜单与 MeowPanel 均改经这些服务 IPC 触发(不再直连 `sys_do_reboot`/爬 `/proc`)。**IPC**:perf 走 `/usr/data/meow_perf.sock` `MSG_PERF_QUERY→RESULT`;power 走 `/usr/data/meow_power.sock` `MSG_POWER_ACTION→STATUS`(服务端 `SO_PEERCRED uid==0` 基础鉴权)。**M2.73**:perf 增 **network** 子集(SYSTEM 子集输出:真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`,单点权威源);power **升级写保护**(`upgrade` 写 mtd5 前校验卡根 `r3proii.upt` 存在+可读+常规文件+size>0,失败不写 mtd5/不重启;**只完整性不签名**,海贝官方固件无签名,期望 md5/hash 二期)与 **`dac status` 读板载 `settings.conf` 的 `audio.volume` 真实值**(`settings_get_int_trim`,弃 amixer dummy;`dac on/off` 因 cs43198 `pwr_gpio=-1` 无控制点→`MEOW_ERR_NODEV`)。新 md5:perf `7ea196de`(M2.75 补 network 闭合括号)、power `306df962`(2B 面板精确绑定)。**电源 CLI `meow_svc_power <cmd>`**(root 免鉴权、单一命令、不覆盖系统 shutdown/reboot):`shutdown`/`reboot[--hiby]`(强制回 MeowOS)/`upgrade`(写 mtd5=`ota:kernel2`,含写保护)/`set-default-meow`/`set-default-hiby` + `status`/`battery`/`reboot-reason`/`upgrade-status`/`screen on|off|timeout<秒>`/`dac on|off|status`。**二期签名鉴权已落地**(agent-185 2A / agent-221 2B):关键 IPC 只接受 **`os.meow.*`** 签权 + `.mpk` 打包签名验签(解包验签/导出保留签名;多公钥数组 + v2 签名块);读级命令鉴权已放宽到任意调用者(agent-189)。**M6.10 log 模块升级(337)**:perf `65f2059f` 挂「**卡死快照+perf 基线**」(主循环 `accept`→`poll` 化 + `snap_monitor_tick`:loadavg 超 `hang_load` 阈值落 `hang_*.snap`(全量 CPU/内存/进程/socket 现场)+`perf_*.log`(监控日志,60s/次、1MB 上限、每 boot ≤4 张、采集全走单文件 /proc 读);新增 `snap`/`snap-check` 子命令)+ crash `f02c4f45`(`log_cleanup_old` 前缀扩 `perf_*`/`hang_*` 入 logs/ 1GB 总纪律)。`test_snap_monitor` 18/18。`[SYSTEM_FRAMEWORK_SERVICES][APPS 附录 §3][meow_svc_perf.c][meow_svc_power.c]`
- 通知(SDK `meow_notify_*`,M2.53 最小版 `[meow_ui.h §9.1]`):
- 常量:`MEOW_NOTIFY_MAX_TYPES=4`/`MEOW_NOTIFY_MAX_PER_TYPE=4`/`MEOW_NOTIFY_MAX_MEDIA=1`/`MEOW_NOTIFY_TIMEOUT_ONGOING=-1`/`MEOW_NOTIFY_TIMEOUT_NEVER=0`
- 分类:`CAT_GENERAL/MESSAGE/EMAIL/EVENT/PROGRESS/MEDIA(=5)/COUNT`;紧急度 `URG_LOW/NORMAL/HIGH/CRITICAL`
- `meow_notify_register_type(cat)`/`meow_notify_send(const meow_notify*)`(返回 id)/`meow_notify_cancel(id)`/`meow_notify_active_count`/`count_in_type`/`has_active`/`urgency_str`/`category_str`
- **本阶段落点=SDK 内部活动表 + meow_log() 打日志**;真实投递通知中心 UI / D-Bus 是后续任务 `[meow_ui.h 注释]`
- `meow_config_*`(app 配置读写,走 meow_settings 后端)——**SDK 缺失清单第 3,未定** `[WORK_STATE]`
- `meow_media_*`(媒体会话)——**依赖阶段 D 落地,未定** `[WORK_STATE]`
- 崩溃捕获/看门狗——**规划中未定** `[WORK_STATE SDK 缺失清单 #5]`
- **通知中心 UI 已落地**(launcher 渲染,M2.65+);**D-Bus 桥(A-14)未实现**(待后续);真实投递走 launcher IPC `[meow_ui.h 注释]`
- `meow_config_*`(app 配置读写,走 meow_settings 后端)——**已落地**(agent-158 · `dcba597`,见 `devdocs/plan/SDK_CONFIG_API.md`)`[WORK_STATE]`
- `meow_media_*`(媒体会话)——**已落地**(agent-204 M2.81 媒体会话 + agent-206 M2.82 SDK `meow_ui_media.c` 懒连重连封装)`[WORK_STATE]`
- 崩溃捕获/看门狗——**已落地**:`meow_crash`(agent-187 · `cf86cbd` 系统级崩溃捕获 + sycd 上报)+ App 崩溃直报(agent-199)+ watchdog(应用看门狗,S89 预置)`[WORK_STATE]`
- `meow_notify_dbus`(org.freedesktop.Notifications 桥)——**已定,待实现** `[FRAMEWORK_DESIGN 附录 C]`
- **meow_svcd 服务管理器**(`patch_v2/meow_svcd/`,mipsel 静态 ~101KB 进固件,不入卡)`[meow_svcd.md]`:
- 类 systemd/systemctl。**已接入系统启动链(M2.54)**:固件 `etc/init.d/S89meow_svcd` 开机自启(`-f` 前台 + 重挂循环,失败不死;置于 S84ssh 之后、S90meow_bootmenu 之前——因 S90 exec 引导选单阻塞 rcS)。由 bootmenu/launcher `fork/exec` + `waitpid` 托管仍为备选。
@@ -144,13 +162,18 @@
- **板载-trigger=boot 语义**:板载服务恒定存在,boot 触发不依赖卡(拔卡保留);TF 服务 boot 仍需卡。
- **服务目录规则**:**固件工具**→板载 `/usr/data/meow/services.d/<name>.conf`(Data 区,拔卡保留);**TF 工具/第三方**→TF `MeowOS/services.d/<name>.conf`(随卡存在/消失);**双源板载优先**(TF 同名被板载屏蔽 + WARN「<name> 板载与 TF 都存在,采用板载」)。
- 崩溃隔离:服务=svcd **独立子进程**(`fork+execv`),svcd 只 `waitpid`/`SIGCHLD` 感知;服务死不影响 svcd/supervisor;重启受 `backoff_ms` 退避防热循环。
- 状态机:`DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;稳定运行 30s 清零 restart_count。CLI(systemctl 式,SSH 直呼):`status/reload/start|stop|restart <name|all>/pid`,示例 `meow_svcd status`/`meow_svcd restart mdnsd`/`meow_svcd stop all`/`meow_svcd reload`;控制走 AF_UNIX 单行文本 `/tmp/meow_svcd.sock`;守护默认后台,`-f/--foreground` 前台供 supervisor 托管。
- 状态机:`DISABLED/STOPPED/RUNNING/STOPPING/FAILED`;稳定运行 30s 清零 restart_count。CLI(systemctl 式,SSH 直呼):`status/reload/start|stop|restart <name|all>/pid`,示例 `meow_svcd status`/`meow_svcd restart meow_mdns`/`meow_svcd stop all`/`meow_svcd reload`;控制走 AF_UNIX 单行文本 `/tmp/meow_svcd.sock`;守护默认后台,`-f/--foreground` 前台供 supervisor 托管。
- **M2.73 悬垂指针修复(md5 `da6ebc66`)**:`scan_and_reconcile` 用 `*s=tmp` 整体拷贝后,`s->argv[]` 仍指向已销毁的 `tmp` 栈帧 → `execv`/`/proc/pid/cmdline` 错乱。修法:新增 `svc_rebuild_argv(s)`,赋值/`memmove` 左移后把 `s->argv[]` 重定位到 `s->exec`/`s->argbuf` 自身成员(删除一个服务后同 tick 内被拉起不再用错位 argv)。`[meow_svcd.c]`
- **卡上外部工具 = `MeowOS/opt/`**(`{bin,nyai,sources.list}, lib/, tools/`):MeowOS 启动时从卡加载,拔卡降级不崩。**`MeowOpt` 已废弃并入此处**;mDNS 已改为自研 `meow_mdns`(固件 `/usr/bin`),不再放卡。`[DEV_NOTES][meow_svcd.md]`。**MeowPanel 本体 = `MeowOS/opt/MeowPanel/`**(后端+前端,svcd 托管 `MeowOS/services.d/meowpanel.conf`,无卡跳过、系统核心不依赖,不进固件;md5 `11ed76ec`,详见 `patch_v2/meowpanel/README.md`)。
- **MeowPanel(外置 TF 卡 Web 面板,M2.73 已落地)**:单进程 HTTP/JSON 后端(8080)+ 静态托管(`/`→index.html、`/assets/*`)+ `meow_svc_perf`/`meow_svc_power` IPC 客户端(读侧透传 perf 聚合、动作侧经 power)+ `settings.conf` 设置镜像(`key = value` 带空格 + 精确匹配)+ 文件白名单(`MeowOS/Music/MeowData/MeowAPPs/book`,`..` 逃逸→403)+ 应用(`nyai install/export/remove`,包名=`app.json.package`)+ 按键注入(evdev,只音量/电源/播放/上下曲,**禁返回158/主页172/确认28/退出96**)+ 日志(`MeowOS/logs`)。M2.73:系统页「以太网」→「**蓝牙**」栏(无网口有蓝牙);**OTA 检查升级 bug 已修**(改读 `GET /api/system` 的 `ota`,不再误触 `POST /api/system/ota` 升级)。**⚠️ 无鉴权**(内网信任模型;`reboot/shutdown/ota/apps/删除/上传` 危险端点勿暴露公网,power 侧有 `SO_PEERCRED uid==0` 基础校验)。`[patch_v2/meowpanel/][devdocs/plan/MEOWPANEL_PROTOCOL.md]`
- **mDNS 应答器 = `meow_mdns`(自研,已拍板替代完整 mdnsd)**:`mips-repo/bins/meow_mdns/`,**IPv4-only**(内核无 IPv6,完整 mDNSResponder/SAResponder 均 `socket(AF_INET6)` 失败),mipsel 静态 ~79KB(md5 b23de7ce)。**进固件 `/usr/bin/meow_mdns`**(不入卡),由 meow_svcd 托管(板载 `/usr/data/meow/services.d/mdnsd.conf`,`trigger=boot`、root 即可绑 5353/组播,`cap=` 空)。功能:join `224.0.0.251:5353`,应答 `<host>.local` A(wlan0 IPv4)/`_ssh._tcp` PTR/SRV/TXT/`_services._dns-sd` PTR;支持 `MEOW_MDNS_HOST`/`_ADDR`/`_PORT`/`_TTL`/`_CF`/`_QD`/`_MCAST`/`_MIF`/`_DBG`。⚠️ **手机热点隔离**:设备→主机组播被热点丢弃,avahi 只收组播应答 → 该热点下 `avahi-resolve <host>.local` 超时(网络层限制,非应答器缺陷),主机需 DNS 兜底 `[mips-repo/bins/meow_mdns/manifest.md][meow_svcd.md]`。
- **卡上外部工具 = `MeowOS/opt/`**(`{bin,nyai,sources.list}, lib/, tools/`):MeowOS 启动时从卡加载,拔卡降级不崩。**`MeowOpt` 已废弃并入此处**;mDNS 已改为自研 `meow_mdns`(固件 `/usr/bin`),不再放卡。`[DEV_NOTES][meow_svcd.md]`。**MeowPanel 本体 = `MeowOS/opt/MeowPanel/`**(后端+前端,svcd 托管 `MeowOS/services.d/meowpanel.conf`,无卡跳过、系统核心不依赖,不进固件;md5 `8108852f`(nyai 路径修复)→ 阶段C 后 新 md5(见 `patch_v2/meowpanel/README.md`),详见 `patch_v2/meowpanel/README.md`)。
- **MeowPanel(外置 TF 卡 Web 面板,M2.73 已落地,阶段 C 实时画面+遥控已落地)**:单进程 HTTP/JSON 后端(8080)+ 静态托管(`/`→index.html、`/assets/*`)+ `meow_svc_perf`/`meow_svc_power` IPC 客户端(读侧透传 perf 聚合、动作侧经 power)+ `settings.conf` 设置镜像(`key = value` 带空格 + 精确匹配)+ 文件白名单(`MeowOS/Music/MeowData/MeowAPPs/book`,`..` 逃逸→403)+ 应用(`nyai install/export/remove`,包名=`app.json.package`,`nyai` 固件内置 `/usr/bin/nyai` 优先+卡上回退)+ 按键注入(evdev,只音量/电源/播放/上下曲,**禁返回158/主页172/确认28/退出96**)+ 日志(`MeowOS/logs`)。M2.73:系统页「以太网」→「**蓝牙**」栏(无网口有蓝牙);**OTA 检查升级 bug 已修**(改读 `GET /api/system` 的 `ota`,不再误触 `POST /api/system/ota` 升级)。**M2.78+ 新增**:可选密码鉴权(agent-196,默认关)、通知中心后端 `/api/notifications`(TF 归档环形 200 条)、文件域 4 bug 修复 + 上传 8MB 上限 + 双重白名单(agent-182)**。**M2.84-88 阶段 C**(agent-224):`/api/screen.jpeg`(fb0 活动帧→自研 JPEG 编码器)+ `/api/input/tap`(坐标注入,本机触摸优先 5s 仲裁)+ 屏幕增量帧 `?delta=1`(8x8 块比对,近静态 27KB→804B,agent-234)+ `/api/notifications` 实时化(优先 IPC 读 launcher 活跃集 source:ipc,agent-234)。**252 新增(2026-09-01)**:`/api/audio`(audiod MSG_AUDIO_QUERY→STATE 只读透传,不在时 available:false)+ 前端「音频」页(5s 轮询只读)与「字体」页(列表/上传进度/导出/409 冲突确认 force 重试)。**⚠️ 鉴权默认关**(内网信任模型;`reboot/shutdown/ota/apps/删除/上传` 危险端点勿暴露公网,power 侧有 `SO_PEERCRED uid==0` 基础校验)。`[patch_v2/meowpanel/][devdocs/plan/MEOWPANEL_PROTOCOL.md]`
- **面板批2 + 加固(2026-09-02)**:崩溃/日志中心 + 音频路由页 + 音量写经 clamp `vol_max` + 后端补 `reboot_reason`;**JSON 输出 UTF-8 加固**(`sA_str_n` 单遍校验、非法序列换 `?`、IPC 原始 JSON 门禁降级,防任一上游坏字节拖垮整页,d7bf62f)。**overview 自校验纪律**:`/api/overview` 服务端发出前先自查(jv_parse)+ 与浏览器口径对齐的**严格平衡检查**,坏则 dump `/tmp/overview_bad.json` + 500(908fc53/67aa5fc/54e737a);前端非 JSON 自动回传原始字节到 `/api/debug/report_bad_json`(落 `/tmp/overview_client_bad.txt`,7a40586/524e5d6);**教训**:后端 `jv_parse` **容忍尾随垃圾**,浏览器 `JSON.parse` **不容忍**——net 块多写一个 `}` 即「Extra data」破页(批2 DNS 并入事故),故自校验必须严格括号/闭合平衡。`[patch_v2/meowpanel/][devdocs/plan/MEOWPANEL_PROTOCOL.md][WORK_STATE 2026-09-02]`
- **mDNS 应答器 = `meow_mdns`(自研,已拍板替代完整 mdnsd)**:`mips-repo/bins/meow_mdns/`,**IPv4-only**(内核无 IPv6,完整 mDNSResponder/SAResponder 均 `socket(AF_INET6)` 失败),mipsel 静态 ~79KB(md5 b23de7ce)。**进固件 `/usr/bin/meow_mdns`**(不入卡),由 meow_svcd 托管(板载 `/usr/data/meow/services.d/meow_mdns.conf`,`trigger=boot`、root 即可绑 5353/组播,`cap=` 空)。功能:join `224.0.0.251:5353`,应答 `<host>.local` A(wlan0 IPv4)/`_ssh._tcp` PTR/SRV/TXT/`_services._dns-sd` PTR;支持 `MEOW_MDNS_HOST`/`_ADDR`/`_PORT`/`_TTL`/`_CF`/`_QD`/`_MCAST`/`_MIF`/`_DBG`。⚠️ **手机热点隔离**:设备→主机组播被热点丢弃,avahi 只收组播应答 → 该热点下 `avahi-resolve <host>.local` 超时(网络层限制,非应答器缺陷),主机需 DNS 兜底 `[mips-repo/bins/meow_mdns/manifest.md][meow_svcd.md]`。
- `meow_settings_*`(设置后端,已定)/`meowcli`/`meow_sys_devattr`/`meow_devattr_set` `[MEOWOS_API]`
- 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`;`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]`
- **独占音频守护 `meow_audiod`(M2.90 已落地,规范 `devdocs/plan/AUDIOD_DESIGN.md`)**:板载 `/usr/bin/meow_audiod`,svcd 托管(板载 `services.d/meow_audiod.conf`,trigger=manual;launcher 确认 MeowOS 模式后拉起;**启动门控:mode≠meow → exit(0),HibyOS 下不存在**)。IPC `/usr/data/meow_audio.sock`(帧层复用 meow_ipc.h,号段 0x00C0-0x00CA:OPEN/OPEN_ACK/WRITE/PAUSE/RESUME/STOP/PREEMPTED/QUERY/STATE/ROUTE/VOLUME)。**独占+抢占**(后开赢,旧流收 PREEMPTED;socket EOF=回收;软混音已否决,留接口位)。**DAC 原生率跟随不做 SRC**;格式归一 S16 直通/S32 升位/mono 复制;切率 mute-重配-恢复。**meow_route 已并入为路由线程并退役独立服务**(⚠️ 退役服务要清板载+TF 双源 conf,否则旧 conf 会双写路由)。**CLI `meow_audio`**(root 免鉴权):`status`/`tone [freq] [ms] [rate]`(正弦测试音,排查无声不开 App)/`route <0|2|3>`/`vol [0-100]`。SDK 客户端 `meow_audio_open/write/pause/close/query/on_preempt`(`meow_ui_audio.c`,懒连接+EPIPE 标死;无 audiod → -ENOENT 不隐式直开,`MEOW_AUDIO_DIRECT=1` 仅调试)。**M2.91-97 加固批**:①**开机武装 hbc3000 FPGA**(M2.91:路由线程启动无条件写 Output Port Switch 4.4→3/3.5→2/无→4,同值先写 5 强制重烧——根治「未烧 FPGA 时 PCM 第二次 `hbc3000_start` 内核硬挂死」,调查底稿 `devdocs/plan/AUDIO_HANG_INVESTIGATION.md`)②**长流护栏**(M2.93:ring 有界等待/cond timedwait/popen 有界收割/O_NONBLOCK+poll 写/wedge 自救/心跳 watchdog)+**几何收窄 period=1024×4**(M2.94:驱动默认巨 buffer 致桥周期中断稀缺 wedge、播 ~12s 停摆;tonepcm2 矩阵 60s 零 stall 实证)③**音量反转**(M2.95:CS43198 音量寄存器=衰减量,四路统一 `dac=255-pct*255/100`)④**炸耳安全**(M2.96:`dac_vol_mute` 写 255 真静音,旧写 0=最大响度)⑤**电平监控**(261,待 M2.97 固件化:播放线程 ~100ms 窗口整数 RMS/peak dBFS(deci-dB,逐帧判边界,-990=-inf)、`MSG_AUDIO_STATE` 追加 `rms=/peak=/lvlseq=`(ABI 兼容)、流活跃时 1s 节拍写 TF `MeowOS/logs/audio_monitor_<bootts>.log`、CLI `meow_audio monitor [秒]` 文本电平条)⑥**孤儿会话两段式回收**(315,M6.5):客户端崩溃/被杀(sig=9)/正常退出后会话完整回收(死亡即清会话+排空后清统计脚),STATE 回干净无流基线。`[patch_v2/meow_audiod/]`
- **功耗管理服务 `meow_powermgr`(期0 调研 + 期1 骨架 M6.0 已落地,期2 M6.1,规范 `devdocs/plan/POWER_MANAGER_DESIGN.md`)**:板载 `/usr/bin/meow_powermgr`,svcd 托管(板载 `services.d/meow_powermgr.conf`,`trigger=boot`;**HibyOS 模式只读降级**——只采样电池,绝不放策略/不碰 wlan0/bt/声卡)。双形态 CLI **`meow_power`**(同二进制 argv[0] 分流)。**期0 事实**(`devdocs/plan/POWER_HW_FACTS.md`):`battery/current_now` **不存在**→实时功率只能 `derived+estimated` 双轨、`cpufreq` **不存在**→省电档无降频点、**背光量程 0-101**(非 255!「亮度跳 100」=满量程、默认 101=满亮的根因)、`axp_battery` 是幻影设备、**mp2731 有 `current_now`**(充电功率可测)、rfkill×4 存在(期3 控制点)、无 thermal/hwmon。**期1**:权威充电态链复用(mp2731-charger/status)+ 经验模型 14 系数(`/data/meow/powermgr/power_model.conf` 可调)+ IPC 0x00D0-D3(QUERY/RESULT/POLICY_GET/POLICY_SET)+ CLI `status/estimate/model`。**期2(M6.1)**:`history`(**30s 环形、4h 不落盘**)+`top`(功耗分解排序,2s)+`events`(充放电/低电/亮灭屏/背光跳变事件流)+模型细化(**WiFi 未关联档**、**BT rfkill/ACL 档**)+ **mp2731 `current_now` 实测充电功率**(`charge_confidence=measured`;放电仍 `estimated`,诚实区分)。系数/状态路径 = **`/data/meow/powermgr/`**(组件持久化目录定案,SPEC_REF §1.1)。IPC 0x00D4-D7(期2 HISTORY/EVENTS)。`[patch_v2/meow_powermgr/]`
- **网络管理服务 `meow_netmgrd`(期1 M6.0 已落地,期2/期3 M6.1,规范 `devdocs/plan/NETMGR_DESIGN.md`)**:板载 `/usr/bin/meow_netmgrd`,svcd 托管(板载 `services.d/meow_netmgrd.conf`,`trigger=boot`);双形态 CLI **`meow_net`**(`status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help`)。**HibyOS 模式只读降级**(接口状态可查,绝不下发控制),RSS 预算 ≤1MB。期0 内核事实(`devdocs/plan/NETWORK_HW_FACTS.md`):**无 iptables 用户态/无 cgroup/无 conntrack → 内核级每应用管控不可行**,主杠杆 = **DNS 代理(域名级)+ 连接普查(inet_diag)+ SDK 自愿通道**;**双行 resolv.conf 崩溃零断流**。**期1 = 接口统一层**(iface 枚举+热插拔/WiFi `wpa_cli` 有界收割/BT `hciconfig`+`bluetoothctl`/rfkill/IP 路由 DNS 只读/udhcpc 续租释放仅 MeowOS)。**期2 = 连接普查 + DNS 代理**:2s `/proc/net/{tcp,udp}` 普查(inode→pid→**三实体** `app:<包名>`(查 amsd)/`svc:<服务名>`(查 svcd)/`proc:<comm>` 归因)+ DNS 代理(127.0.0.1:53,双行 resolv.conf+atexit 恢复+5s 自检,真机实测 enabled)+ 事件日志(1min 去重)+ CLI `apps/conns/top/dns`。**期3(M6.1)= 策略引擎**:`/data/meow/netmgrd/net_policy.conf`(组件持久化目录定案,SPEC_REF §1.1)读改写单键+原子写;**防自锁双层**(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init **拒绝置 block** + 策略文件被手工改坏时的加载校验强制复位);**SOCK_DESTROY 真机实测 = no-op(内核 4.4 接受请求但不销毁 socket)→ 连接级管控不可用,仅 DNS 层生效**(`meow_net policy show` 头明示 `enforce=dns_only`);classify_exe 面板白名单 + apps 归因修复 + WiFi `save_config`。日志 `MeowOS/logs/net_<bootts>.log`(无卡降级 /tmp)。`[patch_v2/meow_netmgrd/]`
- **音频硬件事实(sndprobe/tonepcm2 真机实测,2026-08-31)**:播放 PCM = `/dev/snd/pcmC0D0p`(`cs43198-hifi-0`);`pcmC0D1p`=snd-soc-dummy(勿用)。能力:S16_LE/S24_LE、1-2ch、8k-768k、buffer 2048-1048576 帧、period 512-262144。⚠️ **ALSA UAPI 两坑**(流媒体客户端无声根因,勿再踩):①hw_params 必须「全放行」初始化(mask 全 1、interval [0,UINT_MAX],params_any 语义)再收窄——memset 0 的 interval=「只能取 0」求交必空 → HW_PARAMS 恒 EINVAL;②access/format/subformat mask 必须**先清零再单点**——留全 1 会被内核 choose 成 MMAP_INTERLEAVED,ch>1 时 write 恒 EINVAL。**CS43198 音量寄存器=衰减量(官方数据手册 DS1039 实证,用户供)**:PCM_VOLUME_A/B(0x0B/0x0C),0=0dB 满幅最大声、255=-127.5dB≈静音(0.5dB/步;DSD 通道独立 DSD_VOLUME_A/B)。**混音器 L/R Playback Volume 读回恒 0 是死镜像**(官方播放器走 sa_hgl_dma 硬直通不经此控制,HibyOS 对照实测同读 0),勿据此判音量。**音量寄存器=衰减量(0=最响 255=静音,M2.95 真机实测方向)**:四路(launcher/audio.c+audiod+watchdog+SDK vol)统一 `dac=255-pct*255/100`;mute 必须写 255 真静音(M2.96 炸耳修复)。混音器清单:numid=1/2 L/R Vol、3 Mute Output、4 Digital Filter、5 Soft Mute、6 NOS_EN、7 DRE_EN、8 Balance Lineout En、9 Output Port Switch(0 空闲/1 lineout/2 3.5/3 4.4/4 SPDIF;0 危险勿写;**5 = audiod 内部「强制重烧 FPGA」约定值**(M2.91 开机武装同值改写路径),非常规路由值,App/面板勿写)、10 DOP_EN。`[patch_v2/tools/sndprobe.c][tonepcm2.c][APPS §3.7]`
- 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`(**已落地 M4**:app.json `background`/`background_audio` 字段 + MSG_REGISTER 校验链;播放中退出转后台保活见 FRAMEWORK_DESIGN §9);`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]`。**M6.34+ 追加后台常驻框架**:app.json `background_keepalive`(无限期保活)+ `meow_bg_should_keep` 三参判定 + 宽限期 `MEOW_BG_GRACE_MS`(30min)回收 + amsd `EV BG/FG` 上报(批③后台管理页数据源),设计见 `devdocs/plan/APP_BACKGROUND_FRAMEWORK.md` §7 / `APP_BACKGROUND_KEEPALIVE_P1.md`。
---
@@ -165,7 +188,7 @@
- **全屏机制(M2.5x,`app.json fullscreen` 即 app 级 + `route.<id>.fullscreen` 即 activity 级)**:launcher 启动 app/切 activity 时据声明**预先设 SDK 全屏**(env `MEOW_FULLSCREEN`)。进入/退出全屏:**状态栏向上收出屏顶、只留一条提示线高度**(非全隐藏),带上收/恢复的**自然过渡动画**。**全屏下保留**:左缘返回手势 + 顶部动态伸缩提示线;顶部下滑仍可展开通知中心,但**顶部热区缩小**(不影响全屏 app)`[源:用户 2026-08-30]`(细化既有「全屏=隐藏状态栏/隐藏返回键」的表述,见 §7 A-13)
- 沉浸:`app.json immersive:1` → launcher 写 `/tmp/meowapp_state=1` → 隐藏虚拟返回键,手势归 SDK `[APPS §3.5][GESTURE_BACK §2.4]`
- **横向分页设计模板(M2.5x 预留,见 `MEOW_APP_SDK.md` §5.2)**:主页/子页多页横向滑动复用的设计模板(参照 HibyOS 播放器主页横划进播放页)。与左缘返回/顶部下滑**按起始位置分流**互斥——左缘 ≤热区→**左滑返回**(§5.1);中部左/右滑→**页切换**;顶部下滑→**通知中心**。实现要点:相邻页各预渲染一张快照、切换只 blit 快照 + 跟手平移/吸附(vsync ≤60 帧、零 UI 重算)。**注**:模板 API 为「草案/待定案」,当前未实现 `[源:2026-08-30]`
- **SDK 可复用设计模板清单(M2.5x 预留,见 `MEOW_APP_SDK.md` §5.3)**:把反复出现的界面结构抽象为可复用设计模板——①**分组设置表单** ②**列表+选择** ③**弹窗/对话框** ④**顶/底下拉面板**(A+B 先行,已展开 §5.4-§5.7)+ 媒体播放/沉浸/加载(C)、卡片/导航/列表项(D,随 app/MD3 细化)、拖拽排序/长按菜单/时间轴(E,后置),共 11 项;横向分页另见 §5.2。**所有模板与既有手势约定一致**:左缘返回 + 顶部滑条 + 状态栏收合 + 通知中心热区(本 §5.1/§3.1/A-13)——模板只在内部做命中分流,**不重定义全局手势**;顶部/底部面板与左缘返回/通知中心按起始位置与触达途径分流互斥(快捷设置触达途径为**待定案**)。**注**:A①②/B③④ 模板 API 均为「草案/待定案」,当前未实现 `[源:2026-08-30]`
- **SDK 可复用设计模板清单(M2.5x 预留,见 `MEOW_APP_SDK.md` §5.3)**:把反复出现的界面结构抽象为可复用设计模板——①**分组设置表单** ②**列表+选择** ③**弹窗/对话框** ④**顶/底下拉面板**(A+B 先行,已展开 §5.4-§5.7)+ 媒体播放/沉浸/加载(C)、卡片/导航/列表项(D,随 app/MD3 细化)、拖拽排序/长按菜单/时间轴(E,后置),共 11 项;横向分页另见 §5.2。**所有模板与既有手势约定一致**:左缘返回 + 顶部滑条 + 状态栏收合 + 通知中心热区(本 §5.1/§3.1/A-13)——模板只在内部做命中分流,**不重定义全局手势**;顶部/底部面板与左缘返回/通知中心按起始位置与触达途径分流互斥(快捷设置触达途径为**待定案**)。**注**:A①②/B③④ 模板 API 原为「草案/待定案」。**部分已落地**:B② 列表+选择 → 结构化列表控件 `W_LISTVIEW`(M2.76 agent-186);A① 分组设置表单、B④ 顶/下方面板 已在设置页/通知中心/电源菜单以 launcher 侧实作(非独立公开 SDK 模板 API);视图框架(`meow_ui_view.c` M2.76+)与控件库已覆盖其多数能力,其余(横向分页、C/D/E)仍为草案 `[源:2026-08-30]`
### 5.2 主题
- **日/夜/自动**:SDK `meow_theme` 内置 `meow_theme_light/dark`;模式键 `display.theme_mode`(SYSTEM_SETTINGS 记为 `manual`/`auto`——**与 SDK 枚举 light/dark/auto 冲突**,见 §A);AUTO 判时段 `[SYSTEM_SETTINGS §1][MEOW_APP_SDK §8]`
@@ -178,7 +201,7 @@
- **默认字库目录**:默认字体统一放 **`<卡根>/MeowOS/fonts/default/regular/`**(`font.png`+`font.idx`,可选 `font.json`),不再用 `MeowAPPs/fonts/font.png`;设备旧 `MeowAPPs/fonts/` 及旧 `MeowOS/fonts/<family>/`(无 weight 级)**保留**(勿删,避免破坏老约定/已有文件),仅新引用一律指向 `MeowOS/fonts/default/regular`。
- **`font.file` 变更即 unload+reload 生效**(设置页回调);拔卡 → 回退固件内置。
- **preset 动态化(两级扫描)**:启动/进设置页扫描一次 `MeowOS/fonts/<family>/<weight>/`,重建「字体文件」下拉(默认字体 + ≤16 套卡内 <family>/<weight>;上限防爆)。扫描仅此二时机,勿放热路径。**存量卡片迁移**:旧版扫 `MeowOS/fonts/<family>/`(图片直接在该层);迁移到 `<family>/regular/` 后,旧 `<family>/` 下不再有三件套 → 新扫默认按两级找,故若未重做成 `<family>/<weight>/` 则扫不到(需先用 mv 迁成 `<family>/<weight>/` 再重扫)。
- **MeowPanel 预留**:`meow_font_import_zip()`/`meow_font_export_zip()` 为 TODO 声明(不实现),供 MeowPanel 后续接入(`.zip` 导入校验→解压到 `MeowOS/fonts/<family>/<weight>/`、导出打包)。
- **MeowPanel 预留 → 已落地(2026-08-31 用户定案:**改为 `.tar.gz`、放弃 zip**,与 .mpk 同构走 busybox `gzip -dc | tar` 管道,设备必带 tar/gzip)**:面板以 `POST /api/fonts/import` + `GET /api/fonts/export` 接入(见 §5.7.6 与 `patch_v2/meowpanel/README.md` §5.11)。`meow_font_import_zip()`/`meow_font_export_zip()` 原 TODO 声明**不再按 zip 实现**,语义为 tar.gz(函数名可保留或改 `_tgz`)。导入校验→解压到 `MeowOS/fonts/<family>/<weight>/`、导出打包。
- **多字体(M2.5x,`display.multi_font`)**:按字符类别(CJK/CJK_SYM/NUM/LATIN/LATIN_SYM/OTHER)逐字符路由,每类可各选一套字库(`font.class_*`,默认空=跟随全局 `font.file`);同 family 去重、同时装载不同字体 ≤4 套(超出 WARN「过多不同字体会增内存」并截断);开关关 = 单字体(现状)。详见 §5.7.7「多字体」。
- **字体档位 / 大字库(M2.5x,本次实现)**:抽象出**字库大小档位**,让系统/应用/设置页都知道选的是多大字库 `[源:用户 2026-08-30]`。**定案见 `devdocs/plan/FONT_SYSTEM.md`**(字体系统定案:三级目录 `<family>/<weight>/<tier>/`、`font.json` 增 `tier`/`languages`/`label`、`charset` 改指编码、按语言选字体;本小节以下为旧口径)。
- 档位枚举:`MEOW_FONT_TIER_COMMON`(**默认 / ≈4k** 码位)/ `MEOW_FONT_TIER_GBK`(**大 / ≈20k+**)/ `MEOW_FONT_TIER_FULL`(**全 / 可选**,码位/体积最大)。
@@ -203,7 +226,7 @@
- **⑨ 面板「跟手下拉、半程可回」(launcher 侧,M2.65)**:面板打开/关闭由触发式 180ms 过渡升级为**手指拖动实时跟手**——顶部热区下拉 `progress = 手指位移映射`(复用 `mnp_anim_off`,∈[0,1024],1:1 跟手);松手 `progress≥40%×1024≈410` → 时间轴补到 1024(展开);`<阈值 或 反向拖回` → 回弹到 0 收回(半程取消);关闭同理可跟手。与行左滑(⑦ 横向主导)/列表滚动(竖向溢出)/`meow_edge_back`(左缘)互斥分流。`[NOTIFICATION_SYSTEM §9]`
### 5.4 配置版本单点 / 刷新率 / 息屏
- **现行:M2.73(`/etc/meowos_version` = `0.7.68`,已推卡真机验收;M2.74 / 0.7.69 待打)(M2.72/0.7.67、M2.71/0.7.66、M2.70/0.7.65、M2.69/0.7.64 …)**;版本单点 = `/etc/meowos_version`(见 §1.1,`build_fw_apps.sh -v` 写入)`[DEV_NOTES #4][fw_content.conf]`
- **现行:M6.35(`/etc/meowos_version` = `0.8.35`,已上卡;M6.34/0.8.34、M6.33/0.8.33、M6.32/0.8.32、M6.31/0.8.31、M6.30/0.8.30、M6.29/0.8.29、M6.28/0.8.28、M6.27/0.8.27、M6.26/0.8.26、M6.25/0.8.25、M6.24/0.8.24、M6.23/0.8.23、M6.22/0.8.22、M6.21/0.8.21、M6.20/0.8.20、M6.19/0.8.19;更早 M6.18-0.8.18 …)**;版本单点 = `/etc/meowos_version`(见 §1.1,`build_fw_apps.sh -v` 写入)`[DEV_NOTES #4][fw_content.conf]`
- 刷新率:`display.refresh_rate` enum 60/30/15(默认 60);`display.refresh_always_max` bool false(关=允许 60→30→15 降档)。governor:连续 12 帧超预算降一档,连续 4 批≈48 帧远低预算升一档 `[SYSTEM_SETTINGS §1][PERF_SCHEDULER][MIGRATION_AC §12.1]`
- 息屏:`display.screen_off_timeout` int 秒 默认 **30**(0=常亮);`display.screen_off_mode` enum `panel`(面板下电+背光0+FBIOPBLANK)/`black`(背光0全黑+双击亮屏) `[SYSTEM_SETTINGS §1]`
- 亮度 clamp:`display.brightness` 0-255 默认 101(clamp ≥ `display.brightness_min`);`display.brightness_min` 默认 **1**(M2.39 由 25 改 1)——**与旧文档 25 冲突见 §A** `[SYSTEM_SETTINGS §1]`
@@ -213,7 +236,7 @@
- `<pkg>/` = 程序主体+app.json+资源;`MeowData/<pkg>/{data,cache}` = 数据;首次启动自动建 data/cache 并经 env 注入,exFAT 无软链 `[APPS 附录 §1/§2]`
- `.mpk` = tar.gz(app.json + meow 二进制 + 资源),**不含 data/cache**;导入校验 app.json/哈希→解压→数据 init→登入列表 `[APPS 附录 §3]`
- 包管理 **CLI `nyai`**(静态 MIPS,部署 `MeowOS/opt/bin/nyai`):install/remove/list/info/search/index/export/upgrade/scan;源 `MeowOS/opt/nyai/sources.list`;索引 `Packages`(Package/Version/Filename/Size/SHA256/Depends/Description)`[APPS 附录 §5]`
- 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + **`package`(反域名权威 app_id;==目录名;缺省回退目录名)** + 扩展 `framework`/`immersive`/`icon`/`route.<id>`/`awake.<id>`(keep_awake)/`service.<svc>`/`fullscreen`(app 级;`route.<id>.fullscreen`=activity 级;**本次实现**:launcher 启动/切 activity 据声明预设 SDK 全屏,见 §5.1/§3.1/§A-12)/`fonts`(字体档位 `"full"|"gbk"|"common"`,需大字库时声明,见 §5.7.8) `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2][源:审计 2026-08-30][源:用户 2026-08-30]`
- 应用数上限 **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>`(M4 起服务二进制=App 同目录 `<svc>` 文件)/**`background`/`background_audio`(M4 已落地:无头服务注册权限/后台播放保活)**/**`background_keepalive`(M6.34 起:后台常驻声明——`1`=返回桌面无限期保活不清;缺省走 30min `MEOW_BG_GRACE_MS` 宽限期回收;与 `background_audio`+媒体活跃可叠加无限期保活)**/`fullscreen`(app 级;`route.<id>.fullscreen`=activity 级;**本次实现**:launcher 启动/切 activity 据声明预设 SDK 全屏,见 §5.1/§3.1/§A-12)/`fonts`(字体档位 `"full"|"gbk"|"common"`,需大字库时声明,见 §5.7.8)/`depends`(**二进制依赖**声明:`"curl,jq:optional"` 或行式 `depends=curl,jq:optional,ffmpeg:required`;comma 分项 `<name>[:<mod>]`,mod=required|req(默认)/optional|opt;launcher 启动前查 `/usr/bin/<name>` 与 `/usr/data/mnt/sd_0/MeowOS/opt/bin/<name>`(X_OK,命一即可),required 缺失阻断启动(「缺依赖」toast)、optional 缺失仅 WARN,**仅启动时检查**;nyai install 同查并提示) `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2][源:审计 2026-08-30][源:用户 2026-08-30]`
### 5.6 冷启动充电屏
- 触发(**三者同时**):冷启动(`poweroff` 写 `/data/meow_cold_boot=1`;`reboot` 写 0)+ 在充电(AC online=1 或 battery status=Charging)+(开机瞬间一次性快照,判定后不回头)`[CHARGING_SCREEN §0]`
@@ -313,12 +336,19 @@
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`/`weight` 取自 `font.json.family`/`font.json.weight`(缺省 `regular`)或 zip 名;解压到 `MeowOS/fonts/<family>/<weight>/`;**覆盖冲突需先确认**;导入后应触发 launcher 重扫(刷新 §5.7.1 列表)。
- **应用字库导入**:目标 `MeowAPPs/<pkg>/fonts/<handle>/`,与 `app.json "fonts"` 声明对齐,校验同上。
- **导出**:系统库把 `MeowOS/fonts/<family>/<weight>/` 三件套打包;应用库把 `MeowAPPs/<pkg>/fonts/<handle>/` 打包。
- 约定:`.zip` 布局与校验沿用 `font.json` schema(§5.2);导入/导出后的一致性(列表刷新、应用声明同步)归 MeowPanel + launcher 协作,本小节仅列接口语义。
- **5.7.6 MeowPanel 上传 / 导出(.tar.gz,2026-08-31 用户定案:放弃 zip,与 .mpk 同构)** `[源:meow_font.h][源:patch_v2/meowpanel/README.md §5.11]`
- **已由 MeowPanel 后端落地**:`POST /api/fonts/import`(multipart `file`=<.tar.gz>)+ `GET /api/fonts/export?family=&weight=` +
`GET /api/fonts`(列表)。**不用 zip**——设备 busybox `gzip`/`tar` 必带,后端走 `gzip -dc | tar` 管道(与 nyai .mpk 同款),
零新增依赖、不引入 zlib/miniz/自研 zip。原 `meow_font_import_zip(zip_path)` / `meow_font_export_zip(family,out_zip)` TODO
声明**不再按 zip 实现**,语义即 tar.gz(函名可保留或改 `_tgz`,注明即可)。
- **系统字库导入**:`.tar.gz` 内含 `font.png+font.idx+font.json` 三件套;校验三件套就绪 + `font.json` 可解析;
sha256 沿用 `font.json` 约定(= **WARN**,仅缺件/不可解析硬失败);`family`/`weight` 取自 `font.json.family`/
`font.json.weight`(缺省 `regular`);解压到 `MeowOS/fonts/<family>/<weight>/`;**覆盖冲突需先确认**(409 → 前端 confirm 后
`force=1` 重试);导入后进设备「设置→字体」页自动重扫(扫描时机=启动/进设置页,面板**不**主动触发重扫)。
- **应用字库导入**:目标 `MeowAPPs/<pkg>/fonts/<handle>/`,与 `app.json "fonts"` 声明对齐,校验同上(待面板后续扩展)。
- **导出**:系统库把 `MeowOS/fonts/<family>/<weight>/` 三件套打包为 `<family>-<weight>.font.tar.gz`;应用库同理(待扩展)。
- 约定:`.tar.gz` 布局与校验沿用 `font.json` schema(§5.2);导入/导出后的一致性(列表刷新、应用声明同步)归 MeowPanel +
launcher 协作。**安全**:tar 解出条目逐个校验(拒绝对路径/`..`/软链/多出条目/子目录包裹),沿用 nyai 的 sanitize 思路。
- **5.7.7 多字体(按字符类别逐字符路由,`display.multi_font`,M2.5x 新增)** `[源:meow_font.h][源:MEOWOS_SPEC_REF §5.2/§5.5]`
- **背景**:一款字体难兼顾中文(大图集、字形全)与拉丁/数字(干净、省内存)。多字体让**不同字符类别各用一套字库**,按 codepoint 逐字符路由取字形。
- **设置键**(launcher 读,SDK 经 env 读同一映射):
@@ -340,10 +370,10 @@
---
## 6. 标志性 TODO / 未决(文档未定,勿凭猜实现)
- OPEN_QUESTIONS.md 待定 ❓:#1 背景图加载时机/缓存、#2 非 480×720 图如何处(拉伸/裁剪/回退)、#3 叠加层颜色联动、#4 背景色板具体枚举、#5 浅色主题文字对比度、#7 首期是否做应用图标、#8 通知来源(**最大空白**,M2 已定为 系统+IPC+D-Bus 三路)、#9 下拉手势 vs 左滑返回区分、#10 通知存储上限/FIFO、#11 媒体活跃会话判定、#12 多 app 音频混音/焦点、#13 web 实时画面 JPEG/WS、#14 远程输入注入仲裁、#16 主页左滑=侧边栏 vs 返回 `[OPEN_QUESTIONS]`
- DEVELOPMENT_PLAN:阶段 0(bootmenu 双系统)**待实现**(M0-M4);阶段 B/E/F;M2-M6 阶段 D 里程碑未做 `[DEVELOPMENT_PLAN]`
- 未定/待拍板:`meow_config_*`/`meow_media_*`/崩溃捕获(见 §4);MeowOS 音频输出设置(只读还是可配);OTA 依赖 MeowPanel `[WORK_STATE][OPEN_QUESTIONS #17/#18]`
- **meow_svcd 后续(M2.54 已落地大部分)**:~~接入 supervisor 启动链~~(已改 `etc/init.d/S89meow_svcd` 开机自启;bootmenu/launcher `fork+exec`+`waitpid` 仍为备选);~~写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS~~(已改**板载** `/usr/data/meow/services.d/mdnsd.conf` 预置,`trigger=boot`)。剩余:`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);`wifi-up` 判定收紧。**services.d/svcd 已接入开机链(S89)**,但真机行为待验证。 `[WORK_STATE][meow_svcd.md]`
- OPEN_QUESTIONS.md 待定 ❓:多数已由后续定案/落地覆盖——#1 背景图加载(「启动加载一次+预渲染快照不每帧缩放」已落地)、#2 图处理(**cover 居中裁剪**)、#3 叠加层颜色(**固定跟明暗主题**)、#4 背景色板枚举(**已清结**:MD3+MIUIx 色板 M2.90 落地,`display.background` 按 style×mode 派生)、#7 应用图标(**已做,app.json icon 加载缓存+首字母占位**)、#8 通知来源(**系统事件 + MeowAPP IPC + D-Bus 桥 三路**,中心 UI 已落地)、#9 下拉手势(**顶部下滑=通知中心**,与左滑返回按起始位置分流)、#10 通知存储(**TF 归档 CSV + 10MB 滚动 + 环形 200 条**)、#11 媒体活跃会话(**active_seq 最近活跃源,30s 超时回收**)、#12 多 app 音频(**已被 meow_audiod 取代(M2.90):守护独占 hw:0,0 + 抢占,App 经 SDK meow_audio_*,软混音否决**)、#13 web 实时画面(**`/api/screen.jpeg` + 增量帧**)、#14 输入注入(**本机触摸优先 5s 仲裁**)、#16 主页左滑=侧边栏 vs 返回(**Now Playing 横划页,左缘返回保留**)、#17 音频输出设置(**底层已由 audiod 收口:路由自动+CLI;设置页 UI 项留后续**)。**仍待定**:#5 浅色文字对比度(待实机观感验收)+ 2026-09-01 新增批(~~PCM 内核挂死~~**已根治**:M2.91 FPGA 开机武装 + M2.94 几何收窄/~~WiFi 录入 UI~~**已落地(M6.1 286:设置页 wifi_page/bt_page 迁 netmgr IPC + 扫 SSID→IME 输密码→connect)**/~~amsd DECLARE 残余(`am start` ACK 后 launcher 未真拉起 App)~~**已修(M6.0 274:`meow_ams_start.c` 忙/非法分支返回正错误码致 ACK 假 OK,改 pending-start 2s 重扫)**/流媒体客户端出声终验(链路技术指标已全绿:FPGA 武装+几何+监控 hwrate 健康,待用户耳机终验)/流媒体客户端 UI 专项/感知音量曲线 dB taper) `[OPEN_QUESTIONS]`
- DEVELOPMENT_PLAN:**阶段 0(bootmenu 双系统)已落地(现网即运行中)**;阶段 B/E/F 达成;阶段 D M0-M5 已落地(**M4 后台服务保活+后台播放 2026-09-01 落地**:MSG_REGISTER/SERVICE_STOP/MSG_SYS + 退避熔断 + nav-exit 后台保活,见 FRAMEWORK_DESIGN §9)、**M6 加固 待排期** `[DEVELOPMENT_PLAN]`
- 未定/待拍板:MeowOS 音频输出设置(只读还是可配,OPEN_QUESTIONS #17);OTA 在线「检查更新/下载」依赖 MeowPanel(本地/检查升级已可用,URL 下载待定案 #18)`[WORK_STATE][OPEN_QUESTIONS #17/#18]`
- **meow_svcd 后续(M2.54 已落地大部分)**:~~接入 supervisor 启动链~~(已改 `etc/init.d/S89meow_svcd` 开机自启;bootmenu/launcher `fork+exec`+`waitpid` 仍为备选);~~写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS~~(已改**板载** `/usr/data/meow/services.d/meow_mdns.conf` 预置,`trigger=boot`;M6.1 起服务名统一为 `meow_mdns`)。剩余:`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);`wifi-up` 判定收紧。**services.d/svcd 已接入开机链(S89)**,但真机行为待验证。 `[WORK_STATE][meow_svcd.md]`
---
@@ -1 +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}
{"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,"depends":"curl:optional"}
+102
View File
@@ -0,0 +1,102 @@
#!/bin/sh
#
# S11jpeg_display_shell — 开机 logo 显示(stock 基线 + MeowOS M21 改动)
#
# stock 基线(md5 249220f2489f63a4c946c55d8d981ca9)的问题:
# `cmd_jpeg_display` 画完 logo **之后**才写死 60。画图那一刻用的是驱动默认亮度,而真机
# /module_driver/pwm_backlight.sh 是 `insmod pwm_backlight.ko ... default_brightness=0
# max_brightness=101`,pwm_backlight.ko 的 init 会把 props.brightness 置 0 再 update_status
# (走 PWM 0 分支)—— 即上电是**全黑**:logo 先画在灭屏上,0.1s 后才被写死的 60 照亮,
# 一出现就是 60(≈60% 亮度,用户报「冷启动开机 logo 挺亮」)。
# 本脚本又跑在 S21mount_ubifs 之前:那时 /usr/data 还没挂载,读不到
# /usr/data/meow/settings.conf,所以这里**只能**写一个和用户设置无关的安全低值。
#
# M21 改动:
# 1) 在画 logo **之前**写 BOOT_BL_MAX=20(≈max_brightness 101 的 1/5):logo 以「暗但可见」出现;
# 2) 删除画完后写死 60 的那行 —— 用户亮度改由 S22brightness(/usr/data 挂载后)就位;
# 3) 写入「只降不升」:当前值落在 **1..20**(有效且已更暗,例如 S22 已按用户设置写成 1)才不动;
# **0 / 空 / 非数字一律照写 20** —— 驱动默认就是 0(等于灭屏),把 0 当「已有值」跳过会让本
# 脚本一个值都写不进去(评审 P1:画图时刻亮度 0,且删掉 60 后整段 init 没有可见 logo)。
# 4) /dev/fb0 等待、mtd5 主题判定、后台 `jpeg_display &` 语义与 stock 一致。
#
# 时序(三层写入者 + 运行期):
# 驱动默认 0(灭)→ S11 写 20(画 logo 前,logo 暗而可见)→ S21mount_ubifs
# → S22brightness(用户值,如 1)→ S70/S90 launcher/watchdog 继续按 display.brightness 维持。
#
# 注:rcS 用 `$i start` fork 执行本脚本,`jpeg_display &` 后父进程立即返回。所以写入必须在
# jpeg_display() **函数体内**(子进程自己的时序),不能挪到 case 分支顶层 —— 那是父进程,
# 会和子进程抢时序。
# ⚠️ 也不能放在函数入口:背光模块由排在后面的 S11module_driver_default(rcS 自然序
# S11jpeg_display_shell < S11module_driver_default)才 insmod,入口那一刻
# /sys/class/backlight/backlight_pwm0/ 还不存在,写入必然 no-op(重定向失败还会漏一行错)。
# 能写的时机是 /dev/fb0 出现之后 —— 同一份驱动脚本里 pwm_backlight.ko 先于 soc_fb.ko 加载,
# fb0 一出现背光节点必定已就绪。
BL_BRIGHTNESS="/sys/class/backlight/backlight_pwm0/brightness"
# 画 logo 前的亮度上限(绝对档位 0..max_brightness,不是百分比)。
# 这是「暗但可见」的安全低值,不是用户值;用户值(display.brightness,可能更暗如 1)由 S22brightness 写入。
BOOT_BL_MAX=20
# 只降不升地把背光压到 <= $1:
# 当前读数在 1..$1 = 已有有效且更暗的值(如 S22 已写用户值 1)→ 不动;
# 0 / 空 / 非数字(驱动默认 0 = 灭屏、节点未就绪)→ 视为「没有有效值」,照写。
bl_cap()
{
cap="$1"
cur=$(cat "$BL_BRIGHTNESS" 2>/dev/null)
case "$cur" in
''|*[!0-9]*) ;; # 读不到 / 非数字 → 照写
*) [ "$cur" -ge 1 ] && [ "$cur" -le "$cap" ] && return 0 ;; # 已有 1..cap 的有效值 → 不动
esac
# 2>/dev/null 放在 > 之前:节点尚未就绪时连「重定向失败」的报错一起吞掉(否则每个冷启动漏一行)。
if echo "$cap" 2>/dev/null > "$BL_BRIGHTNESS"; then
echo "backlight: -> $cap (was ${cur:-unreadable})"
fi
return 0
}
jpeg_display()
{
echo "Starting display... "
while true
do
if [ -e "/dev/fb0" ]; then
break
fi
sleep 0.02
done
echo "Wait display... "
# M21:画 logo 前写一次安全低亮度。此时背光节点必定已就绪(同一驱动脚本里
# pwm_backlight.ko 先于 soc_fb.ko 加载,fb0 出现即背光已注册)。只降不升:
# cur 落在 1..20 的有效值就不动;cur=0(驱动默认 = 灭屏)照写 20;
# S22brightness 若已写更暗的用户值也不会被顶回 20。
bl_cap "$BOOT_BL_MAX"
theme=$(nanddump -q -s 0x20000 -l 7 /dev/mtd5 -a)
# stock 写的是 `==`(BusyBox test 接受,dash/POSIX test 不接受);语义不变,改为 `=` 以便移植与宿主验证。
if [ "$theme" = "theme:1" ]; then
cmd_jpeg_display /etc/logo1.jpeg
elif [ "$theme" = "theme:2" ]; then
cmd_jpeg_display /etc/logo2.jpeg
else
cmd_jpeg_display /etc/logo.jpeg
fi
# stock: "R1 lg 35583 need 100ms delay" —— 原先是给「画完后写 60」留的面板 settle 时间。
# 亮度写入已移到画图之前,但该硬件 settle 保留(在后台子进程里,不阻塞 init 链)。
sleep 0.1
echo "Finish display... "
}
case "$1" in
start)
jpeg_display &
;;
*)
exit 1
esac
exit 0
+124
View File
@@ -0,0 +1,124 @@
#!/bin/sh
#
# S22brightness — 开机按用户设置恢复背光(MeowOS M21)
#
# 位置/顺序(/etc/init.d 按文件名自然序执行,见 /etc/init.d/rcS):
# S11jpeg_display_shell 画开机 logo,画前把背光写成 20(驱动默认 0 = 灭屏 → logo 暗而可见)
# → S21mount_ubifs 挂载 /usr/data
# → S22brightness 本脚本:此时才读得到 /usr/data/meow/settings.conf
# → S30dbus … → S70meow_sysui / S90meow_bootmenu launcher/watchdog 接管后继续维持用户亮度
#
# 为什么不能只改 S11:S11 早于 S21,/usr/data 未挂载 → settings.conf 不存在,只能写死一个安全低值
# (且真机 S11 跑在背光模块加载之前,写入时机受 fb0 约束)。
#
# 行为:
# - 读 display.brightness,以 display.brightness_min(下限,缺省 1)和
# /sys/class/backlight/backlight_pwm0/max_brightness(上限,本机实测 101)clamp 后写入。
# - 单位是 0..max_brightness 的**绝对档位**,不是百分比;display.brightness_min 同单位。
# - 读不到文件 / 解析不出 display.brightness / max_brightness 读不到 → **不改背光**
# (S11 未必写过值,真机驱动默认 0 = 灭屏,所以这里只报当前读数、不做假设),
# 打一行日志后 exit 0。
# - 幂等(当前值已是目标值则不写)、无 sleep / 无重试,任何运行期失败都 exit 0(不阻断 init 链)。
BL_BRIGHTNESS="/sys/class/backlight/backlight_pwm0/brightness"
BL_MAX="/sys/class/backlight/backlight_pwm0/max_brightness"
MEOW_SETTINGS="/usr/data/meow/settings.conf"
log()
{
echo "S22brightness: $*"
}
# 当前背光读数(仅用于日志;读不到返回 unreadable)。
cur_bl()
{
v=$(cat "$BL_BRIGHTNESS" 2>/dev/null)
case "$v" in
''|*[!0-9]*) printf 'unreadable' ;;
*) printf '%s' "$v" ;;
esac
}
start()
{
# /usr/data 刚挂载时 settings.conf 可能尚不存在(新机/首次启动)或被正在写:
# 只读一次,读不到就不改背光,不 sleep 重试(不阻塞 init 链)。
if [ ! -r "$MEOW_SETTINGS" ]; then
log "settings.conf 不可读($MEOW_SETTINGS),不改背光(当前 brightness=$(cur_bl))"
return 0
fi
max=$(cat "$BL_MAX" 2>/dev/null)
case "$max" in
''|*[!0-9]*)
log "max_brightness 读不到,不改背光(当前 brightness=$(cur_bl))"
return 0
;;
esac
if [ "$max" -le 0 ]; then
log "max_brightness=$max 非法,不改背光(当前 brightness=$(cur_bl))"
return 0
fi
# 解析 settings.conf:<key> = <value>,容忍两侧空白/CR,忽略注释与非数字值。
b=""
mn=""
while IFS='=' read -r k v; do
k=$(printf '%s' "$k" | tr -d '[:space:]')
v=$(printf '%s' "$v" | tr -d '[:space:]')
case "$k" in
display.brightness)
case "$v" in
''|*[!0-9]*) ;;
*) b="$v" ;;
esac
;;
display.brightness_min)
case "$v" in
''|*[!0-9]*) ;;
*) mn="$v" ;;
esac
;;
esac
done < "$MEOW_SETTINGS"
if [ -z "$b" ]; then
log "settings.conf 无有效 display.brightness,不改背光(当前 brightness=$(cur_bl))"
return 0
fi
[ -n "$mn" ] || mn=1
[ "$mn" -lt 1 ] && mn=1
[ "$mn" -gt "$max" ] && mn="$max"
[ "$b" -lt "$mn" ] && b="$mn"
[ "$b" -gt "$max" ] && b="$max"
cur=$(cat "$BL_BRIGHTNESS" 2>/dev/null)
if [ "$cur" = "$b" ]; then
log "背光已是 $b,跳过写入"
return 0
fi
# 2>/dev/null 放在 > 之前,避免节点不可写时重定向报错漏到控制台。
if echo "$b" 2>/dev/null > "$BL_BRIGHTNESS"; then
log "开机背光 -> $b(min=$mn max=$max)"
else
log "写背光 $b 失败,保持当前值(brightness=$(cur_bl))"
fi
return 0
}
case "$1" in
start)
start
;;
stop|restart|reload)
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
exit 0
+39
View File
@@ -104,6 +104,7 @@
#define PLAYER_BIN "/usr/bin/hiby_player"
#define PLAYER_LOG "/usr/data/meow_player.log"
#define LAUNCHER_BIN "/usr/bin/meow_launcherd" /* MeowOS 基础主屏(M2 接管) */
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 板载用户设置(display.brightness,与 launcher/watchdog 同源) */
/* ---- 面板/背光(与 meow_launcherd 相同路径,§18.21/§红线)---- */
#ifndef FBIOBLANK
@@ -844,6 +845,37 @@ static void write_brightness(int val)
close(fd);
}
/* M6.25:开机选单亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness)。
* 根因:选单显示路径从不写背光,开机菜单/第二屏亮度纯继承引导早期面板高值;M2.83 起 launcher
* 「开机选单期不抢屏」怠速,移除了 launcher 提前写用户亮度那一步 → 菜单亮在引导高值上。
* 与 meow_watchdog wd_settings_brightness() 同源解析(同文件、同 clamp:下限 display.brightness_min
* 默认 1=极暗但可见、上限 100=防闪屏),自包含零依赖,不链 launcher meow_settings。返回 clamped
* 值;settings 文件不可读 → 返回 -1,调用方**不动**(退化为现状,保安全——选单阶段只写 brightness、
* 绝不碰 FBIOBLANK/blank,§18.24 纪律)。 */
static int bootmenu_settings_brightness(void)
{
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
if (!f) return -1;
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
char buf[128];
while (fgets(buf, sizeof buf, f)) {
char *eq = strchr(buf, '=');
if (!eq) continue;
*eq = 0;
char *k = buf; int kl = (int)strlen(k);
while (kl > 0 && (k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0;
char *v = eq + 1; while (*v == ' ' || *v == '\t') v++;
int iv = atoi(v);
if (!strcmp(k, "display.brightness_min")) { if (iv >= 1 && iv <= 100) mn = iv; }
else if (!strcmp(k, "display.brightness")) { b = iv; }
}
fclose(f);
if (b < mn) b = mn;
if (b > 100) b = 100;
return b;
}
/* 面板 unblank(FBIOBLANK 0)。仅用于杀播放器后把屏点亮,以便 MeowOS 接管;
* 播放器已死,故不涉及「播放器 state machine」一致性(§18.11 关注点)。
* bootmenu 选单/自检阶段绝不调用本函数(§18.24 纪律:不碰 panel blank)。 */
@@ -1704,6 +1736,13 @@ int main(int argc, char **argv)
return 0;
}
/* M6.25(方案A):选单显示前把背光按用户设置恢复(display.brightness,clamp [min||1,100])。
* settings 不可读/值非法 → 不动(退化为现状)。只在显示前写一次,不碰 FBIOBLANK/blank(§18.24)。 */
{
int b = bootmenu_settings_brightness();
if (b > 0) write_brightness(b);
}
if (fb_open() != 0) return 1;
fb_snapshot_player(); /* 快照当前显示缓冲(恢复播放器帧用) */
fb_pan_buf0(); /* 画前 pan 到 yoffset=0(buffer0) */
+15 -2
View File
@@ -22,7 +22,8 @@ patch_v2/files_app/
│ ├── fsx.h # 纯业务接口(不依赖 meow_ui)
│ └── fsx.c # 路径安全/格式化/枚举排序/虚拟化区间/写操作(可宿主测)
└── test/
└── test_fsx.c # 宿主单元自测(gcc 直接编,指向临时卡根)
├── test_fsx.c # 宿主单元自测(gcc 直接编,指向临时卡根)
└── test_row_text_fit.c # 宿主几何断言:列表行高 ≥ 双行文本块(M17)
```
## 构建
@@ -55,12 +56,20 @@ v1 无需持久化,暂未使用(启动恒进卡根 `/usr/data/mnt/sd_0/`)
cd patch_v2/files_app
gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
test/test_fsx.c logic/fsx.c -o /tmp/test_fsx && /tmp/test_fsx
# 列表行高几何(无需 meow_ui.a;Termux 下产物落 $TMPDIR)
gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c \
-o "$TMPDIR/test_row_text_fit" && "$TMPDIR/test_row_text_fit"
```
覆盖:路径安全(`within_sd`/`join`/`sanitize`/`dirname`/`basename`)、展示格式化(`fmt_size`/`fmt_time`)、
目录枚举排序(`scan_dir`/`sort`)、虚拟化区间(`visible_range`/`max_scroll`)、写操作(`mkdir`/`rename`/
`delete`,含卡外/越界拒绝)、非文本写拒绝。宿主全 PASS。
`test_row_text_fit` 是 M17 行高回归:目录列表行恒为双行档位,行高必须 ≥ 文本块
`2*meow_ui_text_height(font_body) + 2`(真机 20px 字库行高 30 → 62),否则 SDK 居中后首行标题
`ty < 视口顶` 被裁剪守卫整条跳过。断言矩阵覆盖真机五种字库行高 + 旧固定 56 的 bug 形态。
## 交互
- 触摸:行 body=进目录/开文本预览;右侧 `⋯` 或长按→BottomSheet(删除[确认弹窗]/重命名[IME]/新建目录[IME]/属性[大小·mtime]);
@@ -78,6 +87,10 @@ gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
位移 ≤ 容忍」→ 经 `meow_ui_set_long_press_cb` 注册的回调带坐标交给本页;`browser_on_long_press`
用 `meow_listview_hit` 行命中打开 BottomSheet。App 层长按计时(`browser_tick`/`g_press_ms`)已删除,
其抬手 TAP 由 SDK 抑制(长按不再触发点击)。
- **行高按字体行高派生(M17)**:`files_row_h()` = `max(APP_ROW_H, 2*行高 + 2)`(当前主题正文
`font_body` 的 `meow_ui_text_height`)。目录行是双行档位,行高小于双行文本块时 SDK 居中后首行
标题落在视口顶之上被裁剪(20px 字库行高 30 → 块 62,旧的显式 56 首行名称空白)。滚动/命中/
最大滚动量由 SDK 按同一行高换算,页脚几何独立(`r_list` 单独扣 `APP_FOOTER_H`)。
2. **中文字形**:SDK 默认 `common` 字库(≈4k)不含 CJK 全量;exFAT 中文文件名/中文文本预览在 common 档会显示缺字豆腐。
预览页不声明 `fonts`(app.json 未加 `"fonts":"full"`/`"gbk"`),如需中文预览可在 app.json 声明大字号档。
3. **预览仅文本**:按扩展名白名单 + 首 512 字节 NUL 嗅探判文本;二进制/图片无预览(提示「not a text file」)。
@@ -88,7 +101,7 @@ gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
## 验证状态
- 编译:`-Wall -Wextra` 0 warning/0 error,产物 `files`(MIPS32r2 静态 ELF)。
- 宿主:`test/test_fsx.c` 全 PASS。
- 宿主:`test/test_fsx.c` 全 PASS;`test/test_row_text_fit.c` 32 断言全 PASS(M17 行高几何)。
- qemu-mipsel(无 /dev/fb0 → SDK 离屏 480×720):正常运行到事件循环,导出离屏帧,
确认 init/建页/topbar(返回+NEW)/空态(NO STORAGE CARD)渲染正确、SIGTERM 干净退出。
- 实机目录列表渲染(有真实 TF 卡目录)需刷机/设备挂载验证——宿主模拟无法造出 `/usr/data/mnt/sd_0`。
Binary file not shown.
+26 -1
View File
@@ -37,13 +37,38 @@ void picker_cancel(void); /* 取消:发 code=MEOW_ERR
#define APP_W 480
#define APP_H 720
#define APP_TOPBAR_H 56
#define APP_ROW_H 56 /* 列表行高 */
#define APP_ROW_H 56 /* 列表行高下限(旧固定值;实际按字体行高派生,见 files_row_h) */
#define APP_PAD 16
#define APP_ICON_W 26 /* 图标占宽 */
#define APP_OP_W 52 /* 右侧操作钮占宽(触达区) */
#define APP_FOOTER_H 26 /* 底部信息行高(列表/文本内容在其之上滚动) */
#define APP_READ_MAX (256 * 1024) /* 文本预览读取上限 */
/* ---- 列表行高:按字体行高派生(M17,首行标题不显示根因修复)----
* SDK meow_listview 的双行渲染把「标题+副标题」文本块
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
* 在行内垂直居中(ty = 行顶 + (item - block) / 2),标题只在 ty >= 视口顶时才画。
* item < block 时 ty = 行顶 - Δ:第一行(行顶 == 视口顶)标题被整条跳过、只剩副标题。
* 真机 source_han_serif 20px(ascent 24 + descent 6)行高 30 → block 62,旧的显式 56 → Δ=3。
* 故行高按当前主题正文行高派生:max(APP_ROW_H, 2*line_h + 2)(行高 30 → 62)。
* 本页行恒为双行档位(browser_src2 填 title + subtitle)→ 用 lines=2 公式。
* 行几何(draw / hit / max_scroll)全由 SDK 按传入的这一个值换算,故滚动与命中天然同步;
* 页脚几何与行高无关(r_list 单独扣 APP_FOOTER_H)。 */
#define FILES_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行距) */
static inline int files_row_h_for_line_h(int line_h)
{
if (line_h < 1) line_h = 1; /* 字体未就绪 → 退回下限 */
int h = 2 * line_h + FILES_ROW_SUB_GAP;
return (h > APP_ROW_H) ? h : APP_ROW_H;
}
/* 当前主题正文行高下的两行列表行高(页面 create 时取一次;字体档位切换走页面重建)。 */
static inline int files_row_h(void)
{
const meow_theme *th = meow_theme_get();
return files_row_h_for_line_h(th ? meow_ui_text_height(th->font_body) : 0);
}
/* ---- App 级复用矩形/几何 ---- */
typedef struct { int x, y, w, h; } app_rect_t;
#define AR(x,y,w,h) ((app_rect_t){ (x),(y),(w),(h) })
@@ -0,0 +1,86 @@
/* test/test_row_text_fit.c — 宿主回归(M17):目录列表行高必须容纳双行文本块。
*
* 背景(真机根因,承接 M12/M15):SDK meow_listview 的双行渲染把
* block = 2 * meow_ui_text_height(font_body) + 2 (标题行 + 2px 行距 + 副标题行)
* 在行内垂直居中:ty = 行顶 + (item - block) / 2;且标题只在 `ty >= 视口顶` 时才画(越界钳制)。
* 行高 item < block 时 ty < 行顶 → **第一行(行顶 == 视口顶)标题被整条跳过**、副标题仍在。
* 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 → 行高 30)block = 62,旧的
* 显式 set_item_h(56) → ty = 行顶 - 3(本 App 目录列表首行名称空白)。
*
* 本测试只验 app 侧行高派生(files_row_h_for_line_h,纯整数、不链 SDK):
* 行高 = max(APP_ROW_H, 2*行高 + FILES_ROW_SUB_GAP) → block ≤ item,首行标题不被钳掉。
*
* 编译/运行(宿主 gcc,只需 SDK 头,不需要 meow_ui.a):
* cd patch_v2/files_app
* gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c -o "$TMPDIR/test_row_text_fit" \
* && "$TMPDIR/test_row_text_fit"
*
* 断言矩阵 = 真机已装字库 20px 面实测行高:source_han_serif 30(当前生效)、
* misans/default 27、google_sans/meslo 26、source_han_sans 21;14 = 宿主 5x7 回退。
* ① block ≤ item(否则居中偏移为负)
* ② 首行 ty ≥ 0 → 绝对 ty_abs ≥ 视口顶(标题不被裁剪守卫跳过)
* ③ 副标题底边 ty + block ≤ item(两行都不越行)
* ④ item ≥ APP_ROW_H 下限(小字库/字体未就绪不缩回固定小值)
* ⑤ 旧固定 56 在行高 30 下 ty < 0(bug 形态复现,防止再写死 56)
*/
#include "../files_internal.h"
#include <stdio.h>
static int g_checks, g_fail;
static void chk(int cond, int line_h, const char *what)
{
g_checks++;
if (!cond) {
g_fail++;
printf(" FAIL lh=%d: %s\n", line_h, what);
}
}
int main(void)
{
static const int lh_all[] = { 14, 21, 26, 27, 30 };
const int n = (int)(sizeof lh_all / sizeof lh_all[0]);
const int list_y = 120; /* 任意列表顶(只验相对几何) */
for (int k = 0; k < n; k++) {
int lh = lh_all[k];
int item = files_row_h_for_line_h(lh);
int block = 2 * lh + FILES_ROW_SUB_GAP;
int ty = (item - block) / 2; /* SDK 双行文本块居中偏移 */
printf("lh=%2d item=%3d block=%3d ty=%d\n", lh, item, block, ty);
chk(block <= item, lh, "文本块必须 <= 行高(否则首行标题被视口顶裁剪)");
chk(ty >= 0, lh, "首行标题 y 不得高于行顶/视口顶");
chk(ty + block <= item, lh, "副标题底边仍在行内");
chk(item >= APP_ROW_H, lh, "行高不得小于 APP_ROW_H 下限");
/* 首行(scroll=0 → 行顶 == 视口顶)的绝对坐标 */
chk(list_y + ty >= list_y, lh, "首行标题绝对 y 不得被视口顶裁掉");
}
/* 行高随字体行高单调不减 */
chk(files_row_h_for_line_h(30) >= files_row_h_for_line_h(26), 30, "行高随字体行高单调不减");
/* 真机当前生效字库(source_han_serif 20px,行高 30):block 62 → item 62 */
chk(files_row_h_for_line_h(30) == 62, 30, "行高 30 → 派生行高 62");
chk(2 * 30 + FILES_ROW_SUB_GAP <= files_row_h_for_line_h(30), 30, "派生行高容纳 62px 文本块");
/* 下限:小字库 / 字体未就绪(line_h=0)仍不缩回固定小值 */
chk(files_row_h_for_line_h(14) == APP_ROW_H, 14, "小字库退回 APP_ROW_H 下限");
chk(files_row_h_for_line_h(0) == APP_ROW_H, 0, "字体未就绪(行高 0)退回下限");
/* 回归哨兵:旧固定行高 56 + 当前默认字体(行高 30)就是 bug 形态 */
{
const int lh = 30;
int block = 2 * lh + FILES_ROW_SUB_GAP; /* 62 */
int ty_legacy = (APP_ROW_H - block) / 2; /* -3 */
chk(ty_legacy < 0, lh, "旧行高 56 + 行高 30 → ty<0(首行标题被跳过,bug 复现)");
chk(files_row_h_for_line_h(lh) > APP_ROW_H, lh, "默认字体下行高必须大于旧值 56");
}
printf("%s: %d checks, %d fail (font line-height matrix: %d)\n",
g_fail ? "FAIL" : "PASS", g_checks, g_fail, n);
return g_fail ? 1 : 0;
}
+1 -1
View File
@@ -421,7 +421,7 @@ static void browser_page_create(meow_view *v, void *ctx)
/* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
browser_src2, NULL, MEOW_LIST_SEL_SINGLE);
meow_listview_set_item_h(g_list, APP_ROW_H);
meow_listview_set_item_h(g_list, files_row_h()); /* M17:按字体行高派生,容纳双行文本块 */
/* 空态/页脚注册为非控件绘制物(列表视口不含页脚;invalidate/paint_full 自动重画) */
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
browser_draw_empty, NULL);
+29 -7
View File
@@ -39,7 +39,27 @@ static int viewer_vp_h(void) { return r_content.h - APP_FOOTER_H; }
/* 屏幕行高(px)。 */
static int viewer_rowh(const meow_theme *th) { return meow_ui_text_height(th->font_body) + 6; }
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。 */
/* UTF-8 码位字节长(1..4):s[bi] 起、串内有效字节数 lim。
* 首字节非法/续字节非法/序列不完整 → 保守按 1(单字节);保证折行绝不断在多字节码位中间,
* 也保证宽测量(meow_ui_text_width 按码位测)对中文/混合文本正确。 */
static int viewer_cp_len(const unsigned char *s, int bi, int lim)
{
unsigned char c = s[bi];
int n;
if (c < 0x80) return 1;
else if ((c & 0xE0) == 0xC0) n = 2;
else if ((c & 0xF0) == 0xE0) n = 3;
else if ((c & 0xF8) == 0xF0) n = 4;
else return 1;
if (lim - bi < n) return 1;
for (int i = 1; i < n; i++)
if ((s[bi + i] & 0xC0) != 0x80) return 1;
return n;
}
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。
* 按「UTF-8 码位」逐字累加显示宽度(meow_ui_text_width 按码位测>0),到内容宽 colw 即断行;
* 多字节(CJK)整字不断开。 */
static int viewer_wrap_line(int li, int intra, char *out, int outsz)
{
if (!g_buf || li < 0 || li >= g_nline) return 0;
@@ -49,13 +69,15 @@ static int viewer_wrap_line(int li, int intra, char *out, int outsz)
while (len > 0 && (g_buf[start + len - 1] == '\n' || g_buf[start + len - 1] == '\r')) len--;
int colw = viewer_colw();
int col = 0, row = 0, outlen = 0;
char ch[2]; ch[1] = '\0';
for (int i = 0; i < len; i++) {
ch[0] = g_buf[start + i];
if (ch[0] == '\r') continue;
int w = meow_ui_text_width(ch, 20);
const unsigned char *s = (const unsigned char *)g_buf + start;
for (int bi = 0; bi < len; ) {
int clen = viewer_cp_len(s, bi, len);
if (clen == 1 && s[bi] == '\r') { bi++; continue; }
char cp[5]; cp[clen] = '\0';
memcpy(cp, s + bi, (size_t)clen); bi += clen;
int w = meow_ui_text_width(cp, 20);
if (col + w > colw && col > 0) { row++; col = 0; }
if (row == intra && out && outlen < outsz - 1) out[outlen++] = ch[0];
if (row == intra && out && outlen + clen < outsz) { memcpy(out + outlen, cp, (size_t)clen); outlen += clen; }
col += w;
}
if (out) out[outlen] = '\0';
+2 -1
View File
@@ -12,7 +12,8 @@ ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
"$CC" -Os -static -Wall -Wextra -I../sdk -I. \
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c ui/common/chrome.c \
app.c ui/home/home.c ui/full.c ui/demo/demo.c ui/demo/list/list.c \
ui/demo/sdk2/sdk2.c ui/common/chrome.c \
logic/notify.c logic/state.c \
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread -o hello
ls -la hello
Binary file not shown.
+40
View File
@@ -33,6 +33,31 @@ extern meow_sheet *g_sheet; /* BottomSheet(阻塞模态;main 创建
typedef struct { int x, y, w, h; } rect_t;
#define RX(x,y,w,h) ((rect_t){ (x), (y), (w), (h) })
/* ---- 两行列表行高:按字体行高派生(M17,首行标题不显示根因修复)----
* SDK meow_listview 的双行渲染把「标题+副标题」文本块
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
* 在行内垂直居中(ty = 行顶 + (item - block) / 2),标题只在 ty >= 视口顶时才画。
* item < block 时 ty = 行顶 - Δ:第一行(行顶 == 视口顶)标题被整条跳过、只剩副标题。
* 真机 source_han_serif 20px(ascent 24 + descent 6)行高 30 → block 62;demo 的 swipe 列表
* 是双行档位(set_item_renderer(2,...)),旧的显式 48 比 block 小 14 → 首行标题必丢。
* 故行高按当前主题正文行高派生:max(HELLO_ROW_H_MIN, 2*line_h + 2)(行高 30 → 62)。
* 行几何(draw / hit / max_scroll)全由 SDK 按传入的这一个值换算,滚动与命中天然同步。 */
#define HELLO_ROW_H_MIN 56
#define HELLO_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行距) */
static inline int hello_row_h_for_line_h(int line_h)
{
if (line_h < 1) line_h = 1; /* 字体未就绪 → 退回下限 */
int h = 2 * line_h + HELLO_ROW_SUB_GAP;
return (h > HELLO_ROW_H_MIN) ? h : HELLO_ROW_H_MIN;
}
/* 当前主题正文行高下的两行列表行高(建列表时取一次;字体档位切换走内容重建)。 */
static inline int hello_row_h(void)
{
const meow_theme *th = meow_theme_get();
return hello_row_h_for_line_h(th ? meow_ui_text_height(th->font_body) : 0);
}
/* ---- 视图控制器(ctx = biz-UI 绑定点)----
* 本批控制器为薄壳:持 meow_view 回指针;页面细粒度视图态(g_ds/g_drag_* /scroll 等)留在各页
* 模块作用域全局(设计定案 B:demo 视图态不迁 logic)。控制器未来可吸收这些状态。 */
@@ -106,6 +131,21 @@ int list_view_down(int x, int y); /* touch_down 命中列表:置
int list_view_move(int y); /* touch_move 拖动列表:1=在列表拖动 */
int list_view_swipe(int del); /* swipe:列表聚焦时滚列表,1=已消费 */
/* ---- SDK 批次2 控件演示区 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)----
* 复刻 list.c 模式:demo 页内容区底部追加三节——枚举选择器 meow_select / 2D 虚拟化宫格
* meow_lazygrid / 列表行左滑删除 meow_listview_set_swipe_delete。子视图自持三节控件与视图态
* (grid 非 meow_widget:on_draw 直绘 meow_lazygrid_draw;swipe-list 行左滑 3 相位手势),
* 对外暴露 sdk2_build_at(demo_build_at 调)与 hit/down/move/swipe/release(demo 分发改派)。 */
meow_view *sdk2_view_create(void); /* 建 CONTAINER 子视图(须即 meow_view_add_child) */
meow_view *sdk2_view_get(void); /* 返回子视图(宿主测嵌套生命周期断言) */
int sdk2_build_at(int yb_base, int ct, int scroll); /* 在 base yb 处建三节控件(owner=ACT_DEMO),返回下一 yb */
void sdk2_release(void); /* 内容释放后:释放 grid/摘控件引用/清几何 */
int sdk2_hit(int x, int y); /* tap 分发:select 按钮/宫格 tap/列表行 tap/行左滑松手 swipe_end */
int sdk2_down(int x, int y); /* touch_down:宫格拖拽或列表行左滑 begin;1=本区消费 */
int sdk2_move(int x, int y); /* touch_move:宫格滚动/列表左滑 move 或列表竖向滚动;1=消费 */
int sdk2_swipe(int del); /* swipe UP/DOWN:宫格或列表聚焦时滚自身;1=已消费 */
int sdk2_dragging(void); /* 1=本子视图拖拽在途(demo_swipe 早退防 on_swipe 二次滚动) */
/* ---- chrome(ui/common/chrome.c)---- */
extern meow_widget *g_tb; /* TopAppBar(chrome 固定) */
extern meow_widget *g_nav; /* NavigationBar(chrome 固定) */
@@ -0,0 +1,87 @@
/* test/test_row_text_fit.c — 宿主回归(M17):demo 的 swipe 列表行高必须容纳双行文本块。
*
* 背景(真机根因,承接 M12/M15):SDK meow_listview 的双行渲染把
* block = 2 * meow_ui_text_height(font_body) + 2 (标题行 + 2px 行距 + 副标题行)
* 在行内垂直居中:ty = 行顶 + (item - block) / 2;且标题只在 `ty >= 视口顶` 时才画(越界钳制)。
* 行高 item < block 时 ty < 行顶 → **第一行(行顶 == 视口顶)标题被整条跳过**、副标题仍在。
* 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 → 行高 30)block = 62;
* demo 的 swipe 列表是双行档位(set_item_renderer(2,...))却显式 set_item_h(48) → ty = 行顶 - 7,
* 「notif 0」首行标题在真机上必然空白。
*
* 本测试只验 app 侧行高派生(hello_row_h_for_line_h,纯整数、不链 SDK):
* 行高 = max(HELLO_ROW_H_MIN, 2*行高 + HELLO_ROW_SUB_GAP) → block ≤ item,首行标题不被钳掉。
*
* 编译/运行(宿主 gcc,只需 SDK 头,不需要 meow_ui.a):
* cd patch_v2/hello_app
* gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c -o "$TMPDIR/test_row_text_fit" \
* && "$TMPDIR/test_row_text_fit"
*
* 断言矩阵 = 真机已装字库 20px 面实测行高:source_han_serif 30(当前生效)、
* misans/default 27、google_sans/meslo 26、source_han_sans 21;14 = 宿主 5x7 回退。
* ① block ≤ item(否则居中偏移为负)
* ② 首行 ty ≥ 0 → 绝对 ty_abs ≥ 视口顶(标题不被裁剪守卫跳过)
* ③ 副标题底边 ty + block ≤ item(两行都不越行)
* ④ item ≥ HELLO_ROW_H_MIN 下限(小字库/字体未就绪不缩回固定小值)
* ⑤ 旧固定 48 在行高 30 下 ty < 0(bug 形态复现,防止再写死 48)
*/
#include "../hello_internal.h"
#include <stdio.h>
static int g_checks, g_fail;
static void chk(int cond, int line_h, const char *what)
{
g_checks++;
if (!cond) {
g_fail++;
printf(" FAIL lh=%d: %s\n", line_h, what);
}
}
int main(void)
{
static const int lh_all[] = { 14, 21, 26, 27, 30 };
const int n = (int)(sizeof lh_all / sizeof lh_all[0]);
const int list_y = 400; /* 任意列表顶(只验相对几何) */
for (int k = 0; k < n; k++) {
int lh = lh_all[k];
int item = hello_row_h_for_line_h(lh);
int block = 2 * lh + HELLO_ROW_SUB_GAP;
int ty = (item - block) / 2; /* SDK 双行文本块居中偏移 */
printf("lh=%2d item=%3d block=%3d ty=%d\n", lh, item, block, ty);
chk(block <= item, lh, "文本块必须 <= 行高(否则首行标题被视口顶裁剪)");
chk(ty >= 0, lh, "首行标题 y 不得高于行顶/视口顶");
chk(ty + block <= item, lh, "副标题底边仍在行内");
chk(item >= HELLO_ROW_H_MIN, lh, "行高不得小于 HELLO_ROW_H_MIN 下限");
/* 首行(scroll=0 → 行顶 == 视口顶)的绝对坐标 */
chk(list_y + ty >= list_y, lh, "首行标题绝对 y 不得被视口顶裁掉");
}
/* 行高随字体行高单调不减 */
chk(hello_row_h_for_line_h(30) >= hello_row_h_for_line_h(26), 30, "行高随字体行高单调不减");
/* 真机当前生效字库(source_han_serif 20px,行高 30):block 62 → item 62 */
chk(hello_row_h_for_line_h(30) == 62, 30, "行高 30 → 派生行高 62");
chk(2 * 30 + HELLO_ROW_SUB_GAP <= hello_row_h_for_line_h(30), 30, "派生行高容纳 62px 文本块");
/* 下限:小字库 / 字体未就绪(line_h=0)仍不缩回固定小值 */
chk(hello_row_h_for_line_h(14) == HELLO_ROW_H_MIN, 14, "小字库退回 HELLO_ROW_H_MIN 下限");
chk(hello_row_h_for_line_h(0) == HELLO_ROW_H_MIN, 0, "字体未就绪(行高 0)退回下限");
/* 回归哨兵:旧固定行高 48 + 当前默认字体(行高 30)就是 bug 形态 */
{
const int lh = 30;
int block = 2 * lh + HELLO_ROW_SUB_GAP; /* 62 */
int ty_legacy = (48 - block) / 2; /* -7 */
chk(ty_legacy < 0, lh, "旧行高 48 + 行高 30 → ty<0(首行标题被跳过,bug 复现)");
chk(hello_row_h_for_line_h(lh) > 48, lh, "默认字体下行高必须大于旧值 48");
}
printf("%s: %d checks, %d fail (font line-height matrix: %d)\n",
g_fail ? "FAIL" : "PASS", g_checks, g_fail, n);
return g_fail ? 1 : 0;
}
+66 -11
View File
@@ -43,6 +43,7 @@ static int g_drag_slider = 0; /* 手指按在 slider */
int g_drag_page = 0; /* 手指按在页内容(非 slider/list):拖拽滚页面 */
static int g_drag_page_last_y = 0; /* 页面拖拽上一帧 y */
static int g_drag_page_delta = 0; /* 页面拖拽累计位移(节流重建,≥8px 才重建一次) */
static uint64_t g_drag_page_tlast = 0; /* 页面拖拽上次重建时刻(359:每 poll 合并一次 rebuild) */
/* SDK 控件不透明:App 自记几何,用于局部 flush / 命中 / scroll 重建。 */
@@ -157,6 +158,7 @@ static void demo_free_content(void)
if (g_slider_cid > 0) { meow_draw_unregister(g_slider_cid); g_slider_cid = 0; }
g_dtitle_n = 0;
list_view_release(); /* 列表控件已在上方 g_dslot 释放;摘容器引用/几何 */
sdk2_release(); /* SDK 批次2 子视图:释放 grid/btn/list 控件引用 + 清几何 */
}
/* 按 scroll 重建全部内容控件(owner=ACT_DEMO:调用方须已 push ACT_DEMO)。
@@ -246,6 +248,15 @@ static void demo_build_at(int scroll)
d_title(DCL_X, SY(yb), "MEDIA SESSION (meta)"); yb += 26;
r_media = RX(DCL_X, SY(yb), DCW, DBH); g_dbtn_media = meow_btn_create_variant(r_media.x, r_media.y, r_media.w, r_media.h, "SEND META", MEOW_BTN_FILLED); SLOT(g_dbtn_media, r_media.x, r_media.y, r_media.w, r_media.h); yb += DCH;
/* ---- SDK 批次2 新控件(meow_select / meow_lazygrid / swipe-delete)----
* 三节由 sdk2 CONTAINER 子视图(ui/demo/sdk2/sdk2.c)承接:章节标题/宫格/select 值文本走
* 子视图 on_draw 直绘(同列表区标题);select 按钮 + swipe 列表 widget 由 sdk2 自持、此处创建。
* 控件创建后几何已内存;跨重建视图态(grid scroll/sel、swipe 行集/scroll、select 值)由
* sdk2_build_at 从子视图持有态恢复。
* 注:sdk2 置于 LazyColumn 之前(列表保持 demo 页内容**末尾**区块),确保「滚动到底列表上屏」
* 的既有宿主 P3 断言坐标假设仍然成立(sdk2 增高在列表上方,列表仍是内容底)。 */
yb = sdk2_build_at(yb, ct, scroll);
/* ---- LazyColumn(100 项列表滚动) ----
* 本区块标题改由列表区 CONTAINER 子视图(ui/demo/list/list.c)的 on_draw 绘制(注册表→
* invalidate 链路);列表滚动/选中由 list_view_bind 从子视图持有态恢复(不再经 g_ds)。 */
@@ -268,7 +279,21 @@ void demo_paint(void)
meow_ui_paint_full(); /* 整帧按 z 序重画 */
}
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用 */
/* 内容视口坐标(每次 build 同步):屏 y ∈ [g_demo_ct, g_demo_cb),x 跨 0..width。 */
static void demo_repaint_viewport(void)
{
int h = g_demo_cb - g_demo_ct;
if (h <= 0) return;
/* 滚动重绘:只清底+按 z 重画内容视口矩形 [0, ct, w, h],不整帧。
* 359 教训(滚动只刷视口 + 局部脏区不整屏重绘铁律):meow_ui_invalidate 内部已把绘制 clip
* 收敛到矩形(meow_gfx_clip_push)+ meow_ui_flush_region 只上屏该区;chrome(TopAppBar/
* NavBar/FAB,z=999,位于视口外)不触及,上一帧像素保留,滚动不再重画 chrome 字形。 */
meow_ui_invalidate(0, g_demo_ct, meow_ui_width(), h);
}
/* 重建内容到指定 scroll(clamp 到 [0, max_scroll]);进入/滚动共用。
* 滚动路径用视口局部刷(demo_repaint_viewport)替代整帧 demo_paint——每刻度重建仍发生,
* 但不再整帧重绘(359 教训:SDK 控件内建局部刷,卡顿源于调用方整页重绘)。 */
void demo_rebuild_to(int scroll)
{
int vp_h = g_demo_cb - g_demo_ct;
@@ -279,7 +304,7 @@ void demo_rebuild_to(int scroll)
demo_free_content();
demo_build_at(scroll);
g_demo_scroll = scroll;
demo_paint();
demo_repaint_viewport();
}
/* ---- demo 交互 ---- */
@@ -299,6 +324,8 @@ static int demo_content_tap(int x, int y)
/* 嵌套命中(子>父,§A.7 设计):列表区 CONTAINER 子视图先发,命中即消费。 */
if (list_view_hit(x, y)) return 1;
/* SDK 批次2 子视图(select 按钮/宫格/列表行左滑松手):命中即消费。 */
if (sdk2_hit(x, y)) return 1;
for (int i = 0; i < 5; i++)
if (meow_widget_hit(g_dbtn_b[i], x, y)) { d_toggle_sel(g_dbtn_b[i], r_btn[i]); g_ds.btn[i] = meow_widget_selected(g_dbtn_b[i]); return 1; }
@@ -344,8 +371,18 @@ static void demo_media_send(void)
}
/* demo 页 on_tap 分发(chrome → 内容) */
/* 页面拖拽抬手把节流窗内遗留的 delta 一次性重建(视口局部刷)——slow drag 结束为 TAP 时也精确落点。 */
static void demo_flush_page_delta(void)
{
if (g_drag_page && (g_drag_page_delta >= 1 || g_drag_page_delta <= -1)) {
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
g_drag_page_delta = 0;
}
}
void demo_on_tap(int x, int y)
{
demo_flush_page_delta(); /* 手势结束:TAP 抬手边沿(<SWIPE_THRESH),把遗留页面 delta 落到位 */
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return; }
@@ -389,44 +426,59 @@ static void demo_slider_drag(int x)
meow_ui_invalidate(r_slider_txt.x, r_slider_txt.y, r_slider_txt.w, r_slider_txt.h); /* 重画值文本 */
}
/* demo 页 on_touch_down:锁一种拖拽(slider > list > 页内容)。
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口)。 */
/* demo 页 on_touch_down:锁一种拖拽(slider > list > sdk2 > 页内容)。
* 列表区走 list_view_down(子>父:命中列表区由列表 CONTAINER 子视图收口);
* SDK 批次2 子视图走 sdk2_down(宫格拖拽/列表行左滑 begin,子>父收口)。 */
void demo_touch_down(int x, int y)
{
g_drag_slider = 0; g_drag_list = 0; g_drag_page = 0; g_drag_page_delta = 0;
if (!g_demo) return;
if (meow_widget_hit(g_dslider, x, y)) { g_drag_slider = 1; demo_slider_drag(x); return; }
if (list_view_down(x, y)) return; /* 列表区:置 focus+drag_list(list.c) */
if (sdk2_down(x, y)) return; /* SDK 批次2:宫格/列表行左滑 begin(sdk2.c) */
g_demo_focus = 0;
if (y >= g_demo_ct && y < g_demo_cb && x >= 0 && x < meow_ui_width()) {
g_drag_page = 1; g_drag_page_last_y = y;
}
}
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / 页面滚动节流重建)。 */
/* demo 页 on_touch_move:拖动过程实时更新(slider 值 / 列表滚动 / sdk2 滚动 / 页面滚动节流重建)。 */
void demo_touch_move(int x, int y)
{
if (!g_demo) return;
if (g_drag_slider) { demo_slider_drag(x); return; }
if (list_view_move(y)) return; /* 列表拖动(list.c) */
if (sdk2_move(x, y)) return; /* SDK 批次2:宫格滚动/列表左滑 move/列表竖向滚动(sdk2.c) */
if (g_drag_page) {
int dy = y - g_drag_page_last_y; g_drag_page_last_y = y;
g_drag_page_delta += -dy; /* 手指上移(y 减) → scroll 增(显示更低内容),与 on_swipe 一致 */
if (g_drag_page_delta >= 8 || g_drag_page_delta <= -8) {
/* 359 教训:滚动期间每个 poll 合并一次 rebuild(勿每触摸事件各自重建/上屏)。
* 累计位移超阈值(≥8) 且(距上次 ≥16ms 或 已是大步 ≥32)才重建一次;未到窗则继续
* 累积 delta(不丢),下个窗口一次性重建到精确位置。 */
uint64_t now = meow_ui_time_ms();
int big = (g_drag_page_delta >= 8 || g_drag_page_delta <= -8);
int merge = (now - g_drag_page_tlast >= 16) ||
(g_drag_page_delta >= 32) || (g_drag_page_delta <= -32);
if (big && merge) {
demo_rebuild_to(g_demo_scroll + g_drag_page_delta);
g_drag_page_tlast = now;
g_drag_page_delta = 0;
}
}
}
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表聚焦滚列表,否则页滚动。 */
/* demo 页 on_swipe:LEFT=回 home;UP/DOWN=列表/宫格/sdk2 聚焦滚自身,否则页滚动。 */
void demo_swipe(int dir, int dist)
{
if (dir == MEOW_SWIPE_LEFT) { meow_view_page_pop(); return; }
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_drag_page || g_drag_list) return; /* 本次手势已实时跟手滚动过 */
if (g_drag_page || g_drag_list || sdk2_dragging()) { /* 本次手势已实时跟手滚动过 */
demo_flush_page_delta(); /* 抬手:flush 节流窗内遗留页面 delta(视口局部刷)精确落点 */
return;
}
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; /* 手指上移 → 偏移增大 */
if (list_view_swipe(del)) return; /* 列表聚焦:滚列表(list.c,子>父消费) */
if (sdk2_swipe(del)) return; /* SDK 批次2:宫格/列表聚焦滚自身(sdk2.c,子>父消费) */
demo_rebuild_to(g_demo_scroll + del);
}
@@ -441,7 +493,8 @@ static void demo_page_enter(meow_view *v, void *ctx)
hello_nav_report(); /* ① 上报 demo 栈深/顶层 */
/* 列表区 CONTAINER 子视图随本页进入由 SDK view_enter 递归驱动(其 on_draw 在 demo_rebuild_to
* →demo_paint 中经注册表绘制;meow_draw_view_entered 含祖先已进入判定,首批可见)。 */
demo_rebuild_to(0);
demo_rebuild_to(0); /* 内容建到 scroll=0(视口局部刷) */
demo_paint(); /* 进入页整帧首绘(含 chrome 置顶 z=999);仅进入一次,非滚动路径 */
meow_log(MEOW_LOG_INFO, "demo enter scroll=0");
}
@@ -475,14 +528,16 @@ int demo_is_active(void)
}
/* demo view:on_create 建 chrome+内容(一次,owner=ACT_DEMO);进入时 rebuild 到顶。
* 视图深度化:先挂列表区 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
* 列表子视图 id 用非页面值,不占 g_view_by_id(见 list.c)。 */
* 视图深度化:先挂列表区 + SDK 批次2 两个 CONTAINER 子视图到本 ACTIVITY view 的 children,再建内容。
* 子视图 id 用非页面值,不占 g_view_by_id(见 list.c/sdk2.c)。 */
static void demo_page_create(meow_view *v, void *ctx)
{
(void)v;
((demo_ctrl *)ctx)->view = v;
meow_view *lv = list_view_create();
if (lv) meow_view_add_child(v, lv);
meow_view *s2 = sdk2_view_create();
if (s2) meow_view_add_child(v, s2);
demo_init();
}
+455
View File
@@ -0,0 +1,455 @@
/* ui/demo/sdk2/sdk2.c — 控件库 Demo 页内「SDK 批次2 新控件」CONTAINER 子视图(agent-363)。
*
* 目标(SDK_VIEW_FRAMEWORK_DESIGN.md §B.4/§E③,同 list.c):把 demo 页底部的三个 SDK 批次2
* 新控件演示区块抽成一个 meow_view CONTAINER 子视图,挂到 demo ACTIVITY view 的 children:
* ① meow_select (§5.8.9 枚举/预设选择器:下拉模态 + 单选列表 + on_change 回调)
* ② meow_lazygrid (§12.4 2 维虚拟化宫格:非 meow_widget,on_draw 直绘 meow_lazygrid_draw)
* ③ meow_listview 左滑删除(§5.5 swipe-delete:swipe_begin/move/end + on_delete + 回弹/删除动画)
*
* 职责边界(诚实,与 list.c 同折中):
* - 本子视图承接三节**整体**:章节标题 + select 按钮 + 宫格 + swipe-列表 的创建/释放与视图态
* (grid 虚拟源/选中/滚动、swipe 行集/滚动、select 选中值)。控件**创建**在 demo_build_at 链路内
* (sdk2_build_at,owner=ACT_DEMO)、**释放**在 sdk2_release(demo_free_content 调)——与 list.c
* 相同折中:完整「控件归容器拥有」需 SDK 侧支持(本批冻结 patch_v2/sdk/)。
* - meow_lazygrid 是**非 meow_widget**:不注册 z 序/绘制注册表。本子视图 on_draw 直接调
* meow_lazygrid_draw(view → 注册表 → invalidate 链路,与 list 区块标题同法)。
* - 章节标题(三段)与 select 当前值文本也走 on_draw 直绘(列表区标题同法),不逐标题注册
* CUSTOM 绘制物;on_draw 是回调、view 不缓存像素(非保留,§5.8.7)。
* - 嵌套生命周期由 SDK view_enter/view_leave 递归驱动(随 demo 页进出,同 list.c);绘制经
* meow_draw_view_entered(含祖先已进入判定)在 meow_ui_paint_full/invalidate 触发。
*
* 事件接线(demo 页把 touch/tap/swipe 分发改派到本子视图,实现「子>父」命中):
* - tap → sdk2_hit (select 按钮 / 宫格 tap / 列表行 tap / 行左滑松手 swipe_end)
* - down → sdk2_down (宫格聚焦拖拽 / 列表行左滑 begin)
* - move → sdk2_move (宫格滚动 / 列表左滑 move 或列表竖向滚动)
* - swipe → sdk2_swipe (宫格/列表聚焦时滚自身;否则回退页面滚动)
*/
#include "hello_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ---- 本子视图(CONTAINER,非页面:id 用越界值避免占用 g_view_by_id 顶层页槽位)---- */
static meow_view *g_sdk2_view = NULL;
/* ---- demo 内容布局常量(与 demo.c 一致):内容区 x16 宽 448 ---- */
#define S2_X 16
#define S2_W 448
#define S2_DCW 212
#define S2_DBH 44
#define S2_CH 52
/* 三节(章节标题几何在 sdk2_build_at 计算后存入,on_draw 直绘用;屏幕坐标)。 */
static int s2_t1_y, s2_t2_y, s2_t3_y; /* 三节标题 y(on_draw 直绘) */
/* ============================================================ */
/* ① meow_select — 枚举/预设选择器 */
/* ============================================================ */
static meow_widget *g_btn_sel = NULL; /* 「打开选择器」按钮(sdk2_release 释放) */
static meow_select *g_sel = NULL; /* 选择器实例(sdk2_view_create 建一次,destroy 释放) */
static rect_t r_selbtn = { 0, 0, 0, 0 };
static rect_t r_selval = { 0, 0, 0, 0 }; /* 当前选择值文本区(on_draw 直绘) */
static int g_sel_cur = 0; /* 当前选中项(跨重建保留;on_change 更新) */
static const char *g_sel_items[] = { /* 枚举演示:音频档位(对齐 M7.2 设置页采样率/码率) */
"Auto", "44.1kHz", "48kHz", "96kHz", "192kHz",
};
#define SEL_N ((int)(sizeof g_sel_items / sizeof g_sel_items[0]))
/* on_change 回调(meow_select_open 确认后触发:更新选中下标 + 重画值文本区)。 */
static void sdk2_sel_change(int idx, void *ctx)
{
(void)ctx;
if (idx >= 0 && idx < SEL_N) g_sel_cur = idx;
if (r_selval.w > 0) meow_ui_invalidate(r_selval.x, r_selval.y, r_selval.w, r_selval.h);
}
/* 打开选择器(阻塞到确认/取消;on_change 已更新选中值)。 */
static void sdk2_select_open(void)
{
if (!g_sel) return;
int r = meow_select_open(g_sel);
meow_log(MEOW_LOG_INFO, "sdk2 select return=%d (%s)",
r, (r >= 0 && r < SEL_N) ? g_sel_items[r] : "cancel");
}
/* ============================================================ */
/* ② meow_lazygrid — 2 维虚拟化宫格(非 widget) */
/* ============================================================ */
static meow_lazygrid *g_grid = NULL;
static rect_t r_grid = { 0, 0, 0, 0 };
static int g_grid_scroll = 0; /* 跨重建保留:宫格滚动偏移 */
static int g_grid_sel = -1; /* 跨重建保留:选中项 */
static int g_drag_grid = 0; /* 触摸拖宫格(down 置位;move 滚动) */
static int g_grid_focus = 0; /* 1=宫格聚焦(swipe 滚宫格而非页面) */
static int g_grid_last_y = 0;
#define GRID_N 36 /* 总项数(count > 视口行数 → 可滚动) */
#define GRID_COLS 3
#define GRID_CW 138
#define GRID_CH 86
#define GRID_GAP 10
#define GRID_H 190
/* 虚拟源:count 总项数(实时查);bind 项进入视口时调一次(同 listview 池语义)。 */
static int grid_count(void *ctx) { (void)ctx; return GRID_N; }
static void grid_bind(int idx, meow_lazygrid_item *item, void *ctx)
{
(void)ctx;
static char t[24], s[24];
snprintf(t, sizeof t, "App %d", idx);
snprintf(s, sizeof s, "v1.%d", idx);
memset(item, 0, sizeof *item);
item->title = t; item->subtitle = s;
item->enabled = (idx % 9 != 8); /* 演示禁用变淡(每 9 个禁用 1 个) */
}
/* 完全自绘 cell(set_item_draw):icon+label 风格,对齐主界面用途。 */
static void grid_cell(int idx, int x, int y, int w, int h, int st, void *ctx)
{
(void)ctx;
const meow_theme *th = meow_theme_get();
int sel = st & MEOW_LAZYGRID_ST_SELECTED;
int dis = st & MEOW_LAZYGRID_ST_DISABLED;
uint32_t bg = sel ? th->primaryContainer : th->surfaceContainerLow;
uint32_t icon = dis ? th->onSurfaceVariant : th->primary;
uint32_t txt = sel ? th->onPrimaryContainer
: (dis ? th->onSurfaceVariant : th->onSurface);
meow_ui_fill_rect(x, y, w, h, bg); /* 格底卡片 */
/* icon 块(无图资源:主题 primary 方块代替主界面图标位) */
int isz = w / 3; if (isz > h / 2) isz = h / 2;
meow_ui_fill_rect(x + (w - isz) / 2, y + 10, isz, isz, icon);
/* 名 + 状态(底部两行,中心对齐) */
static char t[24], s[24];
snprintf(t, sizeof t, "App %d", idx);
snprintf(s, sizeof s, "v1.%d", idx);
int thh = meow_ui_text_height(th->font_body);
meow_ui_text_center(x + w / 2, y + h - thh - 22, t, th->font_body, txt);
meow_ui_text_center(x + w / 2, y + h - thh - 3, s, th->font_body, txt);
if (sel) meow_ui_draw_rect_round(x, y, w, h, th->radius_card, th->primary);
if (dis) meow_ui_fill_rect_alpha(x, y, w, h, th->onSurface, 90);
}
/* 宫格 tap:置选中 + 局部刷 + 日志。 */
static void grid_tap(int idx, void *ctx)
{
(void)ctx;
g_grid_sel = idx;
meow_log(MEOW_LOG_INFO, "sdk2 grid tap idx=%d", idx);
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
}
/* ============================================================ */
/* ③ meow_listview 行左滑删除(swipe-delete) */
/* ============================================================ */
#define LV_MAX 9
static meow_widget *g_lv = NULL; /* W_LISTVIEW(注册表自动画:行 + 右露删除区) */
static rect_t r_lv = { 0, 0, 0, 0 };
static int g_lv_deleted[LV_MAX]; /* 1=已删除(行移除;count 实时收缩) */
static char g_lv_label[LV_MAX][24]; /* 行标题(bind 填充) */
static int g_lv_scroll = 0; /* 跨重建保留:列表滚动 */
static int g_lv_focus = 0; /* 1=swipe-list 聚焦(swipe 滚列表而非页面) */
static int g_lv_swipe = 0; /* 行左滑在途(swipe_begin 命中;hit 里 swipe_end) */
static int g_drag_lv = 0; /* 触摸拖列表(竖向滚动);d. 左滑时同置位 */
static int g_lv_last_y = 0;
#define LV_H 190
static int lv_count_remaining(void)
{
int n = 0;
for (int i = 0; i < LV_MAX; i++) if (!g_lv_deleted[i]) n++;
return n;
}
static int lv_count(void *ctx) { (void)ctx; return lv_count_remaining(); }
/* 显示下标(0..count-1)→ 源行 id(跳过已删除行)。删除后行前移,身份按源行稳定。 */
static int lv_real_index(int display)
{
if (display < 0) return -1;
int c = 0;
for (int i = 0; i < LV_MAX; i++) {
if (g_lv_deleted[i]) continue;
if (c == display) return i;
c++;
}
return -1;
}
static void lv_bind(int idx, meow_lv_item *item, void *ctx)
{
(void)ctx;
int real = lv_real_index(idx);
static char s[24];
memset(item, 0, sizeof *item);
snprintf(s, sizeof s, "swipe left");
item->title = (real >= 0) ? g_lv_label[real] : "";
item->subtitle = s;
item->icon_kind = (real >= 0 && real % 3 == 0) ? MEOW_LIST_ICON_FOLDER : MEOW_LIST_ICON_NONE;
item->right = MEOW_LIST_RIGHT_NONE;
item->enabled = 1;
}
/* on_delete(swipe 删除动画滑出完成回调):删除该显示下标对应的源行 + 局部刷。
* 注:swipe_end 传的是显示下标(含删除前定位的 swipe_idx);映射回源行 id 标记。 */
static void lv_delete(int idx, void *ctx)
{
(void)ctx;
int real = lv_real_index(idx);
if (real >= 0) g_lv_deleted[real] = 1;
meow_log(MEOW_LOG_INFO, "sdk2 swipe delete display=%d real=%d", idx, real);
if (g_lv && r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
}
/* 建三节时填充行标题(demo_init 一次性;跨重建保留)。 */
static void sdk2_init_rows(void)
{
for (int i = 0; i < LV_MAX; i++) {
snprintf(g_lv_label[i], sizeof g_lv_label[i], "notif %d", i);
g_lv_deleted[i] = 0;
}
}
static int s2_built = 0; /* 1=本子视图内容已建(on_draw / 事件分发判空) */
/* ============================================================ */
/* 释放 / 建三节 */
/* ============================================================ */
/* 内容释放(demo_free_content 调):清引用(本子视图建的控件在 sdk2_release 释放)。 */
void sdk2_release(void)
{
if (g_grid) { meow_lazygrid_free(g_grid); g_grid = NULL; }
if (g_btn_sel) { meow_widget_free(g_btn_sel); g_btn_sel = NULL; }
if (g_lv) { meow_widget_free(g_lv); g_lv = NULL; }
r_selbtn = r_selval = r_grid = r_lv = RX(0, 0, 0, 0);
g_drag_grid = 0; g_grid_focus = 0; g_grid_last_y = 0;
g_lv_focus = 0; g_lv_swipe = 0; g_drag_lv = 0; g_lv_last_y = 0;
s2_built = 0;
if (g_sdk2_view) g_sdk2_view->x = g_sdk2_view->y = g_sdk2_view->w = g_sdk2_view->h = 0;
}
/* 内容重建后建三节(demo_build_at 调)。入参:yb_base=base 空间 y;ct/scroll 同 demo.c
* (SY(base)=ct+base-scroll)。返回下一 base y。 */
int sdk2_build_at(int yb_base, int ct, int scroll)
{
#define SY2(b) (ct + (b) - scroll)
int yb = yb_base;
/* ---- ① SELECT ---- */
s2_t1_y = SY2(yb);
yb += 26;
r_selbtn = RX(S2_X, SY2(yb), S2_DCW, S2_DBH);
g_btn_sel = meow_btn_create_variant(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h,
"CHOOSE", MEOW_BTN_OUTLINED);
r_selval = RX(S2_X + S2_DCW + 14, SY2(yb) + 10, S2_W - S2_DCW - 14, 24);
yb += S2_CH;
/* ---- ② LAZYGRID ---- */
s2_t2_y = SY2(yb);
yb += 30;
r_grid = RX(S2_X, SY2(yb), S2_W, GRID_H);
g_grid = meow_lazygrid_create(r_grid.x, r_grid.y, r_grid.w, r_grid.h,
GRID_COLS, GRID_CW, GRID_CH, GRID_GAP);
if (g_grid) {
meow_lazygrid_set_vsource(g_grid, grid_count, grid_bind, NULL);
meow_lazygrid_set_item_draw(g_grid, grid_cell, NULL);
meow_lazygrid_set_empty_text(g_grid, "No apps");
meow_lazygrid_set_on_tap(g_grid, grid_tap, NULL);
meow_lazygrid_set_scroll(g_grid, g_grid_scroll);
meow_lazygrid_set_sel(g_grid, g_grid_sel);
}
yb += GRID_H + 30;
/* ---- ③ SWIPE-DELETE ---- */
s2_t3_y = SY2(yb);
yb += 26;
r_lv = RX(S2_X, SY2(yb), S2_W, LV_H);
g_lv = meow_listview_create(r_lv.x, r_lv.y, r_lv.w, r_lv.h, 0,
NULL, NULL, MEOW_LIST_SEL_NONE);
if (g_lv) {
meow_listview_set_item_h(g_lv, hello_row_h()); /* M17:双行文本块按字体行高派生 */
meow_listview_set_vsource(g_lv, lv_count, lv_bind, NULL);
meow_listview_set_item_renderer(g_lv, 2, 1, 0); /* 双行、左图标、无右尾件 */
meow_listview_set_empty_text(g_lv, "All rows deleted");
meow_listview_set_swipe_delete(g_lv, 1, lv_delete, NULL);
meow_listview_set_scroll(g_lv, g_lv_scroll);
}
yb += LV_H + 10;
if (g_sdk2_view) {
g_sdk2_view->x = S2_X; g_sdk2_view->y = s2_t1_y;
g_sdk2_view->w = S2_W; g_sdk2_view->h = (r_lv.y + r_lv.h) - s2_t1_y;
}
s2_built = 1;
return yb;
#undef SY2
}
/* ---- on_draw:三节标题 + select 当前值 + 宫格(非保留,invalidate 触发重画)---- */
static void sdk2_draw_on(meow_view *v, const meow_theme *th, void *ctx)
{
(void)v; (void)th; (void)ctx;
if (!s2_built) return;
const meow_theme *t = meow_theme_get();
meow_ui_text(S2_X, s2_t1_y, "SELECT (enum/preset)", 20, t->primary);
meow_ui_text(S2_X, s2_t2_y, "LAZYGRID (2D virtualized)", 20, t->primary);
meow_ui_text(S2_X, s2_t3_y, "LISTVIEW swipe-delete", 20, t->primary);
char buf[40];
snprintf(buf, sizeof buf, "sel = %s", g_sel_items[g_sel_cur]);
meow_ui_text(r_selval.x, r_selval.y, buf, 20, t->dim);
if (g_grid) meow_lazygrid_draw(g_grid, t);
}
/* ============================================================ */
/* 事件分发(demo 页转发) */
/* ============================================================ */
/* tap:select 按钮 → 打开模态;列表行左滑松手 → swipe_end;宫格 → tap_at;列表行 → tap。 */
int sdk2_hit(int x, int y)
{
if (!s2_built) return 0;
/* 行左滑【松手】最优先:滑动后手指可能已移出列表矩形(只判在途标志)。 */
if (g_lv && g_lv_swipe) {
meow_listview_set_scroll(g_lv, g_lv_scroll);
int c = meow_listview_swipe_end(g_lv, x, y); /* 判删/回弹;动画由 meow_anim_tick 推进 */
g_lv_swipe = 0;
if (c) return 1; /* 消费(删/回弹动画中);未滑动=0 落行选中 */
}
if (g_btn_sel && meow_widget_hit(g_btn_sel, x, y)) {
sdk2_select_open();
meow_ui_invalidate(r_selbtn.x, r_selbtn.y, r_selbtn.w, r_selbtn.h);
return 1;
}
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
meow_lazygrid_tap_at(g_grid, x, y); /* 置选中 + grid_tap 回调 + 局部刷 */
return 1;
}
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
meow_listview_tap_at(g_lv, x, y); /* 行点击(演示选中)+ 局部刷 */
meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
return 1;
}
return 0;
}
/* touch_down:命中区置拖拽/聚焦。宫格>列表。返回 1=本区消费。 */
int sdk2_down(int x, int y)
{
if (!s2_built) return 0;
/* 新手势先复位本子视图拖拽态:demo_touch_down 只复位 list/slider/page 标志,不触及 sdk2
* 的 g_drag_grid/g_drag_lv/g_lv_swipe(sdk2.c 内部,demo 页经 extern 看不到);不复位则上一条
* sdk2 拖拽残留会在本次「落页内容区」新手势的 move 里继续劫持页面拖动(sdk2_move 守卫误判)。 */
g_drag_grid = 0; g_drag_lv = 0; g_lv_swipe = 0;
if (g_grid && meow_lazygrid_hit(g_grid, x, y) >= 0) {
g_drag_grid = 1; g_grid_focus = 1; g_grid_last_y = y;
return 1;
}
if (g_lv && meow_listview_hit(g_lv, x, y) >= 0) {
meow_listview_set_scroll(g_lv, g_lv_scroll);
g_lv_swipe = meow_listview_swipe_begin(g_lv, x, y); /* 命中可滑行 → 1 */
g_lv_focus = 1; g_drag_lv = 1; g_lv_last_y = y;
return 1;
}
return 0;
}
/* touch_move:宫格滚动 / 列表左滑 move(消费→不滚页面)或列表竖向滚动。返回 1=消费。 */
int sdk2_move(int x, int y)
{
if (!s2_built) return 0;
if (g_drag_grid && g_grid) {
int dy = y - g_grid_last_y; g_grid_last_y = y;
meow_lazygrid_scroll_by(g_grid, -dy);
g_grid_scroll = meow_lazygrid_scroll(g_grid);
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
return 1;
}
/* 列表滚动须在「本次手势命中列表」才消费(g_drag_lv,由 sdk2_down 命中列表置位)。
* 若仅因 g_lv 存在就无条件消费,会劫持 demo 页内容区拖拽(任何 move 都被这里吃掉,
* 页面永不滚动)——与 grid(g_drag_grid 守卫)同构;359 教训同源(SDK 控件局部刷对,
* 错在调用方控制流把页面滚动让给了控件)。 */
if (g_drag_lv && g_lv) {
if (g_lv_swipe) { /* 行左滑在途:喂 move;1=横向消费;0=纵向主导→取消本行 swipe */
meow_listview_set_scroll(g_lv, g_lv_scroll);
if (meow_listview_swipe_move(g_lv, x, y)) return 1;
g_lv_swipe = 0;
}
int dy = y - g_lv_last_y; g_lv_last_y = y; /* 竖向滚动(跟手) */
meow_listview_scroll_by(g_lv, -dy);
g_lv_scroll = meow_listview_scroll(g_lv);
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
return 1;
}
return 0;
}
/* swipe UP/DOWN:宫格/列表聚焦时滚自身(del 已按方向加权),1=已消费。 */
int sdk2_swipe(int del)
{
if (!s2_built) return 0;
if (g_grid_focus && g_grid) {
meow_lazygrid_scroll_by(g_grid, del);
g_grid_scroll = meow_lazygrid_scroll(g_grid);
if (r_grid.w > 0) meow_ui_invalidate(r_grid.x, r_grid.y, r_grid.w, r_grid.h);
return 1;
}
if (g_lv_focus && g_lv) {
meow_listview_scroll_by(g_lv, del);
g_lv_scroll = meow_listview_scroll(g_lv);
if (r_lv.w > 0) meow_ui_invalidate(r_lv.x, r_lv.y, r_lv.w, r_lv.h);
return 1;
}
return 0;
}
/* 本次手势是否已被本子视图实时滚过(demo_swipe 早退用,防 on_swipe 二次滚动——同 list 的 g_drag_list)。 */
int sdk2_dragging(void)
{
return (g_drag_grid || g_drag_lv) ? 1 : 0;
}
/* ============================================================ */
/* 子视图生命周期(on_enter/on_leave 由 SDK 递归驱动;on_destroy 清引用) */
/* ============================================================ */
static void sdk2_page_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
meow_log(MEOW_LOG_INFO, "demo sdk2 view enter");
}
static void sdk2_page_leave(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
meow_log(MEOW_LOG_INFO, "demo sdk2 view leave");
}
static void sdk2_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_sel) { meow_select_free(g_sel); g_sel = NULL; }
s2_built = 0;
g_sdk2_view = NULL; /* meow_view_free 递归释放前清引用,防悬垂 */
}
meow_view *sdk2_view_create(void)
{
static const meow_view_hooks h = {
.on_enter = sdk2_page_enter,
.on_leave = sdk2_page_leave,
.on_destroy = sdk2_page_destroy,
.on_draw = sdk2_draw_on,
};
/* id 用非页面值:容器不占 g_view_by_id(那是 page_push/pop 顶层页查找表)。 */
g_sdk2_view = meow_view_create(0x2000, MEOW_VIEW_CONTAINER, &h, NULL);
/* meow_select 实例一次性创建(控制态随本子视图存活,不受内容重建影响);行标题一次性填充。 */
sdk2_init_rows();
if (g_sdk2_view && !g_sel) {
g_sel = meow_select_create("Sample rate", 0);
if (g_sel) {
for (int i = 0; i < SEL_N; i++) meow_select_add_item(g_sel, g_sel_items[i]);
meow_select_set_on_change(g_sel, sdk2_sel_change, NULL);
}
}
return g_sdk2_view;
}
meow_view *sdk2_view_get(void)
{
return g_sdk2_view;
}
Binary file not shown.
+8 -1
View File
@@ -9,6 +9,7 @@
* am history [pkg] → 事件历史(每 App 最近 8 条;无 pkg=全部)
* am start <pkg>[/<activity>] → 经 amsd→launcher 拉起
* am kill <pkg> → 经 amsd→launcher 终止(SIGTERM 语义)
* am rescan → 手动触发注册表重扫(卡刚挂载/热装 App 后立即可见)
*
* 编译(mipsel 静态):zig-mipsel-cc -Os -Wall -Wextra -static am.c -o am
* 宿主:gcc -Os -Wall -Wextra am.c -o am_host
@@ -125,10 +126,15 @@ static int cmd_kill(const char *pkg)
return am_query(line);
}
static int cmd_rescan(void)
{
return am_query("rescan\n");
}
static void usage(const char *a0)
{
fprintf(stderr,
"usage: %s [--sock <path>] status|list|history [pkg]|start <pkg>[/<act>]|kill <pkg>\n"
"usage: %s [--sock <path>] status|list|history [pkg]|start <pkg>[/<act>]|kill <pkg>|rescan\n"
" %s resolve <pkg> <activity> | picker <action> | picker-status [reqid]\n"
" --sock <path> amsd socket (default %s, or $MEOW_AMS_SOCK)\n",
a0, a0, AMS_SOCK_DEFAULT);
@@ -181,6 +187,7 @@ int main(int argc, char **argv)
if (!strcmp(cmd, "picker-status")) {
return cmd_picker_status(arg);
}
if (!strcmp(cmd, "rescan")) return cmd_rescan();
usage(argv[0]);
return 2;
}
Binary file not shown.
Binary file not shown.
+237 -52
View File
@@ -6,6 +6,8 @@
* svcd fork+exec 直接拉起,故本进程【前台常驻】(不 daemonize),与 meow_crash 同款。
* - 一期:生命周期监控中心 + 应用注册表。launcher 经 AF_UNIX `/usr/data/meow_ams.sock`(0666)
* 上报 launch/exit/crash 事件;`am` CLI 同 socket 查询/下发 start/kill。
* - 注册表 = 启动首扫 + 周期合并重扫(卡未挂载扫空 3s 快重试、常态 30s 自愈;
* `am rescan` 手动触发)。重扫合并式,绝不清运行时状态(M2.90 真机扫空缺陷修复)。
* - 核心目的:从侧面详细抓 App 日志(尤其异常崩溃日志)。launcher 给 App 建 stdout/stderr
* 管道,前台循环按行转发(EV OUT);amsd 落盘 `logs/<pkg>/stdout_<ts>_<pid>.log`。
* - 崩溃双保险:amsd 检测到异常退出时另以 meow_crash 认识的 `SVC ...` 格式转发
@@ -61,10 +63,15 @@
#define AMS_MAX_PENDING 8 /* 进行中 picker 请求(结果关联) */
#define AMS_TICK_MS 1000
#define AMS_CLEANUP_MS 60000
#define AMS_RESCAN_MS 30000 /* 常态合并重扫周期(卡热插拔/App 热安装后注册表自愈) */
#define AMS_RESCAN_RETRY_MS 3000 /* 扫描源尚未成功打开(开机早于卡挂载扫空)→ 快重试 */
#define AMS_PATH_MAX 512
#define AMS_SOCK_PATH_MAX 108
#define AMS_BOOTTS_LEN 64
#define AMS_REQ_MAX 512 /* 命令响应缓冲 */
#define AMS_IPC_TIMEOUT_MS 1200 /* launcher IPC 单次收发超时:amsd 绝不为一次转发阻塞无限久 */
#define AMS_IPC_RETRY 2 /* launcher 一次性不可达/超时 → 短退避重连一次 */
#define AMS_IPC_BACKOFF_MS 250 /* 重连退避基数 */
/* 每 App 状态(一期:STOPPED/FG;BG 预留二期) */
enum { AMS_STOPPED = 0, AMS_FG = 1, AMS_BG = 2 };
@@ -84,7 +91,7 @@ struct ams_route {
struct ams_app {
char pkg[MEOW_APP_ID_MAX]; /* app_id=package */
char name[17];
char name[49]; /* 显示名(UTF-8;截断必须整序列,见 utf8_trim_incomplete) */
char exec[129];
int known; /* 注册表(app.json 扫描)命中 */
int state; /* AMS_* */
@@ -367,6 +374,22 @@ static void poll_card(void)
}
/* ---- 注册表扫描(app.json 迷你解析:package/name/exec) ---- */
/* 把字符串末尾不完整的 UTF-8 序列砍掉(截断到最后一个完整码点边界)。
* 根因记录:name 截断若落在多字节序列中间,下游 JSON 嵌进面板会被浏览器
* JSON.parse 以非法 UTF-8 拒收(「非 JSON 响应」事故 2026-09-02)。 */
static void utf8_trim_incomplete(char *s)
{
size_t n = strlen(s), i = 0, last_good = 0;
while (i < n) {
unsigned char c = (unsigned char)s[i];
int len = (c < 0x80) ? 1 : (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1;
if (i + (size_t)len > n) break; /* 不完整序列 */
i += (size_t)len;
last_good = i;
}
s[last_good] = '\0';
}
static void appjson_get(const char *content, const char *key, char *out, size_t n)
{
out[0] = '\0';
@@ -387,6 +410,7 @@ static void appjson_get(const char *content, const char *key, char *out, size_t
if (l >= n) l = n - 1;
memcpy(out, c, l);
out[l] = '\0';
utf8_trim_incomplete(out);
}
/* §6.2 二期:在 content 二分/扁平解析 route.<id>=<chain>(或 JSON "route.<id>":"<chain>")到 app。
@@ -475,14 +499,31 @@ static void ams_parse_pickers(const char *content, const char *app_id)
}
}
/* 扫描 --apps 下每个目录(app_id=目录名;name/exec 从 app.json 抽,缺省回退)。 */
static void registry_scan(void)
/* 扫描源至少成功打开过一次(空表快重试的终止条件;卡未挂载时恒 0)。 */
static int g_scan_ok = 0;
static struct ams_app *app_find(const char *pkg); /* 定义在后(App 表辅助) */
/* 扫描 --apps 下每个目录(app_id=目录名;name/exec 从 app.json 抽,缺省回退)。
* 合并式(merge-preserving):已存在表项只刷新静态字段(known/name/exec/route 表/picker 表),
* 运行时状态(state/pid/launches/crashes/history、运行时 DECLARE 并集进来的路由)一律保留;
* 新目录追加建项。扫描源不可达(开机早于卡挂载/卡热拔)→ 保留现有表直接返回,绝不清空。
* verbose=1(启动/`am rescan`)记全量明细;周期调用仅在结果变化时记一行摘要(防刷屏)。 */
static void registry_scan(int verbose)
{
static int last_found = -1, last_napps = -1, last_npick = -1;
DIR *d = opendir(g_apps_dir);
if (!d) { emit_log("registry scan: %s not reachable (%s)", g_apps_dir, strerror(errno)); return; }
if (!d) {
if (verbose || !g_scan_ok)
emit_log("registry scan: %s not reachable (%s)", g_apps_dir, strerror(errno));
return;
}
g_scan_ok = 1;
for (int i = 0; i < g_napps; i++) g_apps[i].known = 0;
for (int i = 0; i < g_npickers; i++) g_pickers[i].used = 0;
struct dirent *de;
int n = 0;
while ((de = readdir(d)) != NULL && n < AMS_MAX_APPS) {
int found = 0;
while ((de = readdir(d)) != NULL) {
if (de->d_name[0] == '.') continue;
char nm[96];
scopy(nm, sizeof nm, de->d_name);
@@ -503,37 +544,50 @@ static void registry_scan(void)
fclose(jf);
content[rd] = '\0';
}
struct ams_app *a = &g_apps[n];
memset(a, 0, sizeof *a);
/* app_id 权威 = 目录名(APPS.md §2);app.json package 字段作为兜底校验(不一致时用目录名) */
scopy(a->pkg, sizeof a->pkg, de->d_name);
/* app_id 权威 = 目录名(APPS.md §2);已有表项(含运行时 EV/DECLARE 建的)原位刷新 */
struct ams_app *a = app_find(nm);
if (!a) {
if (g_napps >= AMS_MAX_APPS) continue;
a = &g_apps[g_napps++];
memset(a, 0, sizeof *a);
scopy(a->pkg, sizeof a->pkg, nm);
}
a->known = 1;
char tmp[96];
appjson_get(content, "name", tmp, sizeof tmp);
if (!tmp[0]) scopy(tmp, sizeof tmp, de->d_name);
if (!tmp[0]) scopy(tmp, sizeof tmp, nm);
scopy(a->name, sizeof a->name, tmp);
utf8_trim_incomplete(a->name); /* scopy 截断同样走整序列边界 */
appjson_get(content, "exec", tmp, sizeof tmp);
if (tmp[0]) scopy(a->exec, sizeof a->exec, tmp);
else {
else if (!a->exec[0] || !strcmp(a->exec, "?")) {
scopy(a->exec, sizeof a->exec, dir);
path_append(a->exec, sizeof a->exec, "/meow");
}
/* §6.2 二期:路由权威表(静态 app.json route.*)+ picker 表(picker.<action>=<chain>) */
ams_parse_routes(content, a);
ams_parse_pickers(content, a->pkg);
n++;
found++;
}
closedir(d);
g_napps = n;
emit_log("registry scan: %d app(s) from %s", g_napps, g_apps_dir);
for (int i = 0; i < g_napps; i++) {
struct ams_app *a = &g_apps[i];
emit_log(" app %s name=%s exec=%s routes=%d", a->pkg, a->name, a->exec, a->nroute);
for (int r = 0; r < a->nroute; r++)
emit_log(" route %s=%s", a->routes[r].act, a->routes[r].chain);
if (verbose || found != last_found || g_napps != last_napps || g_npickers != last_npick) {
emit_log("registry scan: %d dir(s) from %s, %d app(s) registered",
found, g_apps_dir, g_napps);
if (verbose) {
for (int i = 0; i < g_napps; i++) {
struct ams_app *a = &g_apps[i];
emit_log(" app %s name=%s exec=%s known=%d routes=%d",
a->pkg, a->name, a->exec, a->known, a->nroute);
for (int r = 0; r < a->nroute; r++)
emit_log(" route %s=%s", a->routes[r].act, a->routes[r].chain);
}
for (int i = 0; i < g_npickers; i++)
if (g_pickers[i].used)
emit_log(" picker %s -> %s (chain=%s)",
g_pickers[i].action, g_pickers[i].app_id, g_pickers[i].chain);
}
}
for (int i = 0; i < g_npickers; i++)
emit_log(" picker %s -> %s (chain=%s)", g_pickers[i].action, g_pickers[i].app_id, g_pickers[i].chain);
last_found = found; last_napps = g_napps; last_npick = g_npickers;
}
/* ---- App 表辅助 ---- */
@@ -730,16 +784,49 @@ static void out_append(const char *pkg, pid_t pid, long epoch, long up, const ch
}
/* ---- launcher IPC 转发(xchg 一帧,读回一帧) ---- */
/* 最近一次 ipc_xchg 的失败原因:区分「连不上」与「连上但没及时回」。此前两者都记成
* "launcher offline",2026-11-04 真机即误诊——launcher 活着且回了 ACK,只是晚于 1200ms 超时。 */
enum { IPC_ERR_NONE = 0, IPC_ERR_CONN, IPC_ERR_IO, IPC_ERR_TIMEOUT, IPC_ERR_PROTO };
static int g_ipc_err = IPC_ERR_NONE;
static const char *ipc_err_str(void)
{
switch (g_ipc_err) {
case IPC_ERR_CONN: return "connect failed";
case IPC_ERR_IO: return "io error";
case IPC_ERR_TIMEOUT: return "timeout, no reply";
case IPC_ERR_PROTO: return "unexpected reply";
default: return "ok";
}
}
static int ipc_fail(int fd, int err)
{
if (fd >= 0) close(fd);
g_ipc_err = err;
return -1;
}
static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
uint8_t *rbuf, uint32_t rcap)
{
g_ipc_err = IPC_ERR_NONE;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
if (fd < 0) { g_ipc_err = IPC_ERR_IO; return -1; }
/* 收发加超时:launcher 刚刚重启/主循环未就绪(接受但不回包)时,amsd 的阻塞
* read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发
* 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。
* 注意:tv_usec 必须 < 1000000(否则 setsockopt 返回 EINVAL,超时静默失效)。 */
struct timeval tv;
tv.tv_sec = AMS_IPC_TIMEOUT_MS / 1000;
tv.tv_usec = (AMS_IPC_TIMEOUT_MS % 1000) * 1000;
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, g_ipc_sock, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; }
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) return ipc_fail(fd, IPC_ERR_CONN);
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen;
uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */
@@ -750,49 +837,61 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
size_t off = 0;
while (off < sizeof hdr) {
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
if (w <= 0) { close(fd); return -1; }
if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
off += (size_t)w;
}
if (len && payload) {
size_t p = 0;
while (p < len) {
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
if (w <= 0) { close(fd); return -1; }
if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
p += (size_t)w;
}
}
off = 0;
while (off < sizeof hdr) {
ssize_t r = read(fd, hdr + off, sizeof hdr - off);
if (r <= 0) { close(fd); return -1; }
if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
off += (size_t)r;
}
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2);
memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) { close(fd); return -1; }
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) return ipc_fail(fd, IPC_ERR_PROTO);
if (rlen > rcap) rlen = rcap;
off = 0;
while (off < rlen) {
ssize_t r = read(fd, rbuf + off, rlen - off);
if (r <= 0) { close(fd); return -1; }
if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
off += (size_t)r;
}
close(fd);
return (int)rtype;
}
/* 向 launcher 申请 start/kill。返回 0=launcher 回 code(a_out 输出);-1=launcher 不通。 */
/* 向 launcher 申请 start/kill。返回 0=launcher 回 code(a_out 输出);-1=launcher 不通。
* launcher 重启窗口(svcd/sysui 重启 launcher、或 amsd 先行启动)内连接会暂不可达/超时:
* 这里做「指数式短退避重试」——launcher 数秒内回来即自动恢复转发(无需人工重发)。 */
static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uint32_t *code)
{
uint8_t rbuf[64];
int rt = ipc_xchg(type, payload, len, rbuf, sizeof rbuf);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
if (code) *code = a.code;
return 0;
for (int attempt = 0; attempt < AMS_IPC_RETRY; attempt++) {
int rt = ipc_xchg(type, payload, len, rbuf, sizeof rbuf);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
if (code) *code = a.code;
return 0;
}
if (rt == MSG_ERR) { g_ipc_err = IPC_ERR_PROTO; return MEOW_ERR_INTERNAL; }
/* 一次连接失败/超时(launcher 未就绪)→ 短退避再试;仍失败则返回 -1。 */
if (attempt + 1 < AMS_IPC_RETRY)
usleep((useconds_t)(AMS_IPC_BACKOFF_MS * (attempt + 1)) * 1000);
}
if (rt == MSG_ERR) return MEOW_ERR_INTERNAL;
if (g_ipc_err == IPC_ERR_NONE) g_ipc_err = IPC_ERR_PROTO; /* 收到非 ACK/ERR 帧:不是「无回执」 */
return -1;
}
@@ -800,10 +899,22 @@ static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uin
static void handle_launch(char **t, int nt)
{
if (nt < 4) return;
const char *pkg = t[0], *act = t[1], *chain = t[2];
pid_t pid = (pid_t)atoi(t[3]);
long epoch = (nt > 4) ? atol(t[4]) : (long)time(NULL);
long up = (nt > 5) ? atol(t[5]) : read_uptime_sec();
const char *pkg = t[0], *act = t[1], *chain = "-";
pid_t pid;
long epoch, up;
if (nt == 5) {
/* 兼容旧格式(M2.90 固件 launcher 实证):EV LAUNCH <pkg> <act> <pid> <epoch> <up>
* ——无 chain 字段。不兼容会把 epoch 错当 pid(am status 显示假 pid、
* `am kill` 兜底直发 SIGTERM 打错进程),故按 nt==5 识别旧布局。 */
pid = (pid_t)atoi(t[2]);
epoch = atol(t[3]);
up = atol(t[4]);
} else {
chain = t[2];
pid = (pid_t)atoi(t[3]);
epoch = (nt > 4) ? atol(t[4]) : (long)time(NULL);
up = (nt > 5) ? atol(t[5]) : read_uptime_sec();
}
struct ams_app *a = app_get(pkg);
if (!a) return;
a->state = AMS_FG;
@@ -874,6 +985,41 @@ static void handle_out(char **t, int nt)
out_append(pkg, pid, epoch, up, text);
}
/* 批①后台常驻上报(单向 best-effort,launcher → amsd):
* EV BG <app> <pid> → AMS_BG(转后台保活)
* EV FG <app> → AMS_FG(回前台)。旧 launcher 不发、本层不识别也不崩。 */
static void handle_bg(char **t, int nt)
{
if (nt < 2) return;
const char *pkg = t[0];
pid_t pid = (pid_t)atoi(t[1]);
long epoch = (nt > 2) ? atol(t[2]) : (long)time(NULL);
long up = (nt > 3) ? atol(t[3]) : read_uptime_sec();
struct ams_app *a = app_get(pkg);
if (!a) return;
a->state = AMS_BG;
a->pid = pid;
char detail[192];
snprintf(detail, sizeof detail, "pid=%ld", (long)pid);
hist_push(a, "bg", detail, epoch, up);
emit_log("BG %s pid=%ld", pkg, (long)pid);
}
static void handle_fg(char **t, int nt)
{
if (nt < 1) return;
const char *pkg = t[0];
long epoch = (nt > 1) ? atol(t[1]) : (long)time(NULL);
long up = (nt > 2) ? atol(t[2]) : read_uptime_sec();
struct ams_app *a = app_find(pkg);
if (!a) return;
a->state = AMS_FG;
char detail[192];
snprintf(detail, sizeof detail, "bg->fg");
hist_push(a, "fg", detail, epoch, up);
emit_log("FG %s", pkg);
}
/* ---- §6.2 二期:picker 编排(amsd 路由权威) ---- */
static struct ams_picker *picker_find(const char *action)
@@ -996,6 +1142,13 @@ static int cmd_picker_status(const char *reqstr, char *resp, int rn)
return l;
}
/* `am rescan`:手动触发注册表重扫(卡刚挂载/热装 App 后立即可见,不等 30s 周期)。 */
static int cmd_rescan(char *resp, int rn)
{
registry_scan(1);
return snprintf(resp, rn, "OK rescan %d app(s)\n", g_napps);
}
/* `am resolve <pkg> <act>`:查 amsd 权威路由表。 */
static int cmd_resolve(const char *pkg, const char *act, char *resp, int rn)
{
@@ -1100,12 +1253,18 @@ static int cmd_start(const char *spec, char *resp, int rn)
hist_push(a, "ams_start", "intent (forwarded)", (long)time(NULL), read_uptime_sec());
snprintf(detail, sizeof detail, "ok forwarded");
emit_log("AMS start %s%s forwarded to launcher", pkg, act[0] ? "/" : "");
} else if (rt == 0 && code == (uint32_t)MEOW_ERR_BUSY) {
} else if (rt == 0 && code == (uint32_t)(-MEOW_ERR_BUSY)) {
/* launcher ACK ack.code 携带正幅度(busy=8,见 util.c `rc<0?-rc:MEOW_OK`),非二补。 */
snprintf(detail, sizeof detail, "busy (app running)");
emit_log("AMS start %s -> BUSY (app running)", pkg);
} else if (rt == 0) {
snprintf(detail, sizeof detail, "launcher refused code=%d", (int)code);
emit_log("AMS start %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
} else {
snprintf(detail, sizeof detail, "launcher offline");
emit_log("AMS start %s -> launcher offline", pkg);
/* rt!=0 只代表「这次没拿到回执」,不等于 launcher 死了(同 cmd_kill 注释)。
* 响应串保留 "launcher offline"(MeowPanel 据此映射 503),日志补真实原因。 */
snprintf(detail, sizeof detail, "launcher offline (%s)", ipc_err_str());
emit_log("AMS start %s -> launcher offline (%s)", pkg, ipc_err_str());
}
snprintf(resp, rn, "%s %s/%s\n", strstr(detail, "ok forwarded") ? "OK" : "ERR",
pkg, act[0] ? act : "(default)");
@@ -1126,14 +1285,21 @@ static int cmd_kill(const char *pkg, char *resp, int rn)
uint32_t code = 0;
int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code);
if (rt != 0 && a->pid > 0) {
/* 兜底:launcher 不在,直接 SIGTERM(与 launcher 的 SIGTERM 语义一致) */
/* 兜底:launcher 没给回执 → 直接 SIGTERM(与 launcher 的 SIGTERM 语义一致)。
* 注意 rt!=0 **不等于 launcher 死了**:主循环被 IPC 洪泛拖慢时 ACK 会晚于
* AMS_IPC_TIMEOUT_MS,故日志写实际原因(connect failed / timeout, no reply / ...),
* 而不是笼统的 "launcher offline"(2026-11-04 真机即此误判)。 */
kill(a->pid, SIGTERM);
emit_log("AMS kill %s -> direct SIGTERM (launcher offline)", pkg);
emit_log("AMS kill %s -> direct SIGTERM (launcher %s)", pkg, ipc_err_str());
} else if (rt != 0) {
return snprintf(resp, rn, "ERR launcher offline: %s\n", pkg);
/* 响应串保留 "launcher offline" 字样(MeowPanel api_ams_ctl 依此映射 503 AMS_UNAVAILABLE),
* 只在括号里补真实原因,便于区分「连不上」与「连上但超时」。 */
return snprintf(resp, rn, "ERR launcher offline (%s): %s\n", ipc_err_str(), pkg);
} else if (code != MEOW_OK) {
emit_log("AMS kill %s -> launcher refused code=%u", pkg, code);
return snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pkg, code);
/* launcher 明确拒绝:不改判为直杀(pid 可能已被回收,盲杀会打错进程),如实回错。
* code 是负错误码的 uint32 呈现(-2 → 4294967294),括号里给有符号值便于对照。 */
emit_log("AMS kill %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
return snprintf(resp, rn, "ERR launcher refused: %s (code=%d)\n", pkg, (int)code);
}
hist_push(a, "ams_kill", "terminate (via launcher)", (long)time(NULL), read_uptime_sec());
int m = snprintf(resp, rn, "OK terminate %s\n", pkg);
@@ -1162,6 +1328,8 @@ static int dispatch_line(char *line, char *resp, int rn)
if (!strcmp(tok[0], "LAUNCH")) { handle_launch(tok + 1, nt - 1); }
else if (!strcmp(tok[0], "EXIT")) { handle_exit(tok + 1, nt - 1); }
else if (!strcmp(tok[0], "OUT")) { handle_out(tok + 1, nt - 1); }
else if (!strcmp(tok[0], "BG")) { handle_bg(tok + 1, nt - 1); }
else if (!strcmp(tok[0], "FG")) { handle_fg(tok + 1, nt - 1); }
return 0;
}
@@ -1199,13 +1367,14 @@ static int dispatch_line(char *line, char *resp, int rn)
return 0;
}
/* 命令:status/list/history/start/kill/resolve/picker/picker-status */
/* 命令:status/list/history/start/kill/resolve/picker/picker-status/rescan */
char *save = NULL;
char *cmd = strtok_r(line, " ", &save);
if (!cmd) return 0;
char *arg = strtok_r(NULL, " ", &save);
if (!strcmp(cmd, "status")) return cmd_status(resp, rn) > 0;
if (!strcmp(cmd, "list")) return cmd_list(resp, rn) > 0;
if (!strcmp(cmd, "rescan")) return cmd_rescan(resp, rn) > 0;
if (!strcmp(cmd, "history")) {
if (arg && arg[0]) cmd_history(arg, resp, rn);
else cmd_history(NULL, resp, rn);
@@ -1350,7 +1519,7 @@ static int run_daemon(void)
boot_ts(g_bootts, sizeof g_bootts);
log_open_card();
log_cleanup_old();
registry_scan();
registry_scan(1); /* 启动首扫:卡未挂载扫空也照常常驻,主循环 3s 快重试兜底 */
fprintf(stderr, "[meow_amsd] up: sock=%s ipc=%s apps=%s card=%s bootts=%s log=%s\n",
g_sock, g_ipc_sock, g_apps_dir, g_card, g_bootts,
@@ -1362,8 +1531,13 @@ static int run_daemon(void)
for (int i = 0; i < AMS_MAX_CONNS; i++) conns[i].fd = -1;
int nfds = 2;
unsigned long long last_cleanup = 0;
unsigned long long last_cleanup = 0, last_rescan = 0;
while (!g_shutdown) {
/* 每圈清零:谨防上一轮已关闭连接残留的 POLLHUP/POLLERR 落在 pfds[2+i] 上。
* 若不清,accept 后同一轮就用「陈旧 revents」处理新连接——误判其 HUP/ERR
* 而提前 close(am CLI 收到 SIGPIPE、命令被丢弃),launcher 重启后 EV 事件
* 连接密集必触发,表现为 `am start` 不再转发到 launcher。 */
memset(pfds, 0, sizeof pfds);
pfds[0].fd = g_listen_fd; pfds[0].events = POLLIN; pfds[0].revents = 0;
pfds[1].fd = g_sigpipe[0]; pfds[1].events = POLLIN; pfds[1].revents = 0;
for (int i = 0; i < nfds - 2; i++) {
@@ -1424,6 +1598,17 @@ static int run_daemon(void)
poll_card();
log_cleanup_old();
}
/* 注册表自愈重扫:扫描源从未成功打开(开机早于卡挂载扫空,M2.90 真机实证
* 「registry scan: MeowAPPs not reachable」后注册表永久为空 → am start 报
* pkg not registered)→ 3s 快重试直到扫到;常态 30s 合并重扫(卡热插拔 /
* App 热安装自愈,对齐 launcher 的应用列表目录热重载)。合并式重扫不清
* 运行时状态,见 registry_scan 注释。 */
{
unsigned long long nm = now_ms();
unsigned long long iv = g_scan_ok ? (unsigned long long)AMS_RESCAN_MS
: (unsigned long long)AMS_RESCAN_RETRY_MS;
if (nm - last_rescan >= iv) { last_rescan = nm; registry_scan(0); }
}
}
if (g_listen_fd >= 0) close(g_listen_fd);
Binary file not shown.
+138
View File
@@ -0,0 +1,138 @@
/* bt_dev.c — 蓝牙 A2DP 设备自识别(纯逻辑实现,见 bt_dev.h 契约)。
*
* 纯 libc(string / stdlib),无任何 meow_* 调用、无 popen、无文件 I/O —— meow_audiod 负责
* 读取 /usr/data/alsa.conf、执行 `bluealsa-cli list-pcms` 及写回。宿主单测零链路重。
*/
#include "bt_dev.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
* 端点形如 `a2dpsrc/AA:BB:CC:DD:EE:FF`(MAC 在端点 token 内最后一个 '/' 之后到空白前)。
* 逐个**有界行**匹配(strstr 必须在行内,避免跨换行误把后一行当本行端点——如行 1
* "profile/aptx" 与行 3 "a2dp_source/..." 的经典误判)。 */
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
{
if (!listpcms || !mac || macsz < 2) return -1;
mac[0] = 0;
const char *ln = listpcms;
while (ln && *ln) {
const char *nl = strchr(ln, '\n');
const char *le = nl ? nl : (ln + strlen(ln));
size_t linelen = (size_t)(le - ln);
char line[256];
if (linelen >= sizeof line) linelen = sizeof line - 1;
memcpy(line, ln, linelen); line[linelen] = 0;
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
const char *hit = strstr(p, "a2dpsrc");
if (!hit) hit = strstr(p, "a2dp_source");
if (hit) {
const char *te = hit;
while (*te && *te != ' ' && *te != '\t' && *te != '\n') te++;
const char *slash = NULL;
for (const char *q = hit; q < te; q++) if (*q == '/') slash = q;
if (slash && (slash + 1) < te) {
const char *start = slash + 1;
size_t len = (size_t)(te - start);
if (len > 0 && len < macsz) {
memcpy(mac, start, len);
mac[len] = 0;
return 0;
}
}
}
if (!nl) break;
ln = nl + 1;
}
return -1;
}
/* 在单行内把 anchor("DEV=" / "device")后的 MAC 值替换为 mac,保留引号风格。
* 行未含 anchor → 返回 1(out=原样);含 anchor → 写回新行并返回 0。 */
static int line_replace_mac(char *out, size_t outsz, const char *line,
const char *anchor, const char *mac)
{
const char *at = strstr(line, anchor);
if (!at) { snprintf(out, outsz, "%s", line); return 1; }
const char *vp = at + strlen(anchor);
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++; /* 引号风格:值可带双引号 */
const char *ve = vp;
while (*ve && *ve != ' ' && *ve != '\t' && *ve != '\n' &&
*ve != '"' && *ve != '{' && *ve != '}') ve++;
size_t prefix = (size_t)(vp - line); /* 前段(含开引号,若带) */
size_t tail = (size_t)(ve - line); /* 值终点(不含闭引号,若带) */
/* 前段 + mac + 尾段(尾段保留闭引号与其后内容) */
snprintf(out, outsz, "%.*s%s%s", (int)prefix, line, mac, line + tail);
return 0;
}
/* 幂等改写 alsa.conf:只把 `pcm.bluealsa` 块内 `DEV=` 与 `device` 后的 MAC 换成 mac。
* 逐行处理;无 pcm.bluealsa / 无 DEV= / device token → -1;两端已一致 → 1;改了 → 0。 */
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
char **out, size_t *out_len)
{
if (!in || !mac || !mac[0] || !out || !out_len || in_len == 0) return -1;
if (!strstr(in, "pcm.bluealsa") ||
(!strstr(in, "DEV=") && !strstr(in, "device"))) return -1;
size_t cap = in_len + 128;
char *res = malloc(cap ? cap : 1);
if (!res) return -1;
size_t o = 0, changed = 0;
const char *p = in, *end = in + in_len;
while (p < end) {
const char *nl = memchr(p, '\n', (size_t)(end - p));
const char *le = nl ? nl : end;
size_t linelen = (size_t)(le - p);
char line[512], newline[512];
if (linelen >= sizeof line) linelen = sizeof line - 1;
memcpy(line, p, linelen); line[linelen] = 0;
const char *k = line;
while (*k == ' ' || *k == '\t') k++;
int anchored = 0;
if (strstr(k, "DEV=")) {
line_replace_mac(newline, sizeof newline, line, "DEV=", mac);
anchored = 1;
} else if (strncmp(k, "device", 6) == 0 &&
(k[6] == ' ' || k[6] == '\t' || k[6] == 0)) {
line_replace_mac(newline, sizeof newline, line, "device", mac);
anchored = 1;
}
if (anchored) {
if (strcmp(line, newline) != 0) changed = 1;
size_t n = strlen(newline);
if (o + n + 2 > cap) {
cap = o + n + 256;
char *t = realloc(res, cap);
if (!t) { free(res); return -1; }
res = t;
}
memcpy(res + o, newline, n); o += n;
if (nl) res[o++] = '\n';
} else {
size_t n = linelen;
if (o + n + 2 > cap) {
cap = o + n + 256;
char *t = realloc(res, cap);
if (!t) { free(res); return -1; }
res = t;
}
memcpy(res + o, p, n); o += n;
if (nl) res[o++] = '\n';
}
if (!nl) break;
p = nl + 1;
}
if (!changed) { free(res); return 1; }
res[o] = 0;
*out = res;
*out_len = o;
return 0;
}
+27
View File
@@ -0,0 +1,27 @@
/* bt_dev.h — 蓝牙 A2DP 设备自识别(DEV 自动更新纯逻辑,宿主可单测)。
*
* 背景:/usr/data/alsa.conf 的 `pcm.bluealsa`(plug→bluealsa)需把 DEV/device 钉在当前
* 连接的 A2DP sink 设备 MAC 上才能正确出声。本模块提供两段纯逻辑(无 meow_* / popen / 文件
* I/O —— meow_audiod 负责读文件与执行系统命令):
* - bt_dev_detect_mac:从 `bluealsa-cli list-pcms` 输出提取第一个 a2dpsrc 端点的设备 MAC;
* - bt_dev_rewrite:幂等改写 alsa.conf 文本,把 `pcm.bluealsa` 块内 `DEV=<mac>` 与
* `device <mac>` 两处换成目标 MAC(保留引号风格 / codec / 其余 token)。
* 纯 libc(string / stdlib),独立编译,宿主零链接重。
*/
#ifndef BT_DEV_H
#define BT_DEV_H
#include <stddef.h>
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
* 返回 0=找到并写入 mac(NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。macsz 建议 ≥18。 */
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz);
/* 幂等改写 alsa.conf 文本(in_len 不含 NUL):把 `pcm.bluealsa` 块内 `DEV=<mac>` 与
* `device <mac>` 两处 MAC 换成 mac(保留引号风格、codec/其它 token 原样)。
* 返回 0=已改动(*out 与 *out_len 给出新文本,调用方负责 free);1=无需改动(两端已一致);
* -1=无 pcm.bluealsa / 无 DEV= 或 device token / 参数非法(不动)。 */
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
char **out, size_t *out_len);
#endif /* BT_DEV_H */
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
# build_audiod.sh — meow_audiod 交叉编译(mipsel glibc-dynamic)+ 宿主多目标可选。
#
# 目标:R3 Pro II / MeowOS(MIPS32 o32,真实 ABI)。audiod 复用 meow_ipc.h(共享协议头,
# -I../meow_launcherd)。audiod 为 svcd 托管的独立守护;meow_audio 为用户侧 CLI。
#
# 【P2 蓝牙链接定案 2026-09-03】设备为 glibc-2.22(ld-2.22.so / libc-2.22.so,非 musl),且
# libasound 会在运行时 dlopen 单独的 bluealsa pcm 插件(/usr/lib/alsa-lib/libasound_module_pcm_bluealsa.so)
# ——故 audiod 必须以**动态 glibc** 形态链接设备现有 libasound.so.2:
# - 用 zig --target=mipsel-linux-gnueabihf.2.22(硬浮点 o32,glibc-2.22 版本引用,设备 libs 齐);
# libasound 原型在 meow_audiod.c 内联声明(不依赖 asoundlib.h)。
# - 删 -static:musl-static 无 dlopen(实测 "Dynamic loading not supported",无法加载 bluealsa 插件);
# musl-dynamic 需设备不存在的 musl ld.so。libasound 链接库来自 libbt/(设备 libasound.so.2 副本)。
# - meow_audio CLI 无 libasound 依赖,保持 musl-static(zig-mipsel-cc)。
#
# 用法:
# ./build_audiod.sh # 交叉编译 → meow_audiod(mipsel glibc-dynamic,BT 直链 libasound)
# ./build_audiod.sh cli # 交叉编译 meow_audio CLI → meow_audio(mipsel musl-static)
# ./build_audiod.sh host # 宿主编译 *_host 并跑 test_audiod_host 回归(FILE sink)
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
ZIG="$ROOT/.tools/zig-env/bin/zig"
INC="-I$ROOT/patch_v2/meow_launcherd"
if [ "$1" = "host" ]; then
HOST_CC="${CC_HOST:-cc}"
# BT 目标直链宿主 libasound.so.2(无 asoundlib.h,原型内联于 meow_audiod.c;-l: 精确到版本,
# 不依赖 unversioned dev 链接)。删 -ldl:BT 不再用 dlopen。
"$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread -l:libasound.so.2
"$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm
"$HOST_CC" $INC -Os -Wall -Wextra test_audiod_host.c -o test_audiod_host -lm
"$HOST_CC" -Os -Wall -Wextra test_bt_dev.c bt_dev.c -o test_bt_dev -lm
echo "== host builds =="
ls -la meow_audiod_host meow_audio_host test_audiod_host test_bt_dev
file meow_audiod_host meow_audio_host test_audiod_host test_bt_dev
echo "== host regression (test_bt_dev: BT DEV auto-detect) =="
./test_bt_dev || { echo "!! test_bt_dev FAILED"; exit 1; }
echo "== host regression (test_audiod_host) =="
./test_audiod_host || { echo "!! host regression FAILED"; exit 1; }
exit 0
fi
if [ "$1" = "cli" ]; then
"$CC" $INC -Os -Wall -Wextra -static meow_audio.c -o meow_audio -lm
echo "== meow_audio (mipsel static CLI) =="
ls -la meow_audio
md5sum meow_audio
file meow_audio
exit 0
fi
# meow_audiod:mipsel glibc-dynamic,BT 直链 libasound(-Llibbt -lasound)。-lpthread 供播放线程。
# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件镜像自拷贝(idempotent)。
LIBBT="libbt"
ASOUND_SRC="$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0"
mkdir -p "$LIBBT"
if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so"
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2"
"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod \
-lpthread -Llibbt -lasound
echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) =="
ls -la meow_audiod
md5sum meow_audiod
file meow_audiod
echo "-- dynamic NEEDED --"
readelf -d meow_audiod 2>/dev/null | grep -iE "needed" || true
echo "-- glibc version refs (max must be <= 2.22) --"
readelf --version-info meow_audiod 2>/dev/null | grep -oE "GLIBC_2\.[0-9]+" | sort -uV | tail -3 || true
BIN
View File
Binary file not shown.
+454
View File
@@ -0,0 +1,454 @@
#define _GNU_SOURCE
/* meow_audio.c — meow_audiod 用户侧 CLI(root 免鉴权,ssh 直呼)。
*
* 命令:
* meow_audio status 当前流/参数/帧数/underrun/路由/音量
* meow_audio tone [freq] [ms] [rate] 正弦测试音(默认 440Hz 500ms 44100;排查无声不须开 App)
* meow_audio route <0|2|3> 手动切路由(0=auto 2=3.5 3=4.4;调试/面板)
* meow_audio vol [0-100] 设置音量(无参=查询;v1 透传写 DAC numid=1/2 + settings.conf)
* meow_audio output <dac|bt> 输出目标(P2 蓝牙:bt=A2DP source,音量走 AVRCP 不下发 DAC 反转)
* meow_audio monitor [秒] 终端实时电平表(rms/peak dBFS + 文本电平条,200ms 刷新)
*
* 传输:AF_UNIX SOCK_STREAM 到 audiod socket(env MEOW_AUDIO_SOCK / --sock 覆盖,宿主/qemu)。
* 帧格式复用 meow_ipc.h 16B 头 + m_audio_* 负载。无 audiod → 连接失败/超时 → 打印友好错误并退出。
*
* 编译(mipsel 静态):build_audiod.sh cli。宿主编译:build_audiod.sh host。
*/
#include "meow_ipc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <math.h>
#include <time.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/un.h>
#define AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock"
static char g_sock[108] = AUD_SOCK_DEFAULT;
static int g_fd = -1;
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 -1; }
if (w == 0) return -1;
off += (size_t)w;
}
return 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 -1; }
if (r == 0) return -1;
off += (size_t)r;
}
return 0;
}
/* 连接 audiod:connect 非阻塞 + poll 2s 有界(对端 backlog 满/wedge 时绝不永久阻塞);
* 建连后 SO_SNDTIMEO 2s(tone 长写在 audiod 停摆时报错退出而非挂死)。 */
static int connect_sock(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", g_sock);
int r = connect(fd, (struct sockaddr *)&a, sizeof a);
if (r < 0 && errno == EINPROGRESS) {
struct pollfd p;
p.fd = fd; p.events = POLLOUT; p.revents = 0;
if (poll(&p, 1, 2000) > 0) {
int err = 0;
socklen_t el = sizeof err;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
} else {
r = -1;
}
}
if (r < 0) { close(fd); return -1; }
if (fl >= 0) fcntl(fd, F_SETFL, fl);
struct timeval tv;
tv.tv_sec = 2; tv.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
return fd;
}
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) != 0) return -1;
if (len && payload && write_all(fd, payload, len) != 0) return -1;
return 0;
}
/* 读一帧头部+负载;返回负载长(≥0),-1=断/坏帧/超时。 */
static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
if (read_all(fd, hdr, sizeof hdr) != 0) return -1;
uint32_t magic, len; uint16_t ver;
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2);
memcpy(&len, hdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
if (len > rcap) len = rcap;
if (len > 0 && read_all(fd, rbuf, len) != 0) return -1;
return (int)len;
}
/* 请求-响应:发一帧、读响应(带 3s 超时防挂)。返回响应负载长;-1=失败。 */
static int req_resp(uint16_t type, const void *pl, uint32_t len,
uint16_t *otype, uint8_t *obuf, uint32_t ocap)
{
if (send_frame(g_fd, type, 1, pl, len) != 0) return -1;
struct pollfd p;
p.fd = g_fd; p.events = POLLIN; p.revents = 0;
int r = poll(&p, 1, 3000);
if (r <= 0 || !(p.revents & POLLIN)) return -1;
return read_frame(g_fd, otype, obuf, ocap);
}
static int fmt_bps(int fmt)
{
switch (fmt) {
case AUDIO_FMT_S16: return 2;
case AUDIO_FMT_S24:
case AUDIO_FMT_S32:
case AUDIO_FMT_FLOAT:
case AUDIO_FMT_DOP: return 4;
default: return 2;
}
}
static const char *fmt_name(int fmt)
{
switch (fmt) {
case AUDIO_FMT_S16: return "S16";
case AUDIO_FMT_S24: return "S24";
case AUDIO_FMT_S32: return "S32";
case AUDIO_FMT_FLOAT: return "FLOAT";
case AUDIO_FMT_DOP: return "DoP";
default: return "?";
}
}
static int cmd_status(void)
{
uint8_t rbuf[MEOW_IPC_MAX];
uint16_t rt;
int n = req_resp(MSG_AUDIO_QUERY, NULL, 0, &rt, rbuf, sizeof rbuf);
if (n < 0) { fprintf(stderr, "meow_audio: no audiod (%s), status unavailable\n", g_sock); return 1; }
if (rt != MSG_AUDIO_STATE) { fprintf(stderr, "meow_audio: unexpected reply type 0x%04x\n", rt); return 1; }
rbuf[n < (int)sizeof rbuf - 1 ? n : (int)sizeof rbuf - 1] = 0;
fputs((const char *)rbuf, stdout);
return 0;
}
/* ---- monitor:终端实时电平表(200ms 刷新,文本电平条;数据源=MSG_AUDIO_STATE 的 rms/peak 键) ---- */
static volatile sig_atomic_t g_mon_stop = 0;
static void on_mon_sig(int sig) { (void)sig; g_mon_stop = 1; }
/* 从 STATE 文本取整型字段(key 含 '=',如 "rms=";缺省 def) */
static int txt_get_int(const char *t, const char *key, int def)
{
const char *p = strstr(t, key);
return p ? atoi(p + strlen(key)) : def;
}
static void txt_get_str(const char *t, const char *key, char *out, size_t n)
{
const char *p = strstr(t, key);
if (!p) { snprintf(out, n, "-"); return; }
p += strlen(key);
size_t k = 0;
while (p[k] && p[k] != ' ' && p[k] != '\n' && p[k] != '\t' && k < n - 1) { out[k] = p[k]; k++; }
out[k] = 0;
}
/* deci-dB → "-13.1"(地板显 "-inf") */
static void fmt_db10(char *out, size_t n, int v)
{
if (v <= -990) { snprintf(out, n, " -inf"); return; }
int neg = v < 0;
int a = neg ? -v : v;
snprintf(out, n, "%s%3d.%d", neg ? "-" : " ", a / 10, a % 10);
}
/* 文本电平条:-60..0 dBFS → 0..30 格 */
static void print_bar(int db10)
{
int cells = 0;
if (db10 > -600) cells = (db10 + 600) * 30 / 600;
if (cells > 30) cells = 30;
putchar('[');
for (int i = 0; i < 30; i++) putchar(i < cells ? '#' : '-');
putchar(']');
}
static int cmd_monitor(int secs)
{
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
signal(SIGINT, on_mon_sig);
signal(SIGTERM, on_mon_sig);
printf("monitor: 5Hz 刷新,%s(Ctrl-C 退出)\n", secs > 0 ? "定时自动停" : "持续监视");
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t t0 = (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
uint8_t rbuf[MEOW_IPC_MAX];
uint16_t rt;
while (!g_mon_stop) {
int n = req_resp(MSG_AUDIO_QUERY, NULL, 0, &rt, rbuf, sizeof rbuf);
if (n < 0 || rt != MSG_AUDIO_STATE) {
fprintf(stderr, "\nmeow_audio: query failed (audiod died?)\n");
return 1;
}
rbuf[n < (int)sizeof rbuf - 1 ? n : (int)sizeof rbuf - 1] = 0;
const char *txt = (const char *)rbuf;
int active = txt_get_int(txt, "active=", 0);
int rms = txt_get_int(txt, "rms=", -990);
int pk = txt_get_int(txt, "peak=", -990);
int vol = txt_get_int(txt, "vol=", -1);
int port = txt_get_int(txt, "port=", -1);
int rate = txt_get_int(txt, "rate=", 0);
char app[40];
txt_get_str(txt, "app=", app, sizeof app);
char rmsb[16], pkb[16];
fmt_db10(rmsb, sizeof rmsb, rms);
fmt_db10(pkb, sizeof pkb, pk);
if (active) {
printf("\rLIVE rms=%sdBFS peak=%sdBFS ", rmsb, pkb);
print_bar(pk);
printf(" %s %dHz port=%d vol=%d%% ", app, rate, port, vol);
} else {
printf("\rIDLE 无活跃流(等待播放…) ");
}
fflush(stdout);
if (secs > 0) {
clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t now = (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
if (now - t0 >= (uint64_t)secs * 1000) break;
}
usleep(200000); /* 200ms 刷新 */
}
putchar('\n');
return 0;
}
static int cmd_route(int port)
{
m_audio_route_t r; r.port = (uint32_t)port;
uint8_t rbuf[256]; uint16_t rt;
int n = req_resp(MSG_AUDIO_ROUTE, &r, sizeof r, &rt, rbuf, sizeof rbuf);
if (n < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
printf("route %d -> %s\n", port, (port == 0) ? "auto" : (port == 3 ? "4.4" : "3.5"));
return 0;
}
static int cmd_vol(int pct)
{
m_audio_volume_t v; v.pct = (int32_t)pct;
uint8_t rbuf[256]; uint16_t rt;
int n = req_resp(MSG_AUDIO_VOLUME, &v, sizeof v, &rt, rbuf, sizeof rbuf);
if (n < 0 || rt != MSG_ACK) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
m_ack_t a; memcpy(&a, rbuf, sizeof a);
printf("vol %u%%%s\n", (unsigned)a.status, (pct >= 0 ? " (set)" : " (query)"));
return 0;
}
/* P2 蓝牙:切换输出目标(0=DAC 1=BT)。audiod 侧重配 PCM;BT 时音量走 AVRCP,不下发 DAC 反转。 */
static int cmd_output(int target)
{
m_audio_output_t o; o.target = (uint32_t)target;
uint8_t rbuf[256]; uint16_t rt;
int n = req_resp(MSG_AUDIO_OUTPUT, &o, sizeof o, &rt, rbuf, sizeof rbuf);
if (n < 0 || rt != MSG_ACK) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
m_ack_t a; memcpy(&a, rbuf, sizeof a);
printf("output %s\n", a.status == AUDIO_OUT_BT ? "bt" : "dac");
return 0;
}
/* 正弦生成:填 nframes 帧(grant 交织格式)到 buf(上限 bufcap,帧对齐)。
* start 为起始帧号;返回实际写入字节数。 */
static size_t gen_sine(uint8_t *buf, size_t bufcap, int grant_fmt, int grant_ch,
double freq, int rate, size_t start, size_t nframes, double amp)
{
int bps = fmt_bps(grant_fmt);
size_t fs = (size_t)grant_ch * (size_t)bps;
if (fs == 0) fs = 1;
size_t n = (bufcap / fs) * fs;
if (n / fs > nframes) n = nframes * fs;
size_t done = 0;
for (size_t i = 0; i < n / fs; i++) {
double ph = 2.0 * M_PI * freq * (double)(start + i) / (double)rate;
double s = sin(ph) * amp;
long v = (long)(s * 32767.0);
if (v > 32767) v = 32767;
if (v < -32768) v = -32768;
long vs = v; /* S16 值 */
if (grant_fmt == AUDIO_FMT_S32 || grant_fmt == AUDIO_FMT_S24) vs = v << 16;
for (int c = 0; c < grant_ch; c++) {
if (grant_fmt == AUDIO_FMT_S16) {
int16_t xx = (int16_t)v;
memcpy(buf + done, &xx, 2); done += 2;
} else {
int32_t xx = (int32_t)(vs);
memcpy(buf + done, &xx, 4); done += 4;
}
}
}
return done;
}
static int cmd_tone(int freq, int ms, int rate, int fmt)
{
m_audio_open_t o;
memset(&o, 0, sizeof o);
snprintf(o.app_id, sizeof o.app_id, "audio-cli");
o.rate = (uint32_t)rate;
o.ch = 2;
o.fmt = (uint16_t)fmt;
uint8_t rbuf[512]; uint16_t rt;
int n = req_resp(MSG_AUDIO_OPEN, &o, sizeof o, &rt, rbuf, sizeof rbuf);
if (n < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
if (rt != MSG_AUDIO_OPEN_ACK) { fprintf(stderr, "meow_audio: unexpected reply 0x%04x\n", rt); return 1; }
m_audio_open_ack_t a; memcpy(&a, rbuf, sizeof a);
if (a.err != MEOW_OK) {
fprintf(stderr, "meow_audio: open failed (err=%d): %s\n", a.err,
a.err == MEOW_ERR_NODEV ? "no usable PCM device" :
a.err == MEOW_ERR_INVAL ? "invalid params" : "error");
return 1;
}
printf("tone: sid=%u grant=%uHz/%uch/%s\n", (unsigned)a.stream_id,
(unsigned)a.grant_rate, (unsigned)a.grant_ch, fmt_name((int)a.grant_fmt));
double amp = 0.3;
size_t total_frames = (size_t)((uint64_t)ms * (uint64_t)rate / 1000);
uint8_t pcm[MEOW_IPC_MAX];
size_t fs = (size_t)a.grant_ch * (size_t)fmt_bps((int)a.grant_fmt);
if (fs == 0) fs = 1;
size_t sent_frames = 0;
while (sent_frames < total_frames) {
size_t want = total_frames - sent_frames;
if (want > (size_t)(sizeof pcm / fs)) want = sizeof pcm / fs;
size_t by = gen_sine(pcm, sizeof pcm, (int)a.grant_fmt, (int)a.grant_ch,
(double)freq, rate, sent_frames, want, amp);
if (by == 0) break;
if (send_frame(g_fd, MSG_AUDIO_WRITE, 0, pcm, (uint32_t)by) != 0) {
fprintf(stderr, "meow_audio: write failed (audiod died?)\n");
return 1;
}
sent_frames += by / fs;
}
/* 停止 + 等待排空(WAV 完整落盘 / 真实 DAC 播放完) */
m_audio_stream_t s; s.stream_id = a.stream_id;
req_resp(MSG_AUDIO_STOP, &s, sizeof s, &rt, rbuf, sizeof rbuf);
uint64_t deadline_ms = (uint64_t)ms + 3000;
uint64_t t0 = 0;
{
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
t0 = (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
for (;;) {
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t now = (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
if (now - t0 > deadline_ms) break;
req_resp(MSG_AUDIO_QUERY, NULL, 0, &rt, rbuf, sizeof rbuf);
if (rt == MSG_AUDIO_STATE) {
if (strstr((const char *)rbuf, "active=0")) break;
}
usleep(100000); /* 100ms 轮询 */
}
printf("tone done: sent %zu frames\n", sent_frames);
close(g_fd);
g_fd = -1;
return 0;
}
static void usage(const char *argv0)
{
fprintf(stderr, "usage: %s status | tone [freq] [ms] [rate] [S16|S24|S32] | route <0|2|3> | vol [0-100] | output <dac|bt> | monitor [秒]\n"
" output: 切换输出目标(dac=本地 hw:0,0 / bt=蓝牙 A2DP source;bt 时音量=AVRCP 侧控)\n"
" monitor: 终端实时电平表(200ms 刷新+文本电平条;缺省 10s,0=持续到 Ctrl-C)\n"
" env: MEOW_AUDIO_SOCK (socket 缺省 %s)\n", argv0, AUD_SOCK_DEFAULT);
}
int main(int argc, char **argv)
{
const char *e = getenv("MEOW_AUDIO_SOCK");
if (e && e[0]) snprintf(g_sock, sizeof g_sock, "%s", e);
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--sock") && i + 1 < argc) { snprintf(g_sock, sizeof g_sock, "%s", argv[++i]); }
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { usage(argv[0]); return 0; }
}
if (argc < 2) { usage(argv[0]); return 2; }
g_fd = connect_sock();
if (g_fd < 0 && strcmp(argv[1], "status") != 0 && strcmp(argv[1], "vol") != 0
&& strcmp(argv[1], "route") != 0 && strcmp(argv[1], "tone") != 0) {
/* 未知命令用 status 兜底说明 */
}
/* 连接失败:status/vol/route/tone 各自报错;未知命令直接 usage */
const char *cmd = argv[1];
if (!strcmp(cmd, "status")) {
return cmd_status();
} else if (!strcmp(cmd, "route")) {
if (argc < 3) { usage(argv[0]); return 2; }
int port = atoi(argv[2]);
if (port != 0 && port != 2 && port != 3) { fprintf(stderr, "route: port 0/2/3\n"); return 2; }
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
int rc = cmd_route(port); close(g_fd); return rc;
} else if (!strcmp(cmd, "vol")) {
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
int pct = (argc >= 3) ? atoi(argv[2]) : -1;
if (pct > 100) pct = 100;
int rc = cmd_vol(pct); close(g_fd); return rc;
} else if (!strcmp(cmd, "output")) {
if (argc < 3) { fprintf(stderr, "output: <dac|bt>\n"); return 2; }
int t = (strcmp(argv[2], "bt") == 0) ? AUDIO_OUT_BT : (strcmp(argv[2], "dac") == 0) ? AUDIO_OUT_DAC : -1;
if (t < 0) { fprintf(stderr, "output: dac|bt\n"); return 2; }
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s)\n", g_sock); return 1; }
int rc = cmd_output(t); close(g_fd); return rc;
} else if (!strcmp(cmd, "tone")) {
int freq = (argc >= 3) ? atoi(argv[2]) : 440;
int ms = (argc >= 4) ? atoi(argv[3]) : 500;
int rate = (argc >= 5) ? atoi(argv[4]) : 44100;
int fmt = AUDIO_FMT_S16;
if (argc >= 6) {
if (!strcmp(argv[5], "S16")) fmt = AUDIO_FMT_S16;
else if (!strcmp(argv[5], "S24")) fmt = AUDIO_FMT_S24;
else if (!strcmp(argv[5], "S32")) fmt = AUDIO_FMT_S32;
else { fprintf(stderr, "tone: fmt S16|S24|S32(DoP 由 App 侧生成,CLI 不产 DoP)\n"); return 2; }
}
if (freq <= 0 || ms <= 0 || rate <= 0) { fprintf(stderr, "tone: bad params\n"); return 2; }
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
return cmd_tone(freq, ms, rate, fmt);
} else if (!strcmp(cmd, "monitor")) {
int secs = (argc >= 3) ? atoi(argv[2]) : 10;
if (secs < 0) secs = 10;
return cmd_monitor(secs);
}
usage(argv[0]);
return 2;
}
Binary file not shown.
BIN
View File
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
BIN
View File
Binary file not shown.
+75
View File
@@ -0,0 +1,75 @@
#define _GNU_SOURCE
/* probe_alsa.c — 探测 R3 Pro II CS43198 PCM 设备的 hw_params 约束(找可用的 access/format/rate)。
* mipsel 交叉编译后推到设备运行,输出 kernel 支持哪些组合,诊断「hw_params ioctl 失败」。 */
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sound/asound.h>
static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); }
static void pivset(struct snd_interval *iv, unsigned val)
{
iv->min = iv->max = val; iv->integer = 1; iv->openmin = iv->openmax = 0; iv->empty = 0;
}
static const char *fmt_name(unsigned f)
{
#define N(x) case x: return #x
switch (f) {
N(SNDRV_PCM_FORMAT_S8); N(SNDRV_PCM_FORMAT_U8); N(SNDRV_PCM_FORMAT_S16_LE);
N(SNDRV_PCM_FORMAT_S16_BE); N(SNDRV_PCM_FORMAT_U16_LE); N(SNDRV_PCM_FORMAT_U16_BE);
N(SNDRV_PCM_FORMAT_S24_LE); N(SNDRV_PCM_FORMAT_S24_BE); N(SNDRV_PCM_FORMAT_S32_LE);
N(SNDRV_PCM_FORMAT_S32_BE); N(SNDRV_PCM_FORMAT_FLOAT_LE); N(SNDRV_PCM_FORMAT_FLOAT_BE);
N(SNDRV_PCM_FORMAT_S24_3LE); N(SNDRV_PCM_FORMAT_S24_3BE); N(SNDRV_PCM_FORMAT_U24_LE);
N(SNDRV_PCM_FORMAT_S20_3LE); N(SNDRV_PCM_FORMAT_S20_3BE);
default: return "?";
}
#undef N
}
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0); /* 每行立即刷出,防 kill 丢输出 */
const char *dev = "/dev/snd/pcmC0D0p";
unsigned access[] = { SNDRV_PCM_ACCESS_MMAP_INTERLEAVED, SNDRV_PCM_ACCESS_RW_INTERLEAVED,
SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED, SNDRV_PCM_ACCESS_RW_NONINTERLEAVED, ~0u };
unsigned fmts[] = { SNDRV_PCM_FORMAT_S16_LE, SNDRV_PCM_FORMAT_S24_LE, SNDRV_PCM_FORMAT_S32_LE,
SNDRV_PCM_FORMAT_S24_3LE, ~0u };
unsigned rates[] = { 44100, 48000, 96000, 88200, ~0u };
for (unsigned ai = 0; access[ai] != ~0u; ai++) {
for (unsigned fi = 0; fmts[fi] != ~0u; fi++) {
printf("== access=%u fmt=%s ==\n", access[ai], fmt_name(fmts[fi]));
struct snd_pcm_hw_params p;
for (unsigned ri = 0; rates[ri] != ~0u; ri++) {
int fd = open(dev, O_RDWR | O_NONBLOCK);
if (fd < 0) { printf(" open: %s\n", strerror(errno)); break; }
/* 「全放行」初始化:memset 0 的 interval =「只能取 0」→ refine 必空 EINVAL
*(2026-08-31 真机全灭的根因;libasound snd_pcm_hw_params_any 语义) */
memset(&p, 0, sizeof p);
unsigned nm = SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1;
for (unsigned i = 0; i < nm; i++)
for (unsigned b = 0; b < sizeof p.masks[i].bits / sizeof p.masks[i].bits[0]; b++)
p.masks[i].bits[b] = ~0u;
unsigned ni = SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1;
for (unsigned i = 0; i < ni; i++) { p.intervals[i].min = 0; p.intervals[i].max = ~0u; }
/* mask 先清零再单点:全 1 会被 choose 取最低 bit(access→MMAP_INTERLEAVED) */
struct snd_mask *am = &p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(am, 0, sizeof *am); pmask(am, access[ai]);
struct snd_mask *fm = &p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(fm, 0, sizeof *fm); pmask(fm, fmts[fi]);
struct snd_mask *sm = &p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(sm, 0, sizeof *sm); pmask(sm, SNDRV_PCM_SUBFORMAT_STD);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], 2);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], rates[ri]);
p.rmask = ~0u; p.info = 0; p.msbits = 0; p.rate_num = 0; p.rate_den = 0; p.flags = 0;
int rc = ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p);
printf(" @%u: %s\n", rates[ri], rc ? strerror(errno) : "OK");
close(fd);
}
}
}
return 0;
}
@@ -0,0 +1,14 @@
# meow_audiod 板载服务配置(svcd 托管;由 S89meow_svcd 首启预置到 /usr/data/meow/services.d/)
#
# trigger=manual:audiod 不在开机自动拉起 —— 由 launcher 在确认 MeowOS 模式后 `meow_svcd start
# audiod`(幂等)。audiod 自身还有启动门控(/tmp/meow_boot_mode != meow → exit(0)),双重保证
# HibyOS 模式下它物理不存在、绝不碰声卡。
# restart=on-fail:运行中崩溃 → svcd 重拉(关流状态由客户端 socket EOF 回收兜底),不级联。
exec=/usr/bin/meow_audiod
args=
enabled=1
restart=on-fail
backoff_ms=2000
max_restart=5
trigger=manual
cap=
Binary file not shown.
File diff suppressed because it is too large. Load diff
+116
View File
@@ -0,0 +1,116 @@
/* test_bt_dev.c — 蓝牙 DEV 自识别宿主验证(纯逻辑,bt_dev.c)。
*
* 覆盖矩阵(对齐任务B 宿主测试要求):
* D1 bt_dev_detect_mac:a2dpsrc/<MAC> 端点 → 提取 MAC(含 a2dp_source、多空行、多设备取第一个、
* 无 a2dpsrc / 空输出 / NULL → -1)。
* D2 bt_dev_rewrite:真实 alsa.conf 文本 → DEV=、device 两处 MAC 换成目标,保留 codec/其余 token;
* 同值 → 1(无需改动);无 pcm.bluealsa / 无 DEV= → -1;带引号风格保留。
*
* 编译(在 patch_v2/meow_audiod 下):
* cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c
* 运行:./test_bt_dev
*/
#include "bt_dev.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_pass = 0, g_fail = 0;
static void check(int cond, const char *name)
{
if (cond) { printf(" PASS %s\n", name); g_pass++; }
else { printf(" FAIL %s\n", name); g_fail++; }
}
int main(void)
{
setbuf(stdout, NULL);
/* ---- D1 设备列表 → 选中 a2dpsrc 连接设备 ---- */
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"a2dpsrc/E0:42:6D:79:C5:98\na2dpsnk/E0:42:6D:79:C5:98\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1a a2dpsrc/<MAC> -> extract MAC");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"profile/aptx\n\na2dp_source/33:DE:2E:21:AD:30\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1b a2dp_source/<MAC> -> extract MAC");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(
"a2dpsrc/AA:BB:CC:DD:EE:FF 0\na2dpsrc/11:22:33:44:55:66 1\n", mac, sizeof mac);
check(rc == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, "D1c multi-device -> first a2dpsrc");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac("\n", mac, sizeof mac);
check(rc == -1, "D1d empty output -> -1");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac("a2dpsnk/11:22:33:44:55:66\n", mac, sizeof mac);
check(rc == -1, "D1e only sink (no a2dpsrc) -> -1");
}
{
char mac[32] = "";
int rc = bt_dev_detect_mac(NULL, mac, sizeof mac);
check(rc == -1, "D1f NULL input -> -1");
}
/* ---- D2 DEV 串入 / 保留其它 token ---- */
{
const char *conf =
"pcm.bluealsa:DEV=E0:42:6D:79:C5:98 {\n"
" type plug\n"
" slave {\n"
" pcm {\n"
" type bluealsa\n"
" device E0:42:6D:79:C5:98\n"
" profile \"a2dp\"\n"
" codec \"LDAC\"\n"
" ldac_eqmid \"LDAC_ABR\"\n"
" }\n"
" }\n"
"}\n";
char *out = NULL; size_t olen = 0;
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
check(rc == 0 && out != NULL, "D2a rewrite returns changed");
check(out && strstr(out, ":DEV=33:DE:2E:21:AD:30 {"), "D2b DEV= updated");
check(out && strstr(out, "device 33:DE:2E:21:AD:30\n"), "D2c device updated");
check(out && strstr(out, "codec \"LDAC\"") && strstr(out, "ldac_eqmid \"LDAC_ABR\""),
"D2d codec/other tokens preserved");
/* 旧 MAC 不再出现 */
check(out && !strstr(out, "E0:42:6D:79:C5:98"), "D2e old MAC gone");
free(out);
}
{
const char *conf =
"pcm.bluealsa:DEV=33:DE:2E:21:AD:30 {\n"
" type plug\n slave {\n pcm {\n"
" type bluealsa\n device 33:DE:2E:21:AD:30\n"
" profile \"a2dp\"\n codec \"LDAC\"\n }\n }\n}\n";
char *out = NULL; size_t olen = 0;
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
check(rc == 1 && out == NULL, "D2f same MAC -> unchanged (idempotent)");
}
{
const char *conf = "pcm.bluealsa {\n type bluealsa\n device \"E0:42:6D:79:C5:98\"\n}\n";
char *out = NULL; size_t olen = 0;
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
check(rc == 0 && out && strstr(out, "device \"33:DE:2E:21:AD:30\""),
"D2g quoted device preserved (no DEV= token)");
free(out);
}
{
char *out = NULL; size_t olen = 0;
int rc = bt_dev_rewrite("pcm.hw {\n type hw\n}\n", 21, "33:DE:2E:21:AD:30", &out, &olen);
check(rc == -1 && out == NULL, "D2h no pcm.bluealsa -> -1");
}
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
Binary file not shown.
+3 -1
View File
@@ -178,7 +178,9 @@ static void log_cleanup_old(void)
struct dirent *de;
while ((de = readdir(d)) != NULL && n < 128) {
const char *nm = de->d_name;
if (strncmp(nm, "meow_", 5) != 0 && strncmp(nm, "crash_", 6) != 0) continue;
/* 收纳 meow_/crash_/perf_/hang_(perf_=RMM 基线、hang_=卡死快照,同 logs/ 1GB 纪律)。 */
if (strncmp(nm, "meow_", 5) != 0 && strncmp(nm, "crash_", 6) != 0 &&
strncmp(nm, "perf_", 5) != 0 && strncmp(nm, "hang_", 5) != 0) continue;
char path[CRASH_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s", logs, nm) >= (int)sizeof path) continue;
struct stat st;
Binary file not shown.
File diff suppressed because it is too large. Load diff
+77 -3
View File
@@ -102,9 +102,10 @@ int settings_cache_registered = 0; /* M2.24:监听只注册一次 */
* settings_volume(写入 DAC 的映射值)为权威跟踪值。 */
void meow_dac_vol_write(int pct)
{
int dac = pct * 255 / 100;
if (dac < 0) dac = 0;
if (dac > 255) dac = 255;
int t = 100 - pct, dac;
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
if (dac > 255) dac = 255; if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
char cmd[160];
snprintf(cmd, sizeof cmd,
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
@@ -153,8 +154,65 @@ static unsigned long long vol_ramp_next_at = 0;
static unsigned long long vol_ramp_press_at = 0; /* M2.18 Bug 4:本方向键按下时刻(长按判据) */
/* ---- 插孔消抖(2026-09-01,真机日志实证)----
* 4.4/3.5 耳机插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生 VOL_UP 幽灵按住
* (EVIOCGKEY 轮询到持续 down),vol_ramp 一路爬到上限:日志 225391「volume UP <- ramp」
* → 227213「ramp end -> 50」,紧接着 audiod 才报「route: jack changed -> 4.4 balance」。
* 修法:/sys/class/switch/{headset,balance}/state 任一变化后 JACK_DEBOUNCE_MS 内忽略音量键
* 边沿(vol_ramp_begin 不起、vol_ramp_tick 在爬也中止)。插拔耳机时没人按音量,误伤可忽略。
* 检测走 1s 节流读缓存——绝不在 10ms 主循环热路径上每圈读文件。 */
/* 宿主测试可 -D 覆盖(同 meow_settings.c MEOW_SETTINGS_FILE 惯例) */
#ifndef JACK_HS_STATE
#define JACK_HS_STATE "/sys/class/switch/headset/state"
#endif
#ifndef JACK_BAL_STATE
#define JACK_BAL_STATE "/sys/class/switch/balance/state"
#endif
static int jack_combo_cached = -1; /* hs*2+bal;-1=未初始化/节点缺失 */
static unsigned long long jack_poll_at = 0; /* 上次节流读时刻 */
static unsigned long long jack_changed_at = 0; /* 最近一次检测到插孔变化的时刻(0=无) */
/* 读两个插孔开关节点合成 hs*2+bal;两节点都不可读返回 -1(不更新缓存,不误判变化)。 */
static int jack_combo_read(void)
{
int hs = read_file_int(JACK_HS_STATE);
int bal = read_file_int(JACK_BAL_STATE);
if (hs < 0 && bal < 0) return -1;
return (hs > 0 ? 2 : 0) | (bal > 0 ? 1 : 0);
}
/* 返回 1=插孔变化后消抖窗口内(音量键边沿应忽略/在爬 ramp 应中止)。
* 自节流:每 JACK_POLL_MS 才真正读一次 sysfs。 */
static int jack_debounce_active(void)
{
unsigned long long now = now_ms();
if (now - jack_poll_at >= JACK_POLL_MS) {
jack_poll_at = now;
int c = jack_combo_read();
if (c >= 0) {
if (jack_combo_cached >= 0 && c != jack_combo_cached) {
jack_changed_at = now;
logmsg("jack state changed (%d -> %d), vol-key debounce %dms",
jack_combo_cached, c, JACK_DEBOUNCE_MS);
}
jack_combo_cached = c;
}
}
return jack_changed_at && (now - jack_changed_at < (unsigned long long)JACK_DEBOUNCE_MS);
}
static void vol_ramp_begin(int delta)
{
if (jack_debounce_active()) { /* 插孔瞬态期:忽略音量键边沿(幽灵按下) */
logmsg("volume adj %+d ignored (jack debounce)", delta);
return;
}
int target = vol_clamp(settings_volume + delta);
if (target == settings_volume) return;
vol_ramp_dir = (target > settings_volume) ? 1 : -1;
@@ -175,6 +233,22 @@ int vol_ramp_tick(int key_held)
unsigned long long now = now_ms();
if (now < vol_ramp_next_at) return 1;
vol_ramp_next_at = now + VOL_RAMP_MS;
/* 插孔消抖:插拔瞬态的幽灵按住正在驱动 ramp(真机实证 24%→上限 50)——
* 一旦检测到插孔变化(1s 节流轮询),立即中止在爬的 ramp。 */
if (jack_debounce_active()) {
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save();
logmsg("volume ramp aborted (jack debounce) -> %d", settings_volume);
return 0;
}
/* 兜底:ramp 总时长上限 VOL_RAMP_MAX_MS——防任何「持续按住」假象(ADC 噪声卡 key_held=1)
* 一次打满/打到底。正常长按 0→上限(通常 ≤2.7s)不受影响;到顶想再调需松手重按。 */
if (now - vol_ramp_press_at >= (unsigned long long)VOL_RAMP_MAX_MS) {
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save();
logmsg("volume ramp force-stop (%dms cap) -> %d", VOL_RAMP_MAX_MS, settings_volume);
return 0;
}
/* 先看是否已到「当前目标值」(settings_volume 等于 target,而非候选 v)——
* 因为一旦在 target 再取 v=settings_volume+dir 便越过 target,导致 hold_wait 失效继续爬。 */
if (settings_volume == vol_ramp_target) {
+10 -7
View File
@@ -41,7 +41,7 @@ void apply_user_brightness(void)
}
int v = meow_settings_brightness_clamped();
if (v <= 0) v = 60; /* 防御:clamp 后不可能 ≤0,仅兜底 */
write_brightness(v);
write_brightness(v, "apply_user_brightness");
g_sys.brightness = v;
meow_brightness_pending = 0;
logmsg("apply user brightness (MeowOS takeover) -> %d", v);
@@ -73,13 +73,16 @@ static void write_file(const char *path, const char *val)
}
void write_brightness(int val)
void write_brightness(int val, const char *src)
{
/* 亮度幽灵排查(2026-09-01:用户报告插耳机瞬间亮度自己跳到 raw 上限,写者未知):
* 每次写背光都留值+调用源线索,下次「亮度自己跳」可直接定位写者。 */
char buf[8];
int l = snprintf(buf, sizeof buf, "%d\n", val);
int fd = open(BACKLIGHT, O_WRONLY);
if (fd < 0) { logmsg("backlight open fail: %s", strerror(errno)); return; }
if (write(fd, buf, l) < 0) logmsg("backlight write %d: %s", val, strerror(errno));
if (fd < 0) { logmsg("backlight <- %d (src=%s) open fail: %s", val, src, strerror(errno)); return; }
if (write(fd, buf, l) < 0) logmsg("backlight <- %d (src=%s): %s", val, src, strerror(errno));
else logmsg("backlight <- %d (src=%s)", val, src);
close(fd);
}
@@ -107,7 +110,7 @@ void ensure_backlight(void)
}
int target = (saved_brightness > 0) ? saved_brightness : 60;
logmsg("backlight off (cur=%d bl_power=%d) -> %d", cur, blp, target);
if (cur <= 0) write_brightness(target);
if (cur <= 0) write_brightness(target, "ensure_backlight");
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
}
@@ -126,7 +129,7 @@ void restore_backlight(void)
{
if (menu_opened_blank) {
logmsg("restore: panel was blanked at menu open, re-blank (0 + FBIOBLANK 4)");
write_brightness(0);
write_brightness(0, "restore_backlight(re-blank)");
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 4); close(fbfd); }
menu_opened_blank = 0;
@@ -138,7 +141,7 @@ void restore_backlight(void)
int target = (saved_brightness > 0) ? saved_brightness : 60;
logmsg("restore backlight (cur=%d bl_power=%d) -> %d (saved=%d)",
cur, blp, target, saved_brightness);
if (cur <= 0) write_brightness(target);
if (cur <= 0) write_brightness(target, "restore_backlight");
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
}
}
+322
View File
@@ -0,0 +1,322 @@
/* bg_page.c — 批③ 后台管理页(桌面顶栏居右按钮进入)。
*
* 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar:
* 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action,
* act=bgm_top_action → bgmgr_kill_all,**不杀受保护/当前前台/非后台**,经 bgmgr_can_kill 门控)。
* 列表(live=1 约 2s 重扫,实时增删):
* - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮**
* (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。
* - 行=MD3 圆角卡(meow_gfx_fill_round r=12 surfaceContainer;卡在行高内上下对称内缩
* BGM_CARD_GAP/2,行中心=卡中心,相邻卡 8px 外距——与主界面列表/ ncm 歌单同手法)。
* - 空态「无后台 App」。
* 数据源=amsd FG/BG 注册表(后台常驻集=launcher meow_bg 表,EV BG/FG 上报进 amsd)+ 每进程
* pcpu/rss(meow_svc_perf 同源 /proc 聚合,本页在 launcher 内同口径取)。
*
* 按钮化(相对 agent-425 的「点行 kill + 顶置清除动作行」):
* - kill:改「点整行」为「行最右独立杀按钮」——行自绘(bgm_row_draw)+ 自定义命中(bgm_row_tap),
* 命中几何=bgm_btn_rect(行右 46×34 圆角按钮);行体其它区域 point 回 0(不杀)。
* - 清:删顶置「一键清除后台」SUB_ACT 动作行,改为顶栏标题栏右侧 action 按钮(top_action)。
*/
#include "meow_launcher_internal.h"
#include "meow_bgplay.h"
#include "meow_bgmgr.h"
#include <ctype.h>
/* 行内杀按钮尺寸(圆心/圆角定位用;命中与绘制共用同一几何,保证所见即所点) */
#define BGM_BTN_W 46
#define BGM_BTN_H 34
/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=SUB_ITEM_H)
* 不变——卡片在其行高内上下对称内缩 BGM_CARD_GAP/2,故行内内容(按行高垂直居中)仍落卡中心;
* 相邻卡顶间距恒=SUB_ITEM_H=卡高+BGM_CARD_GAP。要求偶数,保证上下对称。 */
#define BGM_CARD_GAP 8
static inline int bgm_row_card_y(int row_top) { return row_top + BGM_CARD_GAP / 2; }
static inline int bgm_row_card_h(int h) { return h - BGM_CARD_GAP; }
typedef struct {
struct app *apps;
int napps;
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
int n;
/* 行副文本存储:sub_row_t.sub 被 SDK 行池/自绘在整页会话内持续读取,
* 不能指向 bgm_rows 的栈帧(曾用函数内 char sub[80] → 悬垂,页面副标题为乱码)。 */
char subs[MEOW_BGMR_ROW_MAX][80];
} bgm_ctx_t;
/* 找 struct app(launcher 应用表)by id */
static struct app *bgm_app_find(struct app *apps, int napps, const char *app_id)
{
if (!apps || !app_id) return NULL;
for (int i = 0; i < napps; i++)
if (apps[i].id[0] && !strcmp(apps[i].id, app_id)) return &apps[i];
return NULL;
}
/* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */
static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid,
char *name, int nl, char *icon, int il,
char *protected_app, int *is_fg)
{
(void)pid;
bgm_ctx_t *b = ctx;
struct app *a = bgm_app_find(b->apps, b->napps, app_id);
*protected_app = 0;
*is_fg = 0;
if (!a) return -1; /* 未注册 → name 回退 app_id */
if (a->name[0]) snprintf(name, nl, "%s", a->name);
if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : "");
/* 刻意**不**用 a->framework:app.json 的 framework=1 意为「用 meow_ui.a 的 SDK 框架 App」
* (docs/APP_DEV_GUIDE.md §9,推荐且现有 App 全写 1),不是「系统 App」。曾把它当「禁止 kill」
* 门控 → 后台页所有行的「杀」按钮被置灰、点了不杀也不留日志(2026-11-04 真机 dev.player.ncm)。
* launcher 自身组件(状态栏/设置页/子页…)不在 App 表也不在后台表,无需此位保护。 */
return 0;
}
static int bgm_enumerate(void *ctx, bgmgr_row_t *rows, int max)
{
(void)ctx;
/* 逐槽扫描并**压缩**(g_bg 可能有空洞),跳过已发出终止的条目:
* meow_bg_terminate 只置 term_at_ms 发 SIGTERM,真正的 reap/摘表在 meow_bg_tick(launcher
* 主循环),它在本页 meow_subpage_run 阻塞期间**不跑**——若不在此过滤,kill/清后旧行会一直
* 残留(pid 已死/0 CPU),页面上看就是「点不动」。过滤后 kill/一键清当下即从列表消失(可见反馈)。 */
int k = 0;
for (int i = 0; i < MEOW_BG_MAX && k < max; i++) {
const meow_bg_entry_t *e = meow_bg_at(i);
if (!e || !e->used || !e->app_id[0]) continue;
if (e->term_at_ms) continue; /* 已进入终止序列(kill/宽限回收/卡拔)→ 行不再显示 */
memset(&rows[k], 0, sizeof rows[k]);
snprintf(rows[k].app_id, sizeof rows[k].app_id, "%s", e->app_id);
rows[k].pid = e->pid;
rows[k].keepalive = (e->keepalive ? 1 : 0);
rows[k].on_card = (e->on_card ? 1 : 0);
rows[k].bg_since_ms = e->bg_since_ms;
k++;
}
return k;
}
/* proc_sample:/proc/<pid>/stat(utime+stime 差分) + /proc/<pid>/status(VmRSS)。
* 与 meow_svc_perf 897-983 每进程 pcpu/rss 聚合同口径(同 /proc 源)。 */
static long g_cpu_pid[MEOW_BG_MAX] = {0};
static unsigned long long g_cpu_tick[MEOW_BG_MAX] = {0};
static int bgm_proc_sample(void *ctx, pid_t pid, double *pcpu, long *rss_kb)
{
(void)ctx;
if (pid <= 0) return -1;
char path[64], line[512];
long rss = 0;
snprintf(path, sizeof path, "/proc/%d/status", pid);
int fd = open(path, O_RDONLY);
if (fd >= 0) {
ssize_t nr = read(fd, line, sizeof line - 1); close(fd);
if (nr > 0) { line[nr] = 0;
char *p = strstr(line, "VmRSS:");
if (p) p = strchr(p, ' ');
if (p) { while (*p == ' ' || *p == '\t') p++; rss = atol(p); }
}
}
double pc = 0.0;
snprintf(path, sizeof path, "/proc/%d/stat", pid);
fd = open(path, O_RDONLY);
if (fd >= 0) {
ssize_t nr = read(fd, line, sizeof line - 1); close(fd);
if (nr > 0) {
line[nr] = 0;
char *rp = strrchr(line, ')');
if (rp) {
char *p = rp + 2;
unsigned long long ut[2] = {0, 0};
for (int f = 0; f < 14; f++) {
while (*p == ' ') p++;
char *end; unsigned long long v = strtoull(p, &end, 10);
if (end == p) { p++; continue; }
p = end;
if (f == 11) ut[0] = v; /* utime */
if (f == 12) ut[1] = v; /* stime */
}
unsigned long long cur = ut[0] + ut[1];
static double hz = 0.0;
if (hz <= 0) { hz = (double)sysconf(_SC_CLK_TCK); if (hz <= 0) hz = 100.0; }
unsigned long long prev = 0; int had = 0, slot = -1;
for (int i = 0; i < MEOW_BG_MAX; i++) {
if (g_cpu_pid[i] == pid) { prev = g_cpu_tick[i]; had = 1; slot = i; break; }
}
if (!had) {
for (int i = 0; i < MEOW_BG_MAX; i++) if (g_cpu_pid[i] == 0) { slot = i; break; }
if (slot < 0) slot = 0;
}
if (slot >= 0) { g_cpu_pid[slot] = pid; g_cpu_tick[slot] = cur; }
if (had) { pc = 100.0 * (double)(cur - prev) / hz; if (pc < 0) pc = 0; }
}
}
}
*pcpu = pc;
*rss_kb = rss;
return 0;
}
/* ---- rows(填 sub_row_t 列表;每后台 App 一行,空态一行)---- */
static int bgm_rows(void *ctx, sub_row_t *rbuf, int max)
{
bgm_ctx_t *b = ctx;
bgmgr_ops_t ops;
memset(&ops, 0, sizeof ops);
ops.ctx = b;
ops.enumerate = bgm_enumerate;
ops.app_meta = bgm_app_meta;
ops.proc_sample = bgm_proc_sample;
b->n = bgmgr_collect(&ops, b->rows, MEOW_BGMR_ROW_MAX);
int r = 0;
if (b->n == 0) {
if (r < max) {
rbuf[r].kind = SUB_ROW;
rbuf[r].label = meow_str_zh("bgmgr.empty", "无后台 App");
rbuf[r].sub = NULL;
rbuf[r].act = -1;
r++;
}
return r;
}
for (int i = 0; i < b->n && r < max; i++) {
/* 副文本写进 ctx 的持久缓冲(b->subs):sub_row_t.sub 会被 SDK 行池在整页会话内反复读取,
* 指向本函数栈帧的 buffer 会在返回后变成乱码/越界(真机实证:副标题全是乱码字节)。 */
snprintf(b->subs[i], sizeof b->subs[i], "CPU %.1f%% · %s · %s", b->rows[i].pcpu,
bgmgr_mem_str(b->rows[i].rss_kb), bgmgr_status_str(&b->rows[i]));
rbuf[r].kind = SUB_ROW;
rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id;
rbuf[r].sub = b->subs[i];
rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */
r++;
}
return r;
}
/* kill 通道(bgmgr_kill_all 的函数指针签名:void(*)(const char*)) */
static void bgm_kill_fn(const char *app_id) { meow_bg_terminate(app_id); }
/* ---- 行内杀按钮矩形(绘制与命中共用;行视口矩形 rx,ry,rw,rh → 右侧 46×34 圆角钮)----
* 卡在行高内上下对称内缩 BGM_CARD_GAP/2(行中心=卡中心);按钮垂直居中于卡内,故其中心
* =卡中心=行中心。命中矩形仍为整行距(row_tap 传整行视口),按钮几何不变 → 卡内缩后命中不破。 */
static void bgm_btn_rect(int rx, int ry, int rw, int rh, int *bx, int *by, int *bw, int *bh)
{
*bw = BGM_BTN_W;
*bh = BGM_BTN_H;
*bx = rx + rw - 12 - BGM_BTN_W;
*by = bgm_row_card_y(ry) + (bgm_row_card_h(rh) - BGM_BTN_H) / 2;
}
/* ---- 行完全自绘(sub_page_t.row_draw;覆盖默认 renderer)----
* 空态行(act=-1)居中画「无后台 App」;后台 App 行画 名字 + CPU/内存/状态 + 行右「杀」按钮。 */
static void bgm_row_draw(void *ctx, int idx, sub_row_t *r,
int x, int y, int w, int h, int state)
{
(void)idx;
bgm_ctx_t *b = ctx;
const meow_theme *mt = meow_theme_get();
int i = r->act;
/* MD3 圆角卡:行高内上下对称内缩 BGM_CARD_GAP/2(行中心=卡中心),替代扁平行+底部分隔线。 */
int cy = bgm_row_card_y(y);
int ch = bgm_row_card_h(h);
meow_gfx_fill_round(x, cy, w, ch, 12, mt->surfaceContainer);
if (state & MEOW_LISTVIEW_ST_PRESSED)
meow_gfx_fill_round_alpha(x, cy, w, ch, 12, mt->primary, 26); /* 按压态(实时,限卡内) */
if (i < 0 || i >= b->n) { /* 空态行 */
int th = meow_ui_text_height(18);
meow_ui_text_center(x + w / 2, cy + (ch - th) / 2,
r->label ? r->label : meow_str_zh("bgmgr.empty", "无后台 App"),
18, mt->onSurfaceVariant);
return;
}
int th = meow_ui_text_height(20), tsub = meow_ui_text_height(16);
int rx = x + 16; /* 左内边距 */
int ry = cy + (ch - (th + tsub + 4)) / 2; /* 内容在卡内垂直居中(行中心=卡中心) */
if (r->label) meow_ui_text(rx, ry, r->label, 20, mt->onSurface);
if (r->sub) meow_ui_text(rx, ry + th + 4, r->sub, 16, mt->onSurfaceVariant);
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化。
* 不可杀行(受保护/前台/无进程,bgmgr_can_kill=0)→ 置灰禁点外观,避免误导(按钮可点但不可杀)。 */
int bx, by, bw, bh; bgm_btn_rect(x, y, w, h, &bx, &by, &bw, &bh);
uint32_t btn_col = mt->error; /* 可杀:error 红 */
if (i >= 0 && i < b->n && !b->rows[i].killable)
btn_col = mt->onSurfaceVariant; /* 禁点:置灰(onSurfaceVariant) */
meow_ui_draw_rect_round(bx, by, bw, bh, bh / 2, btn_col);
int bth = meow_ui_text_height(16);
if (bth > bh) bth = bh;
meow_ui_text_center(bx + bw / 2, by + (bh - bth) / 2, "杀", 16, btn_col);
}
/* ---- 行内自定义命中(sub_page_t.row_tap;仅按钮区 kill,行体不响应/不误杀)---- */
static int bgm_row_tap(void *ctx, int idx, sub_row_t *r, int x, int y,
int rx, int ry, int rw, int rh)
{
(void)idx;
bgm_ctx_t *b = ctx;
int i = r->act;
if (i < 0 || i >= b->n) return 0; /* 空态行无按钮 */
int bx, by, bw, bh; bgm_btn_rect(rx, ry, rw, rh, &bx, &by, &bw, &bh);
if (x < bx || x >= bx + bw || y < by || y >= by + bh) return 0; /* 行体/按钮外 → 不杀 */
if (!bgmgr_can_kill(&b->rows[i])) {
/* 禁点:受保护/前台/无进程 → 按钮已置灰,不 kill(返回 1=重绘)。
* 留一行日志:「按了没反应」当年因为没有这行日志,只能靠翻 app.json 才查出是门控。 */
logmsg("bgpage: kill 被门控 app=%s pid=%d fg=%d protected=%d",
b->rows[i].app_id, (int)b->rows[i].pid, b->rows[i].is_fg,
b->rows[i].protected_app ? 1 : 0);
return 1;
}
int rc = meow_bg_terminate(b->rows[i].app_id); /* 置 term_at_ms → bgm_enumerate 下一跳行消失(可见反馈) */
if (rc != 0)
logmsg("bgpage: kill app=%s 未命中后台表 rc=%d(列表/后台表不同步)", b->rows[i].app_id, rc);
return 1;
}
/* ---- 行体点击(sub_page_t.detail):不响应(kill 只认行内按钮)---- */
static int bgm_detail(void *ctx, int idx, int act)
{
(void)ctx; (void)idx; (void)act;
return 0;
}
/* ---- 顶栏右侧「一键清除后台」按钮(sub_page_t.top_action_cb)---- */
static int bgm_top_action(void *ctx)
{
bgm_ctx_t *b = ctx;
bgmgr_ops_t ops;
memset(&ops, 0, sizeof ops);
ops.ctx = b;
ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */
/* kill 只置 term_at_ms(真退/摘表在 launcher 主循环 bgm_tick),此处由 bgm_enumerate 过滤
* term_at_ms 让清后的行当下消失(可见反馈;不可杀行本就在上/一键不杀)。 */
int k = bgmgr_kill_all(&ops, b->rows, b->n, bgm_kill_fn);
if (k < b->n)
logmsg("bgpage: 一键清除 杀 %d/%d(其余为前台/无进程/受保护)", k, b->n);
return 1; /* 重绘:重扫行(已清行消失) */
}
/* ---- 入口:运行后台管理页(阻塞到返回)---- */
int run_bg_page(struct app *apps, int napps)
{
static bgm_ctx_t b;
memset(&b, 0, sizeof b);
b.apps = apps;
b.napps = napps;
sub_page_t p;
memset(&p, 0, sizeof p);
p.title = "后台管理";
p.rows = bgm_rows;
p.detail = bgm_detail;
p.live = 1; /* 后台表/进程变化实时重扫 */
p.ctx = &b;
p.top_action = "一键清除后台"; /* 顶栏标题栏右侧按钮 */
p.top_action_cb = bgm_top_action;
p.row_draw = bgm_row_draw; /* 行自绘(名字/CPU/内存/状态 + 行右杀按钮) */
p.row_tap = bgm_row_tap; /* 行内自定义命中(仅按钮区 kill) */
meow_subpage_run(&p);
/* 清除 CPU 差分缓存(页面会话结束) */
memset(g_cpu_pid, 0, sizeof g_cpu_pid);
memset(g_cpu_tick, 0, sizeof g_cpu_tick);
return 0;
}
+232 -59
View File
@@ -1,46 +1,98 @@
/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76)。
/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76;P1 权威收口重做)。
*
* 页面(sub_page_t 脚手架 subpage.c 驱动):
* - 顶栏:返回 + 标题「蓝牙」 + 右上开关(network.bluetooth_enabled)
* - 已连接设备:tap → bluetoothctl disconnect(角色经 bt_role_refresh 判定)
* - 已配对设备:tap → bluetoothctl connect(已连者标「已连接」)
* - 附近设备:扫描发现(net_bt_scan,后台有界 bluetoothctl 写 /tmp 缓存,异步不阻塞)——
* 未配对者 tap → pair;已配对者 tap → connect。实机配对成功率/超时需真机验证。
* 【P1 背景】BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八:连接/断开/配对/取消配对/扫描/A2DP 状态
* 查询全部收口进 netmgrd(BT_CTL 消息扩展),本子页**只做客户端**——经 netmgrd socket/IPC 查询
* 与下发,不再 bluetoothctl popen 直连(直连仅作 netmgrd 不可达时的回退保留)。
*
* 与网络线共用 net.c(net_bt_connected/paired/scan/scan_trigger/hci_powered/bt_role)。
* 开关写 network.bluetooth_enabled + hciconfig;连接/断开/配对走 bluetoothctl(带超时)。
* 【已连接设备显示不全根因修复】旧实现用 `bluetoothctl devices Connected`(慢 + 只反映 agent
* 缓存视图,popen 1500ms 易截断)→ 已连接列表不全。P1 改权威 = netmgrd 读**内核 ACL**
* (hcitool con,快速/权威/不受 bluetoothctl 过滤影响),名经 paired/devices 缓存兜底,A2DP 详情
* 按 MAC join(codec/采样率/acquired/role)。见 netmgrd `bt connected`/`bt a2dp`。
*
* 【页面结构(sub_page_t,plan §六)】顶栏「蓝牙」+ 右上开关,分组列表:
* ① 蓝牙开关(顶栏)② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC)③ 输出目标
* (蓝牙/本地,切下发 audio.output_bt,P2 接 audiod)④ 已连接设备(tap 断开/详情)⑤ 已配对
* 设备(tap 连接/断开)⑥ 附近设备(扫描节流触发)⑦ 音质/连接管理(codec 选择、P2 参数说明)
* ⑧ 功耗档(扫描节流/发现超时/后台保持说明)。
*
* 【扫描节流(§七)】进页 nm_bt_scan_on、退页/超时 nm_bt_scan_off;7s 节流由 netmgrd 统一控。
*
* 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。开关写 network.bluetooth_enabled +
* netmgrd on/off;连接/断开/配对经 netmgrd(netmgr_ipc.c nm_bt_* 封装)。
*/
#include "meow_launcher_internal.h"
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→disconnect) */
#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→connect) */
#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→pair/connect) */
#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→断开) */
#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→连接/断开) */
#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→配对/连接) */
#define BT_ACT_OUTPUT 5000 /* 输出目标(tap→蓝牙/本地切换) */
#define BT_ACT_CODEC 5001 /* 编解码器(tap→循环选择) */
#define BT_CODEC_AUTO 0
#define BT_CODEC_N 5 /* auto/SBC/AAC/aptX/LDAC */
static const char *bt_codec_names[BT_CODEC_N] = { "auto", "SBC", "AAC", "aptX", "LDAC" };
typedef struct {
meow_net_t conn[S_NET_MAX];
meow_net_t paired[S_NET_MAX];
meow_net_t near[S_NET_MAX];
int conn_n, paired_n, near_n;
bt_ep_t a2dp[S_NET_MAX];
int conn_n, paired_n, near_n, a2dp_n;
int bt_on;
int full; /* 1=本次 rows() 全量重扫(conn/paired popen);0=仅附近(file)+角色(节流) */
int output_bt; /* 0=DAC/本地,1=蓝牙 */
int full; /* 1=本次 rows() 全量重扫(conn/paired/a2dp);0=仅附近(缓存)+角色(节流) */
char mode[48]; /* bt_role 中文描述(次行摘要) */
/* 行副文本池:sub_page_t 行 label/sub 为 const char*,跨 rows() 返回后仍须有效(直到下次
* rows() 重填)。本地栈缓冲会悬垂(agent-396 根因同类),故格式化 sub 一律写进本池。 */
char pool[512];
int pool_off;
} bt_ctx_t;
/* 设备 MAC(net_bt_* 已填 mac) */
/* 格式化并拷入 ctx 行副文本池(返回稳定指针;溢出则绕回——512B 远超单页行数所需)。 */
static char *bt_pool(bt_ctx_t *c, const char *fmt, ...)
{
va_list ap; char tmp[128];
va_start(ap, fmt); vsnprintf(tmp, sizeof tmp, fmt, ap); va_end(ap);
int l = (int)strlen(tmp);
if (c->pool_off + l + 1 > (int)sizeof c->pool) c->pool_off = 0;
char *p = &c->pool[c->pool_off];
memcpy(p, tmp, (size_t)l + 1);
c->pool_off += l + 1;
return p;
}
/* ---- 设备 MAC 相关 ---- */
static int bt_is_conn(const bt_ctx_t *c, const char *mac)
{
if (!mac[0]) return 0;
for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return 1;
return 0;
}
static int bt_is_paired(const bt_ctx_t *c, const char *mac)
{
if (!mac[0]) return 0;
for (int i = 0; i < c->paired_n; i++) if (!strcmp(c->paired[i].mac, mac)) return 1;
return 0;
}
/* 在 a2dp 端点表找 MAC(返回 idx;-1=无) */
static int bt_ep_idx(const bt_ctx_t *c, const char *mac)
{
if (!mac[0]) return -1;
for (int i = 0; i < c->a2dp_n; i++) if (!strcmp(c->a2dp[i].mac, mac)) return i;
return -1;
}
/* 从 conn 表找设备名(返回指针到 c->conn[i].name,未命中返回 mac) */
static const char *bt_conn_name(const bt_ctx_t *c, const char *mac)
{
if (mac[0]) for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return c->conn[i].name;
return mac;
}
/* 顶栏开关当前值 */
@@ -53,51 +105,101 @@ static int bt_top_sw(void *ctx, int *on)
return 1;
}
/* 顶栏开关切换:写设置 + hciconfig up/down + 触发扫描 + 同步状态栏 */
/* 顶栏开关切换:写设置 + on/off(netmgrd IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */
static void bt_top_sw_set(void *ctx, int on)
{
(void)ctx;
meow_settings_set_bool("network.bluetooth_enabled", on);
s_net_toggle_bt(on);
nm_bt_onoff(on);
g_sys.bt_on = on;
bt_role_refresh(1);
paint_sb_right();
meow_settings_save();
if (on) net_bt_scan_trigger();
if (on) nm_bt_scan_on();
}
/* 填行:刷新数据后构建分组列表。
* conn/paired 走 bluetoothctl(慢);仅全量(full)时重扫,避开子页 live tick 反复 spawn。 */
/* 填行:刷新数据后构建分组列表(权威=netmgrd;仅全量(full)时重扫 conn/paired/a2dp,
* 避开子页 live tick 反复 IPC)。 */
static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
{
bt_ctx_t *c = ctx;
int full = c->full; c->full = 0; /* 消费一次全量标记 */
c->bt_on = hci_powered();
if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired */
c->conn_n = net_bt_connected(c->conn, S_NET_MAX);
c->paired_n = net_bt_paired(c->paired, S_NET_MAX);
c->pool_off = 0; /* 行副文本池绕回(上轮行字符串已被重填替换) */
c->bt_on = nm_bt_powered(); /* IPC status;回退直连 hci_powered() */
c->output_bt = 0;
if (meow_settings_get_bool("audio.output_bt", &c->output_bt) != 0) c->output_bt = 0;
if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired/a2dp(全权威) */
c->conn_n = nm_bt_connected(c->conn, S_NET_MAX);
c->paired_n = nm_bt_paired(c->paired, S_NET_MAX);
c->a2dp_n = nm_bt_a2dp(c->a2dp, S_NET_MAX);
if (c->conn_n < 0) c->conn_n = 0;
if (c->paired_n < 0) c->paired_n = 0;
if (c->a2dp_n < 0) c->a2dp_n = 0;
}
c->near_n = net_bt_scan(c->near, S_NET_MAX); /* 附近:读 /tmp 缓存(快,实测无 popen) */
c->near_n = nm_bt_devices(c->near, S_NET_MAX); /* 附近:扫描缓存(netmgrd 读 /tmp,快) */
if (c->near_n < 0) c->near_n = 0;
/* 角色描述:节流(≤5s)重探,读缓存,避免子页驻留期间反复 popen */
net_bt_mode_str(c->mode, sizeof c->mode);
int n = 0;
/* 已连接设备 */
/* 降级占位:netmgrd 不可达 → 提示(直连回退仍在跑,保功能) */
if (!netmgr_ok() && n < max) {
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
}
/* ② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC) */
{
const bt_ep_t *ep = NULL;
for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; }
if (!ep && c->a2dp_n > 0) ep = &c->a2dp[0];
const char *sub;
if (c->conn_n > 0 && n < max) {
const char *nm = bt_conn_name(c, ep ? ep->mac : c->conn[0].mac);
if (ep && (ep->codec[0] || ep->rate[0]))
sub = bt_pool(c, "%s%s%s · 输出=%s",
ep->codec[0] ? ep->codec : "?", ep->codec[0] && ep->rate[0] ? " " : "",
ep->rate[0] ? ep->rate : "", c->output_bt ? "蓝牙" : "DAC");
else
sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC");
rbuf[n].kind = SUB_ROW; rbuf[n].label = nm ? nm : c->conn[0].mac;
rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++;
} else if (n < max) {
sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC");
rbuf[n].kind = SUB_ROW; rbuf[n].label = "未连接蓝牙设备";
rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++;
}
}
/* ③ 输出目标(蓝牙/本地) */
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "输出目标"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
if (n < max) {
rbuf[n].kind = SUB_ACT; rbuf[n].label = "输出目标";
rbuf[n].sub = c->output_bt ? "蓝牙(耳机/远端)" : "本地(DAC)";
rbuf[n].on = c->output_bt; rbuf[n].act = BT_ACT_OUTPUT; n++;
}
/* ④ 已连接设备 */
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已连接设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
if (c->conn_n == 0) {
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已连接设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
} else for (int i = 0; i < c->conn_n && n < max; i++) {
const bt_ep_t *ep = bt_ep_idx(c, c->conn[i].mac) >= 0 ? &c->a2dp[bt_ep_idx(c, c->conn[i].mac)] : NULL;
const char *sub;
if (ep && ep->codec[0])
sub = bt_pool(c, "%s%s%s%s",
ep->codec[0] ? ep->codec : "", ep->rate[0] ? " " : "", ep->rate[0] ? "· " : "",
ep->acquired ? "播放中" : "");
else sub = c->conn[i].mac; /* mac 拷在 ctx(稳定) */
rbuf[n].kind = SUB_ACT;
rbuf[n].label = c->conn[i].name[0] ? c->conn[i].name : c->conn[i].mac;
rbuf[n].sub = c->conn[i].mac;
rbuf[n].sub = sub[0] ? sub : c->conn[i].mac;
rbuf[n].on = 1; rbuf[n].act = BT_ACT_CONN_BASE + i;
n++;
}
/* 已配对设备 */
/* ⑤ 已配对设备 */
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已配对设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
if (c->paired_n == 0) {
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已配对设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
@@ -110,7 +212,8 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
rbuf[n].act = BT_ACT_PAIRED_BASE + i;
n++;
}
/* 附近设备(扫描发现,未配对) */
/* ⑥ 附近设备(扫描发现,未配对;netmgrd 权威扫描缓存) */
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "附近设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
if (c->near_n == 0) {
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "附近无设备(点顶部开关后自动扫描)"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
@@ -123,30 +226,66 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
rbuf[n].on = 0; rbuf[n].act = BT_ACT_NEAR_BASE + i;
n++;
}
/* ⑦ 音质/连接管理 */
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "音质 / 连接管理"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
{
int idx = BT_CODEC_AUTO;
char codec[16] = "auto";
if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0)
for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx = k; break; }
rbuf[n].kind = SUB_ACT; rbuf[n].label = "编解码器";
rbuf[n].sub = bt_pool(c, "%s%s", bt_codec_names[idx], idx ? "(偏好,P2 生效)" : "");
rbuf[n].on = 0;
rbuf[n].act = BT_ACT_CODEC; n++;
}
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "SBC 质量"; rbuf[n].sub = "P2:bluealsa --sbc-quality 启动参数"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "强制 44.1k"; rbuf[n].sub = "P2:--a2dp-force-audio-cd"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "音量控制"; rbuf[n].sub = "蓝牙音量由耳机/远端(AVRCP)控制"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
/* ⑧ 功耗档 */
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "功耗"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "扫描节流"; rbuf[n].sub = "进页自动扫描,退页/7s 停止"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "发现超时"; rbuf[n].sub = "source 无需可发现(有限超时)"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "后台保持"; rbuf[n].sub = "未播放时无流零开销"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
return n;
}
/* 执行 bluetoothctl 命令(带超时)+ 结果弹窗反馈 */
static void bt_run(const char *title, const char *cmd)
/* 动作反馈(netmgrd 结果 / 直连回退结果)弹窗反馈 */
static void bt_feedback(const char *title, int rc)
{
char *o = popen_read_tmo(8000, cmd);
char line[48];
const char *txt[4]; txt[0] = line;
int ok = o && (strstr(o, "successful") || strstr(o, "Connected") ||
strstr(o, "already") || strstr(o, "Paired"));
snprintf(line, sizeof line, "%s", ok ? "操作成功" : "操作失败/超时");
if (o) free(o);
const char *txt[S_NET_MAX]; txt[0] = line;
snprintf(line, sizeof line, "%s", rc == MEOW_OK ? "操作成功" : "操作失败/超时");
s_dd_modal(title, txt, 1, -1);
}
/* 动作行(SUB_ACT)点击:连接/断开/配对 */
/* 动作行(SUB_ACT)点击:连接/断开/配对/取消配对/输出目标/编解码器 */
static int bt_act(void *ctx, int idx, int act)
{
(void)idx;
bt_ctx_t *c = ctx;
char mac[24] = "";
/* 输出目标/编解码器:与本页设备行 act 段(CONN/PAIRED/NEAR base)不冲突,先处理 */
if (act == BT_ACT_OUTPUT) {
c->output_bt = !c->output_bt;
meow_settings_set_bool("audio.output_bt", c->output_bt);
meow_settings_save();
logmsg("bt_page: output_target=%s", c->output_bt ? "bt" : "dac");
return 1; /* 重绘 + 重填行 */
}
if (act == BT_ACT_CODEC) {
char codec[16] = "auto"; int idx0 = BT_CODEC_AUTO;
if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0)
for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx0 = k; break; }
int ni = (idx0 + 1) % BT_CODEC_N;
meow_settings_set_str("bt.codec", bt_codec_names[ni]);
meow_settings_save();
logmsg("bt_page: codec=%s", bt_codec_names[ni]);
return 1;
}
/* 定位 MAC(按 act 段) */
if (act >= BT_ACT_NEAR_BASE) {
int i = act - BT_ACT_NEAR_BASE;
@@ -161,43 +300,75 @@ static int bt_act(void *ctx, int idx, int act)
if (i < 0 || i >= c->conn_n) return 0;
snprintf(mac, sizeof mac, "%s", c->conn[i].mac);
}
if (!mac[0]) return 0;
if (act >= BT_ACT_NEAR_BASE) {
/* 附近未配对 → pair;已配对(理论上过滤了)→ connect */
char cmd[192];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl pair %s 2>/dev/null", mac);
bt_run("配对", cmd);
/* 附近:未配对 → pair;已配对(理论上过滤了)→ connect */
bt_feedback("配对", nm_bt_pair(mac));
} else if (act >= BT_ACT_PAIRED_BASE) {
/* 已配对:未连 → connect;已连 → disconnect */
if (bt_is_conn(c, mac)) {
char cmd[192];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac);
bt_run("断开", cmd);
} else {
char cmd[192];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl connect %s 2>/dev/null", mac);
bt_run("连接", cmd);
}
if (bt_is_conn(c, mac)) bt_feedback("断开", nm_bt_disconnect(mac));
else bt_feedback("连接", nm_bt_connect(mac));
} else {
/* 已连接 → disconnect */
char cmd[192];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac);
bt_run("断开", cmd);
bt_feedback("断开", nm_bt_disconnect(mac));
}
bt_role_refresh(1);
net_bt_scan_trigger();
nm_bt_scan_on();
c->full = 1; /* 动作后:下一次 rows() 全量重扫 */
logmsg("bt_page: act act=%d mac=%s", act, mac);
return 1; /* 重绘 + 重填行 */
}
/* 点普通行(A2DP 摘要等):弹 A2DP/设备详情。仅 A2DP 摘要(act=-1)响应;其余 info 行(act=-2)无操作。 */
static int bt_detail(void *ctx, int idx, int act)
{
(void)idx;
if (act != -1) return 0;
bt_ctx_t *c = ctx;
if (c->a2dp_n > 0) {
const bt_ep_t *ep = &c->a2dp[0];
char buf[S_NET_MAX][48];
const char *item[S_NET_MAX];
int n = 0;
for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; }
snprintf(buf[n], 48, "设备名: %s", bt_conn_name(c, ep->mac)); item[n] = buf[n]; n++;
snprintf(buf[n], 48, "MAC: %s", ep->mac[0] ? ep->mac : "-"); item[n] = buf[n]; n++;
snprintf(buf[n], 48, "codec: %s", ep->codec[0] ? ep->codec : "-"); item[n] = buf[n]; n++;
snprintf(buf[n], 48, "采样率: %s", ep->rate[0] ? ep->rate : "-"); item[n] = buf[n]; n++;
snprintf(buf[n], 48, "声道/位深: %s/%s", ep->chn[0] ? ep->chn : "-", ep->bits[0] ? ep->bits : "-");
item[n] = buf[n]; n++;
snprintf(buf[n], 48, "传输: %s", ep->acquired ? "播放中" : "空闲"); item[n] = buf[n]; n++;
snprintf(buf[n], 48, "角色: %s", !strcmp(ep->role, "src") ? "设备=source(送流)" :
!strcmp(ep->role, "snk") ? "设备=sink(被连)" : "未知");
item[n] = buf[n]; n++;
s_dd_modal("A2DP 详情", item, n, -1);
return 1;
}
if (c->conn_n > 0) {
char buf[S_NET_MAX][48];
const char *item[S_NET_MAX];
int n = 0;
snprintf(buf[n], 48, "设备名: %s", c->conn[0].name[0] ? c->conn[0].name : c->conn[0].mac);
item[n] = buf[n]; n++;
snprintf(buf[n], 48, "MAC: %s", c->conn[0].mac); item[n] = buf[n]; n++;
snprintf(buf[n], 48, "状态: 已连接"); item[n] = buf[n]; n++;
snprintf(buf[n], 48, "输出: %s", c->output_bt ? "蓝牙" : "DAC"); item[n] = buf[n]; n++;
s_dd_modal("蓝牙详情", item, n, -1);
return 1;
}
return 0;
}
/* 运行蓝牙独立子页面(阻塞到返回) */
int run_bt_page(void)
{
static bt_ctx_t c;
memset(&c, 0, sizeof c);
c.conn_n = -1;
sub_page_t p;
memset(&p, 0, sizeof p);
p.title = "蓝牙";
@@ -205,11 +376,13 @@ int run_bt_page(void)
p.top_sw_set = bt_top_sw_set;
p.rows = bt_rows;
p.act = bt_act;
p.detail = NULL; /* 蓝牙动作都走 SUB_ACT(act),无 SUB_ROW 详情 */
p.detail = bt_detail; /* SUB_ROW(A2DP 摘要等)→ A2DP 详情弹窗 */
p.live = 1; /* 附近扫描驻留更新(约 2s 重扫行;仅附近/角色,不反复 spawn) */
p.ctx = &c;
c.full = 1; /* 首行全量重扫 */
net_bt_scan_trigger(); /* 进页立即触发一次附近扫描(异步,不阻塞) */
nm_bt_scan_on(); /* 进页:触发一次附近扫描(节流;不进蓝牙不空转 §七) */
bt_role_refresh(1);
return meow_subpage_run(&p);
int rc = meow_subpage_run(&p);
nm_bt_scan_off(); /* 退页:扫描收尾(§七 省电) */
return rc;
}
+6 -3
View File
@@ -25,9 +25,12 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
"$CC" -I"../sdk" -Os -static \
main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \
notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
subpage.c wifi_page.c bt_page.c now_playing.c \
font_theme.c keys.c util.c \
meow_touch.c meow_settings.c meow_notif.c meow_media.c \
subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.c \
font_theme.c keys.c util.c meow_dep.c key_router.c \
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
meow_bgmgr.c \
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
"../sdk/meow_lyric.c" "../sdk/meow_lyric_view.c" "../sdk/meow_img_color.c" \
"../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
echo "== meow_launcherd =="
ls -la meow_launcherd
+1 -4
View File
@@ -21,7 +21,7 @@
#include <stdint.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <poll.h>
#include <linux/input.h>
#ifndef EVIOCGRAB
@@ -147,18 +147,15 @@ static void loop_forward(const char *dev, int fwd)
fflush(stdout);
struct pollfd pfd = { fd, POLLIN, 0 };
unsigned char ev[16];
unsigned long long nread = 0, nwritten = 0;
for (;;) {
int r = poll(&pfd, 1, -1);
if (r < 0) { perror("poll"); break; }
if (pfd.revents & POLLIN) {
ssize_t n = read(fd, ev, sizeof ev);
if (n != 16) { if (n < 0) perror("read"); continue; }
nread++;
print_ev("G", ev);
if (fwd) {
ssize_t w = write(fd, ev, sizeof ev);
nwritten++;
if (w != 16) fprintf(stderr, " write-back failed: %s\n", strerror(errno));
}
}
+34
View File
@@ -0,0 +1,34 @@
PASS S1 drain required (app was SIGSTOP/frozen)
PASS S1 no drain needed (media keep-alive, never stopped)
PASS S1 drain window > SDK 200ms throttle
PASS S1b keep-alive app needs EVIOCGRAB isolation
PASS S1b frozen app -> SIGCONT drain, not grab
PASS S1b keep-alive: grab path (drain==0)
PASS S1b frozen: drain path (grab==0)
PASS S2 force status bar re-sync after unlock
PASS S2b lock re-cover ms > SDK suppression window (200ms)
PASS S2b lock re-cover ms = 300 (marker-window margin)
PASS S1c wake, no sleep -> no lock
PASS S1c timeout sleep (auto) -> lock
PASS S1c manual power sleep (manual) -> lock (was missing)
PASS S1c both -> lock
PASS S4 system 0 (never) -> lock caps 30s
PASS S4 system absent -> lock caps 30s
PASS S4 system 30 -> 30
PASS S4 system 60 -> 30 (cap, shorter)
PASS S4 system 10 -> 10 (respect shorter)
PASS S4 system 1 -> 1 (respect shorter)
PASS S4 idle>=off & screen on -> self sleep
PASS S4 idle<off -> not yet
PASS S4 screen off -> no re-sleep
PASS S4 power edge -> wake to lock
PASS S4 touch -> wake to lock
PASS S4 no wake input -> stay asleep
PASS S3 app died -> unified exit path (redraw home)
PASS S3 app alive -> normal restore
PASS S3 abort=0 (menu lock, no overlay pid)
PASS S3 abort=0 (no overlay pid, zombie ignored)
PASS S3 abort=0 (overlay pid, app alive)
PASS S3 abort=1 (overlay pid + zombie -> abort lock)
ALL PASS (32 passed, 0 failed)
+74 -23
View File
@@ -474,7 +474,7 @@ static void font_tier_marker(const char *charset, int glyph_count, char *out, in
* 输出:label=显示名 `<name> <Weight>(common≈4k)`(如 `MiSans Regular(common≈4k)`);
* charset/glyph_count=档位信息;wtout=weight slug(缺省 "regular")。 */
static int meow_font_probe_weight(const char *dirpath, const char *family, const char *weight,
const char *tier_dir,
const char *tier_dir, int verify_sha,
char *label, int llen,
char *charset, int clen, int *glyph_count,
char *wtout, int wtlen, char *nameout, int namelen)
@@ -506,16 +506,20 @@ static int meow_font_probe_weight(const char *dirpath, const char *family, const
snprintf(label, llen, "%s %s(%s)", name[0] ? name : family, wd, marker);
if (charset) snprintf(charset, clen, "%s", tier);
if (glyph_count) *glyph_count = gc;
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */
char apng[80] = "", aidx[80] = "";
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
logmsg("fonts: '%s/%s%s%s' sha256(png) 不可用,跳过校验", family, wt, tsep, tiernm);
else if (jsha_png[0] && !font_sha_eq(apng, jsha_png))
logmsg("fonts: '%s/%s%s%s' WARN sha256_png 不匹配(仍可选)", family, wt, tsep, tiernm);
if (font_sha256_file(ipath, aidx, sizeof aidx) != 0)
logmsg("fonts: '%s/%s%s%s' sha256(idx) 不可用,跳过校验", family, wt, tsep, tiernm);
else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx))
logmsg("fonts: '%s/%s%s%s' WARN sha256_idx 不匹配(仍可选)", family, wt, tsep, tiernm);
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。
* M3:verify_sha=0(进设置页重扫)跳过逐套 sha256sum(gbk/full 套数 MB,逐套双 sha256
* 真机耗数秒,入口卡顿主因之一);完整性校验保留给启动全量扫描(verify_sha=1)。 */
if (verify_sha) {
char apng[80] = "", aidx[80] = "";
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
logmsg("fonts: '%s/%s%s%s' sha256(png) 不可用,跳过校验", family, wt, tsep, tiernm);
else if (jsha_png[0] && !font_sha_eq(apng, jsha_png))
logmsg("fonts: '%s/%s%s%s' WARN sha256_png 不匹配(仍可选)", family, wt, tsep, tiernm);
if (font_sha256_file(ipath, aidx, sizeof aidx) != 0)
logmsg("fonts: '%s/%s%s%s' sha256(idx) 不可用,跳过校验", family, wt, tsep, tiernm);
else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx))
logmsg("fonts: '%s/%s%s%s' WARN sha256_idx 不匹配(仍可选)", family, wt, tsep, tiernm);
}
logmsg("fonts: add '%s/%s%s%s' (%s) tier=%s glyph_count=%d", family, wt, tsep, tiernm, label, tier, gc);
return 1;
}
@@ -637,7 +641,7 @@ static int font_presets_add(const char *family, const char *weight, const char *
* 选库)与 meow_font_reload_multifont 在运行期持续读 font_presets_n,清空会造成大字库
* 档位退化(red line「保持档位/多字体行为」)。如需「零驻留」,须把这几个核心路径改为
* 直接读 index(较大重构,超出本批边界,留待后续)。 */
void meow_font_scan_presets_root(const char *card_root)
void meow_font_scan_presets_root(const char *card_root, int verify_sha)
{
font_presets_n = 0;
font_presets[0] = "MeowOS/fonts/default/regular/font.png"; /* 默认字库 = MeowOS/fonts/default/regular */
@@ -684,7 +688,7 @@ void meow_font_scan_presets_root(const char *card_root)
char label[96] = "", cs[8] = ""; int gc = 0; char wt[32] = "", nm[64] = "";
if (access(pngcheck, R_OK) == 0) {
/* 两级(旧结构向后兼容):<family>/<weight>/font.png,tier 读 font.json charset。 */
if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, NULL,
if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, NULL, verify_sha,
label, sizeof label, cs, sizeof cs, &gc,
wt, sizeof wt, nm, sizeof nm) == 0)
continue; /* 缺组件/损坏 → 跳过该套 */
@@ -702,7 +706,7 @@ void meow_font_scan_presets_root(const char *card_root)
strcmp(te->d_name, "full")) continue;
char tpath[420];
snprintf(tpath, sizeof tpath, "%s/%s", dirpath, te->d_name);
if (meow_font_probe_weight(tpath, de->d_name, we->d_name, te->d_name,
if (meow_font_probe_weight(tpath, de->d_name, we->d_name, te->d_name, verify_sha,
label, sizeof label, cs, sizeof cs, &gc,
wt, sizeof wt, nm, sizeof nm) == 0)
continue; /* 缺组件/损坏 → 跳过该 tier 套 */
@@ -728,13 +732,20 @@ void meow_font_scan_presets_root(const char *card_root)
}
/* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。 */
void meow_font_scan_presets(void)
/* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。
* verify_sha=1:启动/热插拔全量扫描(含逐套 sha256 完整性校验)。
* verify_sha=0:进设置页重扫(只枚举+读 font.json+落 index,跳过 sha256 —— 入口不许卡数秒)。 */
void meow_font_scan_presets(int verify_sha)
{
meow_font_scan_presets_root("/usr/data/mnt/sd_0");
meow_font_scan_presets_root("/usr/data/mnt/sd_0", verify_sha);
}
#if 0 /* ================================================================
* M7.0b 主题退役:launcher 不再自持 4 套静态色板 —— s_ui_theme() 改读 SDK
* meow_theme_export()(单一权威:MD3/MIUIx × 明暗 由 SDK meow_theme 解析)。
* 本块保留作回退位(删除 #if 0 行即可恢复旧手绘多色板路径)。
* ================================================================ */
/* MD3 tonal palette(565 参考值见 MD3_UI_SPEC §1;此处存 24-bit RGB,#hex) */
static const meow_ui_theme_t meow_ui_theme_dark = {
0x141824, 0x1E2436, 0x263038, 0x4A6A9A, 0xFFFFFF,
@@ -746,6 +757,18 @@ static const meow_ui_theme_t meow_ui_theme_light = {
0x4A4F5A, 0x9AA4C8, 0xB3261E, 0x2A5A8A
};
/* MIUIx 族(§8.1.3 定案):暖色基面 + 小米橙 primary,供 launcher 主界面/设置页/通知中心/
* 电源菜单(s_ui_theme())在 ui.style=miuix 时使用(与 SDK meow_theme MIUIx 同色系)。 */
static const meow_ui_theme_t meow_ui_theme_miui_dark = {
0x181310, 0x2A231C, 0x2E2620, 0xFF8C4A, 0xEDE5DC,
0xAB9884, 0x4A3F35, 0xFFB4AB, 0xFF8C4A
};
static const meow_ui_theme_t meow_ui_theme_miui_light = {
0xF7F3EE, 0xE9DFD0, 0xF4EDE3, 0xF57200, 0x251A10,
0x7D6A5A, 0xCBBFB2, 0xB3261E, 0xF57200
};
/* 获取当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用 */
static int s_now_min_of_day(void)
{
@@ -768,13 +791,18 @@ static int theme_should_dark(int now_min, int start, int end)
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
}
/* 当前主题(dark/light 两套;接 display.theme_mode + display.theme)。
/* 当前主题(style×明暗 两套;接 ui.style + display.theme_mode + display.theme)。
* style(§8.1.1):ui.style 的 md3/miuix → 返回对应体系(MD3 tonal / MIUIx 暖色+橙)。
* manual(mode==THEME_MODE_MANUAL):直读 display.theme(dark/light),保持原有行为。
* auto(mode==THEME_MODE_AUTO):读深色时段两键 + 当前本地分钟;start==end 未配置 →
* 回退 manual(读 display.theme,避免误判全天深色)。
* 读取失败一律回退 dark。 */
const meow_ui_theme_t *s_ui_theme(void)
{
int style = 0; /* ui.style:0=md3 / 1=miuix */
if (meow_settings_get_enum("ui.style", &style) != 0) style = 0;
const meow_ui_theme_t *light = (style == 1) ? &meow_ui_theme_miui_light : &meow_ui_theme_light;
const meow_ui_theme_t *dark = (style == 1) ? &meow_ui_theme_miui_dark : &meow_ui_theme_dark;
int mode = THEME_MODE_MANUAL;
if (meow_settings_get_enum("display.theme_mode", &mode) != 0) mode = THEME_MODE_MANUAL;
int v = 0; /* display.theme(manual)当前索引 */
@@ -785,13 +813,36 @@ const meow_ui_theme_t *s_ui_theme(void)
meow_settings_get_int("display.theme_dark_end", &end);
int d = theme_should_dark(s_now_min_of_day(), start, end);
if (d < 0) { /* 未配置:回退 manual(display.theme) */
if (have_manual && v == 1) return &meow_ui_theme_light;
return &meow_ui_theme_dark;
if (have_manual && v == 1) return light;
return dark;
}
return d ? &meow_ui_theme_dark : &meow_ui_theme_light;
return d ? dark : light;
}
/* manual:照旧读 display.theme */
if (have_manual && v == 1) return &meow_ui_theme_light;
return &meow_ui_theme_dark;
if (have_manual && v == 1) return light;
return dark;
}
#endif /* M7.0b 退役:launcher 4 套静态色板 → SDK meow_theme 单一权威 */
/* M7.0b 主题薄壳:launcher 主题 = SDK meow_theme_export()(MD3/MIUIx × 明暗 由 SDK 解析,
* launcher 侧不再读 ui.style/display.theme 自行选板)。只做一次拷贝 + 9 字段映射到 launcher
* 绘制便利结构 meow_ui_theme_t(§8.1.5)。返回静态缓存(调用方立即使用,不长期持有)。
* 注:meow_theme_export() 在 g_loaded==0 时先读 settings.conf,宿主/脱机也可靠。 */
const meow_ui_theme_t *s_ui_theme(void)
{
meow_theme th;
static meow_ui_theme_t s;
meow_theme_export(&th);
/* 映射:SDK meow_theme → launcher meow_ui_theme_t(9 字段,§8.1.5) */
s.surface = th.surface;
s.surface_variant = th.surfaceVariant;
s.surface_container = th.surfaceContainer;
s.primary = th.primary;
s.on_surface = th.onSurface;
s.on_surface_variant = th.onSurfaceVariant;
s.outline = th.outline;
s.error = th.error;
s.surface_tint = th.surfaceTint;
return &s;
}
+142
View File
@@ -0,0 +1,142 @@
/* key_router.c — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
*
* 本模块持有一个进程内唯一的音量键防抖/基线:
* kp_keys / kp_power(事件设备 poll 态,key_router_init 单开)
* kd_vold / kd_volu(40ms 防抖态,KEY_DEBOUNCE_MS)
* m_prev_vold / m_prev_volu(音量边沿基线)
* 各 launcher-UI 页每圈调 key_router_pump() —— 它只产「音量边沿 + 原始 held + ramp 状态」,
* 产出的边沿由「谁在跑循环」天然归属当前页;页据此决定 settings_volume_adj(±step) /
* 光标导航 / ramp 推进。这样 run_menu 的 meow_mode 分支(HibyOS overlay 音量=导航)不被破坏,
* 其余页(设置/通知中心/Now Playing/子页)用默认「音量=调音量」。
*
* 边界(§3 P1 / §4):
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸;launcher-UI
* 下 sink 恒空,此闸是「不改变行为」的保险)。
* - 播放键/电源键 P1 不路由(各页现状不动;播放键严禁 cmd=6)。
* - 锁屏(EVIOCGRAB)/App 会话层不调用本模块 —— 边界由「调用栈里当前循环是不是 launcher-UI 页」
* 自动强加。
*/
#include "meow_launcher_internal.h"
static int g_inited = 0;
/* 进程唯一事件设备 poll 态(key_router_init 单开;各页另自开 kp_* 仅供播放/电源键用)。 */
static struct keypoll kp_power, kp_keys;
/* 进程唯一音量键防抖态 + 边沿基线。 */
static struct kd_state kd_vold, kd_volu;
static int m_prev_vold = 0, m_prev_volu = 0;
/* 当前活动页哨兵(key_router_page_enter/exit 登记;与 sdk meow_ui_activity 交叉校验用)。 */
static int g_page_sentinel = 0;
void key_router_init(void)
{
if (g_inited) return;
g_inited = 1;
memset(&kp_power, 0, sizeof kp_power);
memset(&kp_keys, 0, sizeof kp_keys);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
m_prev_vold = 0; m_prev_volu = 0;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
/* 基线对齐当前 raw:进入时已按住的音量键不产假边沿。 */
key_router_rebase();
}
void key_router_rebase(void)
{
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
if (!g_inited) return;
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
}
key_evt_t key_router_pump(void)
{
key_evt_t ev;
memset(&ev, 0, sizeof ev);
if (!g_inited) key_router_init();
/* 设备晚就绪兜底:init 时 open 失败则此处每圈重试(open 失败快速返回,无阻塞)。 */
if (!kp_keys.ok) kp_open(&kp_keys, DEV_KEYS);
if (!kp_power.ok) kp_open(&kp_power, DEV_POWER);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
}
/* 原始(未防抖)值:长按 ramp held 用(松开立即停止;同 run_menu/now_playing 现状)。 */
ev.vol_held_dn = vold;
ev.vol_held_up = volu;
(void)kd_tick(&kd_vold, vold);
(void)kd_tick(&kd_volu, volu);
vold = kd_vold.stable; volu = kd_volu.stable;
ev.vol_st_dn = vold; ev.vol_st_up = volu;
if (meow_volpage_armed()) {
/* 防御闸:App 前台 volume_page 武装时(launcher-UI 下恒空)不放行音量边沿。
* 音量键在那态下由 app_launcher 的 meow_volpage_key 消费(翻页),不落系统调音。 */
ev.vol_dn_edge = 0;
ev.vol_up_edge = 0;
} else {
ev.vol_dn_edge = (vold && !m_prev_vold);
ev.vol_up_edge = (volu && !m_prev_volu);
}
ev.ramping = (vol_ramp_dir != 0);
m_prev_vold = vold;
m_prev_volu = volu;
return ev;
}
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input)
{
if (ev->vol_dn_edge) {
settings_volume_adj(-settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("key_router: volume DOWN <- step=%d", settings_volume_step);
}
if (ev->vol_up_edge) {
settings_volume_adj(+settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("key_router: volume UP <- step=%d", settings_volume_step);
}
/* 在 settings_volume_adj 之后重查 vol_ramp_dir:同圈「起 ramp」即立刻迈第一步(同现状
* 各页「settings_volume_adj 后 if(vol_ramp_dir) vol_ramp_tick」),且长按续爬随每圈推进。 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? ev->vol_held_up : ev->vol_held_dn;
(void)vol_ramp_tick(held);
}
}
void key_router_page_enter(int sentinel)
{
g_page_sentinel = sentinel;
/* 与 sdk 活动栈交叉校验:哨兵非 0 且栈顶不一致 → 记录(不中止,防宿主/真机栈深度差异)。 */
if (sentinel) {
int act = meow_ui_activity_active();
if (act && act != sentinel)
logmsg("key_router: page_enter sentinel=%#x but active=%#x", sentinel, act);
}
}
void key_router_page_exit(int sentinel)
{
if (g_page_sentinel == sentinel) g_page_sentinel = 0;
/* 页退出即重播基线(§6 嵌套返回风险项:返回瞬间不产假边沿)。 */
key_router_rebase();
}
+65
View File
@@ -0,0 +1,65 @@
/* key_router.h — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
*
* 设计(devdocs/plan/KEY_ROUTER_UNIFY.md §2.5 方案 C / §3 P1):
* - 进程内**唯一**的 kd_*(防抖态)+ m_prev_*(边沿基线)+ kp_*(事件设备 poll 态),
* 把「读边沿 + 防抖 + 长按 held + ramp 推进」这层从 5 个会话(run_menu /
* run_settings_page / run_notif_center / run_now_playing / meow_subpage_run)
* 收敛到一处。
* - 每个 launcher-UI 页在其 for(;;) 每圈调一次 key_router_pump(),得到「防抖后的
* 音量边沿 + 原始 held + ramp 状态」;**真正的动作**(音量边沿 →
* settings_volume_adj(±step),还是光标导航)由调用页决定 —— 这样 run_menu 的
* meow_mode 分支(HibyOS overlay:音量=导航;MeowOS:音量=调音量)得以保留。
* - 非导航页可经 key_router_default_volume()(内含 settings_volume_adj + vol_ramp_tick +
* last_input 复位)直接用;run_menu 走自己的 meow_mode 分支。
*
* 范围与边界(§3 P1,勿越界):
* - 本模块只收敛**音量键**(KEY_VOLUMEDOWN/KEY_VOLUMEUP)。播放键/电源键 P1 不路由,
* 维持各页现状(播放键严禁改成 cmd=6;电源键单击/菜单/8s 状态机不动)。
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸)。
* - 锁屏层(EVIOCGRAB)/App 会话层从不调用 key_router_pump,天然退出本路由。
*/
#ifndef MEOW_KEY_ROUTER_H
#define MEOW_KEY_ROUTER_H
/* 一框音量边沿/held/ramp 输出。P1 只填音量;播放/电源字段后续期(P2/P3)再扩展。
* 字段语义:
* vol_dn_edge / vol_up_edge —— 防抖后**稳定按下边沿**(= stable ∧ ¬上一 stable;
* 1=本圈新按下)。这是「音量边沿」的唯一权威判定。
* vol_held_dn / vol_held_up —— **原始(未防抖)**按住态,ramp 长按 held 用
* (松开立即停止,同 run_menu/now_playing 现状)。
* vol_st_dn / vol_st_up —— **防抖稳定**按住态(wake_suppress「全键抬起」判定用)。
* ramping —— vol_ramp_dir != 0(线性渐变是否在跑;repaint 用)。
*/
typedef struct {
int vol_dn_edge;
int vol_up_edge;
int vol_held_dn;
int vol_held_up;
int vol_st_dn;
int vol_st_up;
int ramping;
} key_evt_t;
/* 进程级单开 kp_*(事件设备)+ 初始化防抖态/基线(幂等,可安全重复调用)。 */
void key_router_init(void);
/* 每页 for(;;) 每圈调用一次:poll 两设备 → 防抖音量键 → 产边沿/held/ramp。
* 不做 settings_volume_adj、不驱动 vol_ramp_tick、不复位 last_input —— 皆由调用页决定
* (见 key_router_default_volume 与各页自己的分支)。 */
key_evt_t key_router_pump(void);
/* 模态/菜单/嵌套会话返回/唤醒后重播基线:kd_* 清零 + m_prev_* 对齐当前 raw
* (对齐现状各页「memset(kd_*) + kp_poll + m_prev_*=当前 raw」的语义,防假边沿)。
* 各页在原本做该重播处调用。 */
void key_router_rebase(void);
/* 页进入/退出登记(记录当前哨兵;与 sdk meow_ui_activity_active 交叉校验)。 */
void key_router_page_enter(int sentinel);
void key_router_page_exit(int sentinel);
/* 默认「音量键=调音量」页回调(非导航页直接用):音量边沿 → settings_volume_adj(±step);
* 若有 ramp 在爬 → vol_ramp_tick(held);边沿 → 复位 *last_input。
* run_menu 不走本函数(自行 meow_mode 分支)。 */
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input);
#endif /* MEOW_KEY_ROUTER_H */
+101 -2
View File
@@ -40,7 +40,7 @@ void meow_panel_off(void)
{
meow_saved_brightness = read_brightness();
if (meow_saved_brightness <= 0) meow_saved_brightness = 60;
write_brightness(0);
write_brightness(0, "meow_panel_off");
if (screen_off_mode == 0) {
/* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */
int f = open(FB_DEV, O_RDWR);
@@ -58,7 +58,17 @@ void meow_panel_on(void)
{
int f = open(FB_DEV, O_RDWR);
if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); }
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60);
/* 亮度恢复以**用户设置**为准,而非 meow_saved_brightness(可能过期)。
* watchdog 独立息屏(wd_panel_off 直接 write_brightness(0),见 meow_watchdog.c)**不会**
* 更新本进程 meow_saved_brightness——若用其恢复会回错值(真机「PIN 亮屏亮度跳高」,
* 2026-09-03)。settings 已从盘上读到→按 display.brightness(clamp)恢复;未读到(早启动)
* →回退 meow_saved_brightness 现值(保持旧行为,防开机竞态)。 */
if (meow_settings_loaded()) {
int b = meow_settings_brightness_clamped();
if (b > 0) { write_brightness(b, "meow_panel_on"); meow_saved_brightness = b; }
} else {
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60, "meow_panel_on");
}
meow_screen_on = 1;
/* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在
* 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位
@@ -202,3 +212,92 @@ void load_config(void)
menu_bg, seq_len, seq_codes[0], seq_gap_ms);
}
/* =========================================================================
* 系统级音量键翻页门(READER_UI_DESIGN §E;用户 2026-09-04 拍板)。
*
* 语义:音量键是系统资源(默认调音量)。本门在「当前台 App 为其 app.json 声明
* volume_page 的应用(MVP:os.meow.reader)」时,把 VOL_UP/VOL_DOWN 转换成系统翻页
* 信号(MSG_KEY_PAGEUP/PAGEDOWN,meow_ipc.h)经 IPC 下发给该前台 App;其余情况一概
* 放行(return 0),音量基本行为/插孔消抖/音量反转等绝不破坏。
*
* 职责分离(关键,且最小侵入):
* - 本模块只提供「判定 + 门」:meow_volpage_app_allows() 读 app.json 声明;
* meow_volpage_key() 在「已武装 sink 且按下 VOL」时消费并发下发;
* - 武装(sink 注册)= 前台应用进入体积页声明 app 时调用 meow_volpage_sink()
* (应用存在连接后其 fd 生效);离开/退出时 meow_volpage_sink_clear()。
* 该接线落在「前台会话的按键路径」(app_launcher.c / meow_launcherd.c 主循环),
* 属主会话统一收编范围,此处不越界改动。
*
* 声明字段(app.json):`"volume_page": true`(或 1)。未来其它 App 可声明复用。
* ========================================================================= */
/* 前台翻页 sink(本模块私有;单写者=前台会话上下文,读=按键门)。 */
static char g_volpage_app[MEOW_APP_ID_MAX] = "";
static int g_volpage_fd = -1;
void meow_volpage_sink_clear(void); /* 前向声明(sink 内调用) */
/* 读 <MEOWAPPS>/<app_id>/app.json 是否声明 volume_page=true|1。1=声明;0=否/读不到。 */
int meow_volpage_app_allows(const char *app_id)
{
if (!app_id || !app_id[0]) return 0;
char path[320];
snprintf(path, sizeof path, "%s/%s/app.json", MEOWAPPS, app_id);
FILE *f = fopen(path, "r");
if (!f) return 0;
/* 轻量扫描:找 "volume_page" → 跳过空白 → true/1。app.json 为紧凑 JSON。 */
char buf[1024];
buf[0] = 0;
size_t rd = fread(buf, 1, sizeof buf - 1, f);
buf[rd] = 0;
fclose(f);
const char *p = buf;
while ((p = strstr(p, "volume_page")) != NULL) {
p += strlen("volume_page");
while (*p == ' ' || *p == '\t' || *p == ':' || *p == '"') p++;
if (strncmp(p, "true", 4) == 0) return 1;
if (*p == '1' && (p[1] < '0' || p[1] > '9')) return 1;
/* 未命中则继续找(防字段重复);跳过本段 */
while (*p && *p != ',' && *p != '}') p++;
}
return 0;
}
/* 武装/清除 sink。app_id 非空且 fd>=0 → 记录;否则清除。幂等(同 app_id+fd 不重复记日志,
* 供前台会话循环每圈安全重调,也兼容断线重连换 fd)。 */
void meow_volpage_sink(const char *app_id, int fd)
{
if (!app_id || !app_id[0] || fd < 0) { meow_volpage_sink_clear(); return; }
if (g_volpage_fd == fd && !strcmp(g_volpage_app, app_id)) return; /* 同 app_id+fd:幂等 */
snprintf(g_volpage_app, sizeof g_volpage_app, "%s", app_id);
g_volpage_fd = fd;
logmsg("volpage: sink armed app=%s fd=%d", g_volpage_app, g_volpage_fd);
}
void meow_volpage_sink_clear(void)
{
if (g_volpage_fd >= 0)
logmsg("volpage: sink cleared app=%s", g_volpage_app[0] ? g_volpage_app : "?");
g_volpage_app[0] = 0;
g_volpage_fd = -1;
}
int meow_volpage_armed(void) { return g_volpage_fd >= 0; }
/* 按键门:按下 VOL_UP/DOWN 且 sink 已武装 → 下发翻页信号并消费(返回 1)。
* 未武装 / 非 VOL / 抬起 → 返回 0(放行给音量路径)。 */
int meow_volpage_key(int code, int is_down)
{
if (!is_down) return 0; /* 只响应按下边沿 */
if (g_volpage_fd < 0) return 0; /* 未武装:音量照旧 */
if (code == KEY_VOLUMEUP) {
(void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEUP, 1, NULL, 0);
return 1;
}
if (code == KEY_VOLUMEDOWN) {
(void)meow_ipc_send_pub(g_volpage_fd, MSG_KEY_PAGEDOWN, 1, NULL, 0);
return 1;
}
return 0;
}
+389
View File
@@ -0,0 +1,389 @@
/* lock_screen.c — MeowOS 锁屏 4 位 PIN(纯逻辑;宿主可单测 lock_screen.h)。
*
* 与 meowpanel.c 的访问级 PIN(PANEL_ENHANCE_PLAN)**同源同格式**:`<salt_hex>:<sha256_hex(salt||pin)>`。
* 内联 SHA-256 取自 meow_verify/sha256.c(FIPS 180-4,公有领域参考写法),前缀 lck_ 避免符号冲突;
* 算法与 meowpanel.c 的 mp_sha256 逐字一致 → 面板生成的 panel.pin 无需重设即可被本模块校验。
*
* 日志用 fprintf(stderr)(前缀 [lock]),不依赖 launcher 的 logmsg —— 宿主测试可直接 #include 本文件。
* 依赖 meow_settings_* API(同一 settings.conf),持久化走 meow_settings_save()(含 boot.default
* 同步,幂等)。
*/
#include "lock_screen.h"
#include "meow_settings.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#define LCK_LOG(...) fprintf(stderr, "[lock] " __VA_ARGS__)
/* ---- 最小 SHA-256(FIPS 180-4,公有领域参考写法;内联拷贝自 meow_verify/sha256.c,前缀 lck_) ---- */
static const uint32_t lck_sha256_k[64] = {
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
};
#define LCK_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
#define LCK_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
#define LCK_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define LCK_BSIG0(x) (LCK_ROR(x,2) ^ LCK_ROR(x,13) ^ LCK_ROR(x,22))
#define LCK_BSIG1(x) (LCK_ROR(x,6) ^ LCK_ROR(x,11) ^ LCK_ROR(x,25))
#define LCK_SSIG0(x) (LCK_ROR(x,7) ^ LCK_ROR(x,18) ^ ((x) >> 3))
#define LCK_SSIG1(x) (LCK_ROR(x,17) ^ LCK_ROR(x,19) ^ ((x) >> 10))
typedef struct lck_sha256_ctx {
uint32_t state[8];
uint64_t count;
uint8_t buf[64];
size_t buflen;
} lck_sha256_ctx;
static void lck_sha256_init(lck_sha256_ctx *c)
{
c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u;
c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au;
c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu;
c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u;
c->count = 0; c->buflen = 0;
}
static void lck_sha256_block(lck_sha256_ctx *c, const uint8_t *p)
{
uint32_t w[64];
int i;
for (i = 0; i < 16; i++)
w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) |
((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3];
for (i = 16; i < 64; i++)
w[i] = LCK_SSIG1(w[i-2]) + w[i-7] + LCK_SSIG0(w[i-15]) + w[i-16];
uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3];
uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7];
for (i = 0; i < 64; i++) {
uint32_t t1 = h + LCK_BSIG1(e) + LCK_CH(e,f,g) + lck_sha256_k[i] + w[i];
uint32_t t2 = LCK_BSIG0(a) + LCK_MAJ(a,b,cc);
h = g; g = f; f = e; e = d + t1;
d = cc; cc = b; b = a; a = t1 + t2;
}
c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d;
c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h;
}
static void lck_sha256_update(lck_sha256_ctx *c, const uint8_t *data, size_t len)
{
c->count += (uint64_t)len;
if (c->buflen) {
size_t need = 64 - c->buflen;
size_t take = len < need ? len : need;
memcpy(c->buf + c->buflen, data, take);
c->buflen += take; data += take; len -= take;
if (c->buflen == 64) { lck_sha256_block(c, c->buf); c->buflen = 0; }
}
while (len >= 64) { lck_sha256_block(c, data); data += 64; len -= 64; }
if (len) { memcpy(c->buf, data, len); c->buflen = len; }
}
static void lck_sha256_final(lck_sha256_ctx *c, uint8_t out[32])
{
uint64_t bitlen = c->count * 8;
uint8_t *b = c->buf;
size_t bl = c->buflen;
int i;
b[bl++] = 0x80;
if (bl > 56) {
while (bl < 64) b[bl++] = 0;
lck_sha256_block(c, b);
bl = 0;
}
while (bl < 56) b[bl++] = 0;
for (i = 0; i < 8; i++)
b[bl++] = (uint8_t)(bitlen >> (56 - i * 8));
lck_sha256_block(c, b);
for (i = 0; i < 8; i++) {
out[i*4] = (uint8_t)(c->state[i] >> 24);
out[i*4+1] = (uint8_t)(c->state[i] >> 16);
out[i*4+2] = (uint8_t)(c->state[i] >> 8);
out[i*4+3] = (uint8_t)(c->state[i]);
}
}
#undef LCK_ROR
#undef LCK_CH
#undef LCK_MAJ
#undef LCK_BSIG0
#undef LCK_BSIG1
#undef LCK_SSIG0
#undef LCK_SSIG1
/* ---- 哈希基元(格式 `<salt_hex>:<sha256_hex(salt||pin)>`,与面板全等)---- */
static void lck_hex(const unsigned char *raw, size_t n, char *out)
{
static const char hex[] = "0123456789abcdef";
for (size_t i = 0; i < n; i++) {
out[i*2] = hex[raw[i] >> 4];
out[i*2+1] = hex[raw[i] & 0xf];
}
out[n*2] = 0;
}
static int lck_read_random(unsigned char *buf, size_t n)
{
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
size_t got = 0;
while (got < n) {
ssize_t rd = read(fd, buf + got, n - got);
if (rd <= 0) { close(fd); return -1; }
got += (size_t)rd;
}
close(fd);
return 0;
}
static int lck_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */
{
unsigned char raw[16];
if (outcap < 33) return -1;
if (lck_read_random(raw, sizeof raw) < 0) return -1;
lck_hex(raw, sizeof raw, out);
return 0;
}
static int lck_pass_hash(const char *salt, const char *password, char *out, int outcap)
{
lck_sha256_ctx c;
uint8_t digest[32];
if (outcap < LCK_HASH_LEN + 1) return -1;
lck_sha256_init(&c);
lck_sha256_update(&c, (const uint8_t *)salt, strlen(salt));
lck_sha256_update(&c, (const uint8_t *)password, strlen(password));
lck_sha256_final(&c, digest);
lck_hex(digest, sizeof digest, out);
return 0;
}
static int lck_make_hash(const char *password, char *out, int outcap) /* salt:hex */
{
char salt[33], h[LCK_HASH_LEN + 1];
if (lck_make_salt(salt, sizeof salt) < 0) return -1;
if (lck_pass_hash(salt, password, h, sizeof h) < 0) return -1;
snprintf(out, (size_t)outcap, "%s:%s", salt, h);
return 0;
}
/* 校验 `salt:hex` 是否匹配明文(恒定比较,不反解)。stored 为 `salt:hex` 完整串。 */
static int lck_hash_matches(const char *stored, const char *password)
{
if (!stored || !stored[0] || !password) return 0;
char tmp[160];
snprintf(tmp, sizeof tmp, "%s", stored);
char *colon = strchr(tmp, ':');
if (!colon) return 0;
*colon = 0;
char want[LCK_HASH_LEN + 1];
if (lck_pass_hash(tmp, password, want, sizeof want) != 0) return 0;
if (strlen(colon + 1) != LCK_HASH_LEN) return 0;
return memcmp(want, colon + 1, LCK_HASH_LEN) == 0;
}
/* ---- 设置读取辅助(NULL 表示键缺失/空)---- */
static char *lck_get(const char *key)
{
char buf[MEOW_SET_MAX_LEN];
if (meow_settings_get_str(key, buf, sizeof buf) != 0) return NULL;
if (!buf[0]) return NULL; /* 空值=未设 */
char *r = strdup(buf);
return r;
}
static int lck_get_int(const char *key)
{
int v = 0;
if (meow_settings_get_int(key, &v) != 0) v = 0;
return v;
}
static int lck_set_int(const char *key, int v)
{
return meow_settings_set_int(key, v);
}
static int lck_persist(void)
{
return meow_settings_save();
}
/* ---- 公开接口 ---- */
int lck_pin_valid(const char *pin)
{
if (!pin) return 0;
size_t l = strlen(pin);
if (l != LCK_PIN_LEN) return 0;
for (size_t i = 0; i < l; i++)
if (pin[i] < '0' || pin[i] > '9') return 0;
return 1;
}
int lck_pin_configured(void)
{
char *sp = lck_get(LCK_KEY_PIN);
int s = (sp != NULL);
free(sp);
if (s) return 1;
char *pp = lck_get(LCK_KEY_PANEL_PIN);
int p = (pp != NULL);
free(pp);
return p;
}
int lck_lock_required(int allow_disable)
{
if (!lck_pin_configured()) return 0; /* 未设凭据:锁屏无鉴别物,不开 */
if (allow_disable) {
int wake = 1;
if (meow_settings_get_int(LCK_KEY_WAKE, &wake) != 0) wake = 1;
return wake ? 1 : 0;
}
return 1; /* 手动锁:不看 WAKE 键 */
}
int lck_pin_check(const char *pin)
{
if (!lck_pin_valid(pin)) return 0;
char *sp = lck_get(LCK_KEY_PIN);
if (sp) {
int ok = lck_hash_matches(sp, pin);
free(sp);
return ok;
}
/* system.pin 未设:迁移期回退校验 panel.pin */
char *pp = lck_get(LCK_KEY_PANEL_PIN);
if (pp) {
int ok = lck_hash_matches(pp, pin);
free(pp);
return ok;
}
return 0;
}
int lck_pin_set(const char *pin)
{
if (!lck_pin_valid(pin)) return MEOW_ERR_INVAL;
char hash[160]; /* 32(salt) + ':' + 64(sha256 hex) + NUL = 98 */
if (lck_make_hash(pin, hash, sizeof hash) != 0) return MEOW_ERR_IO;
if (meow_settings_set_str(LCK_KEY_PIN, hash) != MEOW_OK) return MEOW_ERR_INVAL;
lck_set_int(LCK_KEY_FAILS, 0);
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
lck_persist();
LCK_LOG("pin set (%d digits, hash stored salt:sha256)\n", LCK_PIN_LEN);
return MEOW_OK;
}
int lck_pin_clear(void)
{
if (meow_settings_set_str(LCK_KEY_PIN, "") != MEOW_OK) return MEOW_ERR_INVAL;
lck_set_int(LCK_KEY_FAILS, 0);
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
lck_persist();
LCK_LOG("pin cleared (unlock)\n");
return MEOW_OK;
}
int lck_pin_success(void)
{
char *sp = lck_get(LCK_KEY_PIN);
if (!sp) {
/* 首次成功:把 panel.pin 并入 system.pin(一次性迁移),此后 system.pin 为准 */
char *pp = lck_get(LCK_KEY_PANEL_PIN);
if (pp) {
meow_settings_set_str(LCK_KEY_PIN, pp);
LCK_LOG("success: migrated panel.pin -> system.pin\n");
free(pp);
} else {
LCK_LOG("success: no system.pin and no panel.pin?!\n");
}
}
free(sp);
lck_set_int(LCK_KEY_FAILS, 0);
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
lck_persist();
return MEOW_OK;
}
int lck_pin_fail(void)
{
time_t now = time(NULL);
int fails = lck_get_int(LCK_KEY_FAILS);
int until = lck_get_int(LCK_KEY_LOCK_UNTIL);
/* 已锁定:不再递增(锁屏页本应禁输入;防御) */
if (until > 0 && (time_t)until > now)
return (int)((time_t)until - now);
fails++;
if (fails >= LCK_PIN_MAX_FAILS) {
until = (int)now + LCK_PIN_LOCK_SEC;
fails = 0;
LCK_LOG("fail: %d consecutive -> LOCK %ds (until=%d)\n",
LCK_PIN_MAX_FAILS, LCK_PIN_LOCK_SEC, until);
} else {
until = 0;
LCK_LOG("fail: attempt %d/%d\n", fails, LCK_PIN_MAX_FAILS);
}
lck_set_int(LCK_KEY_FAILS, fails);
lck_set_int(LCK_KEY_LOCK_UNTIL, until);
lck_persist();
return (until > 0 && (time_t)until > now) ? (int)((time_t)until - now) : 0;
}
time_t lck_lock_remaining(void)
{
time_t now = time(NULL);
int until = lck_get_int(LCK_KEY_LOCK_UNTIL);
if (until > 0 && (time_t)until > now)
return (time_t)until - now;
if (until > 0) {
/* 截止已过:清锁定戳+计数(进程内解禁),落盘 */
LCK_LOG("lock expired (%d < now=%ld), clearing\n", until, (long)now);
lck_set_int(LCK_KEY_LOCK_UNTIL, 0);
lck_set_int(LCK_KEY_FAILS, 0);
lck_persist();
}
return 0;
}
/* ---- 面板一次性解锁恢复口(tmpfs 标志)---- */
int lck_unlock_once_consume(void)
{
if (access(LCK_UNLOCK_ONCE_FILE, F_OK) != 0) return 0; /* 不存在:不影响 */
unlink(LCK_UNLOCK_ONCE_FILE);
LCK_LOG("unlock-once: bypass file present -> consume + release once\n");
return 1;
}
int lck_manual_lock_set(void)
{
int fd = open(LCK_MANUAL_LOCK_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { LCK_LOG("manual-lock: open %s fail\n", LCK_MANUAL_LOCK_FILE); return MEOW_ERR_IO; }
write(fd, "1", 1); close(fd);
LCK_LOG("manual-lock: set %s\n", LCK_MANUAL_LOCK_FILE);
return MEOW_OK;
}
void lck_manual_lock_clear(void)
{
if (access(LCK_MANUAL_LOCK_FILE, F_OK) == 0) {
unlink(LCK_MANUAL_LOCK_FILE);
LCK_LOG("manual-lock: cleared\n");
}
}
int lck_wake_should_lock(void)
{
if (!lck_pin_configured()) return 0;
int wake = 1;
if (meow_settings_get_int(LCK_KEY_WAKE, &wake) != 0) wake = 1;
if (wake) return 1;
/* 唤醒锁已关,但存在「主动锁」标志(电源菜单「锁定」)→ 仍必锁屏 */
return access(LCK_MANUAL_LOCK_FILE, F_OK) == 0;
}
+108
View File
@@ -0,0 +1,108 @@
/* lock_screen.h — MeowOS 锁屏 4 位 PIN(系统级安全,纯逻辑;宿主可单测)。
*
* 模型(用户定案,2026-09-02):
* - 设置键 `system.pin`:4 位数字 PIN 的 sha256+salt 哈希,存 settings.conf。
* 格式与面板 panel.pin **全等**:`<salt_hex>:<sha256_hex(salt||pin)>`,salt 16B
* /dev/urandom → 32 hex;绝不存明文、绝不在日志/响应中输出 hash。
* - 面板同源迁移:`panel.pin` 已设而 `system.pin` 未设时,锁屏接受 panel.pin,并在
* **首次验证成功后并入 system.pin**(lck_pin_success 把 panel.pin 值拷入 system.pin)。
* - 锁屏时机:息屏唤醒后(默认开,`system.pin_lock_on_wake=1` 可关)+ 电源菜单「锁定」
* 手动锁;**开机不锁**(boot 进主屏不拦,本逻辑不被 boot 路径调用)。
* - 错 5 次锁 5 分钟:`system.pin_fails`(连续错计数)与 `system.pin_lock_until`(锁定
* 截止 epoch 秒)**持久化到 settings.conf**——launcher 重启不清零(面板的进程内存方案
* 在重启后失效,这里要落盘)。
* - 兜底:SSH root 删 `system.pin` 键即解锁(无其它后门)。
*
* 本头只声明纯逻辑接口;渲染/输入循环在 lock_screen_ui.c(run_lock_screen)。逻辑依赖
* meow_settings_* API(与 meowpanel 的 settings_get_value 语义同源,走同一 settings.conf)。
* 宿主测试:#include "lock_screen.c" + 真实 meow_settings.c(-DMEOW_SETTINGS_FILE 覆盖)。
*/
#ifndef MEOW_LOCK_SCREEN_H
#define MEOW_LOCK_SCREEN_H
#ifdef __cplusplus
extern "C" {
#endif
#include <time.h>
#define LCK_PIN_LEN 4 /* 恰好 4 位数字 */
#define LCK_PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */
#define LCK_PIN_LOCK_SEC 300 /* 锁定 5 分钟 */
#define LCK_HASH_LEN 64 /* sha256 hex 长度 */
/* ---- 设置键(必须已在 meow_settings.c g_defs 注册)---- */
#define LCK_KEY_PIN "system.pin"
#define LCK_KEY_WAKE "system.pin_lock_on_wake"
#define LCK_KEY_LOCK_UNTIL "system.pin_lock_until"
#define LCK_KEY_FAILS "system.pin_fails"
#define LCK_KEY_PANEL_PIN "panel.pin" /* 面板 PIN(read-only 兼容源) */
/* ---- 文件标志(tmpfs,重启自然消失)---- */
#define LCK_UNLOCK_ONCE_FILE "/tmp/meow_pin_unlock_once" /* 面板一次性解锁恢复口:存在即消费放行 */
#define LCK_MANUAL_LOCK_FILE "/tmp/meow_pin_locked" /* 电源菜单「锁定」主动锁标志:唤醒必锁屏 */
/* PIN 合法:恰好 4 位数字(0-9)。NULL/长度不符/含非数字 → 0。 */
int lck_pin_valid(const char *pin);
/* 是否已配锁屏凭据:system.pin 设 或 panel.pin 设(锁屏可开)。 */
int lck_pin_configured(void);
/* 唤醒是否应锁屏:system.pin_lock_on_wake(默认 1) 且 已配凭据。
* 仅由「息屏唤醒」路径调用——boot 路径不调,天然开机不锁。
* allow_disable=0 时忽略 WAKE 键(电源菜单「锁定」手动锁用,锁前需已配凭据)。 */
int lck_lock_required(int allow_disable);
/* 明文校验:system.pin 优先;未设时回退校验 panel.pin(迁移)。恒定比较,不反解。 */
int lck_pin_check(const char *pin);
/* 设置/修改 system.pin(明文→sha256+salt 落盘):校验 4 位数字;清连续错计数+锁定戳;**落盘**。
* 返回 MEOW_OK / 负错误。设置页/CLI 设锁屏 PIN 走此。 */
int lck_pin_set(const char *pin);
/* 清除 system.pin(解锁兜底,SSH root 删键即解锁的同义 API):清计数+锁定戳,**落盘**。 */
int lck_pin_clear(void);
/* 记录一次成功验证:首次成功时把 panel.pin 并入 system.pin(迁移);清连续错计数 + 清
* 锁定戳;**落盘**。返回 MEOW_OK。 */
int lck_pin_success(void);
/* 记录一次失败:递增连续错计数(持久化);达 LCK_PIN_MAX_FAILS → 设锁定戳(now+300s)并清
* 计数。已处于锁定(now<截止)时不递增。返回**剩余锁定秒**(0=未锁)。
* 调用方须先 lck_lock_remaining() 确认未锁才让输入;已锁时本函数不改变截止。 */
int lck_pin_fail(void);
/* 剩余锁定秒(0=未锁定)。若截止已过,自动清锁定戳+计数并落盘(进程内已解禁)。 */
time_t lck_lock_remaining(void);
/* ---- 面板一次性解锁恢复口(tmpfs 标志文件)----
* 锁屏页进入时 / 每次重试前调用:LCK_UNLOCK_ONCE_FILE 存在 → **删除并返回 1(放行进入系统一次)**;
* 不存在 → 0。不校验 PIN、不清除 system.pin 本身、不重置失败计数(重启标志自然消失)。 */
int lck_unlock_once_consume(void);
/* ---- 电源菜单「锁定」主动锁标志(tmpfs,重启消失)----
* 主动锁屏:写 LCK_MANUAL_LOCK_FILE → 之后唤醒 `lck_wake_should_lock()` 无论 system.pin_lock_on_wake
* 是否开都必走锁屏;解锁(PIN 或面板绕过)时清除。 */
int lck_manual_lock_set(void); /* 写标志;返回 MEOW_OK */
void lck_manual_lock_clear(void); /* 删标志(幂等) */
/* 唤醒是否应锁屏:已配凭据 且 (system.pin_lock_on_wake=1 或 主动锁标志存在)。
* 取代 lck_lock_required(1) 作为唤醒门——主动锁强制锁屏,覆盖「关闭唤醒锁」的手动锁场景。 */
int lck_wake_should_lock(void);
/* ── 锁屏叠加期 App 存活探针(§310 残影修复,纯判定,宿主可单测;header-only inline)----
* 锁屏运行在 App 前台叠加会话中(overlay_pid>0,run_app_lock_overlay 置位)且该进程已成僵尸
* (调用方 proc_is_zombie 判定后传入 proc_zombie=1)→ 应中止锁屏走统一退出(返回 1)。
* 菜单直接锁屏(overlay_pid=0,无 App 可死)恒 0;此谓词不实际读 /proc。 */
static inline int lck_overlay_should_abort(int overlay_pid, int proc_zombie)
{
return (overlay_pid > 0 && proc_zombie) ? 1 : 0;
}
/* run_lock_screen() 返回值:1=已解锁,0=锁屏期间电源键息屏。叠加期 App 崩溃时返回
* LCK_OVERLAY_RET_DIED(-1)——调用方 run_app_lock_overlay 据此外走「统一退出路径」
* (不再 SIGCONT 死进程/不恢复死帧,交回 waitpid 循环回收,run_menu 重绘主屏)。 */
#define LCK_OVERLAY_RET_DIED (-1)
#ifdef __cplusplus
}
#endif
#endif /* MEOW_LOCK_SCREEN_H */
+747
View File
@@ -0,0 +1,747 @@
/* lock_screen_ui.c — MeowOS 锁屏页(系统级全屏页面:launcher 渲染,App 不可见不可触)。
*
* 用户定案(2026-09-02):
* - 息屏唤醒后(默认开,system.pin_lock_on_wake=1 可关)+ 电源菜单「锁定」手动锁;开机不锁。
* - 数字键盘直接用 SDK 按钮画(不调 IME);输入 4 位自动校验;错 5 次锁 5 分钟(持久化)。
* - 正确 → 进主屏(return 1);锁屏页期间电源键=息屏(允许再睡,return 0),音量键不动;
* 不响应左缘返回 / 通知中心手势(本函数不实现这些手势,触摸只认键盘 tap)。
* - 输入不回显数字(● 遮罩)。兜底见 lock_screen.c(SSH root 删 system.pin 即解锁)。
*
* 渲染/输入循环与电源菜单同构(meow_touch + kd_* + kp_*),阻塞到解锁或电源息屏。
* 纯逻辑在 lock_screen.c(可宿主单测);本文件只做 UI 与输入胶水。
*/
#include "meow_launcher_internal.h"
#include "overlay_gate.h" /* OVERLAY_LOCK_COVER_MS(锁屏重铺阈值,与单测同源) */
/* §310 残影:锁屏处于 App 前台叠加会话时置位当前前台 App pid(run_app_lock_overlay 写入)。
* run_lock_screen 每圈据此探测 App 是否已死(组合 App 冻结缓冲 evdev 会在解锁瞬间回放成点击 /
* 叠加期 OOM 杀让 fb0 留残帧——探到死即中止锁屏,交回 waitpid 循环回收 + run_menu 重绘主屏)。
* 菜单直接锁屏(无 App)时保持 0 = 不探测。 */
int g_lock_overlay_pid = 0;
/* ---- 键盘几何(480×720):3 列×4 行圆角按钮 ---- */
#define LK_X0 32 /* 首列左缘 */
#define LK_COLW 130 /* 列宽 */
#define LK_COLG 8 /* 列间隙 */
#define LK_Y0 320 /* 首行顶缘 */
#define LK_ROW_H 82 /* 行高 */
#define LK_ROWG 10 /* 行间隙 */
#define LK_ROWS 4
#define LK_COLS 3
/* 12 键(行优先):digit=-2 清空 / -1 退格 / >=0 数字 */
#define LK_ACT_CLR (-2)
#define LK_ACT_BS (-1)
static const int lk_keys[LK_ROWS][LK_COLS] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 },
{ LK_ACT_CLR, 0, LK_ACT_BS },
};
/* ---- M7.1b 锁屏控件化:PIN 数字键盘 / 时钟 / 提示 / PIN 圆点 改用 SDK 控件 ----
* 旧自绘锁屏 UI 保留在 #if 0(回退位,见 lk_draw/lk_key_hit),新 SDK 渲染默认启用;
* run_lock_screen 会话骨架(EVIOCGRAB / 自睡 / 解锁脏区修复 / 物理键转发)不动,只替换
* 它调用的绘制/命中函数体。控件 owner 钉 LK_SENTINEL_ACT(子页 0x52 之外的独立哨兵)。 */
#define LK_SENTINEL_ACT 0x51 /* 锁屏控件 owner 哨兵(与子页 SUB_SENTINEL_ACT=0x52 区分) */
#define LK_DOWN_NONE (-10) /* 无键按下哨兵(关键值范围 LK_ACT_CLR=-2/-1/0-9) */
static meow_widget *lk_key_w[LK_ROWS][LK_COLS]; /* 12 键按钮(meow_widget_custom) */
static int lk_widgets_ready = 0;
static int g_lk_down_key = LK_DOWN_NONE; /* 当前按下的键(跟手指按压态) */
/* 键盘按钮 row*3+col 的命中矩形(x,y,w,h) */
static void lk_key_rect(int row, int col, int *x, int *y, int *w, int *h)
{
*x = LK_X0 + col * (LK_COLW + LK_COLG);
*y = LK_Y0 + row * (LK_ROW_H + LK_ROWG);
*w = LK_COLW;
*h = LK_ROW_H;
}
/* 居中文本(真实字体优先;回退 ASCII 点阵) */
static void lk_text(int cx, int y, const char *s, int size, uint32_t color)
{
if (mfont_ready) {
int w = meow_font_width_text(s, size);
meow_font_drawtext((uint16_t *)fb, cx - w / 2, y, s, rgb565(color), size);
} else {
draw_text(cx - (int)strlen(s) * 3, y + 1, s, 1, color);
}
}
/* 单个 PIN 圆点:filled=1 实心(已输入),0=空心(未输入)。 */
static void lk_pin_dot(int cx, int cy, int r, int filled, uint32_t ink, uint32_t bg)
{
if (filled) {
fill_circle(cx, cy, r, ink);
} else {
fill_circle(cx, cy, r, ink); /* 外圈(描边) */
fill_circle(cx, cy, r - 3, bg); /* 内孔(表面底色) */
}
}
static void lk_draw_pin(int entered, const meow_ui_theme_t *th)
{
int cx = fb_xres / 2;
int y = 210;
int gap = 48, r = 20;
int x0 = cx - (3 * gap) / 2; /* 首圆圆心;四圆等距 gap */
for (int i = 0; i < LCK_PIN_LEN; i++)
lk_pin_dot(x0 + i * gap, y, r, i < entered, th->primary, th->surface);
}
/* ---- M7.1b 控件化:SDK meow_widget 键盘 + meow_font 文字 ---- */
/* 数字键标签(1-9/0 单字符;static 缓冲各调用立即消费,无跨调用残留) */
static const char *lk_key_digit(int key)
{
static char d[2];
d[0] = (char)('0' + key);
d[1] = 0;
return d;
}
/* 键渲染:surfaceContainer 圆角底(MD3)+ 按压态 primary 26% α + 居中标签(SDK meow_font)。
* 颜色全部取 meow_theme_get()(SDK 单一权威:MD3/MIUIx × 明暗自动);读 w->state 的
* MEOW_WS_PRESSED 位(跟手指,由 lk_set_down_key 置/清)。 */
static void lk_key_render(meow_widget *w, const meow_theme *th, void *ctx)
{
int key = (int)(intptr_t)ctx;
int rc[4]; meow_widget_rect(w, rc); /* 绝对坐标 x,y,w,h(控件不透明,用 SDK 只读几何) */
int x = rc[0], y = rc[1], ww = rc[2], hh = rc[3];
unsigned st = meow_widget_state(w);
int rad = (th->radius > 0) ? th->radius : 10;
meow_gfx_fill_round(x, y, ww, hh, rad, th->surfaceContainer);
if (st & MEOW_WS_PRESSED)
meow_gfx_fill_round_alpha(x, y, ww, hh, rad, th->primary, 66);
const char *label;
int size;
if (key >= 0) { size = 30; label = lk_key_digit(key); }
else if (key == LK_ACT_CLR) { size = 22; label = mfont_ready ? meow_str_zh("lock.clear", "清空") : "C"; }
else { size = 22; label = mfont_ready ? meow_str_zh("lock.backspace", "退格") : "DEL"; }
int cx = x + ww / 2;
int cy = y + hh / 2 - meow_ui_text_height(size) / 2;
meow_ui_text_center(cx, cy, label, size, th->onSurface);
}
/* 懒创建 12 键(owner=LK_SENTINEL_ACT;仅首次调用建一次)。布局几何沿用 lk_key_rect。 */
static void lk_widgets_build(void)
{
if (lk_widgets_ready) return;
meow_ui_activity_push(LK_SENTINEL_ACT);
for (int r = 0; r < LK_ROWS; r++) {
for (int c = 0; c < LK_COLS; c++) {
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
lk_key_w[r][c] = meow_widget_custom_create(x, y, w, h, lk_key_render, NULL,
(void *)(intptr_t)lk_keys[r][c]);
}
}
meow_ui_activity_pop();
lk_widgets_ready = 1;
}
/* 键值 -> 按钮(按压态置/清位用)。 */
static meow_widget *lk_widget_for_key(int key)
{
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_keys[r][c] == key) return lk_key_w[r][c];
return NULL;
}
/* 坐标 -> 键值(SDK meow_widget_hit;owner==active 已由调用方保证)。未命中返回 LK_DOWN_NONE。 */
static int lk_key_value_at(int x, int y)
{
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_key_w[r][c] && meow_widget_hit(lk_key_w[r][c], x, y))
return lk_keys[r][c];
return LK_DOWN_NONE;
}
/* 更新按压态(跟手指):清旧键状态位、置新键;返回 1=按键变化(调用方据此重绘)。 */
static int lk_set_down_key(int key)
{
if (key == g_lk_down_key) return 0;
meow_widget *old = lk_widget_for_key(g_lk_down_key);
if (old) meow_widget_set_state(old, MEOW_WS_PRESSED, 0);
meow_widget *nw = lk_widget_for_key(key);
if (nw) meow_widget_set_state(nw, MEOW_WS_PRESSED, 1);
g_lk_down_key = key;
return 1;
}
/* PIN 输入圆点(● 遮罩:已输入=primary 实心;未输入=primary 环 + surface 孔)。 */
static void lk_draw_pin_dots(int entered, const meow_theme *mt)
{
int cx = fb_xres / 2;
int y = 210;
int gap = 48, r = 20;
int x0 = cx - (3 * gap) / 2;
for (int i = 0; i < LCK_PIN_LEN; i++) {
int dx = x0 + i * gap;
fill_circle(dx, y, r, mt->primary);
if (i >= entered) fill_circle(dx, y, r - 3, mt->surface);
}
}
/* 键命中:旧自绘(lk_key_rect+mt_in)保留在 #if 0(回退位);M7.1b 走 SDK meow_widget_hit
* (owner==active + enabled 双闸门在 SDK 内;命中点=按下原点 mt->down_x/down_y,与原 mt_in 同源)。 */
#if 0
static int lk_key_hit(const meow_touch *mt, int row, int col)
{
int x, y, w, h; lk_key_rect(row, col, &x, &y, &w, &h);
return mt_in(mt, x, y, w, h);
}
#else
static int lk_key_hit(const meow_touch *mt, int row, int col)
{
if (!lk_key_w[row][col]) return 0;
return meow_widget_hit(lk_key_w[row][col], mt->down_x, mt->down_y);
}
#endif
#if 0
/* ============ 旧自绘锁屏整帧(M7.1b 回退位)============
* 原 lk_draw:fill_rect 全屏底 + lk_text(时钟/状态)+ lk_draw_pin(●)+ fill_round_rect 键盘。
* 一键回退:把下面 #if 0 改成 #if 1(并把新 lk_draw 的 #else 分支由 #else 改走旧轮廓),设备即回旧观感。 */
static void lk_draw(int entered, const char *status, time_t locked_until)
{
const meow_ui_theme_t *th = s_ui_theme();
fill_rect(0, 0, fb_xres, fb_yres, th->surface); /* 全屏表面底色 */
lk_text(fb_xres / 2, 120, locked_until > 0 ? "已锁定" : "输入 PIN 解锁", 24, th->on_surface);
lk_draw_pin(entered, th);
lk_text(fb_xres / 2, 260, status, 18, status[0] ? th->error : th->on_surface_variant);
for (int r = 0; r < LK_ROWS; r++) {
for (int c = 0; c < LK_COLS; c++) {
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
fill_round_rect(x, y, w, h, 12, th->surface_container);
int key = lk_keys[r][c];
if (key >= 0) {
char d[2] = { (char)('0' + key), 0 };
if (mfont_ready) {
int fw = meow_font_width_text(d, 30);
meow_font_drawtext((uint16_t *)fb, x + w / 2 - fw / 2,
y + LK_ROW_H / 2 - meow_font_line_height(30) / 2,
d, rgb565(th->on_surface), 30);
} else {
draw_text(x + w / 2 - 7, y + LK_ROW_H / 2 - 14, d, 2, th->on_surface);
}
} else if (key == LK_ACT_CLR) {
const char *s = "C"; const char *t = s;
if (mfont_ready) t = "清空";
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t, 22, th->on_surface);
} else { /* LK_ACT_BS */
const char *t = "DEL";
if (mfont_ready) t = "退格";
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t, 22, th->on_surface);
}
}
}
meow_dirty_full();
fb_pan_buf0_force();
meow_ui_flush();
}
#else
/* ============ M7.1b 锁屏控件化整帧(默认启用)============
* 整帧不透明 surface 底(§310 脏区修复关键:全屏覆盖,无 App 帧透出)+ SDK 字体时钟/提示
* + 主题色 PIN 圆点 + meow_widget 键盘(surfaceContainer 圆角 / 按压态 primary 26% α / MD3 主题)。
* 颜色全部取 meow_theme_get()(SDK 单一权威:MD3/MIUIx × 明暗自动)。 */
static void lk_draw(int entered, const char *status, time_t locked_until)
{
const meow_theme *mt = meow_theme_get();
meow_ui_fill_rect(0, 0, fb_xres, fb_yres, mt->surface); /* 全屏不透明表面底 */
lk_widgets_build(); /* 懒创建 12 键 */
/* 时钟/提示(居中,SDK meow_font):锁定倒计时标题 */
meow_ui_text_center(fb_xres / 2, 120, locked_until > 0 ? meow_str_zh("lock.locked", "已锁定")
: meow_str_zh("lock.prompt", "输入 PIN 解锁"),
24, mt->onSurface);
/* PIN 输入圆点(● 遮罩) */
lk_draw_pin_dots(entered, mt);
/* 状态行:错误提示(PIN 错误)着 error;倒计时/其它信息用 onSurfaceVariant */
int is_err = status[0] && (strstr(status, meow_str_zh("lock.pin_wrong", "PIN 错误")) != NULL ||
strstr(status, "WRONG PIN") != NULL);
uint32_t status_col = status[0] ? (is_err ? mt->error : mt->onSurfaceVariant)
: mt->onSurfaceVariant;
meow_ui_text_center(fb_xres / 2, 260, status, 18, status_col);
/* 12 键(SDK meow_widget;命中走 lk_key_hit → meow_widget_hit) */
for (int r = 0; r < LK_ROWS; r++)
for (int c = 0; c < LK_COLS; c++)
if (lk_key_w[r][c]) meow_widget_draw(lk_key_w[r][c]);
meow_dirty_full();
fb_pan_buf0_force();
meow_ui_flush();
}
#endif
/* 阻塞式锁屏页。返回 1=已解锁,0=锁屏期间电源键息屏。 */
int run_lock_screen(void)
{
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } /* 亮起面板 */
meow_screen_on = 1;
fb_pan_buf0_force();
meow_touch mt; mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
/* §310 输入穿透(媒体保活 App 不被 SIGSTOP,实时读 evdev):锁屏页独占触控设备——
* EVIOCGRAB 使本 fd 之外的其它进程(保活 App 的 SDK 输入线程 / watchdog 手势)在该设备
* 上收不到事件(事件仅投递抓取者),且抓取期事件不会为未抓取 fd 缓冲/回放。由此无论 App
* 冻结与否,锁屏期**任何** App 都收不到触摸(真全模态)。失败(驱动不支持)则记日志降级,
* 仍靠 keep_running/SIGSTOP+标记隔离。关闭 tfd 时抓取自动解除(仍显式释放一次)。 */
if (tfd >= 0) {
int grab = 1;
if (ioctl(tfd, EVIOCGRAB, &grab) < 0)
logmsg("lock: EVIOCGRAB touch fail: %s (graceful degrade, marker+SIGSTOP isola only)",
strerror(errno));
}
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
int entered = 0;
char digits[LCK_PIN_LEN + 1] = {0};
char status[64] = "";
time_t lock_until = 0;
{
time_t rem = lck_lock_remaining();
if (rem > 0) { lock_until = time(NULL) + rem; snprintf(status, sizeof status, "%s", meow_str_zh("lock.locked", "已锁定")); }
}
time_t last_countdown = 0;
/* 面板一次性解锁恢复口:进入时消费(存在即删除+放行一次,不校验 PIN/不清 PIN/不重置失败计数) */
if (lck_unlock_once_consume()) {
lck_manual_lock_clear(); /* 进入系统即视为解除主动锁 */
logmsg("lock: unlock-once bypass consumed at entry -> release");
return 1;
}
/* M7.1 解锁恢复:锁屏是整帧不透明覆盖(lk_draw fill_rect 全屏),退出后底层界面不会自动重绘
* —— 只有 App 自绘的控件区域刷新,未刷新区残留锁屏 PIN 最后一帧(解锁后整屏脏区根因)。此处
* 在锁屏首扑前把底层帧(SDK back_buf = 进入前的 App 帧/主界面帧)快照下来;解锁(unlocked=1)
* 时整帧回写 back_buf + present,强制底层界面完整重绘(app_launcher.c 的 lock-overlay 关闭路径
* 随后 pm_capture_app_frame 读 fb0 即得到正确 App 帧,不再读到锁帧)。 */
unsigned char *lk_bg_snap = NULL;
size_t lk_bg_len = 0;
if (back_buf_ok && fb && fb_line > 0 && fb_yres > 0) {
lk_bg_len = (size_t)fb_line * (size_t)fb_yres;
lk_bg_snap = malloc(lk_bg_len);
if (lk_bg_snap) memcpy(lk_bg_snap, fb, lk_bg_len);
else { lk_bg_len = 0; logmsg("lock: bg snapshot malloc fail (len=%zu)", lk_bg_len); }
}
/* M7.1b:锁屏控件 owner 闸门打开(meow_widget_hit / 按压态需 active==LK_SENTINEL_ACT)。
* 会话骨架(EVIOCGRAB/自睡/解锁脏区修复)不受影响;done/died 处对称 pop。 */
meow_ui_activity_push(LK_SENTINEL_ACT);
g_lk_down_key = LK_DOWN_NONE;
lk_draw(entered, status, lock_until); /* 首帧(全屏不透明主题底 + 控件化键盘) */
int unlocked = 0;
unsigned long long entry_ms = now_ms();
int covered_once = 0; /* §310 问题2-②:标记节流窗后重铺一次全屏底,盖掉 App 残帧 */
/* §310 M6.7 锁屏页自睡(294 遗留):锁屏页不应常亮耗电。
* 阈值 = min(系统 screen_off_timeout, 30s):系统未设/设 0(常亮)/超 30 → 锁屏页封顶 30s,
* 系统设更短则尊重更短。超阈值 → meow_panel_off()(与息屏路径一致)+ asleep=1(锁态保持,
* 仍留在锁屏循环);asleep 后电源键按下边沿/触摸活动 → meow_panel_on() + 重画锁屏页回锁。 */
int lk_off_sec = overlay_lock_idle_off_sec(screen_off_timeout);
unsigned long long lk_off_ms = (unsigned long long)lk_off_sec * 1000;
unsigned long long last_act = now_ms();
int asleep = 0;
int prev_pwr_down = 0;
/* M7.1 锁屏期物理键保留语义(音量/媒体键):边沿跟踪基线(锁屏是 EVIOCGRAB 独占真模态,
* 这些键只能在本层显式处理,不会转发到系统)。 */
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_prv = 0, prev_nxt = 0;
for (;;) {
time_t now = time(NULL);
int redraw = 0;
/* 面板一次性解锁恢复口:锁屏页停留期(每次重试前/每轮)也消费 */
if (lck_unlock_once_consume()) {
lck_manual_lock_clear();
logmsg("lock: unlock-once bypass consumed in-loop -> release");
unlocked = 1;
goto done;
}
/* §310 问题2-②:媒体保活 App 不冻结、SDK present 抑制有 ≤200ms 标记节流窗——锁屏首扑后
* App 仍可能再进 1-2 帧(部分脏区盖到锁屏上)。等标记窗过后(≥300ms)重铺一次全屏不透明底
* 盖掉 App 残帧;App 此后被抑制不再上屏,锁屏稳定遮盖(真机「App 控件盖到锁屏上」)。 */
if (!covered_once && !asleep && now_ms() - entry_ms >= OVERLAY_LOCK_COVER_MS) {
covered_once = 1;
logmsg("lock: re-cover full opaque after marker window (app stave suppressed)");
lk_draw(entered, status, lock_until);
}
/* §310 残影:锁屏叠加期 App 崩溃(僵尸)→ 中止锁屏走统一退出路径(交回 waitpid 循环
* 回收 + run_menu 重绘主屏含状态栏)。仅当 run_app_lock_overlay 置位 g_lock_overlay_pid
* 才探(菜单直接锁屏无 App 可死,恒不中止)。 */
if (g_lock_overlay_pid > 0 &&
lck_overlay_should_abort(g_lock_overlay_pid, proc_is_zombie(g_lock_overlay_pid))) {
logmsg("lock: overlay app pid %d died during lock -> abort (unified exit path)",
g_lock_overlay_pid);
g_lock_overlay_pid = 0;
goto died;
}
/* 锁定到期自动解禁(清计数+戳),仅重画状态行 */
if (lock_until > 0 && now >= lock_until) {
lck_lock_remaining();
lock_until = 0;
status[0] = 0;
redraw = 1;
}
if (lock_until > 0 && now != last_countdown) {
last_countdown = now;
time_t rem = lock_until - now;
if (mfont_ready) snprintf(status, sizeof status, meow_str_zh("lock.locked_wait", "已锁定,请 %d:%02d 后重试"),
(int)(rem / 60), (int)(rem % 60));
else snprintf(status, sizeof status, "LOCKED %d:%02d", (int)(rem / 60), (int)(rem % 60));
redraw = 1;
}
/* 电源键/音量键轮询 + 输入活动检测(重置自睡计时) + 自睡/唤醒切换。
* 活跃态:电源键按下 → 息屏并退出锁屏(回 App/主屏,下次唤醒再锁)——沿用原语义;
* 空闲超 lk_off_ms → 自睡(asleep=1,锁态保持,仍在本循环)。
* asleep:电源键按下边沿 / 触摸活动 → meow_panel_on + 重画锁屏页(本循环唤醒链回锁)。
* 音量键本页不动(仅视为输入活动,重置计时)。 */
{
int chg = 0;
if (kp_power.ok) chg |= kp_poll(&kp_power, NULL, NULL);
chg |= kp_poll(&kp_keys, NULL, NULL);
if (chg) last_act = now_ms(); /* 任意按键活动重置自睡计时 */
int pwr_down = kp_is_down(&kp_power, KEY_POWER);
int pwr_edge = pwr_down && !prev_pwr_down; /* 按下边沿 */
prev_pwr_down = pwr_down;
if (asleep) {
int touch_ready = (tfd >= 0);
if (touch_ready) {
struct pollfd tp = { tfd, POLLIN, 0 };
if (poll(&tp, 1, 0) <= 0) touch_ready = 0;
}
if (pwr_edge || touch_ready) {
logmsg("lock: wake from self-sleep (power=%d touch=%d)", pwr_edge, touch_ready);
meow_panel_on();
asleep = 0;
last_act = now_ms();
g_lk_down_key = LK_DOWN_NONE; /* M7.1b:唤醒复位按压态基线 */
lk_draw(entered, status, lock_until); /* 重画锁屏页回锁 */
continue;
}
} else {
/* 电源键=息屏(允许再睡;下次唤醒按 lck_wake_should_lock 再锁) */
if (pwr_down) {
logmsg("lock: power key -> screen off (stay locked)");
meow_panel_off();
break; /* return 0 */
}
/* M7.1 锁屏期物理键仍执行其语义(用户定案:音量键调音量、媒体键控播放/上下曲)。
* 锁屏层 EVIOCGRAB 独占这些输入(真模态),不转发系统,故必须在此显式处理,不能依赖别处。
* 音量沿用 launcher 既有递增/递减路径(settings_volume_adj —— 起 1% 线性渐变,
* 长按由下方 vol_ramp_tick 每圈推进);播放/上下曲经 meow_media_playctl 下发媒体指令。 */
int vold = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
int volu = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_VOLUMEUP);
int play = (kp_keys.ok) && kp_is_down(&kp_keys, KEY_PLAYPAUSE);
int prv = (kp_keys.ok && kp_is_down(&kp_keys, KEY_PREVIOUSSONG)) ||
(kp_power.ok && kp_is_down(&kp_power, KEY_PREVIOUSSONG));
int nxt = (kp_keys.ok && kp_is_down(&kp_keys, KEY_NEXT)) ||
(kp_power.ok && kp_is_down(&kp_power, KEY_NEXT));
if (vold && !prev_vold) { settings_volume_adj(-settings_volume_step); last_act = now_ms(); }
if (volu && !prev_volu) { settings_volume_adj(+settings_volume_step); last_act = now_ms(); }
if (play && !prev_play) {
const meow_media_session_t *ms = meow_media_current();
if (ms) {
uint32_t c = (ms->state == MEOW_MEDIA_PLAYING) ? 2u : 1u; /* 暂停/播放 */
int rc = meow_media_playctl(c, 0);
logmsg("lock: play key -> media cmd=%u rc=%d (%s)", c, rc, ms->app_id);
} else {
logmsg("lock: play key -> media-reserved (no media source)");
}
last_act = now_ms();
}
if (prv && !prev_prv) {
if (meow_media_current()) { int rc = meow_media_playctl(3, 0); logmsg("lock: prev key -> cmd=3 rc=%d", rc); }
else logmsg("lock: prev key -> media-reserved (no media source)");
last_act = now_ms();
}
if (nxt && !prev_nxt) {
if (meow_media_current()) { int rc = meow_media_playctl(4, 0); logmsg("lock: next key -> cmd=4 rc=%d", rc); }
else logmsg("lock: next key -> media-reserved (no media source)");
last_act = now_ms();
}
prev_vold = vold; prev_volu = volu; prev_play = play; prev_prv = prv; prev_nxt = nxt;
/* 长按音量键线性渐变(同主菜单 vol_ramp 路径,每圈推进 1%)。锁屏态不刷状态栏
* (状态栏被锁屏页覆盖),仅保持真实音量/DAC 同步。 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0)
? (kp_keys.ok && kp_is_down(&kp_keys, KEY_VOLUMEUP))
: (kp_keys.ok && kp_is_down(&kp_keys, KEY_VOLUMEDOWN));
int going = vol_ramp_tick(held);
if (!going) logmsg("lock: volume ramp end -> %d", settings_volume_get());
}
/* 空闲超时 → 自睡(锁态保持,仍在锁屏循环,唤醒回锁屏页) */
if (meow_screen_on && now_ms() - last_act >= lk_off_ms) {
logmsg("lock: idle %ds -> self screen off (stay locked)", lk_off_sec);
meow_panel_off();
asleep = 1;
continue;
}
}
}
/* 触摸:只认键盘 tap(滑动手势忽略:不左缘返回、不通知中心下拉);触摸即输入活动,重置自睡计时 */
if (tfd >= 0 && !asleep) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
last_act = now_ms(); /* §310 M6.7:任意触摸重置自睡计时 */
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
/* M7.1b 跟手指按压态:down 且两轴有效才定键(goodix 时序免疫);
* 键变化(滑动换键)即时重绘按压高亮。 */
if (mt.down && mt.last_x >= 0 && mt.last_y >= 0) {
if (lk_set_down_key(lk_key_value_at(mt.last_x, mt.last_y)))
redraw = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !mt.down) {
mt_down(&mt);
} else if (!val && mt.down) {
lk_set_down_key(LK_DOWN_NONE); /* M7.1b:抬手清按压态 */
redraw = 1;
int dx, dy;
if (mt_up(&mt, &dx, &dy)) {
int handled = 0;
for (int r = 0; r < LK_ROWS && !handled; r++) {
for (int c = 0; c < LK_COLS && !handled; c++) {
if (!lk_key_hit(&mt, r, c)) continue; /* M7.1b SDK 控件命中 */
handled = 1;
int key = lk_keys[r][c];
if (lock_until > 0) break; /* 锁定期间键盘不响应 */
if (key >= 0) {
if (entered < LCK_PIN_LEN) {
digits[entered++] = (char)('0' + key);
digits[entered] = 0;
}
} else if (key == LK_ACT_CLR) {
entered = 0; digits[0] = 0;
} else if (key == LK_ACT_BS) {
if (entered > 0) { entered--; digits[entered] = 0; }
}
redraw = 1;
if (entered == LCK_PIN_LEN) {
if (lck_pin_check(digits)) {
lck_pin_success();
lck_manual_lock_clear(); /* 主动锁标志消解 */
logmsg("lock: PIN OK -> unlock");
unlocked = 1;
goto done;
} else {
int rem = lck_pin_fail();
entered = 0; digits[0] = 0;
if (rem > 0) {
lock_until = time(NULL) + rem;
last_countdown = 0;
snprintf(status, sizeof status, "%s", meow_str_zh("lock.locked", "已锁定"));
} else {
if (mfont_ready) snprintf(status, sizeof status, "%s", meow_str_zh("lock.pin_wrong", "PIN 错误"));
else snprintf(status, sizeof status, "WRONG PIN");
}
}
}
}
}
}
}
}
}
}
}
/* 整页重绘(锁屏低频交互,简单整页即可);自睡(面板已关)期间不重绘(唤醒时统一重画) */
if (redraw && !asleep) lk_draw(entered, status, lock_until);
usleep(POLL_MS * 1000);
}
done:
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
g_lk_down_key = LK_DOWN_NONE;
/* M7.1 解锁恢复:把锁屏首扑前快照的底层帧整帧回写 back_buf + present,强制底层 App/主界面
* 完整重绘(消除「解锁后整屏 = PIN 最后一帧,仅 App 控件自绘区刷新」的脏区残留)。 */
if (unlocked && lk_bg_snap && back_buf_ok && fb && lk_bg_len > 0) {
memcpy(fb, lk_bg_snap, lk_bg_len);
meow_dirty_full();
meow_ui_flush();
logmsg("lock: unlock -> restore underlying frame (full repaint)");
}
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
if (tfd >= 0) {
int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 也会自动解除,双保险) */
ioctl(tfd, EVIOCGRAB, &grab);
close(tfd);
}
kp_close(&kp_power);
kp_close(&kp_keys);
logmsg("lock: exit unlocked=%d screen_on=%d", unlocked, meow_screen_on);
return unlocked;
died:
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
g_lk_down_key = LK_DOWN_NONE;
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
if (tfd >= 0) {
int grab = 0;
ioctl(tfd, EVIOCGRAB, &grab);
close(tfd);
}
kp_close(&kp_power);
kp_close(&kp_keys);
logmsg("lock: exit overlay-app-died -> LCK_OVERLAY_RET_DIED");
return LCK_OVERLAY_RET_DIED;
}
/* 电源菜单「锁定」:**主动锁屏**(用户定案 2026-09-02 强化)。
* 语义 = 立即熄屏 + 进入锁定 + 作废挂起的面板绕过标志:
* ① rm /tmp/meow_pin_unlock_once(主动锁屏作废一次性绕过);
* ② 写 /tmp/meow_pin_locked(主动锁标志 → 唤醒时无论 system.pin_lock_on_wake 是否开都必锁屏);
* ③ meow_panel_off() 立即熄屏(唤醒时经主屏唤醒路径弹锁屏页)。
* 不立即弹锁屏页(已熄屏);未配凭据 → no-op。返回 0(关菜单,run_menu 继续)。 */
int meow_lock_screen_engage(void)
{
if (!lck_pin_configured()) {
logmsg("lock: no system.pin/panel.pin configured -> skip manual lock (no-op)");
return 0;
}
if (access(LCK_UNLOCK_ONCE_FILE, F_OK) == 0) {
unlink(LCK_UNLOCK_ONCE_FILE);
logmsg("lock: manual active-lock revoked pending unlock-once bypass");
}
lck_manual_lock_set();
meow_panel_off(); /* 立即熄屏;唤醒时必走锁屏 */
logmsg("lock: manual active-lock -> screen off + lock on wake");
return 0;
}
/* ---- 设置页「锁屏 PIN」数字输入模态(system 级 PIN 设置,用 SDK 按钮画数字键盘)----
* 全屏遮罩:title + 4 圆点(●)+ 数字键盘(1-9 / C / 0 / 退格)。输入满 4 位即返回 1 并拷贝到 out
* (不校验——调用方做双输确认/旧 PIN 校验);电源键=取消返回 0;音量键不动。 */
int lck_input_pin_modal(const char *title, char *out, int outcap)
{
meow_screen_on = 1;
fb_pan_buf0_force();
meow_touch mt; mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
/* §310 输入穿透:设置页 PIN 模态同样独占触控设备(后台保活 App 不冻结实时读 evdev,防其
* 同坐标控件被 PIN 键盘点击双重触发)。失败降级(仍靠标记+SIGSTOP 隔离)。 */
if (tfd >= 0) {
int grab = 1;
if (ioctl(tfd, EVIOCGRAB, &grab) < 0)
logmsg("pin-modal: EVIOCGRAB touch fail: %s (graceful degrade)", strerror(errno));
}
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
int entered = 0;
char digits[LCK_PIN_LEN + 1] = {0};
int result = 0;
int dirty = 1; /* 首帧必画 */
for (;;) {
/* 只在首帧/输入变化时整页重绘(消频闪:锁屏低频交互),后续轮次不动 LCDC */
if (dirty) {
const meow_ui_theme_t *th = s_ui_theme();
fill_rect(0, 0, fb_xres, fb_yres, th->surface);
lk_text(fb_xres / 2, 120, title, 24, th->on_surface);
lk_draw_pin(entered, th);
for (int r = 0; r < LK_ROWS; r++) {
for (int c = 0; c < LK_COLS; c++) {
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
fill_round_rect(x, y, w, h, 12, th->surface_container);
int key = lk_keys[r][c];
if (key >= 0) {
char d[2] = { (char)('0' + key), 0 };
if (mfont_ready) {
int fw = meow_font_width_text(d, 30);
meow_font_drawtext((uint16_t *)fb, x + w / 2 - fw / 2,
y + LK_ROW_H / 2 - meow_font_line_height(30) / 2,
d, rgb565(th->on_surface), 30);
} else {
draw_text(x + w / 2 - 7, y + LK_ROW_H / 2 - 14, d, 2, th->on_surface);
}
} else if (key == LK_ACT_CLR) {
const char *t2 = "C"; if (mfont_ready) t2 = meow_str_zh("lock.clear", "清空");
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t2, 22, th->on_surface);
} else {
const char *t2 = "DEL"; if (mfont_ready) t2 = meow_str_zh("lock.backspace", "退格");
lk_text(x + w / 2, y + LK_ROW_H / 2 - 9, t2, 22, th->on_surface);
}
}
}
meow_dirty_full();
fb_pan_buf0_force();
meow_ui_flush();
dirty = 0;
}
if (kp_power.ok) {
kp_poll(&kp_power, NULL, NULL);
if (kp_is_down(&kp_power, KEY_POWER)) { result = 0; break; } /* 取消 */
}
kp_poll(&kp_keys, NULL, NULL);
(void)kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
(void)kp_is_down(&kp_keys, KEY_VOLUMEUP);
if (tfd >= 0) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !mt.down) { mt_down(&mt); }
else if (!val && mt.down) {
int dx, dy;
if (mt_up(&mt, &dx, &dy)) {
for (int r = 0; r < LK_ROWS; r++) {
for (int c = 0; c < LK_COLS; c++) {
int x, y, w, h; lk_key_rect(r, c, &x, &y, &w, &h);
if (!mt_in(&mt, x, y, w, h)) continue;
int key = lk_keys[r][c];
if (key >= 0) {
if (entered < LCK_PIN_LEN) { digits[entered++] = (char)('0' + key); digits[entered] = 0; }
} else if (key == LK_ACT_CLR) { entered = 0; digits[0] = 0; }
else if (key == LK_ACT_BS) { if (entered > 0) { entered--; digits[entered] = 0; } }
dirty = 1;
if (entered == LCK_PIN_LEN) {
if (out && outcap > LCK_PIN_LEN) { snprintf(out, (size_t)outcap, "%s", digits); result = 1; }
else result = 1;
goto done2;
}
break;
}
}
}
}
}
}
}
}
usleep(POLL_MS * 1000);
}
done2:
if (tfd >= 0) {
int grab = 0; /* 显式释放 EVIOCGRAB(关闭 tfd 亦自动解除,双保险) */
ioctl(tfd, EVIOCGRAB, &grab);
close(tfd);
}
kp_close(&kp_power);
kp_close(&kp_keys);
return result;
}
+145
View File
@@ -0,0 +1,145 @@
/* lock_settings.c — MeowOS 系统设置「锁屏 PIN」独立子页面(subpage.c 脚手架,用户定案 2026-09-02)。
*
* 页面(sub_page_t 驱动,同 wifi_page/bt_page):
* - 状态行(SUB_ROW):未设置 / 已设置 / 锁定中(剩余 mm:ss)
* - [未设] 设置 PIN:4 位数字双输入确认(lck_input_pin_modal,SDK 数字键)
* - [已设] 修改 PIN:需旧 PIN 一次校验 + 新 PIN 双输入确认
* - [已设] 清除 PIN:**无需旧密码**(物理持机即授权,仅 s_dd_modal 二次确认)→ lck_pin_clear
* - 唤醒后锁屏(system.pin_lock_on_wake)开关行
* 全程走 lock_screen.c 纯逻辑 + meow_settings 后端(无旁路)。
*/
#include "meow_launcher_internal.h"
#define LS_ACT_STATUS 0
#define LS_ACT_SET 1
#define LS_ACT_MODIFY 2
#define LS_ACT_CLEAR 3
#define LS_ACT_WAKE 4
static char g_ls_status[48]; /* 状态行摘要(rows 填,paint 后不再变) */
static void ls_status_text(char *buf, int len)
{
if (!lck_pin_configured()) { snprintf(buf, len, "%s", meow_str_zh("lock.status_unset", "未设置")); return; }
time_t rem = lck_lock_remaining();
if (rem > 0) snprintf(buf, len, meow_str_zh("lock.status_locked", "锁定中(%d:%02d)"), (int)(rem / 60), (int)(rem % 60));
else snprintf(buf, len, "%s", meow_str_zh("lock.status_set", "已设置"));
}
static int ls_rows(void *ctx, sub_row_t *rbuf, int max)
{
(void)ctx;
int n = 0;
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = meow_str_zh("lock.title", "锁屏 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
ls_status_text(g_ls_status, sizeof g_ls_status);
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = meow_str_zh("lock.status_row", "状态"); rbuf[n].sub = g_ls_status; rbuf[n].on = 0; rbuf[n].act = LS_ACT_STATUS; n++; }
int configured = lck_pin_configured();
if (!configured) {
if (n < max) { rbuf[n].kind = SUB_ACT; rbuf[n].label = meow_str_zh("lock.act_set", "设置 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = LS_ACT_SET; n++; }
} else {
if (n < max) { rbuf[n].kind = SUB_ACT; rbuf[n].label = meow_str_zh("lock.act_modify", "修改 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = LS_ACT_MODIFY; n++; }
if (n < max) { rbuf[n].kind = SUB_ACT; rbuf[n].label = meow_str_zh("lock.act_clear", "清除 PIN"); rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = LS_ACT_CLEAR; n++; }
}
if (n < max) {
int wake = 1;
if (meow_settings_get_int("system.pin_lock_on_wake", &wake) != 0) wake = 1;
rbuf[n].kind = SUB_SW; rbuf[n].label = meow_str_zh("lock.wake_lock", "唤醒后锁屏"); rbuf[n].sub = NULL;
rbuf[n].on = wake; rbuf[n].act = LS_ACT_WAKE; n++;
}
return n;
}
static void ls_modal(const char *title, const char *line)
{
const char *it[1]; it[0] = line;
s_dd_modal(title, it, 1, -1);
}
static int ls_do_modify(void)
{
char old[LCK_PIN_LEN + 1];
if (!lck_input_pin_modal(meow_str_zh("lock.enter_cur", "输入当前 PIN"), old, sizeof old)) return 0;
if (!lck_pin_check(old)) { ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.cur_wrong", "当前 PIN 错误")); return 1; }
char a[LCK_PIN_LEN + 1], b[LCK_PIN_LEN + 1];
if (!lck_input_pin_modal(meow_str_zh("lock.set_new", "设置新 PIN"), a, sizeof a)) return 0;
if (!lck_input_pin_modal(meow_str_zh("lock.confirm_new", "再次确认新 PIN"), b, sizeof b)) return 0;
if (strcmp(a, b) != 0) { ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.mismatch", "两次输入不一致")); return 1; }
if (lck_pin_set(a) == MEOW_OK) ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.modified", "已修改"));
else ls_modal(meow_str_zh("lock.act_modify", "修改 PIN"), meow_str_zh("lock.set_failed", "设置失败"));
return 1;
}
static int ls_do_set(void)
{
char a[LCK_PIN_LEN + 1], b[LCK_PIN_LEN + 1];
if (!lck_input_pin_modal(meow_str_zh("lock.enter_4", "设置 4 位 PIN"), a, sizeof a)) return 0;
if (!lck_input_pin_modal(meow_str_zh("lock.confirm_input", "再次输入确认"), b, sizeof b)) return 0;
if (strcmp(a, b) != 0) { ls_modal(meow_str_zh("lock.act_set", "设置 PIN"), meow_str_zh("lock.mismatch", "两次输入不一致")); return 1; }
if (lck_pin_set(a) == MEOW_OK) ls_modal(meow_str_zh("lock.act_set", "设置 PIN"), meow_str_zh("lock.status_set", "已设置"));
else ls_modal(meow_str_zh("lock.act_set", "设置 PIN"), meow_str_zh("lock.set_failed", "设置失败"));
return 1;
}
static int ls_do_clear(void)
{
const char *items[2]; items[0] = meow_str_zh("power.confirm_yes", "是"); items[1] = meow_str_zh("power.confirm_no", "否");
int pick = s_dd_modal(meow_str_zh("lock.clear_confirm", "确定清除锁屏 PIN?"), items, 2, 1); /* 默认「否」(安全) */
if (pick != 0) return 1;
if (lck_pin_clear() == MEOW_OK) ls_modal(meow_str_zh("lock.act_clear", "清除 PIN"), meow_str_zh("lock.cleared", "已清除"));
else ls_modal(meow_str_zh("lock.act_clear", "清除 PIN"), meow_str_zh("lock.clear_failed", "清除失败(settings 不可写)"));
return 1;
}
static int ls_act(void *ctx, int idx, int act)
{
(void)ctx; (void)idx;
switch (act) {
case LS_ACT_SET: return ls_do_set();
case LS_ACT_MODIFY: return ls_do_modify();
case LS_ACT_CLEAR: return ls_do_clear();
case LS_ACT_WAKE: {
int v = 1;
if (meow_settings_get_int("system.pin_lock_on_wake", &v) != 0) v = 1;
meow_settings_set_int("system.pin_lock_on_wake", v ? 0 : 1);
meow_settings_save();
logmsg("lock_settings: wake-lock toggle -> %d", v ? 0 : 1);
return 1; /* 重绘 + 重填行(开关态) */
}
default: return 0;
}
}
static int ls_detail(void *ctx, int idx, int act)
{
(void)ctx; (void)idx;
if (act == LS_ACT_STATUS) {
char line[80];
ls_status_text(line, sizeof line);
int rem = (int)lck_lock_remaining();
if (lck_pin_configured() && rem > 0) {
char buff[96];
snprintf(buff, sizeof buff, meow_str_zh("lock.locked_retry", "连续错误已锁定,剩余 %d:%02d 后重试"), rem / 60, rem % 60);
ls_modal(meow_str_zh("lock.title", "锁屏 PIN"), buff);
} else {
ls_modal(meow_str_zh("lock.title", "锁屏 PIN"), line);
}
return 1;
}
return 0;
}
/* 运行「锁屏 PIN」独立子页面(阻塞到返回) */
int run_lock_settings_page(void)
{
sub_page_t p;
memset(&p, 0, sizeof p);
p.title = meow_str_zh("lock.title", "锁屏 PIN");
p.top_sw = NULL;
p.top_sw_set = NULL;
p.rows = ls_rows;
p.act = ls_act;
p.detail = ls_detail;
p.live = 0;
p.ctx = NULL;
return meow_subpage_run(&p);
}
File diff suppressed because it is too large. Load diff
+129
View File
@@ -0,0 +1,129 @@
/* meow_ams_start.c — MSG_AMS_START 拉起决策(AMSD_DESIGN §4.3)。
*
* 独立模块(无 launcher 内部依赖,仅 meow_ipc.h):持有 pending-start 状态 + 拉起决策,
* 经 ops 注入真实启动链路(launch_app/launch_activity + resolve_chain + logmsg),
* 使「am start」在 run_menu 主循环中真正拉起目标 App。
*
* 根因修复:历史实现 `meow_ams_pending_start_set` 返回 `-MEOW_ERR_BUSY`(=+8,正)而非
* 负错误码,ipc_dispatch 的 `rc<0?-rc:MEOW_OK` 因此把「有前台 App 拒绝」误判为 OK(0),
* 且拒绝发生在置 pending 之前 → amsd 报 OK 但 App 永不拉起。本模块统一为负 MEOW_ERR_*。
*
* 单一写者约定:pending 状态只在主循环上下文写入/消费(ipc_dispatch 写、run_menu 消费),
* 与 launcher 「单写者」纪律一致,无需锁。
*/
#include "meow_ams_start.h"
#include <stdio.h>
#include <string.h>
struct meow_ams_start_state {
char pkg[MEOW_APP_ID_MAX];
char act[MEOW_ACT_MAX];
int pending;
/* picker 上下文(二期;pick_armed=1 时 next launch 注入 env) */
char pick_action[24];
char pick_caller[MEOW_APP_ID_MAX];
uint32_t pick_req;
int pick_armed;
};
static struct meow_ams_start_state g_ams; /* 主循环单写者,无锁 */
static struct meow_ams_ops g_ops;
static int g_busy; /* =g_app_session_active 镜像(主循环每圈同步) */
void meow_ams_ops_init(const struct meow_ams_ops *ops)
{
if (ops) g_ops = *ops;
else memset(&g_ops, 0, sizeof g_ops);
}
void meow_ams_set_busy(int busy)
{
g_busy = busy;
}
int meow_ams_pending_start_set(const char *pkg, const char *act)
{
if (!pkg || !pkg[0]) return MEOW_ERR_INVAL; /* 负错误码(根因修复) */
if (g_busy) return MEOW_ERR_BUSY; /* 有前台 App:拒绝(负错误码) */
snprintf(g_ams.pkg, sizeof g_ams.pkg, "%s", pkg);
g_ams.act[0] = 0;
if (act && act[0]) snprintf(g_ams.act, sizeof g_ams.act, "%s", act);
g_ams.pending = 1;
return 0;
}
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
const char *pick_action, uint32_t pick_req,
const char *pick_caller)
{
int rc = meow_ams_pending_start_set(pkg, act);
if (rc != 0) return rc; /* 拒绝(有前台 App / 参数非法) */
g_ams.pick_action[0] = 0;
g_ams.pick_caller[0] = 0;
g_ams.pick_req = 0;
g_ams.pick_armed = 0;
if (pick_action && pick_action[0]) {
snprintf(g_ams.pick_action, sizeof g_ams.pick_action, "%s", pick_action);
if (pick_caller) snprintf(g_ams.pick_caller, sizeof g_ams.pick_caller, "%s", pick_caller);
g_ams.pick_req = pick_req;
g_ams.pick_armed = 1;
}
return 0;
}
int meow_ams_take_picker(char *action, int action_len,
char *caller, int caller_len, uint32_t *req)
{
if (!g_ams.pick_armed) return 0;
g_ams.pick_armed = 0;
if (action && action_len > 0) snprintf(action, action_len, "%s", g_ams.pick_action);
if (caller && caller_len > 0) snprintf(caller, caller_len, "%s", g_ams.pick_caller);
if (req) *req = g_ams.pick_req;
return 1;
}
int meow_ams_start_consume(const char *const *ids, int napps, int state,
char *toast, int toast_len, int *app_ran, int *idx_out)
{
if (app_ran) *app_ran = 0;
if (idx_out) *idx_out = -1;
if (!g_ams.pending || g_busy) return 0; /* 无待拉起 / 前台 App 会话中 */
if (state != 0) {
/* 应用列表未就绪:保留 pending(不丢「am start」),等 run_menu 2s 重扫后重试。 */
if (toast && toast_len > 0) snprintf(toast, toast_len, "AMS START: app list not ready");
return 2;
}
int idx = -1;
for (int i = 0; i < napps; i++)
if (ids[i] && !strcmp(ids[i], g_ams.pkg)) { idx = i; break; }
if (idx < 0) {
g_ams.pending = 0;
if (toast && toast_len > 0) snprintf(toast, toast_len, "AMS START: NO APP %s", g_ams.pkg);
return 1;
}
char chain[MEOW_CHAIN_MAX] = "";
if (g_ams.act[0]) {
int depth = 0;
int rc = g_ops.resolve ? g_ops.resolve(g_ams.pkg, g_ams.act, chain, &depth)
: MEOW_ERR_NOENT;
if (rc != MEOW_OK) {
g_ams.pending = 0;
if (toast && toast_len > 0)
snprintf(toast, toast_len, "AMS: NO ROUTE %s/%s", g_ams.pkg, g_ams.act);
return 1;
}
}
g_ams.pending = 0;
if (idx_out) *idx_out = idx;
/* 复用既有启动链路(launch_app/launch_activity 阻塞至 app 退出或 exec 失败)
* chain 空=裸启动;非空=经 resolve_chain 的 activity 链(含参透传)。
* 启动失败(lr<0)不在此设 toast:调用方经 idx_out 拿到 apps[idx].name 打 launch_fail_toast。 */
int lr = g_ops.launch ? g_ops.launch(idx, chain[0] ? chain : NULL) : -MEOW_ERR_INTERNAL;
if (app_ran) *app_ran = (lr == 0);
return 1;
}
+62
View File
@@ -0,0 +1,62 @@
/* meow_ams_start.h — MSG_AMS_START 拉起决策(AMSD_DESIGN §4.3)。
*
* 独立模块以便宿主单测:pending 状态 + 拉起决策都在此,启动/解析/日志经 ops 注入
* (生产=launcher 既有链路 launch_app/launch_activity + resolve_chain + logmsg;
* 测试=假件,见 test_ams_start.c)。
*
* 约定(与 launcher 其余模块一致):本模块函数返回负的 MEOW_ERR_*(-1=INVAL / -8=BUSY)
* 表示拒绝,0 表示受理。ipc_dispatch 用 `a.code=(uint32_t)(rc<0?-rc:MEOW_OK)` 把负错误码
* 转成正的 ack.code(幅度);amsd 侧按 `(uint32_t)(-MEOW_ERR_BUSY)` 识别 busy。
* —— 历史上 set() 误返回 `-MEOW_ERR_BUSY`(=+8,正)导致 ipc_dispatch 把拒绝当 OK,
* 表现为「am start 报 OK 但 App 未拉起」;本模块统一为负错误码(根因修复)。
*/
#ifndef MEOW_AMS_START_H
#define MEOW_AMS_START_H
#include "meow_ipc.h" /* MEOW_APP_ID_MAX / MEOW_ACT_MAX / MEOW_CHAIN_MAX / MEOW_ERR_* */
/* 启动/解析/日志回调注入(单测=假件;生产在 meow_ipc_init 末尾组装)。 */
struct meow_ams_ops {
/* 执行启动。idx=调用方应用表下标;chain 空=裸启动。
* 复用 launcher 既有链路:launch_app(&apps[idx]) / launch_activity(&apps[idx], chain)。
* 返回 0=已受理(app 已运行至退出);<0=启动失败/未找到。 */
int (*launch)(int idx, const char *chain);
/* 解析 activity 链(resolve_chain)。返回 MEOW_OK / MEOW_ERR_NOENT。 */
int (*resolve)(const char *app_id, const char *activity, char *chain, int *depth);
/* 日志(可空=静默)。 */
void (*log)(const char *fmt, ...);
};
/* 组装 ops(launcher meow_ipc_init 末尾 / 测试 main 调用)。launch/resolve 须非 NULL。 */
void meow_ams_ops_init(const struct meow_ams_ops *ops);
/* 同步「前台 App 会话」占用位(=g_app_session_active)。
* busy=1 时 pending_start_set 拒绝(-MEOW_ERR_BUSY)且 consume 不动作。 */
void meow_ams_set_busy(int busy);
/* 置 pending(ipc_dispatch MSG_AMS_START 时调)。
* pkg 空 → -MEOW_ERR_INVAL;busy → -MEOW_ERR_BUSY;否则置位并返回 0。 */
int meow_ams_pending_start_set(const char *pkg, const char *act);
/* 二期 picker 扩载荷:置 pending + 武装 picker 上下文(pick_action 非空时,launch_app_chain
* 入口经 meow_ams_take_picker 注入 MEOW_PICKER_ACTION/REQ_ID/CALLER)。错误语义同 set。 */
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
const char *pick_action, uint32_t pick_req,
const char *pick_caller);
/* 消费 pending(各主循环排空 IPC 后 / run_menu 循环调用)。
* ids=napps 个应用主键串指针(apps[i].id);state=菜单态(0=就绪;非 0=列表未就绪不拉起,
* pending 保留待重扫重试)。toast/toast_len=错误提示槽(可空)。
* app_ran=输出:1=app 已运行;0=未运行(拒绝/失败)。idx_out=输出:本次使用的应用表下标
* (-1=未命中应用;>=0 且 app_ran=0 表示启动失败,调用方应据 apps[idx].name 打 launch_fail_toast)。
* 返回:0=无 pending 或 busy(暂不动作);1=已消费(拉起成功/拒绝并清 pending,toast 已设);
* 2=列表未就绪(pending 保留待重扫重试,toast 已设「not ready」,调用方勿红冲)。 */
int meow_ams_start_consume(const char *const *ids, int napps, int state,
char *toast, int toast_len, int *app_ran, int *idx_out);
/* 消费拾取 picker 上下文(launch_app_chain 入口调用,一次性清位)。
* 返回 1=有 picker 上下文;0=无。 */
int meow_ams_take_picker(char *action, int action_len,
char *caller, int caller_len, uint32_t *req);
#endif /* MEOW_AMS_START_H */
+69
View File
@@ -0,0 +1,69 @@
/* meow_bgmgr.c — 批③ 后台管理页 逻辑层(数据汇聚 + kill 门控)。实现见 meow_bgmgr.h。
* 纯 C(无 launcher static/SDK 依赖),宿主 gcc 直编:gcc -Wall -Wextra -std=gnu99 \
* test_bgmgr.c meow_bgmgr.c -o test_bgmgr。 */
#include "meow_bgmgr.h"
#include <stdio.h>
#include <string.h>
int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
{
if (!ops || !ops->enumerate || !ops->app_meta || !rows || max <= 0) return 0;
int n = ops->enumerate(ops->ctx, rows, max);
if (n < 0) n = 0;
if (n > max) n = max;
for (int i = 0; i < n; i++) {
bgmgr_row_t *r = &rows[i];
r->protected_app = 0;
r->is_fg = 0;
r->name[0] = 0;
r->icon[0] = 0;
r->pcpu = 0.0;
r->rss_kb = 0;
ops->app_meta(ops->ctx, r->app_id, r->pid,
r->name, (int)sizeof r->name, r->icon, (int)sizeof r->icon,
&r->protected_app, &r->is_fg);
if (!r->name[0]) snprintf(r->name, sizeof r->name, "%s", r->app_id);
if (ops->proc_sample && ops->proc_sample(ops->ctx, r->pid, &r->pcpu, &r->rss_kb) < 0) {
r->pcpu = 0.0; r->rss_kb = 0;
}
r->killable = bgmgr_can_kill(r);
}
return n;
}
int bgmgr_can_kill(const bgmgr_row_t *r)
{
if (!r) return 0;
if (r->protected_app) return 0; /* 系统级受保护 App 不 kill(铁律;framework 是 SDK 标志,不在此列) */
if (r->is_fg) return 0; /* 当前前台 App 不 kill */
if (r->pid <= 0) return 0; /* 非后台/无进程 */
return 1;
}
int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n,
void (*kill_fn)(const char *app_id))
{
(void)ops;
if (!kill_fn) return 0;
int k = 0;
for (int i = 0; i < n; i++) {
if (bgmgr_can_kill(&rows[i])) { kill_fn(rows[i].app_id); k++; }
}
return k;
}
const char *bgmgr_status_str(const bgmgr_row_t *r)
{
if (!r) return "";
if (r->is_fg) return "前台";
if (r->keepalive) return "常驻";
return "后台";
}
const char *bgmgr_mem_str(long rss_kb)
{
static char buf[24];
if (rss_kb >= 1024) snprintf(buf, sizeof buf, "%.1fMB", (double)rss_kb / 1024.0);
else snprintf(buf, sizeof buf, "%ldKB", rss_kb);
return buf;
}
+75
View File
@@ -0,0 +1,75 @@
/* meow_bgmgr.h — 批③ 后台管理页 逻辑层(数据汇聚 + kill 门控)。自包含、依赖注入、可独立单测。
*
* 定位:把「后台常驻集 → 展示行(图标/名字/CPU/内存/状态)→ kill/一键清」的**纯逻辑**从 UI 抽出,
* 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法:
* UI 提供真实 enumerate/app_meta/proc_sample;单测用假件。**单一写者=launcher 主循环**,无锁。
*
* 铁律遵循:kill/一键清 带 !protected_app 门控(防误杀系统/受保护 App,对齐 app_launcher.c:2088 教训)。
* **收口:门控位不是 app.json 的 `framework`**——那是「用 meow_ui.a 的 SDK 框架 App」标志
* (docs/APP_DEV_GUIDE.md §9,推荐且现有 App 全写 1),拿它当「禁止 kill」会让所有后台 App 变成
* 不可杀(2026-11-04 真机:dev.player.ncm 声明 framework:1 → 后台页杀按钮全被置灰点不动)。
* 不新建 daemon;不引 GDBus;不碰 /data/recovery_all。
*/
#ifndef MEOW_BGMGR_H
#define MEOW_BGMGR_H
#include <stdint.h>
#include <sys/types.h> /* pid_t */
#include "meow_ipc.h" /* MEOW_APP_ID_MAX */
/* 定义于 meow_bgplay.h,此处仅注释:与 launcher 后台表同槽数(MEOW_BG_MAX=4)。 */
#define MEOW_BGMR_ROW_MAX 4
/* 后台管理页一行。enumerate 填 app_id/pid/keepalive/on_card/bg_since_ms;
* bgmgr_collect 再经 app_meta/proc_sample 补元数据/CPU/RSS,并预判 killable。 */
typedef struct bgmgr_row {
char app_id[MEOW_APP_ID_MAX];
char name[49]; /* display name(回退 app_id) */
char icon[129]; /* 图标 path(空=无,页面用首字/占位) */
char protected_app; /* '1'=系统级受保护 App(禁止 kill)。**非** app.json framework;
* 目前 launcher 自身组件(状态栏/设置页…)不在 App 表/后台表里,
* 故实际恒为 0,保留为将来受保护组件的显式标记位。 */
pid_t pid; /* >0 后台进程 */
int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */
int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */
int on_card; /* 1=二进制在 TF 卡 */
uint64_t bg_since_ms; /* 转后台时刻(monotonic ms) */
double pcpu; /* 0-100 */
long rss_kb; /* 常驻内存 */
int killable; /* 预判:bgmgr_can_kill()==1 */
} bgmgr_row_t;
/* 依赖注入(**必须全非空**;enumerate 返回行数,app_meta/proc_sample 返回 0=成功)。 */
typedef struct bgmgr_ops {
/* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms,
* 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */
int (*enumerate)(void *ctx, bgmgr_row_t *rows, int max);
/* 按 app_id 补 App 元数据:name/icon/protected_app/is_fg。返回 0=找到;<0=未注册(name 回退 app_id)。 */
int (*app_meta)(void *ctx, const char *app_id, pid_t pid,
char *name, int nl, char *icon, int il,
char *protected_app, int *is_fg);
/* 每进程 CPU/RSS 取样:返回 0=ok(写 pcpu 与 rss_kb);<0=无数据(pcpu=0/rss=0)。 */
int (*proc_sample)(void *ctx, pid_t pid, double *pcpu, long *rss_kb);
void (*log)(const char *fmt, ...); /* 可空=静默 */
void *ctx;
} bgmgr_ops_t;
/* 枚举后台常驻并补全元数据/CPU/RSS。返回行数(>=0);app_id 空/重复加固在 enumerate 侧。
* 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */
int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max);
/* !protected_app 门控 + 非前台 + pid>0:返回 1=可 kill;0=禁止(受保护/当前前台/无进程)。 */
int bgmgr_can_kill(const bgmgr_row_t *r);
/* 一键清:遍历 rows,bgmgr_can_kill() 的条目逐一调 kill_fn(app_id)。返回发出 kill 数。
* **不杀受保护 App/当前前台/非后台**;调用方负责对返回 0 弹「无后台 App 可清」之类提示。 */
int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n,
void (*kill_fn)(const char *app_id));
/* 状态文案(用于行内「状态」列):「常驻」keepalive=1;「后台」宽限期类;「前台」is_fg=1;空=非后台。 */
const char *bgmgr_status_str(const bgmgr_row_t *r);
/* 内存文案(rss_kb → "x.xMB"/"xxKB")。返回静态缓冲区;勿复用变长调用。 */
const char *bgmgr_mem_str(long rss_kb);
#endif /* MEOW_BGMGR_H */
+622
View File
@@ -0,0 +1,622 @@
/* meow_bgplay.c — M4 后台播放保活 + 无头后台服务运行时表(见 meow_bgplay.h 头注释)。 */
#include "meow_bgplay.h"
#include <stdarg.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
static meow_bgplay_ops g_ops;
static meow_bg_entry_t g_bg[MEOW_BG_MAX];
static meow_svc_rt_t g_svc[MEOW_SVC_RT_MAX];
static uint32_t g_change_seq;
static const uint32_t k_backoff[] = {
MEOW_SVC_BACKOFF_1, MEOW_SVC_BACKOFF_2, MEOW_SVC_BACKOFF_4, MEOW_SVC_BACKOFF_10
};
#define BACKOFF_STEPS ((int)(sizeof(k_backoff) / sizeof(k_backoff[0])))
/* SIGTERM 后 SIGKILL 兜底宽限(ms) */
#define TERM_GRACE_MS 500u
/* ------------------------------------------------------------------ */
static void bg_log(const char *fmt, ...)
{
va_list ap;
char buf[384];
if (!g_ops.log) return;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
g_ops.log("%s", buf);
}
static uint64_t bg_now_ms(void)
{
struct timespec ts;
if (g_ops.now_ms) return g_ops.now_ms();
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}
static int bg_card_present(void)
{
return g_ops.card_present ? g_ops.card_present() : 1;
}
void meow_bgplay_init(const meow_bgplay_ops *ops)
{
memset(&g_ops, 0, sizeof(g_ops));
memset(g_bg, 0, sizeof(g_bg));
memset(g_svc, 0, sizeof(g_svc));
g_change_seq = 0;
if (ops) g_ops = *ops;
}
/* ================= 后台播放保活 ================= */
int meow_bg_should_keep(int bg_audio_declared, int media_resident, int keepalive_declared)
{
if (keepalive_declared) return 1; /* 通用常驻:进后台即保活 */
return bg_audio_declared && media_resident; /* 播放类:播放/暂停/缓冲都保活 */
}
static meow_bg_entry_t *bg_find(const char *app_id)
{
int i;
for (i = 0; i < MEOW_BG_MAX; i++)
if (g_bg[i].used && strcmp(g_bg[i].app_id, app_id) == 0)
return &g_bg[i];
return NULL;
}
/* 填一槽(add 与槽满 LRU 复用)。 */
static void bg_fill(int i, const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive)
{
meow_bg_entry_t *e = &g_bg[i];
memset(e, 0, sizeof(*e));
e->used = 1;
snprintf(e->app_id, sizeof(e->app_id), "%s", app_id);
e->pid = pid;
e->out_fd = out_fd;
e->on_card = on_card;
e->bg_since_ms = bg_now_ms(); /* 批①:转后台时刻(宽限/槽满 LRU 掐最久用的锚点) */
e->keepalive = keepalive ? 1 : 0; /* 批①+宽限:1=声明常驻(无限期);0=宽限期类 */
g_change_seq++;
bg_log("[bgplay] 转后台保活: %s pid=%d on_card=%d keepalive=%d",
app_id, (int)pid, on_card, e->keepalive);
}
/* 真实单调毫秒(不随 ops.now_ms 注入的假时钟走)。槽满 LRU 回收需要真实的有界等超时——
* 注入假时钟的宿主测试若用它算 deadline 则永不超时(假时钟不随 usleep 推进),故独立取真时钟。 */
static uint64_t bg_real_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}
/* 槽满 LRU 回收:找 bg_since_ms 最旧的**未声明且媒体不活跃**槽,走真退序列 SIGTERM→有界等→
* SIGKILL→reap→on_bg_dead(通知 amsd + 清媒体会话)→ 摘表。返回被回收槽下标;-1=无可回收
* (全为声明常驻/媒体活跃,按保安全拒绝新入)。 */
static int bg_evict_lru(void)
{
uint64_t oldest = UINT64_MAX;
int cand = -1;
int i;
for (i = 0; i < MEOW_BG_MAX; i++) {
if (!g_bg[i].used) continue;
if (g_bg[i].keepalive) continue; /* 声明常驻不回收 */
if (g_ops.media_resident && g_ops.media_resident(g_bg[i].app_id))
continue; /* 媒体活跃不回收 */
if (g_bg[i].bg_since_ms < oldest) { oldest = g_bg[i].bg_since_ms; cand = i; }
}
if (cand < 0) return -1;
{
meow_bg_entry_t *e = &g_bg[cand];
char id[MEOW_APP_ID_MAX];
pid_t pid = e->pid;
int ofd = e->out_fd;
int st = 0;
uint64_t deadline;
snprintf(id, sizeof(id), "%s", e->app_id);
bg_log("[bgplay] 后台表满 LRU 回收: %s pid=%d(最久未激活·未声明)", id, (int)pid);
kill(pid, SIGTERM);
deadline = bg_real_now_ms() + TERM_GRACE_MS;
for (;;) {
pid_t r = waitpid(pid, &st, WNOHANG);
if (r == pid) break;
if (r < 0 && errno != EINTR) break; /* 已回收/非子进程:不再等 */
if (bg_real_now_ms() >= deadline) {
kill(pid, SIGKILL);
deadline = bg_real_now_ms() + 200u; /* SIGKILL 后非阻塞兜底,防 D-state 挂死 */
while (bg_real_now_ms() < deadline) {
if (waitpid(pid, &st, WNOHANG) == pid) break;
usleep(10000);
}
break;
}
usleep(5000);
}
if (ofd >= 0) close(ofd); /* 被回收 App 已死,其 stdout 读端一并关 */
if (g_ops.on_bg_dead) g_ops.on_bg_dead(id, pid, st); /* 通知 amsd + 清媒体会话 */
g_change_seq++;
bg_log("[bgplay] LRU 回收完成: %s 槽释放", id);
memset(e, 0, sizeof(*e));
}
return cand;
}
int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive)
{
int i;
if (!app_id || !app_id[0] || pid <= 0) return MEOW_ERR_INVAL;
if (bg_find(app_id)) return MEOW_ERR_EXISTS;
for (i = 0; i < MEOW_BG_MAX; i++)
if (!g_bg[i].used) { bg_fill(i, app_id, pid, out_fd, on_card, keepalive); return 0; }
/* 槽满:LRU 回收最久未激活的未声明槽;全为声明常驻/媒体活跃 → NOMEM(调用方回落真退)。 */
if (bg_evict_lru() >= 0)
for (i = 0; i < MEOW_BG_MAX; i++)
if (!g_bg[i].used) { bg_fill(i, app_id, pid, out_fd, on_card, keepalive); return 0; }
return MEOW_ERR_NOMEM;
}
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid)
{
meow_bg_entry_t *e;
if (!app_id || !app_id[0]) return -1;
e = bg_find(app_id);
if (!e) return -1;
if (out_fd) *out_fd = e->out_fd;
if (pid) *pid = e->pid;
bg_log("[bgplay] 回前台: %s pid=%d", app_id, (int)e->pid);
memset(e, 0, sizeof(*e));
g_change_seq++;
return 0;
}
int meow_bg_is_backgrounded(const char *app_id)
{
return app_id && app_id[0] && bg_find(app_id) != NULL;
}
int meow_bg_count(void)
{
int i, n = 0;
for (i = 0; i < MEOW_BG_MAX; i++)
if (g_bg[i].used) n++;
return n;
}
const meow_bg_entry_t *meow_bg_at(int i)
{
if (i < 0 || i >= MEOW_BG_MAX || !g_bg[i].used) return NULL;
return &g_bg[i];
}
uint32_t meow_bgplay_change_seq(void)
{
return g_change_seq;
}
void meow_bg_tick(void)
{
uint64_t now = bg_now_ms();
int i;
for (i = 0; i < MEOW_BG_MAX; i++) {
meow_bg_entry_t *e = &g_bg[i];
int st;
pid_t r;
if (!e->used) continue;
/* 触发终止(SIGTERM→SIGKILL 真退序列)的两类条件,仅当尚未进入终止序列(term_at_ms==0):
* a) 卡拔例外:on_card 且卡不在(防 SIGBUS);
* b) 批①+宽限 超时回收:未声明(keepalive=0)且媒体不活跃(media_resident=0),转后台
* 超 MEOW_BG_GRACE_MS 未再启动 → 回收。声明常驻(keepalive=1)/媒体活跃**不清**。 */
if (e->term_at_ms == 0) {
int grace = (e->keepalive == 0 &&
!(g_ops.media_resident ? g_ops.media_resident(e->app_id) : 0) &&
now - e->bg_since_ms > (uint64_t)MEOW_BG_GRACE_MS);
if ((e->on_card && !bg_card_present()) || grace) {
if (grace)
bg_log("[bgplay] 宽限期超时回收后台App: %s pid=%d(超 %ums 未再启动)",
e->app_id, (int)e->pid, (unsigned)MEOW_BG_GRACE_MS);
else
bg_log("[bgplay] 卡拔出,终止卡上后台 App: %s pid=%d",
e->app_id, (int)e->pid);
kill(e->pid, SIGTERM);
e->term_at_ms = now;
}
}
/* SIGTERM 后 SIGKILL 兜底(TERM_GRACE_MS 节流重复发直至 reap;卡拔/宽限/槽满回收共用) */
if (e->term_at_ms && now - e->term_at_ms >= TERM_GRACE_MS) {
kill(e->pid, SIGKILL);
e->term_at_ms = now;
}
r = waitpid(e->pid, &st, WNOHANG);
if (r == e->pid) {
char id[MEOW_APP_ID_MAX];
pid_t pid = e->pid;
int ofd = e->out_fd;
snprintf(id, sizeof(id), "%s", e->app_id);
if (ofd >= 0) close(ofd); /* 回收:后台 App 已死,其 stdout 管道读端一并关(防 fd 泄漏) */
memset(e, 0, sizeof(*e));
g_change_seq++;
bg_log("[bgplay] 后台 App 退出: %s pid=%d status=0x%x", id, (int)pid, st);
if (g_ops.on_bg_dead) g_ops.on_bg_dead(id, pid, st);
}
}
}
/* 批③后台管理页 / `am kill`:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。
* 只置 term_at_ms(tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表),
* 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。
* 调用者:bg_page.c 行内「杀」/「一键清除」+ util.c MSG_AMS_KILL(`am kill`,无前台会话时)
* ——两个入口同一通道,故页面按钮与面板/CLI kill 行为对称。 */
int meow_bg_terminate(const char *app_id)
{
if (!app_id || !app_id[0]) return MEOW_ERR_INVAL;
int i;
for (i = 0; i < MEOW_BG_MAX; i++) {
meow_bg_entry_t *e = &g_bg[i];
if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) {
if (e->term_at_ms == 0) {
bg_log("[bgplay] 用户终止后台App: %s pid=%d(后台管理页/am kill 同通道)",
app_id, (int)e->pid);
kill(e->pid, SIGTERM);
e->term_at_ms = bg_now_ms();
}
return 0;
}
}
return MEOW_ERR_NOENT;
}
/* ================= 无头后台服务 ================= */
static meow_svc_rt_t *svc_find_mut(const char *app_id, const char *svc)
{
int i;
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
if (g_svc[i].used && strcmp(g_svc[i].app_id, app_id) == 0 &&
strcmp(g_svc[i].svc, svc) == 0)
return &g_svc[i];
return NULL;
}
const meow_svc_rt_t *meow_svc_find(const char *app_id, const char *svc)
{
if (!app_id || !svc) return NULL;
return svc_find_mut(app_id, svc);
}
/* spawn 服务子进程:stdio→/dev/null,env={PATH,HOME,MEOW_SERVICE[,MEOW_LOG_DIR/MEOW_LOG_APP]}。
* 父进程返回 pid;子进程不返回(execve 或 _exit(127))。 */
static pid_t svc_spawn(meow_svc_rt_t *s)
{
char env_service[32];
char env_logdir[160];
char env_logapp[64];
char *envp[6];
int n = 0;
pid_t pid;
envp[n++] = (char *)"PATH=/usr/bin:/bin:/usr/sbin:/sbin";
envp[n++] = (char *)"HOME=/root";
snprintf(env_service, sizeof(env_service), "MEOW_SERVICE=%s", s->svc);
envp[n++] = env_service;
if (g_ops.log_dir && g_ops.log_dir[0]) {
snprintf(env_logdir, sizeof(env_logdir), "MEOW_LOG_DIR=%s", g_ops.log_dir);
envp[n++] = env_logdir;
snprintf(env_logapp, sizeof(env_logapp), "MEOW_LOG_APP=%s", s->app_id);
envp[n++] = env_logapp;
}
envp[n] = NULL;
pid = fork();
if (pid != 0) return pid; /* 父:pid 或 -1 */
/* 子进程:脱离终端会话,stdio 全部重定向 /dev/null(无头服务) */
setsid();
{
int dn = open("/dev/null", O_RDWR);
if (dn >= 0) {
dup2(dn, STDIN_FILENO);
dup2(dn, STDOUT_FILENO);
dup2(dn, STDERR_FILENO);
if (dn > STDERR_FILENO) close(dn);
}
}
{
char *argv[2];
argv[0] = s->exec;
argv[1] = NULL;
execve(s->exec, argv, envp);
}
_exit(127); /* exec 失败:父进程 tick 回收后按退避/熔断处理 */
}
int meow_svc_register(const char *app_id, const char *svc, const char *exec,
int restart, int max_restart, int window_ms,
int on_card, int reg_fd)
{
meow_svc_rt_t *s;
pid_t pid;
int i;
if (!app_id || !app_id[0] || !svc || !svc[0] || !exec || !exec[0])
return MEOW_ERR_INVAL;
if (access(exec, X_OK) != 0) return MEOW_ERR_NOENT;
s = svc_find_mut(app_id, svc);
if (s) {
if (s->pid > 0) return MEOW_ERR_EXISTS;
if (!s->circuit) return MEOW_ERR_EXISTS; /* 退避排程中也视为在册 */
/* 熔断槽重注册:复位熔断,重新武装 */
bg_log("[bgplay] 服务熔断复位重注册: %s/%s", app_id, svc);
s->circuit = 0;
s->stopping = 0;
s->restart_count = 0;
s->window_start_ms = bg_now_ms();
s->next_retry_ms = 0;
s->reg_fd = reg_fd;
s->on_card = on_card;
} else {
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
if (!g_svc[i].used) break;
if (i >= MEOW_SVC_RT_MAX) return MEOW_ERR_NOMEM;
s = &g_svc[i];
memset(s, 0, sizeof(*s));
s->used = 1;
snprintf(s->app_id, sizeof(s->app_id), "%s", app_id);
snprintf(s->svc, sizeof(s->svc), "%s", svc);
snprintf(s->exec, sizeof(s->exec), "%s", exec);
s->restart = restart;
s->max_restart = max_restart;
s->window_ms = window_ms;
s->on_card = on_card;
s->reg_fd = reg_fd;
s->window_start_ms = bg_now_ms();
}
pid = svc_spawn(s);
if (pid < 0) {
if (s->pid == 0 && s->restart_count == 0 && !s->circuit)
memset(s, 0, sizeof(*s)); /* 新槽首次 fork 即败:不留尸槽 */
return MEOW_ERR_IO;
}
s->pid = pid;
s->term_at_ms = 0;
bg_log("[bgplay] 服务拉起: %s/%s pid=%d restart=%d max=%d",
app_id, svc, (int)pid, restart, max_restart);
return (int)pid;
}
int meow_svc_stop(const char *app_id, const char *svc)
{
meow_svc_rt_t *s;
if (!app_id || !svc) return MEOW_ERR_INVAL;
s = svc_find_mut(app_id, svc);
if (!s) return MEOW_ERR_NOENT;
bg_log("[bgplay] 服务停止: %s/%s pid=%d", app_id, svc, (int)s->pid);
if (s->pid > 0) {
s->stopping = 1;
kill(s->pid, SIGTERM);
s->term_at_ms = bg_now_ms();
} else {
/* 未在跑(退避中/熔断):直接清槽 */
if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc);
memset(s, 0, sizeof(*s));
}
return 0;
}
/* 服务死亡后的退避/熔断判定。返回 1=已排程重启;0=槽已清或熔断。 */
static int svc_on_exit(meow_svc_rt_t *s, int st)
{
uint64_t now = bg_now_ms();
bg_log("[bgplay] 服务退出: %s/%s pid=%d status=0x%x stopping=%d",
s->app_id, s->svc, (int)s->pid, st, s->stopping);
s->pid = 0;
if (s->stopping || !s->restart) {
if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc);
memset(s, 0, sizeof(*s));
return 0;
}
/* 滑窗:超窗复位计数 */
if (s->window_ms > 0 && now - s->window_start_ms >= (uint64_t)s->window_ms) {
s->restart_count = 0;
s->window_start_ms = now;
}
s->restart_count++;
if (s->max_restart > 0 && s->restart_count > s->max_restart) {
/* 熔断:清通知 + 广播 MSG_SYS{SVC_DEAD},槽保留待重注册复位 */
m_sys_t m;
s->circuit = 1;
s->next_retry_ms = 0;
bg_log("[bgplay] 服务熔断: %s/%s 窗口内重启 %d 次超 max=%d",
s->app_id, s->svc, s->restart_count, s->max_restart);
if (g_ops.notif_clear_app)
g_ops.notif_clear_app(s->app_id);
if (g_ops.send_frame && s->reg_fd >= 0) {
memset(&m, 0, sizeof(m));
m.event = MEOW_SYS_SVC_DEAD;
snprintf(m.app_id, sizeof(m.app_id), "%s", s->app_id);
snprintf(m.svc, sizeof(m.svc), "%s", s->svc);
g_ops.send_frame(s->reg_fd, MSG_SYS, 0, &m, sizeof(m));
}
if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc);
return 0;
}
{
int step = s->restart_count - 1;
uint32_t dly;
if (step >= BACKOFF_STEPS) step = BACKOFF_STEPS - 1;
dly = k_backoff[step];
s->next_retry_ms = now + dly;
bg_log("[bgplay] 服务退避重启: %s/%s 第 %d 次,%ums 后拉起",
s->app_id, s->svc, s->restart_count, (unsigned)dly);
}
return 1;
}
void meow_svc_tick(void)
{
uint64_t now = bg_now_ms();
int i;
for (i = 0; i < MEOW_SVC_RT_MAX; i++) {
meow_svc_rt_t *s = &g_svc[i];
if (!s->used) continue;
/* 卡拔例外:exec 在卡上 → SIGTERM + stopping=1(不重启防空转死循环) */
if (s->on_card && s->pid > 0 && !bg_card_present() && !s->stopping) {
bg_log("[bgplay] 卡拔出,终止卡上服务: %s/%s pid=%d",
s->app_id, s->svc, (int)s->pid);
s->stopping = 1;
kill(s->pid, SIGTERM);
s->term_at_ms = now;
}
/* SIGTERM 宽限超时 → SIGKILL */
if (s->stopping && s->pid > 0 && s->term_at_ms &&
now - s->term_at_ms >= TERM_GRACE_MS) {
kill(s->pid, SIGKILL);
s->term_at_ms = now;
}
if (s->pid > 0) {
int st;
pid_t r = waitpid(s->pid, &st, WNOHANG);
if (r == s->pid) svc_on_exit(s, st);
continue;
}
/* 退避到点 → 重新拉起 */
if (!s->circuit && !s->stopping && s->restart &&
s->next_retry_ms && now >= s->next_retry_ms) {
pid_t pid = svc_spawn(s);
s->next_retry_ms = 0;
if (pid > 0) {
s->pid = pid;
bg_log("[bgplay] 服务重启: %s/%s pid=%d(第 %d 次)",
s->app_id, s->svc, (int)pid, s->restart_count);
} else {
/* fork 失败:压到下一档重试 */
s->next_retry_ms = now + k_backoff[BACKOFF_STEPS - 1];
}
}
}
}
/* ================= 关机前回收 ================= */
void meow_bgplay_kill_all(void)
{
uint64_t deadline;
int i;
/* best-effort 广播 SHUTDOWN */
if (g_ops.send_frame) {
m_sys_t m;
memset(&m, 0, sizeof(m));
m.event = MEOW_SYS_SHUTDOWN;
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
if (g_svc[i].used && g_svc[i].reg_fd >= 0)
g_ops.send_frame(g_svc[i].reg_fd, MSG_SYS, 0, &m, sizeof(m));
}
for (i = 0; i < MEOW_BG_MAX; i++)
if (g_bg[i].used && g_bg[i].pid > 0) kill(g_bg[i].pid, SIGTERM);
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
if (g_svc[i].used && g_svc[i].pid > 0) kill(g_svc[i].pid, SIGTERM);
/* 有界等 1s(轮询 reap,期间 tick 语义不在——直接 waitpid) */
deadline = bg_now_ms() + 1000u;
while (bg_now_ms() < deadline) {
int alive = 0;
for (i = 0; i < MEOW_BG_MAX; i++) {
if (!g_bg[i].used) continue;
if (waitpid(g_bg[i].pid, NULL, WNOHANG) == g_bg[i].pid)
memset(&g_bg[i], 0, sizeof(g_bg[i]));
else
alive++;
}
for (i = 0; i < MEOW_SVC_RT_MAX; i++) {
if (!g_svc[i].used) continue;
if (waitpid(g_svc[i].pid, NULL, WNOHANG) == g_svc[i].pid)
memset(&g_svc[i], 0, sizeof(g_svc[i]));
else
alive++;
}
if (!alive) break;
usleep(50 * 1000);
}
/* 兜底 SIGKILL + 有界 reap(waitpid 阻塞上限由 SIGKILL 必死保证) */
for (i = 0; i < MEOW_BG_MAX; i++)
if (g_bg[i].used) {
kill(g_bg[i].pid, SIGKILL);
waitpid(g_bg[i].pid, NULL, 0);
memset(&g_bg[i], 0, sizeof(g_bg[i]));
}
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
if (g_svc[i].used) {
kill(g_svc[i].pid, SIGKILL);
waitpid(g_svc[i].pid, NULL, 0);
memset(&g_svc[i], 0, sizeof(g_svc[i]));
}
g_change_seq++;
bg_log("[bgplay] kill_all 完成(关机/退出回收)");
}
void meow_bgplay_dump(const char *prefix)
{
int i;
for (i = 0; i < MEOW_BG_MAX; i++)
if (g_bg[i].used)
bg_log("%s bg[%d] %s pid=%d on_card=%d", prefix, i,
g_bg[i].app_id, (int)g_bg[i].pid, g_bg[i].on_card);
for (i = 0; i < MEOW_SVC_RT_MAX; i++)
if (g_svc[i].used)
bg_log("%s svc[%d] %s/%s pid=%d restart=%d count=%d circuit=%d",
prefix, i, g_svc[i].app_id, g_svc[i].svc, (int)g_svc[i].pid,
g_svc[i].restart, g_svc[i].restart_count, g_svc[i].circuit);
}
+167
View File
@@ -0,0 +1,167 @@
/* meow_bgplay.h — MeowOS M4:后台播放保活 + 无头后台服务运行时表(FRAMEWORK_DESIGN §9)。
*
* 定位:
* ① 后台播放保活——退出播放中(声明 background_audio=true 且媒体会话 PLAYING)的 App
* 不 SIGTERM:进程保留(音频经 audiod socket 天然续播),UI 冻结走 §10.5 的
* /tmp/meow_notif_active SDK 自抑机制(present 画进 back_buf 不上屏 + 输入排空)。
* **不做字面 SIGSTOP**:SIGSTOP 会冻结同进程播放线程、断供 PCM(§10.5 真机实证的坑)。
* 点图标/媒体卡片回前台=take 恢复原进程;停播后再退出=真退(走原 SIGTERM 序列)。
* ② 无头后台服务——MSG_REGISTER 注册 app.json `service.<svc>` 声明的服务:launcher
* spawn 子进程(无 UI,stdio→/dev/null),死亡按 1/2/4/10s 退避重启,超 max_restart
* 熔断(清该 App 通知 + 经注册连接广播 MSG_SYS{SVC_DEAD}),重注册可复位熔断。
*
* 边界:
* - 卡拔出例外:二进制在 TF 卡上的后台 App/服务仍 SIGTERM(防 SIGBUS),不适用保活。
* - 崩溃照收:waitpid WNOHANG 回收,异常退出经 on_bg_dead 回调走 crash 上报。
* - 关机前回收:meow_bgplay_kill_all() 有界 SIGTERM→SIGKILL→reap。
*
* 自包含:不依赖 launcher static 符号;日志/时钟/卡检测/通知清理/发帧全经
* meow_bgplay_ops 注入(launcher 用 logmsg/sd_device_present/ipc_send;宿主单测用假件)。
* 单一写者=launcher 主循环(meow_ipc_process 排空点挂 tick),无锁。可独立单测(test_bgplay.c)。
*/
#ifndef MEOW_BGPLAY_H
#define MEOW_BGPLAY_H
#include <stdint.h>
#include <stddef.h>
#include <sys/types.h>
#include "meow_ipc.h"
/* 后台保活 App 上限(同时驻留后台的播放中 App) */
#define MEOW_BG_MAX 4
/* 批①+宽限:无声明/媒体不活跃 App 入后台的宽限期(默认 30 分钟)。
* 语义:转后台(bg_since_ms)后超 MEOW_BG_GRACE_MS 未再启动 → meow_bg_tick 走真退序列
* SIGTERM→SIGKILL 回收(摘表 + 通知 amsd)。**声明常驻(keepalive=1)或媒体活跃
* (meow_media_app_resident=1)不清**。本批为编译期常量(30*60*1000 = 1800000ms);
* 后续可进设置/按 App 可配(待拍板)。 */
#define MEOW_BG_GRACE_MS 1800000u
/* 无头后台服务运行时槽上限(与 meow_acct.svcs 的 MEOW_MAX_SVCS 对齐) */
#define MEOW_SVC_RT_MAX 8
/* 退避重启档位(ms):1s/2s/4s/10s 封顶 */
#define MEOW_SVC_BACKOFF_1 1000u
#define MEOW_SVC_BACKOFF_2 2000u
#define MEOW_SVC_BACKOFF_4 4000u
#define MEOW_SVC_BACKOFF_10 10000u
/* 后台保活 App 槽。out_fd=其 stdout 管道读端(保活期继续 drain,防 App 写满管道阻塞)。 */
typedef struct meow_bg_entry {
int used;
char app_id[MEOW_APP_ID_MAX];
pid_t pid;
int out_fd;
int on_card; /* 二进制在 TF 卡上(卡拔需 SIGTERM 防 SIGBUS) */
uint64_t term_at_ms; /* 卡拔/宽限超时/槽满回收 SIGTERM 发出时刻(0=未发;SIGKILL 兜底计时) */
uint64_t bg_since_ms; /* 批①:转后台时刻(monotonic ms;宽限超时回收/槽满 LRU 掐最久用) */
char keepalive; /* 批①+宽限:1=声明常驻(background_keepalive,无限期不清);0=宽限期类
* (未声明/background_audio,媒体不活跃时超 bg_grace_ms 回收) */
} meow_bg_entry_t;
/* 无头后台服务运行时槽(静态声明在 meow_acct.svcs,此处为运行态)。 */
typedef struct meow_svc_rt {
int used;
char app_id[MEOW_APP_ID_MAX];
char svc[16];
char exec[129];
pid_t pid; /* >0 在跑;0=等待退避重启/已熔断 */
int reg_fd; /* 注册方 IPC 连接 fd(熔断广播投递用;-1=静态自动拉起) */
int restart; /* 1=死亡后退避重启 */
int max_restart; /* 熔断阈值(窗口内重启次数上限;0=不熔断) */
int window_ms; /* 重启窗口(0=累计不滑窗) */
int restart_count; /* 当前窗口内已重启次数 */
int circuit; /* 1=已熔断(清通知+广播过,不再拉起;重注册复位) */
int stopping; /* 1=主动停止/卡拔终止中(SIGTERM 已发,不重启) */
int on_card; /* exec 在 TF 卡上 */
uint64_t window_start_ms; /* 滑窗起点 */
uint64_t next_retry_ms; /* 退避下次拉起时刻(0=无排程) */
uint64_t term_at_ms; /* SIGTERM 发出时刻(0=未发;500ms 未死 SIGKILL) */
} meow_svc_rt_t;
/* 依赖注入。全 NULL 可用(log 静默、now_ms 内部 CLOCK_MONOTONIC、card_present 恒 1、
* 通知清理/发帧跳过)。
* on_bg_dead:后台保活 App 进程退出(status=waitpid 原始状态;正常/异常由回调方判);
* media_resident:该 App 媒体会话是否「活跃可驻留」(launcher 注入 meow_media_app_resident)。
* 批①+宽限用:宽限超时回收 / 槽满 LRU 判「媒体活跃不清不回收」;NULL=恒 0(视为不活跃)。
* on_svc_dead:服务槽最终消亡(stopping 走完/不重启死亡;熔断不算消亡,槽保留待复位);
* notif_clear_app:熔断时清该 App 全部通知,返回清除条数(<0=失败);
* send_frame:熔断广播 MSG_SYS(launcher 侧须先查连接存活再发);
* log_dir:svc 子进程 MEOW_LOG_DIR 注入(NULL 不注入)。 */
typedef struct meow_bgplay_ops {
void (*log)(const char *fmt, ...);
uint64_t (*now_ms)(void);
int (*card_present)(void);
void (*on_bg_dead)(const char *app_id, pid_t pid, int status);
int (*media_resident)(const char *app_id);
void (*on_svc_dead)(const char *app_id, const char *svc);
int (*notif_clear_app)(const char *app_id);
size_t (*send_frame)(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len);
const char *log_dir;
} meow_bgplay_ops;
/* 注入 ops(拷贝);NULL=全清空复位(含两张表)。launcher 在 meow_ipc_init 末尾调一次。 */
void meow_bgplay_init(const meow_bgplay_ops *ops);
/* ---- 后台播放保活判定(纯函数,无副作用)---- */
/* 退出时是否转后台保活(批①三参化):
* keepalive_declared=1(声明 background_keepalive)→ 恒保活(通用常驻,不依赖媒体会话);
* 否则 bg_audio_declared(声明 background_audio)且 media_resident(该 App 媒体会话活跃
* =PLAYING/PAUSED/BUFFER,见 meow_media_app_resident)→ 保活。
* 未声明/无活跃会话 → 0(走原真退序列)。 */
int meow_bg_should_keep(int bg_audio_declared, int media_resident, int keepalive_declared);
/* ---- 后台保活 App 表 ---- */
/* 转后台:登记 app_id/pid/out_fd,keepalive 标记常驻类型(1=background_keepalive 类=无限期;
* 0=宽限期类=未声明/background_audio,媒体不活跃时超 bg_grace_ms 回收)。批①+宽限:**全 App**
* nav-exit 都经此入表;槽满(MEOW_BG_MAX=4)时 LRU 回收最久未激活(bg_since_ms 最旧)的**未声明**
* 槽(不回收声明常驻/媒体活跃);全为声明常驻/媒体活跃 → MEOW_ERR_NOMEM(调用方回落真退,保安全)。
* 返回 0=成功;MEOW_ERR_EXISTS=已在后台;MEOW_ERR_NOMEM=满且无可回收。 */
int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive);
/* 回前台:按 app_id 摘除槽(进程归还前台会话)。成功返回 0 且 *out_fd(可 NULL)带回
* 管道读端;未在后台返回 -1。 */
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid);
/* 该 App 是否在后台保活。 */
int meow_bg_is_backgrounded(const char *app_id);
/* 用户终止后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。调用者:后台管理页行内「杀」/
* 「一键清除」+ launcher IPC MSG_AMS_KILL(`am kill`)——两入口同通道,页面与面板/CLI 行为对称。
* 置 e->term_at_ms=now 由 meow_bg_tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表。
* 返回 0=已受理(在后台表且 pid>0);MEOW_ERR_NOENT=不在后台/无进程;MEOW_ERR_INVAL=app_id 空。 */
int meow_bg_terminate(const char *app_id);
/* 当前后台保活数。 */
int meow_bg_count(void);
/* 取第 i 槽(0..MEOW_BG_MAX-1)只读指针;空槽/越界返回 NULL。供 drain out_fd。 */
const meow_bg_entry_t *meow_bg_at(int i);
/* 表变化计数(add/take/回收 递增);菜单 1s tick 比对刷新「·后台」标注用。 */
uint32_t meow_bgplay_change_seq(void);
/* 周期调(主循环 ~10ms 排空点):waitpid WNOHANG 回收死亡后台 App(→on_bg_dead 清槽)。
* 终止触发(SIGTERM,TERM_GRACE_MS 后 SIGKILL):
* - 卡拔例外:on_card 槽在卡不在时终止(防 SIGBUS);
* - 批①+宽限 超时回收:未声明(keepalive=0)且媒体不活跃(media_resident=0)的槽,转后台超
* MEOW_BG_GRACE_MS 未再启动 → 回收(走真退序列 + 摘表 + 通知 amsd)。声明常驻/媒体活跃不清。 */
void meow_bg_tick(void);
/* ---- 无头后台服务 ---- */
/* 注册/拉起服务。exec 须可执行(先 access(X_OK),拒绝返回 MEOW_ERR_NOENT 不污染槽)。
* 返回 pid(>0)=已拉起;MEOW_ERR_EXISTS=同名服务在跑;MEOW_ERR_NOENT=exec 不可执行;
* MEOW_ERR_NOMEM=槽满;MEOW_ERR_IO=fork 失败。同 svc 已熔断槽重注册=复位熔断重新拉起。 */
int meow_svc_register(const char *app_id, const char *svc, const char *exec,
int restart, int max_restart, int window_ms,
int on_card, int reg_fd);
/* 请求停止服务:SIGTERM,标记 stopping(不触发重启)。返回 0=已受理;MEOW_ERR_NOENT=无此服务。 */
int meow_svc_stop(const char *app_id, const char *svc);
/* 查服务槽(只读);无返回 NULL。 */
const meow_svc_rt_t *meow_svc_find(const char *app_id, const char *svc);
/* 周期调:回收死亡子进程;按 restart/退避档 1/2/4/10s 重启;滑窗复位计数;超
* max_restart 熔断(notif_clear_app + send_frame MSG_SYS{SVC_DEAD} + circuit=1);
* 卡拔:on_card 槽 SIGTERM + stopping=1(exec 随卡消失,不重启防空转)。 */
void meow_svc_tick(void);
/* 关机前回收:向各服务注册连接广播 MSG_SYS{SHUTDOWN}(best-effort),随后全部
* bg/svc 子进程 SIGTERM→有界等 1s→SIGKILL→有界 reap。调用方负责 unlink NOTIF_ACTIVE。 */
void meow_bgplay_kill_all(void);
/* 调试打印(经 ops->log,每槽一行)。 */
void meow_bgplay_dump(const char *prefix);
#endif /* MEOW_BGPLAY_H */
+117
View File
@@ -0,0 +1,117 @@
/* meow_dep.c — app.json 二进制依赖声明(depends=<name>[:<mod>][,...])解析与启动前检查。
*
* 背景(用户定案):App 某些复杂功能若依赖现成 Linux 二进制程序(板载 /usr/bin 或卡上
* MeowOS/opt/bin)能效更好,故在 app.json 声明二进制依赖;launcher 在启动 App 前检查
* required 依赖是否都存在,缺 required 则不启动(toast「缺依赖: <name>」+ 日志);
* optional 缺失仅 WARN 放行。检查只在启动时做(不做周期复查)。
*
* 与 meow_parse_app_json 的 line 解析同风格:取值串后逐项切分(不引 JSON 解析器)。
* 本文件独立成 TU,由 build_launcher.sh 编入;宿主测试用「假根(env 覆写搜索目录)」验证。 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include "meow_launcher_internal.h"
/* 最近一次因缺 required 依赖而阻断启动的首个缺失名(供 menu toast「缺依赖: <name>」)。
* 每次 launch_app_chain 入口清空(meow_dep_clear_reason);无缺失则 ""。 */
static char g_dep_block_reason[32] = "";
void meow_dep_clear_reason(void)
{
g_dep_block_reason[0] = 0;
}
const char *meow_dep_block_reason(void)
{
return g_dep_block_reason;
}
/* 二进制搜索目录(真机默认):板载 /usr/bin ← 卡上 <卡根>/MeowOS/opt/bin。
* 宿主测试经 MEOW_DEP_BIN / MEOW_DEP_ROOT 覆写为假根(与 nyai MEOWROOT 同思路)。 */
static const char *dep_bin_root(void)
{
const char *e = getenv("MEOW_DEP_BIN");
return (e && e[0]) ? e : "/usr/bin";
}
static const char *dep_card_root(void)
{
const char *e = getenv("MEOW_DEP_ROOT");
return (e && e[0]) ? e : "/usr/data/mnt/sd_0";
}
/* 解析 depends 值串 → 逐个填充 deps[](最多 max 项,超量丢弃;空项/纯空白丢弃)。
* <name>[:<mod>];<mod> 缺省 required;:opt/:req 等价 optional/required;
* 未知修饰符按 required 处理并 logmsg WARN。返回写入项数。 */
int meow_parse_depends(const char *val, struct meow_dep *deps, int max)
{
if (!val || !deps || max <= 0) return 0;
int n = 0;
const char *p = val;
while (*p && n < max) {
while (*p && (*p == ' ' || *p == '\t' || *p == ',')) p++;
if (!*p || *p == '"') break; /* 结束(含 JSON 值尾引号) */
const char *start = p; /* 名称起点 */
while (*p && *p != ':' && *p != ',' && *p != '"' && *p != ' ' && *p != '\t') p++;
int nl = (int)(p - start);
if (nl > 0) {
if (nl > 31) nl = 31;
char nm[32];
memcpy(nm, start, (size_t)nl);
nm[nl] = 0;
int req = 1; /* 默认 required */
if (*p == ':') { /* 修饰符 */
const char *ms = p + 1;
while (*ms == ' ' || *ms == '\t') ms++;
const char *m = ms;
while (*m && *m != ',' && *m != '"' && *m != ' ' && *m != '\t') m++;
int ml = (int)(m - ms);
if (ml == 8 && !strncmp(ms, "optional", 8)) req = 0;
else if (ml == 3 && !strncmp(ms, "opt", 3)) req = 0;
else if (ml == 8 && !strncmp(ms, "required", 8)) req = 1;
else if (ml == 3 && !strncmp(ms, "req", 3)) req = 1;
else logmsg("WARN: depends '%s' unknown mod '%.*s' (treated required)", nm, ml, ms);
p = m;
}
snprintf(deps[n].name, sizeof deps[n].name, "%s", nm);
deps[n].req = (char)(req ? '1' : '0');
n++;
}
if (*p == ',') p++; /* 消费分隔符 */
}
return n;
}
/* 依赖二进制存在性:按序 <board>/<name> → <card>/MeowOS/opt/bin/<name>(X_OK)。返回 1=存在。 */
static int dep_bin_exists(const char *name)
{
if (!name || !name[0]) return 0;
char p[320];
snprintf(p, sizeof p, "%s/%s", dep_bin_root(), name);
if (access(p, X_OK) == 0) return 1;
snprintf(p, sizeof p, "%s/MeowOS/opt/bin/%s", dep_card_root(), name);
if (access(p, X_OK) == 0) return 1;
return 0;
}
/* 启动前检查依赖。returns: 0=全部满足 / 1=缺 required(missing 回填首个缺失名,可 NULL)。 */
int meow_check_app_depends(const struct app *a, char *missing, int mlen)
{
if (!a) return 0;
for (int i = 0; i < a->ndep; i++) {
if (dep_bin_exists(a->deps[i].name)) continue;
if (a->deps[i].req == '1') {
logmsg("launch blocked: missing required dep '%s'", a->deps[i].name);
if (missing && mlen > 0) snprintf(missing, (size_t)mlen, "%s", a->deps[i].name);
snprintf(g_dep_block_reason, sizeof g_dep_block_reason, "%s", a->deps[i].name);
return 1;
}
logmsg("WARN: optional dep '%s' missing, continuing", a->deps[i].name);
}
return 0;
}
+368 -8
View File
@@ -13,10 +13,11 @@
* uint32_t len; = payload 字节数
* };
*
* 本阶段(D · M0+M1+M2+M3+M4 前置)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT +
* 本阶段(D · M0+M1+M2+M3+M4)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT +
* MSG_ACT_FOCUS/BACK + MSG_NOTIFY/PROGRESS/QUERY/DISMISS* + MSG_NAV(导航栈上报/退出)
* + MSG_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B)。
* picker(0x0070/0x0071)仅预留协议位,本批不实现。
* + MSG_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B)
* + MSG_REGISTER/SERVICE_OK/SERVICE_STOP/MSG_SYS(后台服务注册/停止/熔断事件,M4)。
* picker(0x0070/0x0071)已落地(二期)。
*/
#ifndef MEOW_IPC_H
#define MEOW_IPC_H
@@ -34,6 +35,12 @@
/* 列表编码上限(count 个 NUL 字符串时,distinct 工作缓冲上限) */
#define MEOW_IPC_CARGO 1024u /* 守护进程入队拷贝上限(可小于 MEOW_IPC_MAX) */
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE.md,③混合)----
* meow_svc_perf(C:/proc/meminfo Cached 监控降载)把「是否暂停下载/预取」写入此共享状态文件
* (一行式 key=value;App/SDK meow_cachegov 只读,系统级单点,不重复起线程)。
* 路径可被 env MEOW_CACHEGOV_C_FILE 覆盖(宿主测试隔离)。 */
#define MEOW_CACHEGOV_C_FILE "/tmp/meow_cachegov_c.state"
/* ---- 消息类型(FRAMEWORK_DESIGN §1 表)---- */
enum {
MSG_NONE = 0x0000,
@@ -74,10 +81,47 @@ enum {
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
/* ---- 系统翻页信号(READER_UI_DESIGN §E,用户 2026-09-04 拍板):0x0080-0x0081 ----
* 语义:launcher 按键层(meow_launcherd/keys.c)在「当前台 App 为其 app.json 声明
* volume_page 的前台应用」时,把物理 VOL_UP/VOL_DOWN 转换成系统翻页信号下发(否则照旧
* 调音量——音量基本行为/插孔消抖/音量反转绝不破坏)。App 侧(如 reader_app/app/reader_ipc.c)
* 持持久连接收本消息 → 上一页/下一页。方向由消息号本身决定(UP=上一页,DOWN=下一页);
* 无负载(len=0)。属系统级能力,未来其它 App 可声明复用。 */
MSG_KEY_PAGEUP = 0x0080, /* ↓ 系统级:上一页(VOL_UP 语义,仅前台声明 app 收) */
MSG_KEY_PAGEDOWN = 0x0081, /* ↓ 系统级:下一页(VOL_DOWN 语义,仅前台声明 app 收) */
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义) */
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义):后台保活表命中→就地真退序列;
* 否则有前台会话→置 pending 交会话消费;都没有→ACK -2(MEOW_ERR_NOENT) */
/* ---- audiod 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md §2):0x00C0-0x00CA ----
* 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。
* 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */
MSG_AUDIO_OPEN = 0x00C0, /* ↑ 开流(m_audio_open;client 原生 rate/ch/fmt/flags) */
MSG_AUDIO_OPEN_ACK = 0x00C1, /* ↓ 授权(stream_id + grant_rate/ch/fmt + err;grant=客户端应产出的格式) */
MSG_AUDIO_WRITE = 0x00C2, /* ↑ PCM 字节流(按 grant 格式交织;单帧 ≤4096B,多帧连续发,无回执) */
MSG_AUDIO_PAUSE = 0x00C3, /* ↑ 暂停(u32 stream_id) */
MSG_AUDIO_RESUME = 0x00C4, /* ↑ 恢复(u32 stream_id) */
MSG_AUDIO_STOP = 0x00C5, /* ↑ 停止并关流(u32 stream_id) */
MSG_AUDIO_PREEMPTED= 0x00C6, /* ↓ 被抢占通知(m_audio_preempted;旧流被后开流赶走,App 收 UI 提示) */
MSG_AUDIO_QUERY = 0x00C7, /* ↑ 状态快照查询(空负载) */
MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */
MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */
MSG_AUDIO_VOLUME = 0x00CA, /* ↑ 音量(i32 pct:-1=查询 0-100=设置;v1 透传写 DAC numid=1/2) */
/* P2 蓝牙(BLUETOOTH_FULL_OVERHAUL_PLAN §五):输出目标切换。audiod 读 settings.conf
* `audio.output_bt`(0/1) 或收本消息改写目标(0=OUT_DAC 1=OUT_BT),重配 PCM。
* 与物理路由 numid=9 解耦:蓝牙非物理端口,不写 numid=9、不触发 FPGA 烧录。 */
MSG_AUDIO_OUTPUT = 0x00CB, /* ↑ 输出目标(u32 target:0=DAC 1=BT;ui/CLI 下发) */
/* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ----
* 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。
* 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power:
* QUERY/HISTORY/EVENTS=读级任意调用者;POLICY_SET=动作级(期 3 落地时接 SO_PEERCRED 同源代码)。 */
MSG_PM_QUERY = 0x00D0, /* ↑ 功耗聚合查询(m_pm_query_t{subset}:PM_SUBSET_* 位;0=全部) */
MSG_PM_RESULT = 0x00D1, /* ↓ 聚合 JSON(电池深加工/功耗估算/分解/策略,形状对齐 §3/§4.1) */
/* 0x00D2 MSG_PM_POLICY_GET / 0x00D3 MSG_PM_POLICY_SET —— 期 3(profile/低电阈值读改,SET 动作级鉴权)
* 0x00D4 MSG_PM_HISTORY(u32 hours)/ 0x00D5 MSG_PM_HISTORY_RESULT(统计 JSON)—— 期 2
* 0x00D6 MSG_PM_EVENTS / 0x00D7 MSG_PM_EVENTS_RESULT(异常耗电事件清单)—— 期 2 */
MSG_SYS = 0x00F0 /* ↓ 系统事件(守护关闭/服务死亡/播放器状态) */
};
@@ -134,6 +178,38 @@ typedef struct m_err {
char msg[64];
} m_err_t;
/* ================= 后台服务注册(M4,FRAMEWORK_DESIGN §9) = */
/* MSG_REGISTER ↑:无头后台服务注册。调用方(App 前台进程)须:
* ① app.json 声明 `background=true` 或 HELLO flags 含 MF_BACKGROUND(否则 ERR_PERM);
* ② app.json `service.<svc>=mode:restart:max:window_ms` 静态声明该服务(否则 ERR_NOENT);
* ③ 注册后保持 IPC 连接不断(launcher 以连接存活做熔断广播的投递判定)。
* svc=NUL 结尾服务名(≤15 字符);flags 按位:MF_BACKGROUND(申请后台常驻)。 */
typedef struct m_register {
char app_id[MEOW_APP_ID_MAX]; /* 32 */
char svc[16]; /* 服务名(app.json service.<svc>) */
uint32_t flags; /* MF_BACKGROUND 等 */
} m_register_t; /* 52 B */
/* MSG_SERVICE_OK ↓:用 m_ack_t(code=0;status=服务子进程 pid)。 */
/* MSG_SERVICE_STOP ↑:请求停止自己已注册的服务(SIGTERM,不触发重启)。 */
typedef struct m_service_stop {
char app_id[MEOW_APP_ID_MAX]; /* 32 */
char svc[16];
} m_service_stop_t; /* 48 B */
/* MSG_SYS ↓:系统事件广播(守护关闭/服务死亡/播放器状态)。
* event:MEOW_SYS_*;app_id/svc 为事件主体(SHUTDOWN 时可空)。 */
enum { MEOW_SYS_SHUTDOWN = 1, /* 守护进程即将关闭(关机/重启前回收) */
MEOW_SYS_SVC_DEAD = 2, /* 后台服务熔断(超 max_restart 不再拉起) */
MEOW_SYS_PLAYER_STATE = 3 }; /* 播放器状态变更(预留) */
typedef struct m_sys {
uint32_t event; /* MEOW_SYS_* */
char app_id[MEOW_APP_ID_MAX]; /* 32 */
char svc[16]; /* SVC_DEAD 时为熔断服务名 */
} m_sys_t; /* 52 B */
/* MSG_ACT_QUERY ↑:解析某 app 的 activity 链路 */
typedef struct m_act_query {
char app_id[MEOW_APP_ID_MAX];
@@ -201,15 +277,40 @@ typedef struct m_meta {
} m_meta_t; /* 128+128+128+16+192+192+32 = 828 B */
/* MSG_PLAYCTL ↓(launcher→App)/↑(控制器→launcher 转发到最近活跃媒体会话):播放控制。
* cmd:1=播放 2=暂停 3=上一首 4=下一首 5=seek;其余保留。
* cmd:1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停切换(toggle,源侧按当前态翻转);其余保留。
* seek_ms:cmd=5 时目标位置(ms);其余置 0。
* 语义:通知中心卡片/播放页按钮、物理媒体键 → meow_media_playctl(cmd,seek_ms) 经
* 「最近活跃媒体会话」的连接 fd 下发;宿主验证经 meow_ipc playctl 发到 launcher 转发。 */
* 「最近活跃媒体会话」的连接 fd 下发;宿主验证经 meow_ipc playctl 发到 launcher 转发。
* 物理播放/暂停键(164)在 SDK 侧恒映射 cmd=6(不再本地判 state:走自有 IPC 线程的源
* 不经 SDK meta 上报,SDK 侧 state 可能恒 0);launcher 透传 cmd 不改写。 */
typedef struct m_playctl {
uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek */
uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停toggle */
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
/* ---- 媒体会话歌词上报(阶段 D · 批3:live lyric 数据源框架)----
* App 播放时把当前曲歌词(lrc 原文 + tlrc 翻译)上报到 meow_media 会话,供桌面歌词页/播放页
* 随播放位置实时滚动。约束:launcher 环缓冲 `payload[MEOW_IPC_CARGO(1024)]` 为单帧入队上限,
* 故歌词按分片传输(每片 ≤ MEOW_LYRIC_CHUNK 字节)。
* - seq = 歌曲代际(换歌/换词自增;launcher 见新 seq 清旧词缓存,同曲分片共用)
* - kind = 0 原文 lrc / 1 翻译 tlrc
* - total= 该 kind 总字节数(不含 NUL;0=清空该 kind)
* - off/len = 本片起始偏移 / 有效字节(首片 off=0;分片按序发送)
* 边界:lrc 上限 MEOW_MEDIA_LRC_MAX / tlrc 上限 MEOW_MEDIA_TLRC_MAX(会话内每 App 一份缓冲,
* 超限截断)。payload 定长且 == MEOW_IPC_CARGO(即 sizeof(m_lyric_t)=1024)。 */
#define MEOW_MEDIA_LRC_MAX 3584u /* 会话内歌词原文缓冲上限 */
#define MEOW_MEDIA_TLRC_MAX 1568u /* 会话内歌词翻译缓冲上限 */
#define MEOW_LYRIC_CHUNK (MEOW_IPC_CARGO - 44u) /* 单分片数据上限(44=头:32+4+2+2+2+2) */
typedef struct m_lyric {
char app_id[MEOW_APP_ID_MAX]; /* 32: 上报源头(与 HELLO/META 的 app_id 一致) */
uint32_t seq; /* 歌曲代际(新歌/新词自增;分片同 seq) */
uint16_t kind; /* 0=原文 lrc;1=翻译 tlrc */
uint16_t total; /* 该 kind 总字节数(不含 NUL;0=清空该 kind) */
uint16_t off; /* 本片起始偏移(≤ total;首片=0) */
uint16_t len; /* 本片有效字节(≤ MEOW_LYRIC_CHUNK) */
char data[MEOW_LYRIC_CHUNK]; /* 本片内容(len 后 NUL 结尾,接收侧再截断) */
} m_lyric_t; /* 44 + 980 = 1024 B = MEOW_IPC_CARGO */
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
@@ -325,6 +426,28 @@ typedef struct m_power_status {
char msg[96]; /* 结果/状态文本 */
} m_power_status_t;
/* ---- meow_powermgr 功耗管理(POWER_MANAGER_DESIGN §6.1;服务端 meow_powermgr)---- */
/* MSG_PM_QUERY 子集位(0=全部) */
#define PM_SUBSET_BATTERY (1u << 0) /* 电池深加工:state/pct/mv/health/续航估算 */
#define PM_SUBSET_ESTIMATE (1u << 1) /* 当前功耗估算:est_mw + confidence(estimated/derived) */
#define PM_SUBSET_BREAKDOWN (1u << 2) /* 功耗分解:各子系统分量(§3.2 经验模型,系数待标定) */
#define PM_SUBSET_POLICY (1u << 3) /* 策略档位快照(期 3;期 1 恒 null) */
/* MSG_PM_QUERY ↑:功耗聚合查询 */
typedef struct m_pm_query {
uint32_t subset; /* 0=全部;PM_SUBSET_* 组合 */
} m_pm_query_t; /* 4 B */
/* MSG_PM_RESULT ↓:聚合 JSON(payload 为 NUL 结尾 JSON 文本,同 m_perf_result_t 口径) */
typedef struct m_pm_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
uint32_t subset; /* 回显请求 subset */
uint16_t json_len; /* json 有效字节(不含 NUL) */
uint16_t pad;
char json[MEOW_IPC_MAX - 12]; /* 聚合 JSON(§3/§4.1 形状) */
} m_pm_result_t;
/* ---- amsd 控制(AMSD_DESIGN §4.2)---- */
/* MSG_AMS_START →:请求 launcher 拉起某 App。app_id=包名(权威);activity 空=裸启动
@@ -341,7 +464,9 @@ typedef struct m_ams_start {
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
} m_ams_start_t; /* 160 B */
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。 */
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。
* 后台保活 App:launcher 就地走 meow_bg_terminate 真退序列(与后台管理页「杀」同一通道);
* 前台 App:置 pending 交前台会话消费;两者皆非 → ACK code=(uint32_t)MEOW_ERR_NOENT(-2)。 */
typedef struct m_ams_kill {
char app_id[MEOW_APP_ID_MAX]; /* 32 */
} m_ams_kill_t; /* 32 B */
@@ -369,4 +494,239 @@ typedef struct m_picker_result {
char value[256]; /* 返回项(路径/文本);空=cancel */
} m_picker_result_t; /* 320 B */
/* ================= audiod 独占音频服务(AUDIOD_DESIGN.md) = */
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE
* 5=AUDIO_FMT_DOP(DoP DSD 透明容器格式)。v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32
* 处理;FLOAT v1 拒绝。AUDIO_FMT_DOP 为 **独立格式值**(A3+DSD 定案:避免 S24 名义(3B)/实际(4B)歧义),
* 仅在 MSG_AUDIO_OPEN.flags 带 AUDIO_FLAG_DSD 时合法;audiod 只做 DoP 容器透传(不引 dsd2pcm)。 */
enum {
AUDIO_FMT_S16 = 1,
AUDIO_FMT_S24 = 2,
AUDIO_FMT_S32 = 3,
AUDIO_FMT_FLOAT = 4,
AUDIO_FMT_DOP = 5 /* DoP DSD(32-bit 容器:bits[31:16]=16bit DSD 数据,bits[15:8]=标记 0xFA/0x05,bits[7:0]=0),
容器率 = DSD 位率/16(DSD64→176400 / DSD128→352800 / DSD256→705600) */
};
/* MSG_AUDIO_OPEN.flags 位(预留曾恒 0;A3+DSD 起使用) */
#define AUDIO_FLAG_DSD 0x00000001u /* fmt==AUDIO_FMT_DOP:本流为 DoP DSD(App 侧已解码),audiod 只透传 */
/* MSG_AUDIO_OPEN ↑:请求开流(App 原生声源参数)。grant_*(ACK)回 audiod 实际授权给客户端的
* 产出格式(率/声道/位深)——客户端必须按 grant 格式产出 PCM(SDK meow_ui_audio 负责归一:
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。fmt==AUDIO_FMT_DOP
* 须带 AUDIO_FLAG_DSD(否则 ERR_INVAL);DSD 经 DoP 透传,audiod 不引 dsd2pcm。 */
typedef struct m_audio_open {
char app_id[MEOW_APP_ID_MAX]; /* 32 */
uint32_t rate; /* 原生采样率(应用源,如 44100/48000;DSD 容器率) */
uint16_t ch; /* 原生声道数(1=mono 2=stereo) */
uint16_t fmt; /* 原生格式(AUDIO_FMT_*;DSD 用 AUDIO_FMT_DOP) */
uint32_t flags; /* AUDIO_FLAG_DSD 等 */
} m_audio_open_t; /* 32+4+2+2+4 = 44 B */
/* MSG_AUDIO_OPEN_ACK ↓:授权结果。err=MEOW_OK 才有效(stream_id>0 且 grant_* 为实际授权值);
* err<0 时 stream_id=0、grant_* 视为无效(客户端 open 返回负错误码)。 */
typedef struct m_audio_open_ack {
uint32_t stream_id; /* 授权流 id(>0) */
uint32_t grant_rate; /* 客户端应产出的采样率(=客户端 rate,无 SRC) */
uint16_t grant_ch; /* 客户端应产出的声道数(DAC 双声道 → 通常 2) */
uint16_t grant_fmt; /* 客户端应产出的格式(AUDIO_FMT_*,=硬件最优) */
int32_t err; /* MEOW_OK / MEOW_ERR_INVAL / MEOW_ERR_NODEV / MEOW_ERR_BUSY 等 */
} m_audio_open_ack_t; /* 4+4+2+2+4 = 16 B */
/* MSG_AUDIO_PAUSE/RESUME/STOP ↑:流控制(按 stream_id 定位)。 */
typedef struct m_audio_stream {
uint32_t stream_id;
} m_audio_stream_t; /* 4 B */
/* MSG_AUDIO_PREEMPTED ↓:被抢占通知(后开流赢;旧流收此消息,App 应暂停 UI/提示)。
* 旧流仍可读但 PCM 被丢弃(draining),直到客户端 CLOSE/断开。 */
typedef struct m_audio_preempted {
uint32_t stream_id; /* 被抢占的流 id */
char by_app[MEOW_APP_ID_MAX]; /* 32:抢占者 app_id */
} m_audio_preempted_t; /* 4+32 = 36 B */
/* MSG_AUDIO_ROUTE ↑:手动切换输出路由(调试/面板用)。port:0=auto(跟随插孔) 2=3.5 3=4.4。 */
typedef struct m_audio_route {
uint32_t port; /* 0=auto 2=3.5 3=4.4 */
} m_audio_route_t; /* 4 B */
/* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2
* + **DSD_VOLUME(音量收编 v2 泛化双组写,保证 DSD↔PCM 切换连续/防爆音)** + 持久化 settings.conf
* `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时**不下发 DAC 音量反转**
* (音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */
typedef struct m_audio_volume {
int32_t pct; /* -1=查询;0-100=设置 */
} m_audio_volume_t; /* 4 B */
/* 输出目标枚举(msg AUDIO_OUTPUT / settings.conf `audio.output_bt` 共用;亦用于 CLI output) */
enum {
AUDIO_OUT_DAC = 0, /* 本地 DAC(hw:0,0,独占/period=1024×4/FPGA numid=9/音量反转) */
AUDIO_OUT_BT = 1 /* 蓝牙 A2DP source(pcm.bluealsa 播放向,透传+格式归一,AVRCP 控音量) */
};
/* MSG_AUDIO_OUTPUT ↑:切换输出目标(ui/CLI 下发)。0=DAC 1=BT。audiod 侧重配 PCM。 */
typedef struct m_audio_output {
uint32_t target; /* AUDIO_OUT_DAC / AUDIO_OUT_BT */
} m_audio_output_t; /* 4 B */
/* ================= meow_netmgrd 网络管理服务(devdocs/plan/NETMGR_DESIGN.md §3/§10)=====
* 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。
* 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr:
* svcd 托管 trigger=boot + 单一二进制双形态(/usr/bin/meow_netmgrd 守护 + /usr/bin/meow_net CLI)。
* 鉴权分级同 svc_power(§10/期1):读级(STATUS/IFACE、WIFI_CTL 内 status/networks、BT_CTL 内
* status/paired)任意调用者;写级(WIFI_CTL 内 scan/connect/disconnect/on/off/rfkill_set/udhcpc、
* BT_CTL 内 on/off)仅系统身份(SO_PEERCRED → /usr/bin/meow_* 或 os.meow.* 包)。
* HibyOS 模式(/tmp/meow_boot_mode=hiby):读级可查,写级拒 + 记日志。
* 期 1 实装 0x00E0-0x00E7;0x00E8-0x00EF 保留(期2:连接普查/DNS 代理;期3:管控策略/事件推送)。 */
enum { /* 与主 enum 分列,追加式不改既有行 */
MSG_NET_STATUS_REQ = 0x00E0, /* ↑ 概览查询(空负载=全部) */
MSG_NET_STATUS_RESULT = 0x00E1, /* ↓ 概览 JSON */
MSG_NET_IFACE_REQ = 0x00E2, /* ↑ 接口列表/详情(name 空=全部) */
MSG_NET_IFACE_RESULT = 0x00E3, /* ↓ 接口 JSON */
MSG_NET_WIFI_CTL = 0x00E4, /* ↑ 子命令字串(scan/connect/... + rfkill/udhcpc) */
MSG_NET_WIFI_RESULT = 0x00E5, /* ↓ 结果/文本 */
MSG_NET_BT_CTL = 0x00E6, /* ↑ 子命令字串(status/on/off/paired) */
MSG_NET_BT_RESULT = 0x00E7 /* ↓ 结果/文本 */
/* 0x00E8-0x00EF 保留:0xE8 连接表 / 0xEA 策略读写 / 0xEC DNS 控制 / 0xEE 事件推送(期 2/3) */
};
/* MSG_NET_STATUS_REQ ↑(空负载=全部;预留 subset 位,期 1 恒 0)→ MSG_NET_STATUS_RESULT ↓:
* 概览 JSON(mode/ifaces[]/wifi/bt/rfkill[],见 NETMGR_DESIGN §3 统一模型)。*/
typedef struct m_net_status_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
uint16_t json_len; /* json 有效字节(不含 NUL) */
uint16_t pad;
char json[MEOW_IPC_MAX - 8]; /* 概览 JSON(.data 形状) */
} m_net_status_result_t;
#define NET_IFACE_NAME_MAX 32 /* 接口名上限(lo/wlan0/usb-eth) */
/* MSG_NET_IFACE_REQ ↑:name 空=全部接口;否则单接口详情。→ MSG_NET_IFACE_RESULT ↓。 */
typedef struct m_net_iface_req {
char name[NET_IFACE_NAME_MAX];
} m_net_iface_req_t; /* 32 B */
typedef struct m_net_iface_result {
uint32_t code;
uint16_t json_len;
uint16_t pad;
char json[MEOW_IPC_MAX - 8];
} m_net_iface_result_t;
/* MSG_NET_WIFI_CTL / MSG_NET_BT_CTL ↑:子命令字串 + 变长 arg(connect 的 ssid/psk)。→ *_RESULT ↓。
* cmd(WIFI_CTL): status|scan|networks|connect|disconnect|on|off + rfkill/udhcpc 命名空间
* (rfkill arg="list"|"<type> <on|off>";udhcpc arg="renew <if>"|"release <if>")。
* cmd(BT_CTL) : status|on|off|paired。 */
typedef struct m_net_cmd {
char cmd[24]; /* 子命令字串(大写不敏感;接收侧截断) */
char arg[132]; /* connect: "<ssid>[,<psk>]";rfkill: "<type> <on|off>";udhcpc: "renew|release <if>" */
} m_net_cmd_t; /* 156 B */
typedef struct m_net_cmd_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / MEOW_ERR_NODEV */
uint16_t text_len; /* text 有效字节(不含 NUL) */
uint16_t pad;
char text[MEOW_IPC_MAX - 8];
} m_net_cmd_result_t;
/* ---- 期 2:观测层(连接普查)+ DNS 代理(NETMGR_DESIGN §4/§5)----
* 0x00E8/0x00EC 实现取代上方「0x00E8-0x00EF 保留」注释中的连接表/DNS 控制两项;
* 0x00EA(策略读写,期3)/0x00EE(事件推送,期2 订阅制预留)仍保留未实装。 */
enum { /* 与主 enum 分列,追加式不改既有行 */
MSG_NET_CONNS_REQ = 0x00E8, /* ↑ 连接表查询(entity 空=全部) */
MSG_NET_CONNS_RESULT = 0x00E9, /* ↓ 连接表文本(定长行;超帧截断标记) */
MSG_NET_DNS_CTL = 0x00EC, /* ↑ dns status|stats|flush */
MSG_NET_DNS_RESULT = 0x00ED /* ↓ 结果/统计文本 */
/* 0x00EA 策略读写(期3)/ 0x00EE 事件推送(预留,期2 订阅制)/ 0xEB、0xEF 余量 */
};
/* MSG_NET_CONNS_REQ ↑:entity 前缀过滤(空=全部)。entity 形如 `app:<pkg>`/`svc:<svc>`/`proc:<comm>`。 */
typedef struct m_net_conns_req {
char entity[48];
} m_net_conns_req_t; /* 48 B */
/* MSG_NET_CONNS_RESULT ↓:连接表文本(每行 `entity|lip:lport|rip:rport|state|age_s|domain`)。
* 超单帧上限时截断并置 truncated=1(app/top 由客户端按行聚合,容纳全部)。 */
typedef struct m_net_conns_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
uint16_t text_len;
uint16_t truncated; /* 1=被截断 */
char text[MEOW_IPC_MAX - 8];
} m_net_conns_result_t;
/* MSG_NET_DNS_CTL ↑:cmd=status|stats|flush。status/stats 读级任意;flush(清缓存)为动作级系统身份。 */
typedef struct m_net_dns_ctl {
char cmd[16];
} m_net_dns_ctl_t; /* 16 B */
typedef struct m_net_dns_result {
uint32_t code;
uint16_t text_len;
uint16_t pad;
char text[MEOW_IPC_MAX - 8];
} m_net_dns_result_t;
/* ---- 期 3:管控层(NETMGR_DESIGN §6;默认 allow,按实体手动 block + 域名白/黑)----
* 0x00EA MSG_NET_POLICY(同时承载 GET/SET,op 区分)。GET=读级任意;SET=写级仅系统身份
* (SO_PEERCRED → /usr/bin/meow_* / os.meow.* / MeowPanel)。防自锁实体(dropbear/wpa_supplicant/
* udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init)拒绝置 block(服务端硬校验)。
* 策略持久化:`/data/meow/netmgrd/net_policy.conf`(/data=/usr/data 符号链接),读改写单键原子写。 */
enum { /* 与主 enum 分列,追加式不改既有行 */
MSG_NET_POLICY = 0x00EA, /* ↑ op=0 查询 / op=1 写(set net / set domain / del) */
MSG_NET_POLICY_RESULT = 0x00EB /* ↓ 策略文本/结果 */
/* 0x00EE 事件推送(预留,期2 订阅制)/ 0xEF 余量(均为预留) */
};
/* MSG_NET_POLICY 操作码 */
enum {
NET_POLICY_SHOW = 0, /* show [entity]:entity 空=全部 */
NET_POLICY_SET_NET = 1, /* set <entity> allow|block */
NET_POLICY_SET_DOMAIN = 2, /* domain <entity> +host|-host */
NET_POLICY_DEL = 3 /* del <entity> */
};
typedef struct m_net_policy {
uint32_t op; /* NET_POLICY_* */
char entity[48]; /* app:<pkg> | svc:<svc> | proc:<comm> | global;空=适用 SHOW 的全量 */
char arg[64]; /* SET_NET:allow|block;SET_DOMAIN:+host|-host;其余空 */
} m_net_policy_t; /* 120 B */
typedef struct m_net_policy_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / (防自锁)MEOW_ERR_PERM */
uint16_t text_len;
uint16_t pad;
char text[MEOW_IPC_MAX - 8];
} m_net_policy_result_t;
/* ---- 期 4:SDK 网络自愿通道(NETMGR_DESIGN §6.3/§4;0xEE/0xEF)----
* 一、meow_net_policy_check(host):框架 App 联网前自查 host 是否被 netmgrd 策略放行——
* 服务端以调用方 app_id(msg.app_id)→ 实体 app:<pkg>,评估 policy_net_blocked(entity)
* || dns_query_domain_denied(entity, host),放行=allow 1 / 拒绝=block 0;netmgrd 不在 → SDK 侧放行。
* 二、meow_net_report(rx,tx):App 自愿上报本进程收发字节数(内核无 per-socket/per-uid 计数,
* §4 诚实声明的「字节级记账走 SDK 自报」);netmgrd 记入 per-实体统计(status JSON entity_bytes)。
* 读级/上报任意调用者(自愿自报,无副作用;block 判定由服务端策略引擎权威执行)。
* (0x00EE 曾留作「事件推送 订阅制」,期 4 起改作 SDK 通道;事件推送期 2 从未实装。) */
enum { /* 与主 enum 分列,追加式不改既有行 */
MSG_NET_SDK = 0x00EE, /* ↑ op=0 策略检查 / op=1 字节上报 */
MSG_NET_SDK_RESULT = 0x00EF /* ↓ code + (CHECK)allow + 状态文本 */
};
enum { NET_SDK_CHECK = 0, NET_SDK_REPORT = 1 };
typedef struct m_net_sdk {
uint32_t op; /* NET_SDK_CHECK / NET_SDK_REPORT */
char app_id[MEOW_APP_ID_MAX]; /* 调用方 application(SDK 用 env MEOW_LOG_APP;CLI/测试可任意) */
char host[64]; /* CHECK:待查域名 */
uint32_t rx; /* REPORT:本进程累计 RX 字节 */
uint32_t tx; /* REPORT:本进程累计 TX 字节 */
} m_net_sdk_t; /* 4+32+64+4+4 = 108 B */
typedef struct m_net_sdk_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
int32_t allow; /* CHECK:1=allow 0=block(block 由服务端策略引擎判定) */
uint16_t text_len;
uint16_t pad;
char text[MEOW_IPC_MAX - 12];
} m_net_sdk_result_t;
#endif /* MEOW_IPC_H */
+177 -17
View File
@@ -28,6 +28,7 @@ extern "C" {
#include <sys/stat.h>
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
#include <pthread.h> /* M0:IPC 线程 */
#include "key_router.h" /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1) */
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
#include <sys/socket.h> /* M0:AF_UNIX IPC */
#include <sys/un.h> /* M0:sockaddr_un */
@@ -37,9 +38,21 @@ extern "C" {
#include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */
#include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */
#include "meow_media.h" /* 阶段D附录B:媒体会话注册表(MSG_META/MSG_PLAYCTL/最近活跃源) */
#include "meow_bgplay.h" /* M4:后台播放保活 + 无头后台服务运行时表(§9) */
#include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */
#include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
#include "lock_screen.h" /* M2.x:锁屏 4 位 PIN 纯逻辑(设置/校验/错5锁定持久化/panel.pin 迁移/唤醒判定) */
/* M7.0b 文案抽取(SDK meow_str i18n 雏形):`meow_str(id)` 在 id 未进内嵌表(且无 TF 覆盖表)
* 时原样返回含'.'的 id —— 系统 UI 硬编码中文案经本辅助抽取,未知 id 回退 `zh` 兜底,保证默认
* 中文不被裸 id 顶替;其 en/自定义文案可由 TF `strings/<locale>.txt` 覆盖表或后续并入 SDK 内嵌表补齐。
* 注:static inline 不跨 TU 产生未用告警(GCC 忽略 `unused static inline`)。 */
static inline const char *meow_str_zh(const char *id, const char *zh)
{
const char *s = meow_str(id);
return (s && s[0] && !strchr(s, '.')) ? s : zh;
}
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */
#define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL, PLAY, NEXT */
@@ -140,6 +153,9 @@ extern "C" {
#define MEOW_VOL_MAX_DEFAULT 100 /* 默认最大音量上限(尊重上限;可配 audio.volume_max) */
#define VOL_DRAG_DAC_MS 60 /* 滑条拖动期间两次真实 DAC 写入的最短墙钟间隔 */
#define VOL_RAMP_MS 15 /* 每 1% 步进的最短墙钟间隔 */
#define VOL_RAMP_MAX_MS 3000 /* ramp 总时长上限(防 ADC 噪声假按住一次打满;正常长按到上限 <3s) */
#define JACK_POLL_MS 1000 /* 插孔状态节流读取间隔(绝不在 10ms 热路径每圈读文件) */
#define JACK_DEBOUNCE_MS 800 /* 插孔变化后忽略音量键边沿的窗口(插拔时没人按音量,误伤可忽略) */
#define BT_ROLE_OFF 0 /* 关 */
#define BT_ROLE_ON_NO_CONN 1 /* 开未连接 */
#define BT_ROLE_MASTER 2 /* 主模式:连了外围(如耳机) */
@@ -163,7 +179,7 @@ extern "C" {
#define NCP_DRAG_ENTER_SLOP 16
#define NCP_DRAG_THR_PCT 40
#define TAP_ARM_DELAY_MS 300
#define PM_NUM 5
#define PM_NUM 6
#define PM_R 50 /* 圆钮半径(直径 100) */
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
#define PM_GAPY 140 /* 相邻行圆心 y 间距 */
@@ -185,6 +201,7 @@ extern "C" {
#define NET_LIST_ABOUT 6 /* 系统设置「关于本机」:只读设备信息弹窗(复用 s_dd_modal) */
#define NET_LIST_WIFI_PAGE 7 /* M2.76:WiFi 独立子页面入口(tap → run_wifi_page) */
#define NET_LIST_BT_PAGE 8 /* M2.76:蓝牙独立子页面入口(tap → run_bt_page) */
#define NET_LIST_PIN_PAGE 9 /* M6.x:锁屏 PIN 独立子页面入口(tap → run_lock_settings_page) */
#define S_NET_MAX 8 /* 网络下拉一次最多展示项(屏幕高度限制) */
#define BG_PRESETS_N 3
#define HK_PRESETS_N 2
@@ -291,10 +308,19 @@ struct meow_svc {
};
/* 二进制依赖行:depends=<name>[:<mod>][,...]。<name>=二进制名;<mod>=required|req(默认)/optional|opt。
* launcher 启动前检查(required 缺失阻断,optional 缺失放行),nyai 安装时提示。 */
#define MEOW_MAX_DEPS 8
struct meow_dep {
char name[32]; /* 二进制名(/usr/bin/<name> 或 <卡>/MeowOS/opt/bin/<name>) */
char req; /* '1'=required(缺失阻断启动) '0'=optional(缺失仅 WARN 放行) */
};
struct 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[49]; /* display name(UTF-8,截断走 json_str 整序列保护) */
char exec[129]; /* absolute path */
int ok; /* exec exists and X_OK */
/* ---- 阶段 D · M1:app.json 路由/框架/服务/图标(scan_apps 解析)---- */
@@ -302,11 +328,16 @@ struct app {
char immersive; /* '1'=immersive=true(隐藏虚拟返回键,返回手势归 SDK) */
char fullscreen; /* '1'=app.json fullscreen=true(注入 MEOW_FULLSCREEN=1,SDK 预先设全屏) */
char fonts; /* 字库档:0=默认4K常用 1=大字库GBK 2=Full(app.json "fonts";注入 MEOW_FONT_TIER) */
char background; /* M4:'1'=app.json background=true(可注册无头后台服务) */
char background_audio; /* M4:'1'=background_audio=true(播放中退出转后台保活,§9) */
char background_keepalive; /* 批①:'1'=background_keepalive=true(进入后台即保活,不依赖媒体会话=通用常驻) */
char icon[129]; /* icon= 路径 */
int nroute; /* route.<id> 行数 */
struct meow_route routes[MEOW_MAX_ROUTES];
int nsvc;
struct meow_svc svcs[MEOW_MAX_SVCS];
int ndep; /* app.json depends 二进制依赖项数(meow_dep.c) */
struct meow_dep deps[MEOW_MAX_DEPS];
};
@@ -314,7 +345,7 @@ struct app {
* 与 MSG_ACT_DECLARE/HELLO(运行时)并集填充;单一写者=主循环,无需锁。 */
struct meow_acct {
char app_id[MEOW_APP_ID_MAX];
char name[17];
char name[49];
char exec[129];
int ok;
char framework;
@@ -324,6 +355,9 @@ struct meow_acct {
int nsvc;
struct meow_svc svcs[MEOW_MAX_SVCS];
int declared; /* 是否经 HELLO/ACT_DECLARE 运行时声明过 */
char background; /* M4:'1'=app.json background=true(MSG_REGISTER 权限位0) */
char background_audio; /* M4:'1'=background_audio=true(MSG_REGISTER MF_PLAYBACK 权限位1) */
char background_keepalive; /* 批①:'1'=background_keepalive=true(进入后台即保活,不依赖媒体会话) */
};
@@ -425,8 +459,10 @@ typedef struct {
/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作)。
* M2.76:追加 mac[24](AP BSSID / 蓝牙设备 MAC),供连接/配对/详情展示;无则空串。 */
typedef struct { int id; char name[48]; char mac[24]; } meow_net_t;
* M2.76:追加 mac[24](AP BSSID / 蓝牙设备 MAC),供连接/配对/详情展示;无则空串。
* M6.0:追加 sec[16](WiFi 扫描结果安全类型:Open/WPA-PSK/...;无则空=未知),
* 供「WiFi 新网络录入」判断是否需要弹 IME 输密码。 */
typedef struct { int id; char name[48]; char mac[24]; char sec[16]; } meow_net_t;
/* ================================================================== */
@@ -458,12 +494,26 @@ typedef struct sub_page {
int (*detail)(void *ctx, int idx, int act);/* 点 SUB_ROW 行(如连接/设备详情);返回 1=重绘 */
int live; /* 1=驻留时周期性(约2s)重扫行(蓝牙/附近扫描刷新用) */
void *ctx; /* 页面私有上下文 */
/* ---- 批③ 后台管理页按钮化(可选;NULL/0=按旧路径,未填页/WiFi/蓝牙/锁屏等不受影响)----
* 提供下方任一回调即启用对应渲染/命中定制;全 NULL 时行为与旧版完全一致。 */
const char *top_action; /* 顶栏标题栏右侧动作按钮文案(NULL=无按钮) */
int (*top_action_cb)(void *ctx); /* 点顶栏动作按钮;返回 1=重绘 */
/* 行完全自绘(覆盖默认 renderer;页内由 r->kind 区分行型)。state=MEOW_LISTVIEW_ST_*(选中/按压/禁用)。 */
void (*row_draw)(void *ctx, int idx, sub_row_t *r,
int x, int y, int w, int h, int state);
/* 点 SUB_ROW 行(自定义命中:页内自行区分「行内按钮区 vs 行体」;x,y=tap,rx..rh=行视口矩形);
* 返回 1=重绘。仅 kind==SUB_ROW 且本回调非空时走此路径,否则走 detail。 */
int (*row_tap)(void *ctx, int idx, sub_row_t *r, int x, int y,
int rx, int ry, int rw, int rh);
} sub_page_t;
/* 运行子页(阻塞到返回;返回 0)。由 wifi_page.c / bt_page.c 各自实现。 */
int meow_subpage_run(sub_page_t *p);
int run_wifi_page(void);
int run_bt_page(void);
/* 批③:后台管理页(bg_page.c;桌面顶栏居右按钮进入;阻塞到返回)。 */
int run_bg_page(struct app *apps, int napps);
/* chip 索引(⑥:第 5 个 = 电源菜单,点击唤起既有系统电源菜单 show_power_menu) */
@@ -626,6 +676,15 @@ int kp_open(struct keypoll *kp, const char *dev);
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
void *ctx);
int launch_app(struct app *a);
/* M6.40:最近一次启动失败的具体原因(fork OOM / exec 失败 / 缺依赖 / 路径不存在等;
* launch 成功或未失败则 "")。launch_app/launch_activity 返回 -1 前由 app_launcher.c 填好,
* main_menu.c 的 launch_fail_toast 据以显示「CANNOT START: %s(%s)」。 */
const char *launch_fail_reason(void);
/* ---- app.json 二进制依赖(depends)解析与启动前检查(meow_dep.c)---- */
int meow_parse_depends(const char *val, struct meow_dep *deps, int max);
int meow_check_app_depends(const struct app *a, char *missing, int mlen);
void meow_dep_clear_reason(void);
const char *meow_dep_block_reason(void);
/* M3:以指定 activity 链路冷启动某 app(注入 MEOW_ACTIVITY/MEOW_CHAIN + 设 nav_stack 栈深)。
* chain=NULL 或空 = 裸启动(进 App 主屏/首页,不注入 activity env)。 */
int launch_activity(struct app *a, const char *chain);
@@ -646,20 +705,40 @@ void meow_nav_exit(void); /* 清栈(应用退出/EXIT) */
void meow_nav_request_exit(void); /* MSG_NAV{EXIT}:置位让前台 waitpid 循环终止 app */
int meow_nav_exit_pending(void); /* 读取并清除退出请求(前台 launch_* 循环轮询) */
/* ---- amsd 控制挂点(AMSD_DESIGN §4.3 / §6.2 二期):主循环单写者,跨模块声明 ----
* pending-start/kill 仅在主循环上下文写入与消费(ipc_dispatch 写、run_menu 前台循环/菜单循环读)。 */
int meow_ams_pending_start_set(const char *pkg, const char *act); /* 有前台 App 时拒绝(返回 -MEOW_ERR_BUSY) */
int meow_ams_take_start(char *pkg, int pn, char *act, int an); /* 1=取到待拉起 App(清位);0=无 */
* pending-start 状态/拉起决策在 meow_ams_start.c(自包含、可单测,见 meow_ams_start.h);
* pending-kill 在 app_launcher.c。仅在主循环上下文写入与消费(ipc_dispatch 写、
* run_menu 前台循环/菜单循环读)。 */
void meow_ams_pending_kill_set(const char *pkg); /* 请求终止某 pkg */
int meow_ams_kill_pending(const char *pkg); /* 匹配则清除并返回 1(前台循环轮询) */
/* §6.2 二期:带 picker 上下文的 pending-start(amsd `am picker` 经 MSG_AMS_START 扩载荷)。
* pick_action 非空 → launch_app_chain 注入 MEOW_PICKER_ACTION/REQ_ID/CALLER 到子进程。 */
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
const char *pick_action, uint32_t pick_req,
const char *pick_caller);
/* run_menu 消费 pending-start:复用 launch_app/launch_activity 链真正拉起目标 App。
* 返回 0=无 pending/busy;1=已消费(拉起成功/拒绝);2=列表未就绪(pending 保留待重扫)。
* app_ran=1=app 已运行(据以设 touch 冷却);idx_out>=0 且 app_ran=0 → 启动失败,打 launch_fail_toast。 */
int meow_ams_start_consume(const char *const *ids, int napps, int state,
char *toast, int toast_len, int *app_ran, int *idx_out);
/* launch_app_chain 入口拾取 picker 上下文(一次性清位)。 */
int meow_ams_take_picker(char *action, int action_len,
char *caller, int caller_len, uint32_t *req);
/* 组装 amsd-start ops(生产=launch_app/launch_activity+resolve_chain+logmsg;meow_ipc_init 末尾调)。 */
void meow_ams_launcher_init(void);
/* 供 run_menu/idle 挂当前 apps[] 上下文(ops.launch 的 idx→app 映射用)。 */
void meow_ams_ctx_set(struct app *apps, int napps);
/* §6.2 二期:amsd 上报通道(best-effort,app_launcher.c 的 ams_send 公开包装)。
* util.c 转发运行时 declare 与 picker call/result 给 amsd 用。 */
void meow_ams_send(const char *payload, int len);
/* ---- M4 后台保活/后台服务胶水(app_launcher.c 实现;util.c/main_menu.c 消费)---- */
/* launcher 侧 meow_bgplay ops 组装(meow_ipc_init 末尾调一次)。 */
void meow_bgplay_launcher_init(void);
/* 后台表周期维护:drain 各 bg App 的 stdout 管道 + meow_bg_tick + meow_svc_tick。
* 挂载点=meow_ipc_process 末尾(主循环单写者排空点,四处循环都经它)。 */
void meow_bg_housekeep(void);
/* 查静态声明的 service.<svc>;无返回 NULL。 */
const struct meow_svc *meow_acct_svc_find(const struct meow_acct *ac, const char *svc);
/* 后台权限位:bit0=background(可注册服务)bit1=background_audio(后台音频)。 */
int meow_acct_background_perm(const struct meow_acct *ac);
/* M4:服务 exec 路径约定 = App 二进制同目录下的 <svc> 名文件。 */
void meow_svc_exec_path(const char *app_exec, const char *svc, char *out, int olen);
void meow_dac_vol_write(int pct);
void meow_dac_vol_write_throttled(void);
void meow_dirty_add(int x, int y, int w, int h);
@@ -673,10 +752,24 @@ int meow_font_effective_tier(int app_tier);
int meow_font_ensure(void);
void meow_font_reload_multifont(void);
int meow_font_resolve_path(const char *ref, char *out, int olen);
void meow_font_scan_presets(void);
void meow_font_scan_presets_root(const char *card_root);
void meow_font_scan_presets(int verify_sha);
void meow_font_scan_presets_root(const char *card_root, int verify_sha);
int meow_ipc_init(void);
void meow_ipc_process(void);
/* M4:IPC 连接存活查询(熔断广播投递前判连接未断;util.c ipc_conns 表查 fd)。 */
int meow_ipc_conn_alive(int fd);
/* M4:ipc_send 公开包装(meow_bgplay send_frame 回调用;同 meow_media 的注入口径)。 */
size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len);
/* ---- E:系统级音量翻页(READER_UI_DESIGN §E,用户 2026-09-04 拍板)----
* 前台 App 声明 app.json `volume_page` 时,launcher 把物理 VOL_UP/DOWN 转成系统翻页信号
* (MSG_KEY_PAGEUP/PAGEDOWN,meow_ipc.h)经 IPC 下发给该 App;否则照旧调音量。 */
int meow_volpage_app_allows(const char *app_id); /* keys.c:读 app.json 声明 */
void meow_volpage_sink(const char *app_id, int fd); /* keys.c:武装前台翻页 sink */
void meow_volpage_sink_clear(void); /* keys.c:撤武装 */
int meow_volpage_armed(void); /* keys.c:查询 */
int meow_volpage_key(int code, int is_down); /* keys.c:门(1=已消费/已下发) */
int meow_ipc_app_fd(const char *app_id); /* util.c:前台 App→持久 IPC fd(-1=无) */
/* §10.5:读并清除待处理的 MSG_NOTIF_OPEN(App 前台顶部下滑请求展开通知中心叠加会话)。
* 调用方=launch_app waitpid 循环;返回 1 并填 *out(待处理),0=无。 */
int meow_notif_open_take(m_notif_open_t *out);
@@ -717,7 +810,7 @@ uint16_t rgb565(uint32_t c);
void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int live_open, int lox, int loy);
/* 阶段D附录B:Now Playing 大页(主界面横划进入;左缘右滑返回;阻塞到返回)。 */
void run_now_playing(void);
void run_now_playing(struct app *apps, int napps);
void run_settings_page(void);
void s_about_open(void);
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur);
@@ -730,8 +823,63 @@ int s_net_get(const srow_t *r, char *buf, int len);
void s_net_open(const srow_t *r);
void s_net_refresh(void);
void s_net_sync_from_sys(void);
/* M3:设置页异步网络刷新 —— kick 重置步骤机并触发扫描(非阻塞);pump 每步一个
* 有界 popen,返回 1=数据可能更新(调用方重绘)。run_settings_page 主循环逐步骤驱动。 */
void s_net_refresh_kick(void);
int s_net_refresh_pump(void);
void s_net_toggle_bt(int on);
void s_net_toggle_wifi(int on);
/* ================= meow_netmgrd IPC 客户端(netmgr_ipc.c)=================
* M6.0 设置页迁移(NETMGR_DESIGN §12):wifi_page/bt_page 的查询/操作统一走 netmgrd
* IPC(socket /usr/data/meow_net.sock,env MEOW_NET_SOCK 覆盖)。netmgrd 是 WiFi/BT
* 操作权威;其不可达(NETMGR_UNREACH)时回退 net.c 直连 wpa_cli/hciconfig(双轨保功能)。
* 迁移注释:此封装是「设置页客户端→权威 IPC」的唯一入口,netmgrd 不可达才走直连回退。 */
#define NETMGR_UNREACH (-1000) /* 传输/握手不可达(区别于 netmgrd MEOW_ERR_* 业务码) */
int netmgr_ok(void); /* 1=netmgrd 可达(结果约 1s 缓存,节流) */
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen); /* 返回 code / NETMGR_UNREACH */
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen);
/* WiFi(IPC 优先;网卡链路态 wlan_connected()/hci_powered() 仍读内核,属只读观测) */
int nm_wifi_connected(void);
void nm_wifi_ssid(char *buf, int len);
int nm_wifi_info(char (*info)[48], int max);
int nm_wifi_saved(meow_net_t *out, int max);
int nm_wifi_scan(meow_net_t *out, int max);
void nm_wifi_scan_trigger(void);
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */
/* WiFi 设置页 bug2(2026-09-15):hidden=1 走 netmgrd connecth(scan_ssid=1 + 更长 COMPLETED 等待),
* 回退直连同步对齐;err 回填 netmgrd/直连的诚实结果文本(成功含 ip / 失败含原因)。 */
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen);
int nm_wifi_disconnect(char *err, int elen);
int nm_wifi_toggle(int on);
/* BT(IPC 优先:全量子命令权威收口 P1;netmgrd 不可达回退 net.c 直连)。
* P1 收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八):connect/disconnect/pair/unpair/scan 写级
* 迁 netmgrd;connected/devices/a2dp 读级也迁 netmgrd(launcher 只做客户端)。 */
int nm_bt_powered(void);
int nm_bt_onoff(int on);
int nm_bt_status(int *powered, int *paired_n, char *addr, int alen);
int nm_bt_paired(meow_net_t *out, int max);
int nm_bt_connected(meow_net_t *out, int max);
int nm_bt_devices(meow_net_t *out, int max);
/* A2DP 单端点详情(codec/采样率/声道/位深/acquired/role;netmgrd bt a2dp 解析) */
typedef struct {
char mac[24]; /* 端点设备 MAC(与 connected/paired 列表可 join) */
char codec[16]; /* SBC/AAC/aptX/LDAC...;空=未知 */
char rate[16]; /* 采样率(如 "44100");空=未知 */
char chn[8]; /* 声道数 */
char bits[8]; /* 位深 */
int acquired; /* 1=传输已获取(streaming 中) */
char role[8]; /* "src"/"snk"/"-"(设备侧 A2DP 角色) */
} bt_ep_t;
int nm_bt_a2dp(bt_ep_t *eps, int max); /* 返回端点数 */
int nm_bt_connect(const char *mac);
int nm_bt_disconnect(const char *mac);
int nm_bt_pair(const char *mac);
int nm_bt_unpair(const char *mac);
void nm_bt_scan_on(void);
void nm_bt_scan_off(void);
const meow_ui_theme_t *s_ui_theme(void);
void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h);
int scan_apps(struct app apps[MAX_APPS]);
@@ -743,6 +891,18 @@ void settings_sync_cache_listener(const char *key);
void settings_volume_adj(int delta);
int settings_volume_get(void);
int show_power_menu(void);
/* M2.x 锁屏:阻塞式锁屏页(渲染+输入);返回 1=已解锁(回主屏),0=锁屏期间电源键息屏(屏已关),
* -1(LCK_OVERLAY_RET_DIED)=锁屏叠加期 App 崩溃(run_app_lock_overlay 走统一退出路径)。
* meow_lock_screen_engage() 供电源菜单「锁定」与唤醒路径用;未配凭据时 no-op。 */
int run_lock_screen(void);
int meow_lock_screen_engage(void);
/* §310 残影:锁屏处于 App 前台叠加会话时置位当前 App pid(run_app_lock_overlay 写入,
* run_lock_screen 每圈据此 proc_is_zombie 探测;菜单直接锁屏时保持 0=不探)。 */
extern int g_lock_overlay_pid;
/* 设置页「锁屏 PIN」数字输入模态(系统级 PIN 设置/修改输入):满 4 位返回 1 并拷贝到 out,电源=取消返回 0。 */
int lck_input_pin_modal(const char *title, char *out, int outcap);
/* 设置页「锁屏 PIN」独立子页面(subpage.c 脚手架;阻塞到返回)。 */
int run_lock_settings_page(void);
int stop_player(void);
/* 电源管理服务 IPC(SYSTEM_FRAMEWORK_SERVICES §3.2:launcher 电源动作经 meow_svc_power 执行) */
int pm_power_ipc(uint32_t cmd, uint32_t flag, const char *arg, char *res, int reslen);
@@ -759,7 +919,7 @@ void vol_restore(struct vol_snap s);
struct vol_snap vol_snapshot(void);
int wlan_connected(void);
int wlan_radio_up(void);
void write_brightness(int val);
void write_brightness(int val, const char *src); /* src=调用源线索,随值一并 logmsg(亮度幽灵排查) */
#ifdef __cplusplus
}
Binary file not shown.
+76 -2
View File
@@ -120,6 +120,19 @@ int meow_media_has_source(void)
return (s && s->state != MEOW_MEDIA_STOPPED) ? 1 : 0;
}
int meow_media_app_playing(const char *app_id)
{
const meow_media_session_t *s = meow_media_current();
if (!s || !app_id || !app_id[0]) return 0;
return (s->state == MEOW_MEDIA_PLAYING && strcmp(s->app_id, app_id) == 0) ? 1 : 0;
}
int meow_media_app_resident(const char *app_id)
{
const meow_media_session_t *s = sess_find(app_id);
return (s && s->state != MEOW_MEDIA_STOPPED) ? 1 : 0;
}
static int send_playctl(const meow_media_session_t *s, uint32_t cmd, int64_t seek_ms)
{
if (!s || !s->used || !g_ops || !g_ops->send) return -1;
@@ -190,13 +203,74 @@ uint32_t meow_media_display_pos(const meow_media_session_t *s)
return p;
}
/* 把一帧歌词分片写入会话缓冲(kind=0 lrc / kind=1 tlrc)。含分片边界截断(不越界)。
* 返回 0=写入;-1=参数非法。 */
static int lyric_put(meow_media_session_t *s, const m_lyric_t *l)
{
if (!s || !l) return -1;
char *buf;
uint32_t cap;
uint32_t *lenp;
if (l->kind == 0) { buf = s->lyric_lrc; cap = MEOW_MEDIA_LRC_MAX; lenp = &s->lyric_lrc_len; }
else if (l->kind == 1) { buf = s->lyric_tlrc; cap = MEOW_MEDIA_TLRC_MAX; lenp = &s->lyric_tlrc_len; }
else return -1;
uint32_t off = l->off, n = l->len;
if (off >= cap) { off = cap; n = 0; } /* 起始越界:整片丢弃 */
if (n > cap - off) n = cap - off; /* 末段越界:截断到缓冲上限 */
if (n) memcpy(buf + off, l->data, n);
buf[off + n] = 0; /* 恒 NUL 结尾(供字符串读取) */
uint32_t end = off + n;
if (end > *lenp) *lenp = end;
return 0;
}
int meow_media_lyric(const m_lyric_t *l)
{
if (!l || !l->app_id[0]) return -1;
meow_media_session_t *s = sess_find(l->app_id);
if (!s) {
/* 会话须先经 META 注册(否则 fd=0,PLAYCTL 会误发到 0 号描述符);丢弃。 */
media_log("media: lyric drop app=%s (no session; report META first)", l->app_id);
return -1;
}
/* 新代际(换歌/换词):清旧词缓冲,重开分片写入。 */
if (l->seq != s->lyric_seq) {
s->lyric_seq = l->seq;
s->lyric_lrc_len = 0;
s->lyric_tlrc_len = 0;
s->lyric_lrc[0] = 0;
s->lyric_tlrc[0] = 0;
}
lyric_put(s, l);
media_log("media: LYRIC app=%s seq=%u kind=%u total=%u off=%u len=%u lrc=%u tlrc=%u",
s->app_id, l->seq, l->kind, l->total, l->off, l->len,
s->lyric_lrc_len, s->lyric_tlrc_len);
return 0;
}
int meow_media_lyric_valid(const meow_media_session_t *s)
{
return (s && s->used && s->lyric_lrc_len > 0) ? 1 : 0;
}
const char *meow_media_lyric_lrc(const meow_media_session_t *s)
{
return (s && s->used) ? s->lyric_lrc : "";
}
const char *meow_media_lyric_tlrc(const meow_media_session_t *s)
{
return (s && s->used) ? s->lyric_tlrc : "";
}
void meow_media_dump(const char *prefix)
{
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (!g_sess[i].used) continue;
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\"",
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\" lyric=%u",
prefix ? prefix : "media", i, g_sess[i].app_id, g_sess[i].state,
g_sess[i].fd, (unsigned long long)g_sess[i].last_active_ms,
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title);
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title,
(unsigned)(g_sess[i].lyric_lrc_len > 0 ? 1 : 0));
}
}
+29 -1
View File
@@ -48,7 +48,15 @@ typedef struct meow_media_session {
char artist[128];
char album[128];
char artwork[192];
char lyric[192];
char lyric[192]; /* 歌词源路径/槽位(沿用 META.lyric) */
/* 批3:当前曲 live 歌词(App 经 MSG_META_LYRIC 上报;有界缓冲,新 seq 重置)。
* lyric_lrc/lyric_tlrc 为当前曲歌词原文/翻译文本(桌面歌词页 meow_lyric_parse 用);
* lyric_seq=歌曲代际(App 新歌/新词自增,launcher 见新 seq 清旧词)。 */
uint32_t lyric_seq;
uint32_t lyric_lrc_len;
uint32_t lyric_tlrc_len;
char lyric_lrc[MEOW_MEDIA_LRC_MAX];
char lyric_tlrc[MEOW_MEDIA_TLRC_MAX];
} meow_media_session_t;
/* 依赖注入:send=发一帧(launcher 用 ipc_send 包装;宿主单测用 socket 写);
@@ -72,6 +80,15 @@ const meow_media_session_t *meow_media_current(void);
/* 是否有活动媒体源:当前源存在且 state != 0(停止)。通知中心卡片/播放页据此显示。 */
int meow_media_has_source(void);
/* §10.5 叠加会话保活判定:当前源(最近活跃会话)存在、state==PLAYING 且 app_id 匹配。
* launcher 据此决定通知中心叠加不 SIGSTOP 该 App(保 PCM 供流),SDK present 层自抑上屏。 */
int meow_media_app_playing(const char *app_id);
/* 批①后台常驻判定:该 app 的媒体会话「活跃可驻留」= 会话存在且 state != STOPPED
* (PLAYING/PAUSED/BUFFER 皆常驻)。按 app 自身会话判(sess_find),不读最近活跃源——
* 多源时不误判「当前源≠本 App」。launcher 转后台保活依据。 */
int meow_media_app_resident(const char *app_id);
/* 向当前源下发 PLAYCTL(卡片按钮/播放页/媒体键/控制器)。返回 0=已发,-1=无源/发送失败。 */
int meow_media_playctl(uint32_t cmd, int64_t seek_ms);
@@ -87,6 +104,17 @@ void meow_media_tick(void);
/* 显示用虚拟位置:播放中按墙钟推进 pos_ms(clamp 到 dur);暂停/缓冲/停止原样。 */
uint32_t meow_media_display_pos(const meow_media_session_t *s);
/* ---- 歌词(批3 · live lyric 数据源)----
* App 上报当前曲歌词(MSG_META_LYRIC 分片):按 app_id 归并到会话,seq 为歌曲代际、
* 新代际清旧词;分片按 kind(0=原文/1=翻译)/off 写入会话内有序缓冲(超 MEOW_MEDIA_LRC_MAX/
* MEOW_MEDIA_TLRC_MAX 截断)。会话不存在(未先报 META 注册)→ 丢弃返回 -1(防 fd=0 槽)。
* meow_media_lyric_valid/lrc/tlrc:桌面歌词页从当前会话取歌词文本,交由批1 meow_lyric_parse
* 解析 + meow_lyric_line_at(meow_media_display_pos(s)) 驱动批2 meow_lyric_view 实时滚动。 */
int meow_media_lyric(const m_lyric_t *l); /* 0=已写入;-1=会话不存在/参数非法 */
int meow_media_lyric_valid(const meow_media_session_t *s); /* 会话携带有效歌词(lrc 非空) */
const char *meow_media_lyric_lrc(const meow_media_session_t *s); /* 原文;无则 "" */
const char *meow_media_lyric_tlrc(const meow_media_session_t *s); /* 翻译;无则 "" */
/* 调试打印(经 ops->log,前缀+每会话一行)。 */
void meow_media_dump(const char *prefix);
+39 -3
View File
@@ -66,7 +66,7 @@ static int g_no_file_logged = 0; /* M2.86:本次「未读到」连续段是
/* ---- 默认值表:与 SYSTEM_SETTINGS.md §1 逐键一致(新增设置照附录扩展)---- */
static const meow_setting_def g_defs[] = {
/* 显示 */
{ "display.brightness", SET_INT, "101", 0, 255, NULL, NULL },
{ "display.brightness", SET_INT, "50", 0, 100, NULL, NULL },
{ "display.brightness_min", SET_INT, "1", 0, 255, NULL, NULL },
{ "display.screen_off_mode", SET_ENUM, "panel",0, 0, "panel,black", "面板下电,背光全黑" },
{ "display.screen_off_timeout", SET_INT, "30", 0, 86400, NULL, NULL },
@@ -118,7 +118,7 @@ static const meow_setting_def g_defs[] = {
{ "font.load_full", SET_INT, "0", 0, 1, NULL, NULL },
{ "font.size.title", SET_INT, "28", 0, 0, NULL, NULL },
{ "font.size.body", SET_INT, "20", 0, 0, NULL, NULL },
{ "ui.style", SET_ENUM, "md3", 0, 0, "md3,miui", "Material You,MIUI" },
{ "ui.style", SET_ENUM, "md3", 0, 0, "md3,miuix", "Material You,MIUIx" },
/* 通知 */
{ "notify.center_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
{ "notify.app_quota", SET_INT, "16", 0, 0, NULL, NULL },
@@ -140,6 +140,12 @@ static const meow_setting_def g_defs[] = {
{ "audio.volume_max", SET_INT, "100", 0, 100, NULL, NULL },
{ "audio.volume_step", SET_INT, "1", 1, 5, NULL, NULL },
{ "audio.output", SET_STR, "hw:0,0", 0,0, NULL, NULL },
/* P1 蓝牙 P1:输出目标偏好(0=DAC/本地,1=蓝牙 OUT_BT)。audiod 侧 target 切换在 P2
* 实装;P1 仅作为「输出目标」行/摘要的持久化键(蓝牙子页写,DAC 侧不受影响)。 */
{ "audio.output_bt", SET_BOOL, "0", 0, 0, NULL, NULL },
/* P1 蓝牙:codec 偏好(auto/SBC/AAC/aptX/LDAC;P2 接 alsa.conf pcm.bt/bluealsa 生效)。
* 当前实际协商 codec 由 a2dp 状态实时读,本键只是页面「编解码器」选项的持久化。 */
{ "bt.codec", SET_STR, "auto", 0, 0, NULL, NULL },
/* 网络(M2.39 设置页 WiFi/蓝牙开关;wifi 默认开=允许 ensure_wifi 自动连网,
* 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */
{ "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
@@ -147,6 +153,22 @@ static const meow_setting_def g_defs[] = {
/* 系统(M2.54:主机名,bootmenu S89meow_svcd 开机读 system.hostname sethostname;
* 缺省 meowos;RFC1035 首标签,供 meow_mdns 公布 <host>.local) */
{ "system.hostname", SET_STR, "meowos", 0, 0, NULL, NULL },
/* 锁屏 4 位 PIN(系统级安全;格式与面板 panel.pin 全等:
* `<salt_hex>:<sha256_hex(salt||pin)>`,salt 16B /dev/urandom → 32 hex,绝不存明文。
* 空 = 未设锁屏 PIN(锁屏功能关闭)。与面板同源:panel.pin 已设而本键未设时,锁屏
* 接受 panel.pin 并在首次成功后并入本键(lck_pin_success,见 lock_screen.c)。 */
{ "system.pin", SET_STR, "", 0, 0, NULL, NULL },
/* 息屏唤醒后锁屏(int 0/1,默认 1=开)。0=关(唤醒直进主屏;电源菜单「锁定」手动锁仍可用)。 */
{ "system.pin_lock_on_wake", SET_INT, "1", 0, 1, NULL, NULL },
/* 错 PIN 锁定截止戳(int,epoch 秒;0=未锁定)。**持久化到 settings**(防 launcher 重启
* 清零——面板进程内存方案在重启后失效,此处落盘)。截止前锁屏页禁输入并倒计。 */
{ "system.pin_lock_until", SET_INT, "0", 0, 0, NULL, NULL },
/* 连续错 PIN 计数(int 0-4;达 5 触发锁定并清零)。持久化防「关机重启重试」。 */
{ "system.pin_fails", SET_INT, "0", 0, 0, NULL, NULL },
/* 面板访问级 PIN(meowpanel 写入,格式与本表 system.pin 全等)。launcher 注册它仅为:
* ①锁屏迁移期读取(system.pin 未设时接受 panel.pin);②本表 save 重写全文件时**保留**
* 该行(write_full_conf 只写已注册键,不注册会把面板已设的 panel.pin 抹掉)。launcher 不写它。 */
{ "panel.pin", SET_STR, "", 0, 0, NULL, NULL },
};
/* ---- 日志(自包含,不依赖 launcher 的 logmsg;前缀 "settings:")---- */
@@ -255,6 +277,14 @@ int meow_settings_loaded(void)
return g_loaded_from_file;
}
/* 存储文件路径(= 编译期 MEOW_SETTINGS_FILE,默认 /usr/data/meow/settings.conf)。
* 供跨进程变更轮询(launcher App 前台会话 stat mtime,见 app_launcher.c)等场景取权威路径,
* 避免调用方硬编码与编译期覆盖分叉。 */
const char *meow_settings_file_path(void)
{
return MEOW_SETTINGS_FILE;
}
/* ---- 按类型把字符串值 apply 到 cur(读文件/导入时用;含 clamp/校验)---- */
static void apply_value(meow_setting *s, const char *v)
{
@@ -280,7 +310,13 @@ static void apply_value(meow_setting *s, const char *v)
}
case SET_ENUM: {
int idx = enum_index_of_name_str(s->enum_names, v);
if (idx < 0) idx = atoi(v); /* 向后兼容:数值索引 */
if (idx < 0) {
/* §8.1.1 注:旧版 ui.style 值域为 "md3,miui",本版对齐为 "md3,miuix"——存量
* settings.conf / 备份里的 "miui" 应兼容映射为 "miuix"(而非 atoi 落到 md3=0)。 */
if (!strcmp(s->key, "ui.style") && strcasecmp(v, "miui") == 0)
idx = enum_index_of_name_str(s->enum_names, "miuix");
if (idx < 0) idx = atoi(v); /* 向后兼容:数值索引 */
}
if (idx >= 0 && idx < enum_count_str(s->enum_names)) {
const char *name = enum_at_str(s->enum_names, idx);
if (name) strncpy(s->cur, name, sizeof s->cur - 1);
+4
View File
@@ -82,6 +82,10 @@ int meow_settings_brightness_clamped(void);
* 1 = 文件已读到:键缺失走各自默认(属「已知」态),用户设定可信。 */
int meow_settings_loaded(void);
/* 存储文件绝对路径(编译期 MEOW_SETTINGS_FILE,默认 /usr/data/meow/settings.conf)。
* 供调用方做 mtime 轮询等(App 前台会话状态栏刷新,见 app_launcher.c)。 */
const char *meow_settings_file_path(void);
int meow_settings_set_int (const char *key, int v); /* clamp 到取值域 */
int meow_settings_set_str (const char *key, const char *v);/* 长度截断 MEOW_SET_MAX_LEN */
int meow_settings_set_enum(const char *key, int idx); /* 校验枚举合法,越界拒绝 */
+57
View File
@@ -0,0 +1,57 @@
/* meow_utf8.h — UTF-8 整序列有界拷贝/字符步进纯决策(宿主可单测;无 ui_width/mfont 依赖)。
*
* 背景(问题2,M6.3 真机):主界面应用名「流媒体客户端」被截成 5 字 + 半个字。两处根因:
* ① app_launcher.c `strncpy(a->name, name, 16)` 仍把显示名硬截 16 字节(UTF-8 下=5 汉字+ 半个
* 「端」)——M6.3 只扩了 name[49]+json_str 整序列,漏改此处;
* ② main_menu.c `menu_name_fit` 按字节回退、边界判定错(do..while 读的是刚清零的字节,恒停续字节),
* 停在半截多字节字符;再加 "..." 未为省略号留宽 → 溢出 maxw 被裁切。
* 本头把 UTF-8 整序列处理抽成纯函数(meow_utf8_char_len / meow_utf8_copy_bounded / 截断决策),
* 两处复用并可宿主单测(test_utf8_name.c)。
*/
#ifndef MEOW_UTF8_H
#define MEOW_UTF8_H
#include <stddef.h>
#include <stdio.h> /* snprintf(有界拷贝用) */
#ifdef __cplusplus
extern "C" {
#endif
/* 由首字节 c0 求整字符 UTF-8 字节长(1..4)。非法首字节(0x80..0xBF 续字节)返回 1(按异常字节跳过,
* 绝不把孤立续字节与后字并成大字符)。纯按前导位,不看后续字节是否齐全(那由 copy_bounded 处理)。 */
static inline int meow_utf8_char_len(unsigned char c0)
{
if (c0 < 0x80) return 1;
if (c0 < 0xC0) return 1; /* 孤立续字节:兜底 1 */
if (c0 < 0xE0) return 2;
if (c0 < 0xF0) return 3;
return 4;
}
/* 有界拷贝:把 src 拷入 dst(容量 cap,含 NUL),**绝不劈开多字节字符**。snprintf 截断若落在字符
* 中间(或 src 本身尾部是半截字符),回退到整字符边界:删去最末不完整字符。返回拷贝后字节长(不含
* NUL)。源可为 NULL。持久化/展示名均用——保证 UTF-8 整序列,杜绝「5 汉字+半个字」式残字。 */
static inline int meow_utf8_copy_bounded(char *dst, int cap, const char *src)
{
if (!dst || cap <= 0) return 0;
if (cap == 1) { dst[0] = 0; return 0; }
if (!src) src = "";
snprintf(dst, (size_t)cap, "%s", src);
int n = (int)strlen(dst);
if (n <= 0) return 0;
/* 定位最末一个「起始字节」(非续字节):从尾部回退续字节。 */
int last_start = n - 1;
while (last_start > 0 && ((unsigned char)dst[last_start] & 0xC0) == 0x80)
last_start--;
/* 末字符:起始字节=last_start,实际字节数=have;字符应需 need。have<need → 不完整 → 删。 */
int need = meow_utf8_char_len((unsigned char)dst[last_start]);
int have = n - last_start;
if (have < need) { dst[last_start] = 0; n = last_start; }
return n;
}
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UTF8_H */
+67 -17
View File
@@ -312,6 +312,22 @@ int net_wifi_scan(meow_net_t *out, int max)
while (*se && *se != '\n' && *se != '\r') se++;
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
int has = (se > ssid && *ssid);
/* M6.0:flags 首个方括号内容 = 安全类型([WPA-PSK-CCMP] 等);纯 [ESS]/[IBSS] = 开放。
* 供「WiFi 新网络录入」判断是否需要弹 IME 输密码。 */
char sec[16] = "Open";
if (br) {
char *be = strchr(br + 1, ']');
if (be) {
int sl = (int)(be - (br + 1));
if (sl > 15) sl = 15;
if (sl > 0) {
memcpy(sec, br + 1, (size_t)sl); sec[sl] = 0;
if (!strstr(sec, "WPA") && !strstr(sec, "RSN") && !strstr(sec, "WEP") &&
!strstr(sec, "SAE") && !strstr(sec, "EAP"))
snprintf(sec, sizeof sec, "Open");
}
}
}
/* 去重:同一 BSSID 只留一条,优先具名 SSID */
int idx = -1;
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
@@ -323,6 +339,7 @@ int net_wifi_scan(meow_net_t *out, int max)
named[idx] = has;
out[idx].id = -1;
snprintf(out[idx].mac, sizeof out[idx].mac, "%s", bssid); /* 保留 BSSID 供详情展示 */
snprintf(out[idx].sec, sizeof out[idx].sec, "%s", sec);
if (has) {
/* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */
char raw[96];
@@ -645,23 +662,50 @@ static int g_net_scan_n = 0;
static int g_net_bt_n = 0;
/* 刷新网络信息缓存(进入设置页 / 网络动作后调用;含一次 WiFi 扫描触发) */
/* ---- 设置页异步网络刷新(进设置不再在入口同步跑子进程查询)----
* 背景:进设置页此前在入口同步跑 s_net_refresh()——wpa_cli/bluetoothctl/hcitool 逐条
* popen(各自带 1.2~2.5s 有界超时),5 条串行叠加达数秒,真机表现为「点设置卡 ≥5s」。
* 修法:入口只 draw + 触发扫描(快),wifi/bt 状态与列表改由 run_settings_page 主循环
* 逐步骤 pump(每步一个带超时 popen,数据变化即 need_paint)。s_net_refresh() 保留同步
* 语义(kick + drain)供需要立即结果的调用方(通知中心等)使用。
* 步骤:1=SSID 2=已保存 3=附近WiFi 4=蓝牙角色 5=附近蓝牙(各写 g_net_* 与 g_bt_mode)。 */
static int g_net_rstep = 0; /* 0=idle;1..N=步骤机位置 */
void s_net_refresh_kick(void)
{
net_wifi_scan_trigger(); /* 异步发起扫描(wpa_cli scan 快返回) */
net_bt_scan_trigger(); /* 异步发起 BT 附近扫描(& 立即返回) */
g_net_rstep = 1;
}
int s_net_refresh_pump(void)
{
meow_net_t tmp[S_NET_MAX];
switch (g_net_rstep) {
case 1:
g_net_rstep = 2;
if (wlan_connected()) {
char ss[64]; net_wifi_ssid(ss, sizeof ss);
if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss);
else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接");
} else {
snprintf(g_net_ssid, sizeof g_net_ssid, "未连接");
}
return 1;
case 2: g_net_rstep = 3; g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX); return 1;
case 3: g_net_rstep = 4; g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX); return 1;
case 4: g_net_rstep = 5; bt_role_refresh(1); return 1; /* 蓝牙角色 → g_bt_mode */
case 5: g_net_rstep = 0; g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); return 1;
default: return 0;
}
}
/* 刷新网络信息缓存(同步:kick + 排空全部异步步骤;需要立即结果的调用方用)。
* 进设置页走 s_net_sync_from_sys()(非阻塞:toggle 同步 + kick,由主循环 pump)。 */
void s_net_refresh(void)
{
if (wlan_connected()) {
char ss[64]; net_wifi_ssid(ss, sizeof ss);
if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss);
else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接");
} else {
snprintf(g_net_ssid, sizeof g_net_ssid, "未连接");
}
net_wifi_scan_trigger(); /* 异步发起扫描,稍后附近/计数更接近真实 */
net_bt_scan_trigger(); /* M2.40:异步发起蓝牙附近扫描(写 /tmp 缓存,不阻塞) */
bt_role_refresh(1); /* M2.48:进入设置/网络动作时立即重探蓝牙角色,本行/状态栏最新 */
meow_net_t tmp[S_NET_MAX];
g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX);
g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX);
g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); /* 附近(已配对+扫描发现) */
s_net_refresh_kick();
while (s_net_refresh_pump()) { /* drain: 同步排空所有异步步骤 */ }
}
@@ -677,6 +721,11 @@ int s_net_get(const srow_t *r, char *buf, int len)
case NET_LIST_ABOUT: snprintf(buf, len, "MeowOS v%s", g_meowos_version); return 0;
case NET_LIST_WIFI_PAGE: snprintf(buf, len, "%s", g_net_ssid); return 0; /* M2.76:子页入口摘要 */
case NET_LIST_BT_PAGE: snprintf(buf, len, "%s", g_bt_mode); return 0; /* M2.76 */
case NET_LIST_PIN_PAGE: { /* M6.x:锁屏 PIN 入口摘要 */
if (!lck_pin_configured()) { snprintf(buf, len, "未设置"); }
else if (lck_lock_remaining() > 0) { snprintf(buf, len, "锁定中"); }
else { snprintf(buf, len, "已设置"); }
return 0; }
}
snprintf(buf, len, "-");
return 0;
@@ -712,7 +761,8 @@ void s_net_sync_from_sys(void)
int real = hci_powered() ? 1 : 0;
if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
}
s_net_refresh();
/* 只同步开关(快)+ 异步 kick;状态/列表由 run_settings_page 主循环 pump,绝不阻塞入口。 */
s_net_refresh_kick();
}
@@ -776,6 +826,6 @@ void s_net_open(const srow_t *r)
default:
return;
}
s_net_refresh();
s_net_refresh_kick(); /* 异步刷新;run_settings_page 主循环 pump 逐步骤填充 */
}
+726
View File
@@ -0,0 +1,726 @@
/* netmgr_ipc.c — MeowOS launcher → meow_netmgrd IPC 客户端(M6.0 设置页迁移,NETMGR_DESIGN §12)。
*
* 【迁移注释】wifi_page/bt_page 的 WiFi/BT 查询与操作统一走 netmgrd IPC(socket
* /usr/data/meow_net.sock,env MEOW_NET_SOCK 覆盖)。netmgrd 是 WiFi/BT 操作权威
* (IPC 0xE4 WIFI_CTL / 0xE6 BT_CTL);本文件是「设置页客户端 → 权威 IPC」的唯一入口。
* 【双轨过渡】netmgrd 不可达(NETMGR_UNREACH)时,本文件回退到 net.c 既有的直连
* wpa_cli/hciconfig/busybox 读取(保功能);状态栏物理键语义、sdk/amsd/netmgrd 本体不动。
*
* 鉴权:跟 pm_power_ipc 同款 —— 先 MSG_HELLO(app_id=os.meow.launcher,netmgrd 按
* SO_PEERCRED 认出 /usr/bin/meow_launcherd = 系统身份,写级放行),再发 CTL,收 RESULT。
* 只读观测(wlan_connected()/hci_powered() 读 /sys 链路态)仍在 net.c;此处只接权威操作。
*/
#include "meow_launcher_internal.h"
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
/* IPC 读超时(ms)。netmgrd 单线程 poll,connect/connecth 会阻塞 ~10s/~15s 有界等待
* wpa_state=COMPLETED(bug1),scan 内含 1.5s 触发等待(P3)——客户端 per-command 超时需
* 覆盖这些同步等待,其余读级子命令维持 1.5s 不被卡死。 */
#define NM_TMO_DFL 1500
#define NM_TMO_SCAN 5000
#define NM_TMO_CONNECT 12000
#define NM_TMO_CONNECTH 17000
/* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
static time_t g_nm_ok_ts = 0;
static void nm_cache(int ok) { g_nm_ok = ok; g_nm_ok_ts = time(NULL); }
/* ---- socket 连接 ---- */
static int nm_sock(void)
{
const char *sock = getenv("MEOW_NET_SOCK");
if (!sock || !sock[0]) sock = NETMGR_SOCK_DEFAULT;
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, sock, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
return fd;
}
/* ---- 帧收发(复刻 power_menu.c pw_xchg:读超时 1.5s,绝不让子页主循环卡死)---- */
static int nm_w_all(int fd, const uint8_t *b, size_t n)
{
size_t off = 0;
while (off < n) { ssize_t w = write(fd, b + off, n - off); if (w <= 0) return -1; off += (size_t)w; }
return 0;
}
static int nm_r_all(int fd, uint8_t *b, size_t n)
{
size_t off = 0;
while (off < n) { ssize_t r = read(fd, b + off, n - off); if (r <= 0) return -1; off += (size_t)r; }
return 0;
}
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype, int tmo_ms)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
uint16_t ver = MEOW_IPC_VERSION, t = type;
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2);
memcpy(hdr + 8, &req, 4); memcpy(hdr + 12, &rlen, 4);
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
struct pollfd pf = { fd, POLLIN, 0 };
int pr = poll(&pf, 1, tmo_ms);
if (pr <= 0) return -1;
uint8_t rhdr[MEOW_IPC_HDR_LEN];
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
uint32_t rmagic, rlen2; uint16_t rver, rt;
memcpy(&rmagic, rhdr, 4); memcpy(&rver, rhdr + 4, 2); memcpy(&rt, rhdr + 6, 2);
memcpy(&rlen2, rhdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) return -1;
if (rlen2 > rcap) rlen2 = rcap;
if (rlen2 && nm_r_all(fd, rbuf, rlen2) < 0) return -1;
if (rtype) *rtype = rt;
return 0;
}
static int nm_hello(int fd)
{
m_hello_t h;
memset(&h, 0, sizeof h);
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
h.version = 1;
uint8_t rbuf[256]; uint16_t rtype = 0;
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype, NM_TMO_DFL) < 0 || rtype != MSG_ACK)
return -1;
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
return (ak.code == MEOW_OK) ? 0 : -1;
}
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。
* tmo_ms:读超时(connect/connecth/scan 需覆盖 netmgrd 同步等待,见 NM_TMO_*)。 */
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
char *text, int tlen, int tmo_ms)
{
m_net_cmd_t q;
memset(&q, 0, sizeof q);
snprintf(q.cmd, sizeof q.cmd, "%s", cmd ? cmd : "");
if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg);
int fd = nm_sock();
if (fd < 0) { nm_cache(0); return NETMGR_UNREACH; }
if (nm_hello(fd) < 0) { close(fd); nm_cache(0); return NETMGR_UNREACH; }
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
memset(rbuf, 0, sizeof rbuf);
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype, tmo_ms) < 0 || rtype != res_type) {
close(fd); nm_cache(0); return NETMGR_UNREACH;
}
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
close(fd);
nm_cache(1);
uint32_t tl = res.text_len;
if (tl >= (uint32_t)tlen) tl = (uint32_t)(tlen - 1);
if (text && tlen > 0) { memcpy(text, res.text, tl); text[tl] = 0; }
return (int)res.code;
}
int netmgr_wifi_tmo(const char *cmd, const char *arg, char *text, int tlen, int tmo_ms)
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, tlen, tmo_ms); }
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen)
{ return netmgr_wifi_tmo(cmd, arg, text, tlen, NM_TMO_DFL); }
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen)
{ return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen, NM_TMO_DFL); }
int netmgr_ok(void)
{
if (g_nm_ok >= 0 && time(NULL) - g_nm_ok_ts < 1) return g_nm_ok;
int fd = nm_sock();
if (fd < 0) { nm_cache(0); return 0; }
close(fd); nm_cache(1); return 1;
}
/* ================= WiFi(IPC 优先,net.c 直连回退)================= */
/* —— net.c 的 wpa_decode_ssid 是 static,这里复制最小实现(本模块自含,避免改 net.c 可见性)—— */
static int nm_hex(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static void nm_decode_ssid(const char *src, char *dst, int dlen)
{
int o = 0;
while (*src && o < dlen - 1) {
if (src[0] == '\\' && src[1] == 'x' && nm_hex(src[2]) >= 0 && nm_hex(src[3]) >= 0) {
dst[o++] = (char)((nm_hex(src[2]) << 4) | nm_hex(src[3]));
src += 4;
} else {
dst[o++] = *src++;
}
}
dst[o] = 0;
}
/* P1:拷一个「空格分隔的值 token」到 dst(a2dp 字段 extraction 用)。 */
static void nm_val_field(char **p, char *dst, int dlen)
{
while (**p == ' ' || **p == '\t') (*p)++;
int o = 0;
while (**p && **p != ' ' && **p != '\t' && **p != '\n' && o < dlen - 1) dst[o++] = *(*p)++;
dst[o] = 0;
}
/* —— netmgrd `wifi status` 单行摘要解析:`wifi state=X connected=N ssid=S ip=I rssi=R`。
* 已知字段以「空格+key=」分隔;ssid 可能含空格,需读到下一个已知字段为止。 —— */
static int nm_is_status_key(const char *p)
{
if (!strncmp(p, " ip=", 4) || !strncmp(p, " rssi=", 6) ||
!strncmp(p, " connected=", 11) || !strncmp(p, " state=", 7)) return 1;
return 0;
}
static const char *nm_val(const char *text, const char *key, char *dst, int dlen, int space_ok)
{
const char *p = strstr(text, key);
if (!p) { dst[0] = 0; return p; }
p += strlen(key);
int o = 0;
while (*p && o < dlen - 1) {
if (*p == ' ') {
if (!space_ok) break;
if (nm_is_status_key(p)) break; /* ssid 含空格:停在下一个已知字段 */
}
dst[o++] = *p++;
}
dst[o] = 0;
return p;
}
static int nm_parse_connected(const char *text)
{
char v[8];
nm_val(text, "connected=", v, sizeof v, 0);
return atoi(v);
}
int nm_wifi_connected(void)
{
char text[256];
int rc = netmgr_wifi("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return wlan_connected();
if (rc != MEOW_OK) return 0;
return nm_parse_connected(text);
}
void nm_wifi_ssid(char *buf, int len)
{
buf[0] = 0;
char text[256];
int rc = netmgr_wifi("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { net_wifi_ssid(buf, len); return; }
if (rc != MEOW_OK) return;
if (!nm_parse_connected(text)) return;
char raw[128];
nm_val(text, "ssid=", raw, sizeof raw, 1);
nm_decode_ssid(raw, buf, len);
}
int nm_wifi_info(char (*info)[48], int max)
{
char text[256];
int rc = netmgr_wifi("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_wifi_info(info, max); /* 回退:完整详情(直连 wpa_cli+ip route) */
if (rc != MEOW_OK || !nm_parse_connected(text)) return 0;
char ssid[40] = "", ip[32] = "", rssi[16] = "";
char raw[128];
nm_val(text, "ssid=", raw, sizeof raw, 1);
nm_decode_ssid(raw, ssid, sizeof ssid);
nm_val(text, "ip=", ip, sizeof ip, 0);
nm_val(text, "rssi=", rssi, sizeof rssi, 0);
int n = 0;
if (ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid);
if (ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip);
if (rssi[0]) snprintf(info[n++], 48, "信号: %s dBm", rssi);
if (n == 0) snprintf(info[n++], 48, "已连接(无详情)");
return n;
}
/* —— 解析 netmgrd `wifi networks`(wpa_cli list_networks 原样文本)→ meow_net_t[];
* 直接复用 net.c 既有表头跳行逻辑。返回项数。 —— */
static int nm_parse_saved(const char *txt, meow_net_t *out, int max)
{
int n = 0;
if (!txt || max <= 0) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
const char *line = strchr(txt, '\n');
if (line) line++;
while (line && *line && n < max) {
const char *eol = strchr(line, '\n');
const char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p && *p != '#') {
int id = atoi(p);
const char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++;
while (*sp == ' ' || *sp == '\t') sp++;
const char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++;
const char *bp = se; while (*bp == ' ' || *bp == '\t') bp++;
const char *be = bp; while (*be && *be != ' ' && *be != '\t' && *be != '\n' && *be != '\r') be++;
if (sp != se) {
char raw[160];
int l = (int)(se - sp);
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, sp, (size_t)l); raw[l] = 0;
out[n].id = id;
nm_decode_ssid(raw, out[n].name, sizeof out[n].name);
if (be > bp && (be - bp) < (int)sizeof out[n].mac) {
memcpy(out[n].mac, bp, (size_t)(be - bp)); out[n].mac[be - bp] = 0;
}
out[n].sec[0] = 0;
n++;
}
}
line = eol ? eol + 1 : NULL;
}
return n;
}
/* —— 解析 netmgrd `wifi scan`(wpa_cli scan_results 原样文本)→ meow_net_t[](按 BSSID 去重、
* 优先具名;补充 sec 安全类型供「新网络录入」判断是否弹 IME)。返回项数。 —— */
static int nm_parse_scan(const char *txt, meow_net_t *out, int max)
{
int n = 0;
if (!txt || max <= 0) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
char seen_bssid[S_NET_MAX][24];
int named[S_NET_MAX] = {0};
const char *line = strchr(txt, '\n');
if (line) line++;
while (line && *line && n < max) {
const char *eol = strchr(line, '\n');
const char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p) {
char bssid[24] = "";
{ const char *b = p; while (*p && *p != ' ' && *p != '\t') p++;
int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; }
const char *ssid = "";
char sec[16] = "Open";
const char *br = strchr(p, '[');
if (br) {
/* 安全类型 = 首个方括号内容([WPA-PSK-CCMP] 等);纯 [ESS]/[IBSS] = 开放 */
const char *be = strchr(br + 1, ']');
if (be) {
int sl = (int)(be - (br + 1));
if (sl > 15) sl = 15;
if (sl > 0) {
memcpy(sec, br + 1, (size_t)sl); sec[sl] = 0;
if (!strstr(sec, "WPA") && !strstr(sec, "RSN") &&
!strstr(sec, "WEP") && !strstr(sec, "SAE") && !strstr(sec, "EAP"))
snprintf(sec, sizeof sec, "Open");
}
}
const char *q = br;
while (*q) {
while (*q == ' ' || *q == '\t') q++;
if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; }
else break;
}
ssid = q; while (*ssid == ' ' || *ssid == '\t') ssid++;
}
const char *se = ssid;
while (*se && *se != '\n' && *se != '\r') se++;
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
int has = (se > ssid && *ssid);
int idx = -1;
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
if (idx < 0) {
if (n >= max) { line = eol ? eol + 1 : NULL; continue; }
idx = n++;
strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1);
seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0;
named[idx] = has;
out[idx].id = -1;
snprintf(out[idx].mac, sizeof out[idx].mac, "%s", bssid);
snprintf(out[idx].sec, sizeof out[idx].sec, "%s", sec);
if (has) {
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
nm_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
} else {
snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)");
}
} else if (has && !named[idx]) {
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
nm_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
named[idx] = 1;
}
}
line = eol ? eol + 1 : NULL;
}
return n;
}
int nm_wifi_saved(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_wifi("networks", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_wifi_saved(out, max); /* 直连回退 */
if (rc != MEOW_OK) return 0;
return nm_parse_saved(text, out, max);
}
int nm_wifi_scan(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_wifi_tmo("scan", NULL, text, sizeof text, NM_TMO_SCAN); /* netmgrd 内含触发+有界等待+读结果 */
if (rc == NETMGR_UNREACH) return net_wifi_scan(out, max);
if (rc != MEOW_OK) return 0;
return nm_parse_scan(text, out, max);
}
void nm_wifi_scan_trigger(void)
{
/* IPC:scan 子命令在 netmgrd 内已一次性「触发+等待+读结果」,此处无需单独触发;
* 直连回退:异步 wpa_cli scan(结果稍后 net_wifi_scan 读)。 */
if (!netmgr_ok()) net_wifi_scan_trigger();
}
/* —— 直连回退:与 netmgrd wifi_connect/connecth 对齐(bug1/bug2/bug4)。
* 匹配既有(\xNN 还原同表示)→ 新建或(命中且带新 psk)更新凭据 → hidden 补 scan_ssid=1 →
* enable+select → 有界等待 wpa_state=COMPLETED → 成功 enable_network all + save_config(校验重试);
* 失败时本次新建的网络回收,报文明示原因。仅在 netmgrd 不可达时走此路径。 */
static const char NM_WPA_PREFIX[] = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
static int nm_direct_wpa(char *out, int cap, const char *args, int tmo)
{
char cmd[384];
snprintf(cmd, sizeof cmd, "%s %s 2>/dev/null", NM_WPA_PREFIX, args);
char *o = popen_read_tmo(tmo, cmd);
if (!o) { out[0] = 0; return -1; }
int l = (int)strlen(o);
if (l >= cap) l = cap - 1;
memcpy(out, o, (size_t)l); out[l] = 0;
free(o);
return l;
}
static int nm_direct_save_config(void)
{
char out[128];
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
nm_direct_wpa(out, sizeof out, "set update_config 1", 1500);
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
return 0;
}
static int nm_direct_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
{
char out[1024]; char args[256];
int id = -1;
if (nm_direct_wpa(out, sizeof out, "list_networks", 1500) > 0) {
char *ln = strtok(out, "\n");
while (ln) {
char *tab0 = strchr(ln, '\t');
if (tab0) {
int this_id = atoi(ln);
char *ss = tab0 + 1;
char *tab1 = strchr(ss, '\t');
if (tab1) *tab1 = 0;
char clean[128], unesc[160], norm[128];
size_t cl = strlen(ss);
if (cl >= 2 && ss[0] == '"' && ss[cl - 1] == '"') {
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
} else {
snprintf(clean, sizeof clean, "%s", ss);
}
nm_decode_ssid(clean, unesc, sizeof unesc);
char *p = unesc; while (*p == ' ' || *p == '\t') p++;
snprintf(norm, sizeof norm, "%s", p);
size_t nl = strlen(norm);
while (nl > 0 && (norm[nl - 1] == ' ' || norm[nl - 1] == '\t')) norm[--nl] = 0;
if (strcmp(norm, ssid) == 0) { id = this_id; break; }
}
ln = strtok(NULL, "\n");
}
}
int created = 0;
if (id < 0) {
if (nm_direct_wpa(out, sizeof out, "add_network", 1500) <= 0) {
if (err) snprintf(err, elen, "add_network 无 id");
return MEOW_ERR_INVAL;
}
id = atoi(out);
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
created = 1;
snprintf(args, sizeof args, "set_network %d ssid '\"%s\"'", id, ssid);
nm_direct_wpa(out, sizeof out, args, 1500);
if (psk && psk[0]) { snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk); nm_direct_wpa(out, sizeof out, args, 1500); }
else { snprintf(args, sizeof args, "set_network %d key_mgmt NONE", id); nm_direct_wpa(out, sizeof out, args, 1500); }
} else if (psk && psk[0]) {
snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk);
nm_direct_wpa(out, sizeof out, args, 1500);
}
if (hidden) { snprintf(args, sizeof args, "set_network %d scan_ssid 1", id); nm_direct_wpa(out, sizeof out, args, 1500); }
snprintf(args, sizeof args, "enable_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
snprintf(args, sizeof args, "select_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
int wait_ms = hidden ? 15000 : 10000;
int waited = 0, ok = 0; char ip[40] = "";
for (;;) {
char st[512];
if (nm_direct_wpa(st, sizeof st, "status", 1500) > 0) {
char *ws = strstr(st, "wpa_state=");
if (ws && !strncmp(ws + 10, "COMPLETED", 9)) {
ok = 1;
char *ib = strstr(st, "ip_address=");
if (ib) { ib += 11; char *ie = ib; while (*ie && *ie != '\n' && *ie != '\r') ie++;
int il = (int)(ie - ib); if (il >= (int)sizeof ip) il = (int)sizeof ip - 1;
memcpy(ip, ib, (size_t)il); ip[il] = 0; }
break;
}
}
if (waited >= wait_ms) break;
usleep(200000);
waited += 200;
}
if (!ok) {
if (created) { snprintf(args, sizeof args, "remove_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500); nm_direct_save_config(); }
if (err) snprintf(err, elen, "连接失败:密码错误或信号不佳(超时未关联)");
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d timeout", ssid, hidden, id);
return MEOW_ERR_NODEV;
}
nm_direct_wpa(out, sizeof out, "enable_network all", 1500);
int persisted = nm_direct_save_config();
if (err) snprintf(err, elen, "已连接:%s ip=%s%s", ssid, ip[0] ? ip : "-", persisted ? "" : "(记录未持久化)");
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d ip=%s", ssid, hidden, id, ip[0] ? ip : "-");
return MEOW_OK;
}
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
{
if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; }
char arg[132];
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* 带空 psk → netmgrd 复用既有网络 */
char text[192];
int rc = netmgr_wifi_tmo(hidden ? "connecth" : "connect", arg, text, sizeof text,
hidden ? NM_TMO_CONNECTH : NM_TMO_CONNECT);
if (rc == NETMGR_UNREACH) return nm_direct_connect_ex(ssid, psk, hidden, err, elen);
if (err && elen > 0) snprintf(err, elen, "%s", text);
return rc;
}
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
{ return nm_wifi_connect_ex(ssid, psk, 0, err, elen); }
int nm_wifi_disconnect(char *err, int elen)
{
char text[128];
int rc = netmgr_wifi("disconnect", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 disconnect 2>/dev/null");
popen_read_tmo(1200, cmd);
if (err) snprintf(err, elen, "已下发");
return MEOW_OK;
}
if (err && elen > 0) snprintf(err, elen, "%s", text);
return rc;
}
int nm_wifi_toggle(int on)
{
char text[128];
int rc = netmgr_wifi(on ? "on" : "off", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { s_net_toggle_wifi(on); return MEOW_OK; } /* 直连回退 */
return rc;
}
/* ================= BT(IPC 优先:全量子命令权威收口 P1;不可达回退直连)================= */
static int nm_parse_devices(const char *txt, meow_net_t *out, int max); /* 前向声明 */
int nm_bt_powered(void)
{
char text[128];
int rc = netmgr_bt("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return hci_powered();
if (rc != MEOW_OK) return 0;
char v[8];
nm_val(text, "powered=", v, sizeof v, 0);
return atoi(v);
}
int nm_bt_onoff(int on)
{
char text[128];
int rc = netmgr_bt(on ? "on" : "off", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { s_net_toggle_bt(on); return MEOW_OK; }
return rc;
}
int nm_bt_status(int *powered, int *paired_n, char *addr, int alen)
{
char text[192];
int rc = netmgr_bt("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { /* 直连回退:读内核/tool */
if (powered) *powered = hci_powered();
if (paired_n) *paired_n = -1;
if (addr && alen > 0) addr[0] = 0;
return rc;
}
if (rc == MEOW_OK) {
char v[8];
if (powered) { nm_val(text, "powered=", v, sizeof v, 0); *powered = atoi(v); }
if (paired_n) { nm_val(text, "paired=", v, sizeof v, 0); *paired_n = atoi(v); }
if (addr && alen > 0) nm_val(text, "address=", addr, alen, 0);
}
return rc;
}
int nm_bt_paired(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("paired", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_bt_paired(out, max);
if (rc != MEOW_OK) return 0;
return nm_parse_devices(text, out, max);
}
/* ---- P1 收口:连接/断开/配对/扫描/A2DP 全部迁 netmgrd(权威),不可达回退直连 ---- */
/* 通用 `Device <MAC> <Name>` 行解析(netmgrd bt {connected,paired,devices} 文本;launcher 只做客户端)。 */
static int nm_parse_devices(const char *txt, meow_net_t *out, int max)
{
int n = 0;
if (!txt || max <= 0) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
char *line = (char *)txt;
while (line && *line && n < max) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
char *p = strstr(line, "Device ");
if (p) {
p += 7;
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
char *nm2 = mac; while (*nm2 == ' ' || *nm2 == '\t') nm2++;
int ml = (int)(mac - p); if (ml > 47) ml = 47;
if (ml > 0 && ml < (int)sizeof out[n].mac) {
memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0;
}
snprintf(out[n].name, sizeof out[n].name, "%s", *nm2 ? nm2 : p);
out[n].id = -1; out[n].sec[0] = 0;
n++;
}
line = eol ? eol + 1 : (char *)0;
}
return n;
}
int nm_bt_connected(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("connected", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_bt_connected(out, max); /* 直连回退 */
if (rc != MEOW_OK) return 0;
return nm_parse_devices(text, out, max);
}
int nm_bt_devices(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("devices", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_bt_scan(out, max); /* 直连回退(读扫描缓存) */
if (rc != MEOW_OK) return 0;
return nm_parse_devices(text, out, max);
}
/* A2DP 端点解析:"A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=.. role=.." */
int nm_bt_a2dp(bt_ep_t *eps, int max)
{
if (max <= 0) return 0;
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("a2dp", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH || rc != MEOW_OK) return 0; /* A2DP 详情无直连回退(P2 bluealsa 才接) */
int n = 0;
char *line = text;
while (line && *line && n < max) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
if (!strncmp(line, "A2DP ", 5)) {
char *p = line + 5;
bt_ep_t *e = &eps[n];
memset(e, 0, sizeof *e);
snprintf(e->role, sizeof e->role, "%s", "-");
e->acquired = 0;
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n') mac++;
int ml = (int)(mac - p); if (ml > 23) ml = 23;
if (ml > 0) { memcpy(e->mac, p, (size_t)ml); e->mac[ml] = 0; }
char *t = mac;
while (*t) {
while (*t == ' ' || *t == '\t') t++;
if (!strncmp(t, "codec=", 6)) { t += 6; nm_val_field(&t, e->codec, sizeof e->codec); }
else if (!strncmp(t, "rate=", 5)) { t += 5; nm_val_field(&t, e->rate, sizeof e->rate); }
else if (!strncmp(t, "channels=", 9)) { t += 9; nm_val_field(&t, e->chn, sizeof e->chn); }
else if (!strncmp(t, "bits=", 5)) { t += 5; nm_val_field(&t, e->bits, sizeof e->bits); }
else if (!strncmp(t, "acquired=", 9)) { t += 9; e->acquired = atoi(t); }
else if (!strncmp(t, "role=", 5)) { t += 5; nm_val_field(&t, e->role, sizeof e->role); }
while (*t && *t != ' ' && *t != '\t' && *t != '\n') t++;
}
n++;
}
line = eol ? eol + 1 : NULL;
}
return n;
}
/* 直连回退:bluetoothctl <sub> <mac>(带超时)。返回 MEOW_OK/MEOW_ERR_*。 */
static int bt_direct_cmd(const char *sub, const char *mac)
{
if (!mac || !mac[0]) return MEOW_ERR_INVAL;
char cmd[192];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl %s %s 2>/dev/null", sub, mac);
char *o = popen_read_tmo(8000, cmd);
int ok = o && (strstr(o, "successful") || strstr(o, "Connected") ||
strstr(o, "already") || strstr(o, "Paired"));
if (o) free(o);
return ok ? MEOW_OK : MEOW_ERR_NODEV;
}
int nm_bt_connect(const char *mac)
{
char text[128];
int rc = netmgr_bt("connect", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("connect", mac);
return rc;
}
int nm_bt_disconnect(const char *mac)
{
char text[128];
int rc = netmgr_bt("disconnect", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("disconnect", mac);
return rc;
}
int nm_bt_pair(const char *mac)
{
char text[128];
int rc = netmgr_bt("pair", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("pair", mac);
return rc;
}
int nm_bt_unpair(const char *mac)
{
char text[128];
int rc = netmgr_bt("unpair", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("remove", mac);
return rc;
}
void nm_bt_scan_on(void)
{
if (!netmgr_ok()) { net_bt_scan_trigger(); return; } /* 直连回退 */
netmgr_bt("scan", "on", NULL, 0);
}
void nm_bt_scan_off(void)
{
if (!netmgr_ok()) return;
netmgr_bt("scan", "off", NULL, 0);
}
+417 -238
View File
@@ -23,13 +23,14 @@ int g_notif_ctrl_died = 0;
static int g_ncp_dnd = 0;
/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。
* g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。
* run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint)
* 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */
/* ⑦ 行左滑删除的绘制态(供旧手绘 ncp_draw_list / ncp_repaint_row 画该行位移)——M7.3 已迁 SDK
* meow_listview 左滑删除(SDK 内部持有 swipe_idx/swipe_dx,不再需要本组全局)。
* 保留在 #if 0 回退区内(随旧手绘内层一起重出)。 */
#if 0
static int g_ncp_swipe_row = -1;
static int g_ncp_swipe_off = 0;
#endif
/* ---- 阶段D附录B:通知中心顶部媒体卡片(固定于通知列表上方,活动媒体会话时显示)---- */
@@ -59,6 +60,58 @@ static int ncp_list_view(void) { int v = NCP_LIST_BOT - ncp_list_top(); return v
#define NCP_MEDIA_BTN_R 22 /* play 半径;prev/next 略小 */
#define NCP_MEDIA_PROG_Y 74 /* 相对卡片顶的进度条 top */
/* M7.0b:媒体卡片三控制按钮改用 SDK meow_iconbtn + sb_icons 矢量图标(与 Now Playing 同源),
* 取代原先手绘三角/竖条(ncp_media_tri/ncp_media_btn,用户反馈手绘有 bug)。up: 圆钮尺寸由
* meow_iconbtn 的 size 决定(ink=0.6*size),此处 prev/next 用 NCP_MEDIA_BTN_R-4、play 用
* NCP_MEDIA_BTN_R(与旧几何一致,命中仍按三圆心——ncp_media_card_tap 不变)。控件 lazy 建一次,
* owner=哨兵 0x52(不与电源菜单 0x50 / Now Playing 0x51 冲突);绘制经 meow_widget_draw。 */
#define NCP_MEDIA_ACT 0x52
#define NCP_MEDIA_PREV_R (NCP_MEDIA_BTN_R - 4) /* 18 */
#define NCP_MEDIA_NEXT_R (NCP_MEDIA_BTN_R - 4) /* 18 */
static meow_widget *g_ncp_btn_prev = NULL, *g_ncp_btn_play = NULL;
static meow_widget *g_ncp_btn_pause = NULL, *g_ncp_btn_next = NULL;
static int g_ncp_media_btn_init = 0;
/* 创建三控制按钮(owner 钉哨兵;play/pause 同位叠两钮,按状态只画其一——同 now_playing)。 */
static void ncp_media_btns_create(void)
{
meow_ui_activity_push(NCP_MEDIA_ACT);
int by = NCP_LIST_TOP + NCP_MEDIA_BTN_Y; /* 三钮圆心 y(与 ncp_draw_media_card/ncp_media_card_tap 同源) */
g_ncp_btn_prev = meow_iconbtn_create(NCP_MEDIA_PREV_CX - NCP_MEDIA_PREV_R, by - NCP_MEDIA_PREV_R,
NCP_MEDIA_PREV_R * 2, MEOW_ICON_SKIP_PREV);
g_ncp_btn_play = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PLAY_ARROW);
g_ncp_btn_pause = meow_iconbtn_create(NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R, by - NCP_MEDIA_BTN_R,
NCP_MEDIA_BTN_R * 2, MEOW_ICON_PAUSE);
g_ncp_btn_next = meow_iconbtn_create(NCP_MEDIA_NEXT_CX - NCP_MEDIA_NEXT_R, by - NCP_MEDIA_NEXT_R,
NCP_MEDIA_NEXT_R * 2, MEOW_ICON_SKIP_NEXT);
/* 主播放钮保持「强调动作」观感(与 Now Playing 一致):底色=primary、图标=onPrimary;其余默认。 */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
meow_ui_activity_pop();
g_ncp_media_btn_init = 1;
}
/* 绘制三控制按钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时新圆底盖掉旧)。
* 每次刷新播放钮底色/图标(同 now_playing np_theme_widgets:跨主题切换跟手)。 */
static void ncp_draw_media_btns(const meow_media_session_t *s)
{
if (!g_ncp_media_btn_init) ncp_media_btns_create();
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_ncp_btn_play) meow_widget_set_opts(g_ncp_btn_play, &op);
if (g_ncp_btn_pause) meow_widget_set_opts(g_ncp_btn_pause, &op);
if (g_ncp_btn_prev) { meow_widget_set_selected(g_ncp_btn_prev, 0); meow_widget_draw(g_ncp_btn_prev); }
if (g_ncp_btn_next) { meow_widget_set_selected(g_ncp_btn_next, 0); meow_widget_draw(g_ncp_btn_next); }
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
meow_widget *pb = playing ? g_ncp_btn_pause : g_ncp_btn_play;
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
}
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
static void ncp_strncpy(char *dst, const char *src, size_t len)
@@ -340,7 +393,8 @@ static void ncp_chip_state(int chip, int *on, char *val, int vlen)
} else if (chip == NCP_CHIP_THEME) {
int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */
*on = (v == 1);
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色");
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? meow_str_zh("ncp.theme_light", "浅色")
: meow_str_zh("ncp.theme_dark", "深色"));
} else if (chip == NCP_CHIP_BRIGHT) {
int v = 0; meow_settings_get_int("display.brightness", &v);
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
@@ -386,11 +440,11 @@ static int ncp_chip_apply(int chip)
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
if (mnp_brightness_clamp(cur, vmin) < 100) {
meow_settings_set_int("display.brightness", 100);
write_brightness(100);
write_brightness(100, "notif_center bright_chip");
g_sys.brightness = 100;
} else {
meow_settings_set_int("display.brightness", vmin);
write_brightness(vmin);
write_brightness(vmin, "notif_center bright_chip");
g_sys.brightness = vmin;
}
paint_sb_right();
@@ -406,8 +460,199 @@ static int ncp_chip_apply(int chip)
}
/* ==================================================================== */
/* M7.3:通知中心内层迁 SDK 控件(芯片/双滑条/勿扰开关/通知列表/左滑删除) */
/* --------------------------------------------------------------------
* 定位:只迁「内层绘制/命中」。叠加会话骨架(SIGSTOP / 输入移交 / 恢复帧 / 顶条热区——见
* app_launcher.c run_app_notif_overlay)绝对不动,此处仅把 run_notif_center / draw_notif_center
* 中面板体的绘制与行命中交给 SDK meow_widget。SDK meow_ui.a 已定稿(c3d6371b),勿重编直接用。
*
* owner 钉哨兵 NCP_WIDGET_ACT(不冲突:0x50 电源菜单 / 0x51 锁屏·NowPlaying / 0x52 子页·媒体卡 /
* 0x53 设置页)。绘制经 meow_widget_draw / meow_listview_vs_draw;命中:通知列表走
* meow_listview_hit / meow_listview_swipe_begin·move·end(SDK 手势判定+删除/回弹动画),
* 芯片/滑条/勿扰开关沿用既有人工几何命中(与媒体卡 M7.0b 一致——叠加会话为手绘循环,不跑 SDK
* 输入线程,故不依赖 meow_widget_hit 的 owner==active 闸门)。 */
#define NCP_WIDGET_ACT 0x54
static meow_widget *g_ncp_lv = NULL; /* 通知列表:listview 虚拟化 + 行自绘 + 空态 + 左滑删除 */
static meow_widget *g_ncp_sl_bri = NULL; /* 亮度滑条 */
static meow_widget *g_ncp_sl_vol = NULL; /* 音量滑条 */
static meow_widget *g_ncp_sw_dnd = NULL; /* 勿扰开关 */
static meow_widget *g_ncp_chip[NCP_CHIP_COUNT]; /* 5 个快捷 chip(自定义画布:圆角 + 图标 + 标签) */
static int g_ncp_widget_init = 0;
static int g_ncp_need_paint = 0; /* M7.3:行左滑删除回调置位 → 主循环整面板重画 */
/* ---- 通知列表虚拟化源:count=当前行数(动态,逐帧实时查);bind 仅填 enabled 供
* meow_listview_hit 判定(行内容经 set_item_draw 自绘直读 g_rows[],无池缓存过期问题)。 ---- */
static int ncp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
static void ncp_lv_bind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
it->enabled = (idx >= 0 && idx < g_nrows) ? 1 : 0;
}
/* 文本截断(SDK meow_ui_text_width/meow_ui_text;UTF-8 安全,与旧 ncp_text 同构)。 */
static void ncp_mt_text(int x, int y, const char *s, int size, uint32_t col, int maxw)
{
if (!s) s = "";
if (meow_ui_text_width(s, size) <= maxw) { meow_ui_text(x, y, s, size, col); return; }
int ell = meow_ui_text_width("...", size);
int limit = maxw - ell;
char buf[128]; int len = 0;
while (s[len] && len < (int)sizeof buf - 8) {
unsigned char c0 = (unsigned char)s[len];
int cp = 1;
if ((c0 & 0xE0) == 0xC0) cp = 2;
else if ((c0 & 0xF0) == 0xE0) cp = 3;
else if ((c0 & 0xF8) == 0xF0) cp = 4;
if (len + cp >= (int)sizeof buf - 1) break;
for (int k = 0; k < cp; k++) buf[len + k] = s[len + k];
buf[len + cp] = 0;
if (meow_ui_text_width(buf, size) > limit) { buf[len] = 0; break; }
len += cp;
}
if (len > 0) {
if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; }
meow_ui_text(x, y, buf, size, col);
} else {
meow_ui_text(x, y, "...", size, col);
}
}
/* 通知行自绘(set_item_draw 覆盖默认 renderer)。x 已含左滑位移(SDK 先画右露删除区并把行 x 左移),
* 逐元素对齐旧 ncp_draw_row;颜色全取 meow_theme 语义色(M7 定案 #2 单一权威)。 */
static void ncp_lv_row_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)w; (void)state; (void)ctx; /* w=SDK 视口宽;行卡宽用 NCP_ROW_W(含 NCP_ROW_X 两侧留白) */
if (idx < 0 || idx >= g_nrows) return;
const ncp_row_t *r = &g_rows[idx];
const meow_theme *th = meow_theme_get();
meow_gfx_fill_round(x + NCP_ROW_X, y, NCP_ROW_W, h - 6, 12, th->surfaceContainer);
/* 图标 */
int icx = x + NCP_ICON_X, icy = y + (h - 32) / 2;
if (r->img) meow_ui_img_cover(icx, icy, 32, r->img);
else {
const char *nm = ncp_app_name(r->app_id);
ncp_dot(icx + 16, icy + 16, 16, th->primary);
char ch = nm[0] ? nm[0] : '?';
char cbuf[4] = { ch, 0, 0, 0 };
if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0;
if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } }
int cw = meow_ui_text_width(cbuf, th->font_body);
meow_ui_text(icx + 16 - cw / 2, icy + 16 - meow_ui_text_height(th->font_body) / 2,
cbuf, th->font_body, th->onPrimary);
}
/* app 名 + 时间 */
meow_ui_text(x + NCP_TEXT_X, y + 10, ncp_app_name(r->app_id), th->font_body, th->onSurfaceVariant);
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
meow_ui_text(x + NCP_ROW_X + NCP_ROW_W - 8 - meow_ui_text_width(tbuf, th->font_body),
y + 12, tbuf, th->font_body, th->onSurfaceVariant);
/* 标题 + 内容(SDK 截断) */
ncp_mt_text(x + NCP_TEXT_X, y + 30, r->summary, th->font_body, th->onSurface, NCP_TEXT_W);
ncp_mt_text(x + NCP_TEXT_X, y + 56, r->body, th->font_body, th->onSurfaceVariant, NCP_TEXT_W);
/* 进度(progress>=0):细条 */
if (r->progress >= 0) {
int px = x + NCP_TEXT_X, pw = NCP_TEXT_W, py = y + h - 14;
meow_ui_fill_rect(px, py, pw, 4, th->surfaceVariant);
int fw = r->progress * pw / 100;
if (fw > 0) meow_ui_fill_rect(px, py, fw, 4, th->primary);
}
}
/* 左滑删除完成回调(SDK 删除/回弹动画结束后触发):撤下该通知 + 重采集 + 重排。 */
static void ncp_lv_on_delete(int idx, void *ctx)
{
(void)ctx;
logmsg("notif-center: swipe delete row %d", idx);
if (idx >= 0 && idx < g_nrows) meow_notif_dismiss(g_rows[idx].id);
ncp_refresh_rows();
/* 行数变化 → 列表重排:置位让主循环整面板重画(含媒体卡出现/消失重排 + 列表滚动重钳)。 */
g_ncp_need_paint = 1;
}
/* 芯片自绘(自定义画布 render_cb:TONAL 观感圆角 + 位图图标 + 标签,与旧 draw_notif_center chip 循环一致)。
* launcher 侧 meow_widget 不透明 → 用 meow_widget_rect 取绝对几何(同 power_menu pm_render_cb)。 */
static void ncp_chip_render(meow_widget *w, const meow_theme *th, void *ctx)
{
int c = (int)(intptr_t)ctx;
if (c < 0 || c >= NCP_CHIP_COUNT) return;
int rc[4]; meow_widget_rect(w, rc);
int x = rc[0], y = rc[1], wd = rc[2], ht = rc[3];
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
uint32_t bg = on ? th->primary : th->surfaceContainer;
uint32_t fg = on ? th->onPrimary : th->onSurfaceVariant;
meow_gfx_fill_round(x, y, wd, ht, 12, bg);
int icon_cx = x + (wd - SB_ICON_SIZE) / 2;
int icon_cy = y + 14;
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_THEME) {
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
}
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_POWER) {
int pc = icon_cx + SB_ICON_SIZE / 2;
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
}
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
static const char *chip_id[NCP_CHIP_COUNT] = { "ncp.chip_wifi", "ncp.chip_bt",
"ncp.chip_theme", "ncp.chip_bright",
"ncp.chip_power" };
char label[24]; snprintf(label, sizeof label, "%s", meow_str_zh(chip_id[c], chip_lbl[c]));
if (val[0] && c == NCP_CHIP_BRIGHT) snprintf(label, sizeof label, "%.9s", val);
ui_draw(x + (wd - ui_width(label, 14)) / 2, y + ht - 18, label, fg, 14);
}
/* 芯片自定义画布命中(矩形;未被主循环使用——命中仍走 ncp_chip_hit 人工几何,此处仅满足 W_CUSTOM 契约)。 */
static int ncp_chip_widget_hit(meow_widget *w, int x, int y, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
return (x >= rc[0] && x < rc[0] + rc[2] && y >= rc[1] && y < rc[1] + rc[3]);
}
/* 创建 M7.3 内层控件集(owner 钉哨兵;仅首进创建一次,跨次复用——同媒体卡 M7.0b 惯用法)。 */
static void ncp_widgets_create(void)
{
meow_ui_activity_push(NCP_WIDGET_ACT);
/* 通知列表(视口 = 列表区;行高 NCP_ROW_H;空态 + 左滑删除) */
int lvy = ncp_list_top(), lvh = ncp_list_view();
g_ncp_lv = meow_listview_create(0, lvy, fb_xres, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
if (g_ncp_lv) {
meow_listview_set_vsource(g_ncp_lv, ncp_lv_count, ncp_lv_bind, NULL);
meow_listview_set_item_draw(g_ncp_lv, ncp_lv_row_draw, NULL);
meow_listview_set_item_h(g_ncp_lv, NCP_ROW_H);
meow_listview_set_empty_text(g_ncp_lv, meow_str_zh("ncp.empty", "暂无通知"));
meow_listview_set_swipe_delete(g_ncp_lv, 1, ncp_lv_on_delete, NULL);
}
/* 亮度/音量双滑条(track 中心对齐旧 s_draw_slider:NCP_SL*_Y+3) */
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int bval = 0; meow_settings_get_int("display.brightness", &bval);
bval = mnp_brightness_clamp(bval, bmin);
g_ncp_sl_bri = meow_slider_create(NCP_SLIDER_X, NCP_SL1_Y - 14, NCP_SLIDER_W, bmin, 100, bval);
int vmax = vol_clamp(settings_volume_max);
int vval = vol_clamp(settings_volume_get());
g_ncp_sl_vol = meow_slider_create(NCP_SLIDER_X, NCP_SL2_Y - 14, NCP_SLIDER_W, 0, vmax, vval);
/* 勿扰开关 */
g_ncp_sw_dnd = meow_switch_create(0, 0, g_ncp_dnd ? 1 : 0);
if (g_ncp_sw_dnd) meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48,
NCP_DND_Y + NCP_DND_H / 2 - 14, 48, 28);
/* 5 个快捷 chip(自定义画布;坐标与旧 ncp_chip_x / NCP_CHIP_Y 一致) */
for (int c = 0; c < NCP_CHIP_COUNT; c++)
g_ncp_chip[c] = meow_widget_custom_create(ncp_chip_x(c), NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H,
ncp_chip_render, ncp_chip_widget_hit,
(void *)(intptr_t)c);
meow_ui_activity_pop();
g_ncp_widget_init = 1;
}
/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。
* off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */
#if 0 /* M7.3 回退位:旧手绘行删除底(SDK 左滑删除已接管此绘制,见 meow_listview swipe 渲染)。 */
static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th)
{
if (off >= 0) return;
@@ -520,6 +765,35 @@ static void ncp_repaint_row(int scroll, int row, int off)
meow_dirty_add(0, ry, fb_xres, NCP_ROW_H);
meow_ui_flush();
}
#endif /* M7.3 回退位结束(上述旧手绘 ncp_draw_delete_bg/ncp_draw_row/ncp_draw_list/
* ncp_repaint_list/ncp_repaint_row 重开本区即回退;新路径由下方 SDK 版接管)。 */
/* ---- M7.3:通知列表绘制(SDK listview 版,替换上方 #if 0 的手绘版)---- */
/* 绘制通知列表可见区(scroll=像素偏移)。同步 listview 视口矩形(媒体卡出现/消失时列表顶缘
* 变化)+ 滚动偏移,再画整个列表控件(虚拟化 + set_item_draw 行自绘 + 空态 + 左滑删除渲染
* 全由 SDK 完成)。 */
static void ncp_draw_list(int scroll)
{
if (!g_ncp_widget_init || !g_ncp_lv) return;
int lt = ncp_list_top(), lv = ncp_list_view();
meow_widget_set_rect(g_ncp_lv, 0, lt, fb_xres, lv);
meow_listview_set_scroll(g_ncp_lv, mnp_scroll_clamp(scroll, g_nrows * NCP_ROW_H, lv));
meow_widget_draw(g_ncp_lv);
}
/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底,再画 SDK listview;
* 只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT))——「滚动只重绘列表可视区,绝不整屏」。 */
static void ncp_repaint_list(int scroll)
{
const meow_ui_theme_t *th = s_ui_theme();
int lt = ncp_list_top(), lv = ncp_list_view();
fill_rect(0, lt, fb_xres, lv, th->surface_variant);
ncp_draw_list(scroll);
meow_dirty_add(0, lt, fb_xres, lv);
meow_ui_flush();
}
/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段):
@@ -555,14 +829,20 @@ static void ncp_draw_dnd(const meow_ui_theme_t *th)
meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */
ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */
/* label + 状态 */
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18);
const char *dnd_lbl = meow_str_zh("ncp.dnd", "勿扰");
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, dnd_lbl, th->on_surface, 18);
char st[16];
snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关");
int lw = ui_width("勿扰", 18);
snprintf(st, sizeof st, "%s", dnd_on ? meow_str_zh("common.on", "开") : meow_str_zh("common.off", "关"));
int lw = ui_width(dnd_lbl, 18);
ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st,
dnd_on ? th->primary : th->on_surface_variant, 18);
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */
s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th);
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中)。
* M7.3:迁 SDK meow_switch(绘制经 meow_widget_draw;命中仍走 ncp_dnd_hit 整带)。 */
if (g_ncp_widget_init && g_ncp_sw_dnd) {
meow_widget_set_rect(g_ncp_sw_dnd, NCP_DND_SPLIT_X - 6 - 48, cy - 14, 48, 28);
meow_switch_set(g_ncp_sw_dnd, dnd_on ? 1 : 0);
meow_widget_draw(g_ncp_sw_dnd);
}
}
@@ -576,7 +856,7 @@ static void ncp_draw_clear(const meow_ui_theme_t *th)
if (cw <= 0) return;
meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container);
/* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */
const char *lbl = "清除";
const char *lbl = meow_str_zh("ncp.clear", "清除");
int lw = ui_width(lbl, 18);
int unit = 22; /* 图标占位宽 + 与文字的间距 */
int total = unit + lw;
@@ -620,6 +900,7 @@ static void ncp_media_cover_load(const meow_media_session_t *s)
{
if (!s) return;
if (strcmp(g_ncp_media_cover_path, s->artwork) == 0) return;
if (!s->artwork[0]) return; /* 用户 2026-09-04:空 artwork 保留上一张媒体卡封面(勿清,防暂停/开歌单后封面闪没) */
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
ncp_strncpy(g_ncp_media_cover_path, s->artwork, sizeof g_ncp_media_cover_path);
if (!s->artwork[0]) return;
@@ -629,38 +910,6 @@ static void ncp_media_cover_load(const meow_media_session_t *s)
g_ncp_media_cover = meow_img_load_file(path);
}
/* 实心三角(dir=+1 右向 / -1 左向);size=最大宽度 px。按扫描线 fill_rect 近似 */
static void ncp_media_tri(int cx, int cy, int size, int dir, uint32_t col)
{
int half = size / 2;
for (int dy = -half; dy <= half; dy++) {
int w = half + 1 - (dy < 0 ? -dy : dy); /* 顶宽 1 → 底宽 half+1 */
if (w <= 0) continue;
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
fill_rect(x0, cy + dy, w, 1, col);
}
}
/* 圆形按钮底 + 字形(kind:0=prev 1=play 2=next 3=pause 4=暂停态play)。 */
static void ncp_media_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
uint32_t fg = 0xFFFFFF;
if (kind == 0) { /* prev:左三角 + 左竖条 */
ncp_media_tri(cx + 4, cy, r - 4, -1, fg);
fill_rect(cx - 3, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 1) { /* play:右三角 */
ncp_media_tri(cx + 2, cy, r - 2, 1, fg);
} else if (kind == 2) { /* next:右三角 + 右竖条 */
ncp_media_tri(cx - 4, cy, r - 4, 1, fg);
fill_rect(cx + 1, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 3) { /* pause:双竖条 */
int bw = (r - 6) / 2; if (bw < 2) bw = 2;
fill_rect(cx - 7, cy - (r - 5), bw, (r - 5) * 2, fg);
fill_rect(cx + 7 - bw, cy - (r - 5), bw, (r - 5) * 2, fg);
}
}
/* 画顶部媒体卡片(封面小图 + 标题/歌手 + 播放/暂停/上下曲 + 进度条)。 */
static void ncp_draw_media_card(const meow_ui_theme_t *th)
{
@@ -678,19 +927,15 @@ static void ncp_draw_media_card(const meow_ui_theme_t *th)
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 20);
ui_draw(cover_x + (cover_s - cw) / 2, cover_y + (cover_s - meow_font_line_height(20)) / 2,
t, 0xFFFFFF, 20);
t, meow_theme_get()->onPrimary, 20);
}
int tx = cover_x + cover_s + 14;
int tw = (NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R - 8) - tx; /* 文本宽限到上一首按钮左缘前 8px */
if (tw < 0) tw = 0;
ncp_text(tx, cy + 14, s->title, 18, th->on_surface, tw);
ncp_text(tx, cy + 40, s->artist[0] ? s->artist : s->album, 16, th->on_surface_variant, tw);
/* 控制按钮 */
int by = cy + NCP_MEDIA_BTN_Y;
ncp_media_btn(NCP_MEDIA_PREV_CX, by, NCP_MEDIA_BTN_R - 4, 0, th);
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 /* pause */ : 1 /* play */;
ncp_media_btn(NCP_MEDIA_PLAY_CX, by, NCP_MEDIA_BTN_R, pk, th);
ncp_media_btn(NCP_MEDIA_NEXT_CX, by, NCP_MEDIA_BTN_R - 4, 2, th);
/* 控制按钮(SDK meow_iconbtn + sb_icons,与 Now Playing 一致;命中仍走 ncp_media_card_tap 三圆心) */
ncp_draw_media_btns(s);
/* 进度条 + 时间 */
uint32_t pos = meow_media_display_pos(s);
int px = tx, pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px, py = cy + NCP_MEDIA_PROG_Y;
@@ -702,7 +947,8 @@ static void ncp_draw_media_card(const meow_ui_theme_t *th)
}
}
/* 卡片命中:按钮/进度(含 seek)。返回 1=已处理(内部已下发 PLAYCTL);0=未命中。 */
/* 卡片命中:按钮/进度(含 seek)/卡片体。返回 2=卡片体命中已发 FOCUS(7)(调用方收起面板露出
* 播放页);1=按钮/进度已处理(内部已下发 PLAYCTL,面板保持开启);0=未命中。 */
static int ncp_media_card_tap(int tx, int ty)
{
const meow_media_session_t *s = meow_media_current();
@@ -728,7 +974,14 @@ static int ncp_media_card_tap(int tx, int ty)
cmd = 5;
}
}
if (!cmd) return 0;
if (!cmd) {
/* 卡片体命中(非按钮/进度):向活动媒体源下发 FOCUS(7)(=NCM_CTL_FOCUS,见 ncm
* ipc_media.c case 7 → g_req_focus_player=1 → UI 线程 push ACT_PLAYER),请求打开
* App 播放页,并消费该次点击——返回 2 让调用方收起面板露出播放页。 */
meow_media_playctl(7, 0);
logmsg("notif-card: media tap card body -> FOCUS(7) open player");
return 2;
}
int rc = meow_media_playctl(cmd, seek);
logmsg("notif-card: media tap cmd=%u seek=%lld rc=%d", cmd, (long long)seek, rc);
return 1;
@@ -762,61 +1015,34 @@ static void draw_notif_center(int scroll, int redraw_sb)
int grab_w = 44, grab_x = (W - grab_w) / 2;
meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline);
/* 标题 + 「上滑/点空白关闭」 提示 */
ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22);
ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16);
ui_draw(24, NCP_TITLE_Y, meow_str_zh("ncp.title", "通知中心"), th->on_surface, 22);
ui_draw(W - 24 - ui_width(meow_str_zh("ncp.tap_close", "点空白关闭"), 16), NCP_TITLE_Y + 2,
meow_str_zh("ncp.tap_close", "点空白关闭"), th->on_surface_variant, 16);
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
int chip_x0 = (W - total_chip) / 2;
for (int c = 0; c < NCP_CHIP_N; c++) {
int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP);
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
uint32_t bg = on ? th->primary : th->surface_container;
uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant;
meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg);
int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2;
int icon_cy = NCP_CHIP_Y + 14;
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_THEME) {
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
}
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_POWER) {
/* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */
int pc = icon_cx + SB_ICON_SIZE / 2;
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
}
char label[24];
snprintf(label, sizeof label, "%s", chip_lbl[c]);
if (val[0] && c == NCP_CHIP_BRIGHT)
snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */
ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18,
label, fg, 14);
}
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单)。M7.3:迁 SDK 自定义画布 chip
*(TONAL 观感圆角 + 位图图标 + 标签,见 ncp_chip_render;绘制经 meow_widget_draw)。 */
if (g_ncp_widget_init)
for (int c = 0; c < NCP_CHIP_N; c++)
if (g_ncp_chip[c]) meow_widget_draw(g_ncp_chip[c]);
/* 亮度滑条 */
/* 亮度滑条(M7.3:迁 SDK meow_slider;track 中心 = NCP_SL1_Y+3,与旧 s_draw_slider 对齐) */
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int bval = 0; meow_settings_get_int("display.brightness", &bval);
bval = mnp_brightness_clamp(bval, bmin);
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18);
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, meow_str_zh("ncp.brightness", "亮度"), th->on_surface, 18);
char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th);
if (g_ncp_widget_init && g_ncp_sl_bri) { meow_slider_set(g_ncp_sl_bri, bval); meow_widget_draw(g_ncp_sl_bri); }
/* 音量滑条 */
int vval = 0; meow_settings_get_int("audio.volume", &vval);
vval = vol_clamp(vval);
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18);
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, meow_str_zh("ncp.volume", "音量"), th->on_surface, 18);
char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th);
if (g_ncp_widget_init && g_ncp_sl_vol) { meow_slider_set(g_ncp_sl_vol, vval); meow_widget_draw(g_ncp_sl_vol); }
/* 分隔线 */
fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline);
@@ -833,8 +1059,8 @@ static void draw_notif_center(int scroll, int redraw_sb)
ncp_draw_list(scroll);
/* 底部提示 */
ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭",
th->on_surface_variant, 16);
ui_draw((W - ui_width(meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), 16)) / 2, NCP_FOOT_Y,
meow_str_zh("ncp.swipe_close", "上滑或点空白关闭"), th->on_surface_variant, 16);
/* 状态栏画在面板体之上(0-27 由 SDK 全局状态栏维护,经 draw_statusbar→sb_sdk_refresh
* 喂数据+标脏顶条;不落面板体脏区)。仅 redraw_sb=1(打开/主题变化)才喂/标脏,交互时跳过
@@ -865,7 +1091,7 @@ static int ncp_slider_apply(int which, int tx, int save_now)
int cur = 0; meow_settings_get_int("display.brightness", &cur);
if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */
meow_settings_set_int("display.brightness", v);
write_brightness(v);
write_brightness(v, "notif_center bright_slider");
g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */
paint_sb_right();
if (save_now) meow_settings_save();
@@ -942,16 +1168,15 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
struct kd_state kd_play, kd_pwr;
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
int m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
key_router_rebase(); /* 进入通知中心:音量基线对齐当前 raw(防残留边沿) */
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
@@ -968,16 +1193,10 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */
int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */
int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */
/* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off):
* row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手);
* row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。
* row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */
int row_swipe_row = -1;
int row_off = 0;
int row_settle = 0;
int row_off_target = 0;
unsigned long long row_anim_at = 0;
int row_paint = 0; /* 仅重绘该行带(行左滑专用) */
/* ⑦ 单条通知行左滑删除(M7.3:迁 SDK meow_listview_set_swipe_delete + swipe_begin/move/end)。
* sdk_swipe = 本触摸已交给 SDK 行左滑手势(begin 命中可滑行);SDK 负责方向仲裁(横向=消费 /
* 纵向=让位滚动)、删除/回弹动画(meow_anim_tick 推进)与 on_delete 回调。 */
int sdk_swipe = 0;
/* ⑨ 面板跟手下拉、半程可回:
* panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停);
* drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变);
@@ -1018,6 +1237,9 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
g_ncp_dnd = d;
}
/* M7.3:建内层 SDK 控件集(listview/双滑条/勿扰开关/芯片;owner 钉哨兵,仅首进一次)。 */
ncp_widgets_create();
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0
* 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */
ncp_refresh_rows();
@@ -1081,20 +1303,17 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
}
if (wak) {
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0; list_paint = 0;
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
sdk_swipe = 0; /* 唤醒:清行左滑手势(SDK swipe 内部状态亦重置) */
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
/* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */
anim_p = anim_target = anim_last_target = NCP_ANIM_FIX;
@@ -1110,23 +1329,20 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
}
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
int play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
} else {
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (play && !m_prev_play) {
logmsg("notif-center: play key caught (media-reserved, no exit)");
last_act = now;
@@ -1145,12 +1361,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
show_power_menu();
last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; need_paint = 1;
goto ncp_keys_seeded;
@@ -1169,14 +1384,14 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
}
}
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold;
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
m_prev_play = play; m_prev_pwr = pwr;
ncp_keys_seeded:
;
@@ -1201,42 +1416,27 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
}
/* M7.3:按下原点落在通知列表行 → 交 SDK 行左滑手势(begin 命中可滑行;
* 后续 move 由 SDK 做方向仲裁:横向=消费/纵向=让位滚动)。先同步 listview
* 滚动偏移(meow_listview_hit 用其内部 scroll)。 */
if (open_settled && !drag_slider && !panel_drag && g_ncp_widget_init && g_ncp_lv) {
meow_listview_set_scroll(g_ncp_lv, list_scroll);
sdk_swipe = meow_listview_swipe_begin(g_ncp_lv, tlx, tly);
}
}
if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
/* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop,
* 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。
* 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含
* row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大:
* 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
int ddx = tlx - tdx, ddy = tly - tdy;
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) {
int eff = tdy - ncp_list_top() + list_scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
/* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */
if (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
row >= 0 && row < g_nrows) {
row_swipe_row = row;
row_off = mnp_swipe_clamp(ddx, NCP_ROW_W);
row_settle = 0; row_off_target = 0;
row_anim_at = now_ms();
g_ncp_swipe_row = row; g_ncp_swipe_off = row_off;
row_paint = 1;
}
}
}
/* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */
if (row_swipe_row >= 0 && td_ok && !row_settle) {
row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
row_paint = 1;
/* ⑦ 行左滑(SDK swipe-delete):已交给 SDK 手势 → 喂 move。SDK 方向仲裁:
* 横向小位移超容差 → 消费(返回 1,行左移/删除区由 SDK 画,本触摸不滚/不拖面板);
* 纵向主导 → 取消本行 swipe(返回 0,sdk_swipe=0),回退到下方滚动/面板拖动。 */
if (sdk_swipe && td_ok && drag_slider == 0) {
meow_listview_set_scroll(g_ncp_lv, list_scroll);
if (!meow_listview_swipe_move(g_ncp_lv, tlx, tly)) sdk_swipe = 0;
}
/* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、
* 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y,
* 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */
if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) {
if (td_ok && !drag_slider && !scroll_drag && !sdk_swipe && !panel_drag) {
int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy);
if (ady2 >= S_SCROLL_SLOP) {
int av = g_nrows * NCP_ROW_H;
@@ -1260,7 +1460,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
/* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 +
* 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式);
* 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && !sdk_swipe) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
int total = g_nrows * NCP_ROW_H;
@@ -1290,10 +1490,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0;
/* ⑦ 新触摸:清行左滑(防上一手势残留) */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* ⑦ 新触摸:清行左滑手势(防上一手势残留) */
sdk_swipe = 0;
/* ⑨ 新触摸:清面板跟手(防上一拖动残留) */
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
}
@@ -1311,19 +1509,18 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
if (row_swipe_row >= 0) {
/* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。
* 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */
/* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */
row_off = mnp_swipe_clamp(dx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
if (mnp_swipe_delete(row_off, NCP_ROW_W,
NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) {
row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */
} else {
row_off_target = 0; /* 回弹复原 */
if (sdk_swipe && g_ncp_widget_init && g_ncp_lv) {
/* ⑦ 行左滑松手(SDK swipe-delete):消费 → 删除/回弹动画交 SDK
* (meow_anim_tick 推进;删除完成后 ncp_lv_on_delete 置 g_ncp_need_paint)。
* 本手势不关面板、不走 tap/上滑/点空白。未滑动(=点击某行)→ 未消费,
* 清 sdk_swipe 后走下方 tap 分支(ncp_row_tap 命中该行 → dismiss+关面板)。 */
meow_listview_set_scroll(g_ncp_lv, list_scroll);
if (meow_listview_swipe_end(g_ncp_lv, tlx, tly)) {
sdk_swipe = 0;
td_ok = 0;
goto ncp_release_done;
}
row_settle = 1; row_anim_at = now_ms();
sdk_swipe = 0;
} else if (scroll_drag) {
/* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值);
* 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。
@@ -1359,15 +1556,13 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
/* 菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
@@ -1395,20 +1590,28 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
if (!hit && ncp_clear_hit(tx, ty)) {
int n = ncp_clear_all();
logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n);
/* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* 清除后取消行左滑手势(下标会变),再重采集 + 重钳 + 整面板重画。 */
sdk_swipe = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
hit = 1; need_paint = 1;
}
/* 阶段D附录B:顶部媒体卡片按钮/进度(含 seek)→ 下发 PLAYCTL。
/* 阶段D附录B:顶部媒体卡片按钮/进度(含 seek)→ 下发 PLAYCTL;
* 卡片体 → 下发 FOCUS(7) 请求打开 App 播放页 + 收起面板(返回 2)。
* 卡片命中固定于列表上方,先于列表行命中处理。 */
if (!hit && meow_media_has_source() && ncp_media_card_tap(tx, ty)) {
hit = 1; need_paint = 1;
} else if (!hit && ty >= ncp_list_top() && ty < NCP_LIST_BOT) {
if (!hit && meow_media_has_source()) {
int mh = ncp_media_card_tap(tx, ty);
if (mh) {
hit = 1;
if (mh == 2) { /* 卡片体 FOCUS → 收起面板露出播放页 */
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
need_paint = 1; /* 按钮/进度:仅重绘卡片(面板保持开启) */
}
}
if (!hit && ty >= ncp_list_top() && ty < NCP_LIST_BOT) {
if (ncp_row_tap(tx, ty, list_scroll, click)) {
hit = 1;
if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */
@@ -1430,6 +1633,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
/* 其余横滑/下滑忽略(不关闭不动作) */
}
td_ok = 0;
ncp_release_done:
;
}
}
}
@@ -1453,10 +1658,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
media_paint = 1; /* 已有源:进度/封面随 META 变化 → 仅重绘卡片带 */
}
if (ncp_rows_changed()) {
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑手势,避免行下标错位。⑨ 同时取消面板跟手。 */
sdk_swipe = 0;
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
@@ -1490,41 +1693,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
}
if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */
/* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。
* 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。
* 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */
if (row_swipe_row >= 0 && row_settle) {
unsigned long long rnow = now_ms();
int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0);
row_anim_at = rnow;
int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE);
if (noff != row_off) {
row_off = noff;
g_ncp_swipe_off = noff;
row_paint = 1;
}
if (noff == row_off_target) {
if (row_off_target == 0) {
/* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */
ncp_repaint_row(list_scroll, row_swipe_row, 0);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */
} else {
/* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */
if (row_swipe_row >= 0 && row_swipe_row < g_nrows)
meow_notif_dismiss(g_rows[row_swipe_row].id);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */
}
}
}
/* M7.3:行左滑落位动画已由 SDK 接管(meow_listview_swipe_end → meow_anim → swipe_anim_done
* → on_delete(idx) → ncp_lv_on_delete 置 g_ncp_need_paint)。此处不再需要手绘行位移/回弹/滑出。 */
/* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。
* need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍
@@ -1561,14 +1731,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok);
drag_paint = 0;
}
/* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。
* row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */
if (row_paint) {
last_act = now;
if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target))
ncp_repaint_row(list_scroll, row_swipe_row, row_off);
row_paint = 0;
}
/* M7.3:行左滑跟手/回弹/滑出重绘已由 SDK 接管(swipe_anim_update 经 meow_widget_draw(lv)
* 重绘列表 + meow_anim_set_region 标该行带脏)。此处不再有 row_paint 手绘路径。 */
/* ~1s 状态栏时钟 */
if (now - last_tick >= 1000) {
last_tick = now;
@@ -1576,12 +1740,27 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
fb_norm_buf0();
meow_ui_flush();
}
/* M7.3:推进 SDK 动画(行左滑删除/回弹、meow_scroll fling 等)。左滑删除完成后 on_delete
* 置 g_ncp_need_paint → 下一帧整面板重画。swipe 动画推进时 SDK 已重绘列表(swipe_anim_update
* 调 meow_widget_draw(lv)),此处只把列表带标脏上屏(覆盖 SDK 经 meow_gfx 标脏的列表矩形)。 */
meow_anim_tick();
if (g_ncp_need_paint) { g_ncp_need_paint = 0; need_paint = 1; }
if (g_ncp_widget_init && g_ncp_lv && meow_listview_swipe_active(g_ncp_lv)) {
meow_dirty_add(0, ncp_list_top(), fb_xres, ncp_list_view());
meow_ui_flush();
}
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */
ncp_rows_free();
/* 封面缓存随面板关闭回收(对齐 Now Playing np_cleanup:每次下拉重新加载当前封面)。
* 旧码 g_ncp_media_cover_path 是文件级 static、从不重置——若首次加载失败/或 artwork 初
* 为空的瞬间进面板,其后即便封面落盘并因路径不变而被「按文本缓存」跳过重载,卡片永久
* 停留在「显示歌名」回退/旧封面。关面板即清缓存,保证下次下拉能拿到最新封面。 */
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
g_ncp_media_cover_path[0] = 0;
logmsg("notification center closed");
}
+604 -95
View File
@@ -10,43 +10,142 @@
* 元数据/源变化立即整页重画;交互只重绘按钮/进度带。
*/
#include "meow_launcher_internal.h"
#include "np_gesture.h" /* 批4:双页方向感知手势决策(header-only,宿主可单测) */
#include "meow_lyric_view.h" /* 批2控件:歌词页渲染(批4接入,数据批3 meow_media_lyric_*) */
#include "meow_img_color.h" /* 批5:封面双色取色原语(独立源码,不并入 meow_ui.a) */
#define NP_COVER 300 /* 封面边长(方,居中) */
#define NP_COVER_X ((fb_xres - NP_COVER) / 2)
#define NP_COVER_Y 60
#define NP_TITLE_Y 412
#define NP_ARTIST_Y 450
#define NP_ALBUM_Y 480
#define NP_COVER_Y 84 /* 用户 2026-09-04:控件组整体下移,消除底部大留白 */
#define NP_TITLE_Y 436
#define NP_ARTIST_Y 476
#define NP_ALBUM_Y 508
#define NP_PROG_X 40
#define NP_PROG_W (fb_xres - 80)
#define NP_PROG_Y 518
#define NP_TIME_Y 492
#define NP_BTN_Y 568 /* 控制按钮中心 y */
#define NP_PROG_Y 548
#define NP_TIME_Y 526
#define NP_BTN_Y 612 /* 控制按钮中心 y */
#define NP_PLAY_CX (fb_xres / 2)
#define NP_PREV_CX (NP_PLAY_CX - 78)
#define NP_NEXT_CX (NP_PLAY_CX + 78)
#define NP_PLAY_R 30
#define NP_PREV_R 22
#define NP_LYRIC_Y 640
/* 批② home 指示条(底部上滑回桌面):位于屏底左缘、避开左缘返回区(NP_EDGE_W=24)。
* 与 np_gesture.h np_home_gesture 的起滑区一致(down_x>=NP_EDGE_W 才认 home),保证
* 「指示条可触发手势」自洽。y 距屏底 16px,高 6px(镜像 SDK meow_ui_swipe_hint 提示线高一档)。
*/
#define NP_HOME_BAR_X (NP_EDGE_W + 8) /* 左缘 32:紧贴左缘返回区右侧 */
#define NP_HOME_BAR_W 48
#define NP_HOME_BAR_H 6
#define NP_HOME_BAR_Y (fb_yres - 16)
static meow_img *g_np_cover = NULL;
static char g_np_cover_path[200] = "";
/* 封面解码缓存(artwork 路径变化才重载)。 */
/* ---- 批4:Now Playing 双页状态 + 歌词页控件 ----
* 同一容器两页:NP_PAGE_PLAY(播放页,原有内容)+ NP_PAGE_LYRIC(歌词页,批2 meow_lyric_view)。
* 页面切换靠方向感知手势(np_gesture.h);歌词数据批3(meow_media_lyric_*),主题先走 meow_theme
* 默认;meow_lyric_view_set_colors 为批5 封面取色动态主题预留(本批不取色,见 np_lyric_refresh)。 */
static int g_np_page = NP_PAGE_PLAY;
static meow_lyric_view *g_np_lyric = NULL; /* 歌词视图(lazy 建一次,跨进入复用) */
static uint32_t g_np_lyric_seq = 0xFFFFFFFFu; /* 上次解析的歌曲代际(新 seq 才重解析) */
static char g_np_lyric_app[MEOW_APP_ID_MAX] = ""; /* 上次解析的源 app_id */
static int g_np_lyric_last_valid = -1; /* -1=未初始化 0=无词 1=有词(防重复 clear) */
/* 批4:歌词页宽度测量(launcher ui_width 包装成 meow_lyric_width_fn;meow_lyric_view 需要)。 */
static int np_lyric_width(const char *u, int size, void *ctx)
{
(void)ctx;
return ui_width(u, size);
}
/* 封面解码缓存(artwork 路径变化才重载;路径未变但上次加载失败→允许重试)。
* M6.33 修「artwork 空→复原」封面不重载:原「路径未变即跳过」在「空 artwork 清空封面后再
* 以同路径复原」或「首载失败同路径重报」时封面永不恢复(g_np_cover==NULL 时即使路径未变也重载)。 */
static void np_cover_load(const meow_media_session_t *s)
{
if (!s) return;
if (strcmp(g_np_cover_path, s->artwork) == 0) return;
if (g_np_cover && strcmp(g_np_cover_path, s->artwork) == 0) return; /* 已载且未变 → 跳过 */
if (!s->artwork[0]) return; /* 用户 2026-09-04:空 artwork(如打开歌单清 g_cover_path 致 META 报空)
* 时保留上一张已解码封面,勿据此清封面 → 播放页/歌词页封面不闪没 */
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
snprintf(g_np_cover_path, sizeof g_np_cover_path, "%s", s->artwork);
if (!s->artwork[0]) return;
char path[200];
char path[240]; /* 相对 artwork 加前缀最坏 191+19=210 < 240,防 -Wformat-truncation 实际截断 */
if (s->artwork[0] == '/') snprintf(path, sizeof path, "%s", s->artwork);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", s->artwork);
g_np_cover = meow_img_load_file(path);
}
/* ==================================================================
* 批5:封面双色取色(主色+次色)→ 播放页/歌词页动态主题。
* - 何时取:进入 Now Playing 页 / 封面路径变化时(np_cover_theme_update 幂等,按路径缓存)。
* - 取色成本:meow_img_duocolor 内部自适副采样(等效缩略图,≤~3.6k 样本),不额外解码/分配。
* - 缓存:按 artwork 路径;路径未变不重取(g_np_cc_valid + g_np_cc_path)。
* - 回退:无封面/取色失败(g_np_cc_valid=0)→ 歌词页 set_colors(NULL) 用 meow_theme 默认;
* 播放页背景/文字走 s_ui_theme() 主题面。
* ================================================================== */
static meow_img_color_theme g_np_cc;
static int g_np_cc_valid = 0;
static char g_np_cc_path[200] = "";
/* 校验/刷新取色缓存(封面路径变化才重取;幂等)。 */
static void np_cover_theme_update(void)
{
const meow_media_session_t *s = meow_media_current();
const char *art = s ? s->artwork : "";
if (g_np_cc_valid && strcmp(g_np_cc_path, art) == 0) return; /* 缓存命中 */
snprintf(g_np_cc_path, sizeof g_np_cc_path, "%s", art);
g_np_cc_valid = 0;
if (!g_np_cover || !art[0]) return; /* 无封面 → 回退主题 */
uint32_t mainc = 0, secondc = 0;
if (meow_img_duocolor(g_np_cover, &mainc, &secondc) != 0) return; /* 取色失败 → 回退主题 */
meow_img_color_derive(mainc, secondc, meow_theme_get(), &g_np_cc);
g_np_cc_valid = 1;
}
/* 把封面取色注入歌词页(无取色 → NULL 回退 meow_theme 默认)。 */
static void np_apply_lyric_colors(void)
{
if (!g_np_lyric) return;
meow_lyric_view_set_colors(g_np_lyric, g_np_cc_valid ? &g_np_cc.lyr : NULL);
}
/* 渐变插值(无 GPU,逐行整数;封面主题背景用)。返回 [y0, y0+h) 带内屏幕行 y 的颜色。 */
static uint32_t np_grad_row(uint32_t top, uint32_t bottom, int y0, int h, int y)
{
int t = (h <= 1) ? 0 : (y - y0) * 255 / (h - 1);
if (t < 0) t = 0; else if (t > 255) t = 255;
int tr = (top >> 16) & 255, tg = (top >> 8) & 255, tb = top & 255;
int br = (bottom >> 16) & 255, bg = (bottom >> 8) & 255, bb = bottom & 255;
int r = tr + ((br - tr) * t + 128) / 255;
int g = tg + ((bg - tg) * t + 128) / 255;
int b = tb + ((bb - tb) * t + 128) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
/* 竖向双色渐变填充(封面主题背景用;整页/局部共用 np_grad_row,保证局部重绘与整页渐变无缝)。 */
static void np_fill_gradient(int x, int y, int w, int h, uint32_t top, uint32_t bottom)
{
if (w <= 0 || h <= 0) return;
for (int yy = 0; yy < h; yy++)
fill_rect(x, y + yy, w, 1, np_grad_row(top, bottom, y, h, y + yy));
}
/* 播放页当前主题 · 页背景行色:内容区(状态栏下)屏幕行 y 的背景色。
* 取色有效 → 取色渐变(按整页 span 插值:局部重绘进度带逐行取色与整页渐变无缝衔接);
* 取色无效 → 返回 0(调用方回退 meow_theme 面)。 */
static uint32_t np_page_bg_row(int y)
{
if (!g_np_cc_valid) return 0;
return np_grad_row(g_np_cc.bg_top, g_np_cc.bg_bottom, SB_BAR_H, fb_yres - SB_BAR_H, y);
}
#if 0 /* ================================================================
* M7.0b 退役:Now Playing 手绘控制钮/进度条改走 SDK 控件(meow_iconbtn +
* meow_progress),命中/状态/进度全由 SDK 接管;保留作回退位(删除 #if 0 行恢复旧手绘)。
* ================================================================ */
/* 实心三角(dir=+1 右向 / -1 左向)。 */
static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
{
@@ -59,25 +158,6 @@ static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
}
}
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。 */
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
int W = fb_xres;
if (ui_width(s, size) <= maxw) {
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
return;
}
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
}
/* 圆形控制按钮(kind:0=prev 1=play 2=next 3=pause)。 */
static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
@@ -97,22 +177,268 @@ static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
fill_rect(cx + 8 - bw, cy - (r - 7), bw, (r - 7) * 2, fg);
}
}
#endif /* M7.0b 退役:Now Playing 旧手绘控制钮 */
/* 进度带(进度条 + 时间)绘制;pts = 当前显示位置。 */
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。(M7.0b 保留:封面/歌名/艺术家仍 SD) */
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
int W = fb_xres;
if (ui_width(s, size) <= maxw) {
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
return;
}
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
}
/* 左对齐 + 超宽截断(UTF-8 安全,近似按字节比例切;歌词页顶部元信息用)。 */
static void np_left_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
if (ui_width(s, size) <= maxw) {
ui_draw(x, y, s, color, size);
return;
}
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw(x, y, buf, color, size);
}
/* 歌词页顶部元信息(两行居左,复刻 ncm 歌词页):第一行歌名(大字白)、第二行歌手(小字灰)。
* 多歌手已由源侧拼串(ncm art_name 即「A / B」),此处原样显示 + 超宽截断。
* 左边距与歌词正文一致(LYR_PAD=24);离状态栏留边距(SB_BAR_H + 20);右缘同距截断。 */
static void np_draw_lyric_title(const meow_media_session_t *s)
{
if (!s) return;
if (!s->title[0] && !s->artist[0]) return;
int pad = 24;
int y = SB_BAR_H + 20;
int maxw = fb_xres - 2 * pad;
if (s->title[0])
np_left_text(pad, y, s->title, 28, 0xFFFFFF, maxw);
if (s->artist[0])
np_left_text(pad, y + meow_font_line_height(28) + 6, s->artist, 20, 0xBBBBBB, maxw);
}
/* 歌词页强制 ncm 配色:活动行恒纯白 + 上下逐级灰阶(对齐 ncm lyr_gray_tbl
* {FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}——gray[0]=0 按 cur=0xFFFFFF 自动递减即为该表)。
* 背景恒为 np_draw_lyric_page 铺的黑色半透明叠加层,故关掉 lyric_view 自带 scrim
* (scrim_alpha=0)避免双重压暗;当前行/灰阶不随主题/封面取色(保黑底对比度,夜间可读)。 */
static void np_set_lyric_ncm_colors(void)
{
if (!g_np_lyric) return;
meow_lyric_view_colors c;
memset(&c, 0, sizeof c);
c.scrim = 0x000000;
c.cur = 0xFFFFFF;
c.scrim_alpha = 0; /* 不画 scrim:由 launcher 黑底铺(单一叠加层) */
meow_lyric_view_set_colors(g_np_lyric, &c);
}
/* ==================================================================
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
* - 播放控制三钮 → meow_iconbtn(PREV / PLAY_ARROW|PAUSE / NEXT;按下=PAUSE、暂停=PLAY_ARROW,
* 同位置叠两钮、按当前状态只画其一);
* - 命中走 meow_widget_hit(圆形,owner==哨兵 + enabled 闸门)→ meow_media_playctl 分发;
* - 封面(meow_img contain)与歌名/艺术家(meow_font)不动(本就 SD),仅文案走 meow_str。
* 控件 lazy 创建一次(owner=NP_SENTINEL_ACT),跨多次进入复用;run_now_playing() 进入 push
* 哨兵、退出 pop(owner 闸门使控件命中生效)。
* ================================================================== */
#define NP_SENTINEL_ACT 0x51 /* Now Playing 哨兵 activity */
static meow_widget *g_np_prog = NULL;
static meow_widget *g_np_prev = NULL, *g_np_play = NULL, *g_np_pause = NULL, *g_np_next = NULL;
static int g_np_init = 0;
static void np_widgets_create(void)
{
meow_ui_activity_push(NP_SENTINEL_ACT);
g_np_prog = meow_progress_create(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, 0);
g_np_prev = meow_iconbtn_create(NP_PREV_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_PREV);
g_np_play = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PLAY_ARROW);
g_np_pause = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PAUSE);
g_np_next = meow_iconbtn_create(NP_NEXT_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_NEXT);
/* 主播放钮保持「强调动作」观感:底色=primary、图标=onPrimary;其余两钮默认 surfaceContainerLow。 */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
/* 批4:歌词页视图(覆盖状态栏下整页;宽测量走 launcher ui_width 包装)。
* set_colors 留作批5 封面取色覆盖入口;本批用 meow_theme 默认(不调用)。 */
g_np_lyric = meow_lyric_view_create(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H,
np_lyric_width, NULL);
if (g_np_lyric)
meow_lyric_view_set_empty(g_np_lyric, meow_str_zh("np.no_lyric", "暂无歌词"));
meow_ui_activity_pop();
g_np_init = 1;
}
/* 播放页当前主题 → 控件修饰(整页与局部重绘共用同一次派生,避免两处取色逻辑漂移)。
* 进度条 meow_progress:opts.bg=轨道、opts.fg=填充(批5b W_PROGRESS 新增支持);
* 控制钮 meow_iconbtn:opts.bg=圆底、opts.fg=图标墨色。
* 取色有效(g_np_cc_valid):进度条轨道=fg_dim/填充=accent,三钮圆底=accent、图标=accent_fg;
* 取色无效:全部清 opts 回主题默认(进度条=surfaceVariant/primary、play/pause=primary/onPrimary、
* prev/next=surfaceContainerLow/onSurface——与创建时一致,不回归)。
* 每次绘制前调用(np_draw_page 播放分支 / np_repaint_progress),保证主题跟手。 */
static void np_theme_widgets(void)
{
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
if (g_np_cc_valid) {
op.bg = g_np_cc.fg_dim; op.fg = g_np_cc.accent; /* 进度条:track=fg_dim / fill=accent */
if (g_np_prog) meow_widget_set_opts(g_np_prog, &op);
op.bg = g_np_cc.accent; op.fg = g_np_cc.accent_fg; /* 三钮:圆底=accent / 图标=accent_fg */
if (g_np_prev) meow_widget_set_opts(g_np_prev, &op);
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
if (g_np_next) meow_widget_set_opts(g_np_next, &op);
return;
}
/* 取色无效 → 默认主题(回退):清 prog/prev/next 的 opts,play/pause 回主题 primary。 */
memset(&op, 0, sizeof op);
if (g_np_prog) meow_widget_set_opts(g_np_prog, &op);
if (g_np_prev) meow_widget_set_opts(g_np_prev, &op);
if (g_np_next) meow_widget_set_opts(g_np_next, &op);
if (g_np_play || g_np_pause) {
op.bg = th->primary; op.fg = th->onPrimary; /* 主播放钮:强调动作 */
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
}
}
/* 绘制播放控制三钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时
* 上一帧残留被新画圆底盖掉)。控件配色在调用前由 np_theme_widgets 统一挂载取色主题。 */
static void np_draw_ctl(const meow_media_session_t *s)
{
if (g_np_prev) { meow_widget_set_selected(g_np_prev, 0); meow_widget_draw(g_np_prev); }
if (g_np_next) { meow_widget_set_selected(g_np_next, 0); meow_widget_draw(g_np_next); }
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
meow_widget *pb = playing ? g_np_pause : g_np_play;
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
}
/* 进度带(进度条 + 时间)绘制;pos = 当前显示位置。进度条走 SDK meow_progress。 */
static void np_draw_progress(const meow_media_session_t *s, uint32_t pos, const meow_ui_theme_t *th)
{
fill_rect(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, th->outline);
int fw = (s->dur_ms > 0) ? (int)((uint64_t)pos * NP_PROG_W / s->dur_ms) : 0;
if (fw > NP_PROG_W) fw = NP_PROG_W;
if (fw > 0) fill_rect(NP_PROG_X, NP_PROG_Y, fw, 6, th->primary);
int pct = (s->dur_ms > 0) ? (int)((uint64_t)pos * 100 / s->dur_ms) : 0;
if (pct > 100) pct = 100;
if (g_np_prog) { meow_progress_set(g_np_prog, pct); meow_widget_draw(g_np_prog); }
/* 时间(左=当前,右=总长;MM:SS) */
char l[16], rbuf[16];
unsigned lm = pos / 60000u, ls = (pos % 60000u) / 1000u;
unsigned dm = (s->dur_ms ? s->dur_ms : 0) / 60000u, ds = (s->dur_ms ? s->dur_ms : 0) % 60000u / 1000u;
snprintf(l, sizeof l, "%u:%02u", lm, ls);
snprintf(rbuf, sizeof rbuf, "%u:%02u", dm, ds);
ui_draw(NP_PROG_X, NP_TIME_Y, l, th->on_surface_variant, 16);
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, th->on_surface_variant, 16);
uint32_t tcol = g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant;
ui_draw(NP_PROG_X, NP_TIME_Y, l, tcol, 16);
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, tcol, 16);
}
/* ==================================================================
* 批4:歌词页渲染(批2 meow_lyric_view + 数据批3 meow_media_lyric_*)。
* - 有词:meow_lyric_parse(lrc,tlrc)→meow_lyric_view_set_pos(display_pos) 实时滚动,
* 当前行由 meow_lyric_view 内部 meow_lyric_line_at(pos_ms) 计算(与绘制/点跳同源)。
* - 无词/无源:清空并置 pos=0,视图内建空态文案(np.no_lyric→"暂无歌词")。
* 主题:先走 meow_theme 默认(视图自绘 scrim/活动行纯白/灰阶递减);批5 封面取色动态主题的可
* 接入点为 meow_lyric_view_set_colors(本批不取色,故不调用;接入只需在下方填 meow_lyric_view_colors
* 后调用一次,字段 0=回主题默认)。
* ================================================================== */
static void np_lyric_refresh(const meow_media_session_t *s)
{
if (!g_np_lyric) return;
int valid = meow_media_lyric_valid(s);
if (valid) {
/* 换歌/换源(新 lyric_seq/app_id)才重解析;否则沿用,只推进位置。 */
if (g_np_lyric_seq != s->lyric_seq || strcmp(g_np_lyric_app, s->app_id) != 0) {
meow_lyric_parse(meow_media_lyric_lrc(s), meow_media_lyric_tlrc(s));
g_np_lyric_seq = s->lyric_seq;
snprintf(g_np_lyric_app, sizeof g_np_lyric_app, "%s", s->app_id);
}
meow_lyric_view_set_pos(g_np_lyric, meow_media_display_pos(s));
} else {
/* 无词/无源:仅当上次仍有词才清空(省冗余 memset),置 pos=0。 */
if (g_np_lyric_last_valid != 0) {
meow_lyric_clear();
g_np_lyric_seq = 0xFFFFFFFFu;
g_np_lyric_app[0] = 0;
}
meow_lyric_view_set_pos(g_np_lyric, 0);
}
g_np_lyric_last_valid = valid;
}
/* 批② home 指示条:底部上滑回桌面的可见反馈(镜向左缘返回提示条;唯位置=屏底左缘)。
* 主题用 on_surface_variant(淡灰指示,iOS home 指示同款语义);draw 在整页/空态/歌词页
* 均调用一次(np_draw_page 播放分支/空态分支 + np_draw_lyric_page)。 */
static void np_draw_home_bar(void)
{
const meow_ui_theme_t *th = s_ui_theme();
meow_gfx_fill_round(NP_HOME_BAR_X, NP_HOME_BAR_Y, NP_HOME_BAR_W, NP_HOME_BAR_H,
NP_HOME_BAR_H / 2, th->on_surface_variant);
}
/* 网易云式歌词页封面背景(用户 2026-09-05):有封面 → 等比居中放大铺满内容区(cover-fit,
* 超出部分由画布裁剪,镜像 ncm plr_draw_lyrics 的 meow_ui_img_scaled 算法);无封面 → 纯黑底
* (现状回退)。本函数只铺底;压暗由调用方随后叠黑色半透明叠加层(alpha176)完成。 */
static void np_draw_lyric_cover_bg(int W, int H)
{
int x = 0, y = SB_BAR_H, cw = W, ch = H - SB_BAR_H; /* 只盖内容区(状态栏下),与播放页一致 */
if (!g_np_cover || meow_img_w(g_np_cover) <= 0 || meow_img_h(g_np_cover) <= 0) {
fill_rect(x, y, cw, ch, 0x000000); /* 无封面 → 纯黑底(回退现状) */
return;
}
int iw = meow_img_w(g_np_cover), ih = meow_img_h(g_np_cover);
long dw = cw, dh = (long)ih * cw / iw; /* cover-fit:铺满宽度,高按等比 */
if (dh < ch) { dh = ch; dw = (long)iw * ch / ih; } /* 宽度不足再铺满高度(超出裁剪) */
meow_ui_img_scaled(x + (cw - (int)dw) / 2, y + (ch - (int)dh) / 2, (int)dw, (int)dh, g_np_cover);
}
/* 歌词页整页绘制:网易云式封面背景(封面图当底 + 黑色半透明叠加层压暗,保证歌词对比度)→
* lyric_view(自绘 scrim 关闭,仅歌词/空态)→ 顶部元信息(歌名/歌手)。 */
static void np_draw_lyric_page(int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
if (!g_np_init) np_widgets_create();
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
/* 批5:np_cover_theme_update 取色(仅为播放页文字/按钮主题;歌词页背景不走取色,恒 ncm 白/灰)。
* 批6:有封面时以封面作整页背景(网易云式 cover-fit 铺满,见 np_draw_lyric_cover_bg),
* 无封面回退纯黑底;压暗由下方黑色半透明叠加层(alpha176)统一完成,lyric_view 关内置 scrim。
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
np_cover_load(meow_media_current());
np_cover_theme_update();
/* 网易云式封面背景(用户 2026-09-05):先铺封面底(cover-fit 原图铺满),无封面回退纯黑底;
* 随后统一叠黑色半透明叠加层(alpha176)压暗,保证歌词对比度。歌词配色仍恒 ncm 白/灰
* (不随封面取色——取色仅服务播放页文字/按钮)。歌词行布局/滚动逻辑不变,只换背景。 */
np_draw_lyric_cover_bg(W, H);
np_set_lyric_ncm_colors();
/* 用户 2026-09-04:黑色半透明叠加层(alpha176)——压暗封面底,保证歌词可读;无封面时叠于
* 纯黑底之上(仍纯黑,即现状回退)。 */
meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176);
np_lyric_refresh(meow_media_current());
if (g_np_lyric)
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
np_draw_lyric_title(meow_media_current()); /* 顶部元信息(歌名/歌手,居左,避状态栏) */
np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */
if (redraw_sb) draw_statusbar();
}
/* 整页绘制(空态占位)。redraw_sb=1 时喂/标脏状态栏(打开/主题变化),否则保留上次顶条。 */
@@ -120,21 +446,36 @@ static void np_draw_page(int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
if (!g_np_init) np_widgets_create(); /* M7.0b:lazy 建控件(跨进入复用) */
/* 批4:歌词页整页走 lyric_view(无源/无词空态由视图内建)。 */
if (g_np_page == NP_PAGE_LYRIC) {
np_draw_lyric_page(redraw_sb);
return;
}
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) {
/* 空态占位 */
const char *t1 = "未在播放";
/* 空态占位(M7.0b 文案走 meow_str,未知 id 回退中文) */
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
const char *t1 = meow_str_zh("np.not_playing", "未在播放");
ui_draw((W - ui_width(t1, 26)) / 2, 300, t1, th->on_surface_variant, 26);
const char *t2 = "打开播放 App 以显示正在播放";
const char *t2 = meow_str_zh("np.open_app_hint", "打开播放 App 以显示正在播放");
ui_draw((W - ui_width(t2, 18)) / 2, 348, t2, th->on_surface_variant, 18);
const char *t3 = "左缘右滑返回";
const char *t3 = meow_str_zh("np.edge_back", "左缘右滑返回");
ui_draw((W - ui_width(t3, 16)) / 2, 396, t3, th->on_surface_variant, 16);
np_draw_home_bar(); /* 批②:空态也示 home 指示(底部上滑可回桌面) */
if (redraw_sb) draw_statusbar();
return;
}
np_cover_load(s);
/* 批5:封面取色(主/次色)+ 背景渐变或主题面;并注入歌词页 set_colors。
* np_cover_theme_update 幂等(封面路径不变不重取);无封面/取色失败回退 meow_theme。 */
np_cover_theme_update();
if (g_np_cc_valid)
np_fill_gradient(0, SB_BAR_H, W, H - SB_BAR_H, g_np_cc.bg_top, g_np_cc.bg_bottom);
else
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
np_apply_lyric_colors();
/* 封面:解码缓存 + 圆角裁方;无封面回退主题色占位 + 首字 */
if (g_np_cover) {
meow_gfx_fill_round(NP_COVER_X - 6, NP_COVER_Y - 6, NP_COVER + 12, NP_COVER + 12, 18, th->surface_container);
@@ -145,56 +486,59 @@ static void np_draw_page(int redraw_sb)
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 22);
ui_draw(NP_COVER_X + (NP_COVER - cw) / 2, NP_COVER_Y + (NP_COVER - meow_font_line_height(22)) / 2,
t, 0xFFFFFF, 22);
t, meow_theme_get()->onPrimary, 22);
}
/* 元数据 */
np_center_text(NP_TITLE_Y, s->title, 26, th->on_surface, W - 40);
np_center_text(NP_ARTIST_Y, s->artist, 18, th->on_surface_variant, W - 60);
/* 元数据(批5:封面取色时文字随 fg/fg_dim,否则主题面) */
np_center_text(NP_TITLE_Y, s->title, 26, g_np_cc_valid ? g_np_cc.fg : th->on_surface, W - 40);
np_center_text(NP_ARTIST_Y, s->artist, 18, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 60);
if (s->album[0])
np_center_text(NP_ALBUM_Y, s->album, 16, th->on_surface_variant, W - 80);
/* 进度 */
np_center_text(NP_ALBUM_Y, s->album, 16, g_np_cc_valid ? g_np_cc.fg_dim : th->on_surface_variant, W - 80);
/* 批5b:控件随播放页当前主题(进度条轨道/填充 + 三钮)——整页与局部重绘共用同一次派生。 */
np_theme_widgets();
/* 进度(SDK meow_progress) */
uint32_t pos = meow_media_display_pos(s);
np_draw_progress(s, pos, th);
/* 控制按钮 */
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1;
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, pk, th);
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
/* 歌词占位 */
const char *ly = "歌词(占位)";
ui_draw((W - ui_width(ly, 16)) / 2, NP_LYRIC_Y, ly, th->on_surface_variant, 16);
/* 控制按钮(SDK meow_iconbtn,命中/状态走 SDK) */
np_draw_ctl(s);
/* 播放页底部(M6.33:歌词占位已移除——已有独立歌词页,占位多余;批②:底部仅示
* home 指示条,非歌词占位)。 */
np_draw_home_bar(); /* 批②:播放页示 home 指示(底部上滑可回桌面) */
if (redraw_sb) draw_statusbar();
}
/* 仅重绘进度带 + 按钮(元数据/源变化时整页;纯进度变化时局部)。 */
/* 仅重绘进度带 + 按钮(元数据/源变化时整页;纯进度变化时局部)。
* 批5b:局部重绘也走播放页当前主题——进度带/按钮带底色用取色渐变(np_page_bg_row 与整页同 span
* 无缝衔接),进度条/三钮经 np_theme_widgets 挂取色色;无取色回退 meow_theme 面/默认控件色。 */
static void np_repaint_progress(void)
{
const meow_ui_theme_t *th = s_ui_theme();
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) return;
np_cover_theme_update(); /* 取色当前主题(幂等,封面路径未变不重取) */
uint32_t pos = meow_media_display_pos(s);
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏)。 */
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏);底色随取色渐变或主题面。 */
if (g_np_cc_valid) {
for (int yy = NP_TIME_Y; yy < NP_BTN_Y + NP_PLAY_R; yy++)
fill_rect(0, yy, fb_xres, 1, np_page_bg_row(yy));
} else {
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
}
np_theme_widgets(); /* 进度条/按钮随取色主题(或回退默认) */
np_draw_progress(s, pos, th);
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1, th);
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
np_draw_ctl(s);
meow_dirty_add(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y);
meow_ui_flush();
}
/* 控制按钮命中:返回 cmd(1-4),0=未命中。 */
/* 控制按钮命中:返回 cmd(1-4),0=未命中。走 SDK 圆形命中(owner==哨兵 + enabled 闸门)。
* 播放/暂停两钮同位:命中任一(同矩形)→ 按会话状态分派 play(1)/pause(2)。 */
static int np_btn_tap(int tx, int ty)
{
if (tx >= NP_PREV_CX - NP_PREV_R && tx <= NP_PREV_CX + NP_PREV_R &&
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 3;
if (tx >= NP_PLAY_CX - NP_PLAY_R && tx <= NP_PLAY_CX + NP_PLAY_R &&
ty >= NP_BTN_Y - NP_PLAY_R && ty <= NP_BTN_Y + NP_PLAY_R) {
const meow_media_session_t *s = meow_media_current();
const meow_media_session_t *s = meow_media_current();
if (g_np_prev && meow_widget_hit(g_np_prev, tx, ty)) return 3;
if (g_np_play && meow_widget_hit(g_np_play, tx, ty))
return (s && s->state == MEOW_MEDIA_PLAYING) ? 2 : 1;
}
if (tx >= NP_NEXT_CX - NP_PREV_R && tx <= NP_NEXT_CX + NP_PREV_R &&
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 4;
if (g_np_next && meow_widget_hit(g_np_next, tx, ty)) return 4;
return 0;
}
@@ -211,32 +555,79 @@ static int64_t np_progress_seek(int tx, int ty)
return 0;
}
void run_now_playing(void)
/* 播放页按键动作应用(音量 + 播放切换)。语义与主界面/子页一致:
* - 音量键 = 系统音量(settings_volume_adj 内含插孔消抖 + 线性渐变);
* - 播放键 = 媒体播放/暂停切换(路由最近活跃媒体会话,与主界面同判);
* 边沿由调用方按防抖后稳定值计算(vold_edge = 稳定按下 ∧ 上一稳定为抬起)。
* 抽成独立小函数供宿主单测(np_apply_key_actions 仅依赖 settings_volume_adj/
* meow_media_playctl/now_ms/logmsg,可 -ffunction-sections 链接隔离)。 */
static void np_apply_key_actions(int vold_edge, int volu_edge, int play_edge,
const meow_media_session_t *s,
unsigned long long *last_input)
{
if (vold_edge) {
settings_volume_adj(-settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("np: volume DOWN <- ramp step=%d", settings_volume_step);
} else if (volu_edge) {
settings_volume_adj(+settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("np: volume UP <- ramp step=%d", settings_volume_step);
}
if (play_edge) {
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
if (last_input) *last_input = now_ms();
}
}
void run_now_playing(struct app *apps, int napps)
{
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
struct keypoll kp_keys;
kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_play;
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
kp_open(&kp_power, DEV_POWER);
struct kd_state kd_play; /* 音量键防抖/基线上收 key_router(进程唯一) */
memset(&kd_play, 0, sizeof kd_play);
/* 读播放键按住态(用于边沿) */
int m_prev_play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
}
int np_prev_power = 0; /* 电源键边沿跟踪 */
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1;
key_router_rebase(); /* 进入 Now Playing:音量基线对齐当前 raw(防残留边沿) */
unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
unsigned long long last_prog = now_ms();
unsigned long long np_last_status_tick = now_ms(); /* M6.20:播放页状态栏时钟 1s 刷新(主界面同语义) */
int need_redraw = 1;
int np_exit = 0; /* M6.22:返回手势命中 → 置位退出外层主循环。触摸内的 break 只跳出内层
* 读事件 while,不退出外层 while(!sigterm_flag),故必须靠标志跳出(根因:返回「没反应」)。 */
char last_title[128] = "";
uint32_t last_state = 0xFFFFFFFFu;
char last_app[MEOW_APP_ID_MAX] = "";
char last_art[200] = ""; /* M6.33:上次已刷新封面 artwork 路径(artwork 变化也触发整页重绘) */
meow_ui_activity_push(NP_SENTINEL_ACT); /* M7.0b:进入 push 哨兵(与函数尾 pop 配对)。
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
* owner==active 闸门——漏 push 则 active()!=0x51,
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
key_router_page_enter(NP_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
g_np_lyric_app[0] = 0;
np_draw_page(1);
meow_dirty_full();
meow_ui_flush();
while (!sigterm_flag) {
if (np_exit) break; /* M6.22:返回手势已命中 → 退出本页,走尾部收尾回主界面 */
meow_ipc_process();
meow_media_tick();
unsigned long long now = now_ms();
@@ -256,22 +647,79 @@ void run_now_playing(void)
if (td && !td_ok && tlx >= 0 && tly >= 0) { tdx = tlx; tdy = tly; td_ok = 1; }
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() >= touch_cooldown_until) { td = 1; td_ok = 0; tlx = tly = -1; }
if (now_ms() >= touch_cooldown_until) { td = 1; td_ok = 0; tlx = tly = -1;
np_last_input = now_ms(); }
} else if (!val && td) {
td = 0;
if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
/* 左缘右滑返回(全局手势语义):回到主界面(不清理媒体会话) */
if (meow_edge_back(tdx, dx, dy)) break;
if (adx < 60 && ady < 60) {
int cmd = np_btn_tap(tlx, tly);
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
else {
int64_t seek = np_progress_seek(tlx, tly);
if (seek > 0) meow_media_playctl(5, seek);
/* M6.21 诊断 + 批4 页感知:打印该次抬手手势的完整输入与按当前页
* 的方向感知判定(真机定位「返回滑不出」根因曾依赖此日志;
* 保留,键改为 ges=np_gesture_decide 返回值)。 */
np_gest_t gnp = np_gesture_decide(g_np_page, tdx, tdy, dx, dy);
/* 批②:底部上滑(home)优先——该手势竖向主导、起点在底部起滑区,
* 与左缘返回(横向+左缘起点)/顶部下滑(顶部热区)互斥,故先判 home
* 覆盖(np_gesture_decide 对纵滑返回 NONE,不冲突)。 */
if (np_home_gesture(tdx, tdy, dx, dy, fb_yres))
gnp = NP_GEST_HOME;
logmsg("np up: page=%d down=(%d,%d) d=(%d,%d) adx=%d ady=%d ges=%d",
g_np_page, tdx, tdy, dx, dy, adx, ady, (int)gnp);
switch (gnp) {
case NP_GEST_TOP:
/* 顶部热区下滑 → 打开通知中心(与主界面同语义;mnp_top_swipe_down
* 内部已排除左缘 down_x<24,与下方返回手势互斥)。run_notif_center
* 为阻塞模态,返回后整页重画。 */
{
notif_click_t click;
memset(&click, 0, sizeof click);
click.app_idx = -1;
click.status = NOTIF_CLICK_STATUS_NONE;
run_notif_center(apps, napps, &click, 0, 0, 0);
need_redraw = 1;
meow_dirty_full();
meow_ui_flush();
}
break;
case NP_GEST_EXIT:
/* 播放页左缘右滑 → 回桌面(保留 M6.26 全局手势语义)。 */
logmsg("np: BACK break (exit, page=%d)", g_np_page);
np_exit = 1;
break;
case NP_GEST_HOME:
/* 批②:底部上滑 → 回桌面(后台)。媒体会话续播/进程保活归批①
* keepalive(本页退出不改保活核心),此处仅走回桌面的既有路径。 */
logmsg("np: HOME bottom-swipe-up -> back to desktop (bg)");
np_exit = 1;
break;
case NP_GEST_NEXT:
/* 播放页 右→左滑 → 进歌词页。 */
g_np_page = NP_PAGE_LYRIC;
need_redraw = 1;
logmsg("np: -> lyric page");
break;
case NP_GEST_PREV:
/* 歌词页 左→右滑 → 回播放页(含左缘;不接桌面,避免误退)。 */
g_np_page = NP_PAGE_PLAY;
need_redraw = 1;
logmsg("np: -> play page");
break;
case NP_GEST_TAP:
/* 近点按(按页分派):播放页=三钮/进度看点;歌词页预留点跳
* (本批不启用,见 batch5/后续:借 meow_lyric_view_hit_line + on_seek)。 */
if (g_np_page == NP_PAGE_PLAY) {
int cmd = np_btn_tap(tlx, tly);
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
else {
int64_t seek = np_progress_seek(tlx, tly);
if (seek > 0) meow_media_playctl(5, seek);
}
}
break;
default:
break; /* NP_GEST_NONE:反向/纵向主导,忽略不误切 */
}
if (np_exit) break; /* M6.22:返回手势已命中 → 跳出内层读事件 while */
tdx = tdy = -1;
} else { tdx = tdy = -1; }
}
@@ -280,31 +728,73 @@ void run_now_playing(void)
}
}
/* 播放键边沿 → 播放/暂停切换 */
/* 播放键/音量键边沿 → 媒体播放/暂停切换 + 系统音量(与主界面/子页一致)。
* 修复:run_now_playing 原先只接播放/电源键,未接音量键 → 播放页/歌词页(同一循环,
* 与页无关)音量键无反应。 */
key_evt_t kev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_keys, NULL, NULL);
int play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (play && !m_prev_play) {
const meow_media_session_t *s = meow_media_current();
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
}
(void)kd_tick(&kd_play, play);
play = kd_play.stable;
/* 音量键/播放键边沿 → 动作(与主界面/子页同语义;见 np_apply_key_actions) */
np_apply_key_actions(kev.vol_dn_edge, kev.vol_up_edge,
play && !m_prev_play, meow_media_current(), &np_last_input);
m_prev_play = play;
/* 音量线性渐变(按住长爬;与主界面/子页同语义,状态栏即时刷新)。
* Now Playing 用 raw held(kev.vol_held_*,同现状 rvold/rvolu)。 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
if (!going) logmsg("np: volume ramp end -> %d", g_sys.vol);
}
/* 媒体变化检测(元数据/源/状态):立即整页重画 */
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
* NP 退出后生效(可接受偏差,记日志)。 */
kp_poll(&kp_power, NULL, NULL);
int pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
if (pwr && !np_prev_power) {
np_last_input = now_ms();
if (meow_screen_on) { meow_panel_off(); logmsg("nowplaying: power tap -> panel off"); }
else { meow_panel_on(); need_redraw = 1; logmsg("nowplaying: power tap -> panel on"); }
}
np_prev_power = pwr;
/* M2.92:NP 页自动息屏(与主界面同超时;页面无触摸/按键计时器不走主循环) */
if (screen_off_timeout > 0 && meow_screen_on &&
now_ms() - np_last_input >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
logmsg("nowplaying: idle %ds -> auto panel off", screen_off_timeout);
}
/* 息屏期只低频轮询按键(唤醒用),不重画不读触摸(FBIOBLANK 已挂起 goodix) */
if (!meow_screen_on) { usleep(100 * 1000); continue; }
/* 媒体变化检测(元数据/源/状态/artwork):立即整页重画。
* M6.33 补 artwork:ncm 状态切换可能先报空 artwork 再复原(标题/源/状态未变),
* 原检测漏 artwork → 封面清空后不再触发整页重绘 → 播放页/歌词页封面永不恢复。 */
const meow_media_session_t *s = meow_media_current();
int meta_changed = !s || strcmp(last_app, s->app_id) ||
strcmp(last_title, s->title) || last_state != s->state;
strcmp(last_title, s->title) || strcmp(last_art, s->artwork) ||
last_state != s->state;
if (meta_changed) {
if (s) {
snprintf(last_app, sizeof last_app, "%s", s->app_id);
snprintf(last_title, sizeof last_title, "%s", s->title);
snprintf(last_art, sizeof last_art, "%s", s->artwork);
last_state = s->state;
} else { last_app[0] = 0; last_title[0] = 0; last_state = 0xFFFFFFFFu; }
} else { last_app[0] = 0; last_title[0] = 0; last_art[0] = 0; last_state = 0xFFFFFFFFu; }
need_redraw = 1;
}
/* 空态/无源:立即重画(清占位/回空态) */
if (!s || s->state == MEOW_MEDIA_STOPPED) need_redraw = 1;
/* M2.92 修死循环:空态/停止态只在「变化」时画一次(meta_changed 已覆盖
* NULL↔会话切换),不再每 10ms 整页重画(旧行为 = 100% CPU 忙刷 → 卡死观感)。 */
if (need_redraw) {
np_draw_page(0);
@@ -313,14 +803,33 @@ void run_now_playing(void)
need_redraw = 0;
last_prog = now;
} else if (s && (now - last_prog >= 1000)) {
/* 进度推进:仅重绘进度带 + 按钮 */
/* 进度推进(按页分流):播放页=进度带+按钮;歌词页=歌词实时滚动重绘(当前行随 pos)。 */
last_prog = now;
np_repaint_progress();
if (g_np_page == NP_PAGE_LYRIC) {
/* 批6:整页重绘(封面背景+scrim+歌词),避免 lyric_view 半透明 scrim 每 1s
* 叠涂把封面/背景逐秒压黑(批4 既有「只重绘控件不重铺背景」结构修正)。 */
np_draw_lyric_page(0);
meow_dirty_add(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H);
meow_ui_flush();
} else {
np_repaint_progress();
}
}
/* M6.20:播放页状态栏时钟 1s 刷新(与主界面/子页同语义;draw_clock 喂 SDK 全局
* 状态栏 + 标脏顶条,SDK present() 重绘——否则时间冻结在进入播放页那一刻)。 */
if (now - np_last_status_tick >= 1000) {
np_last_status_tick = now;
draw_clock();
}
usleep(10000);
}
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
key_router_page_exit(NP_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
if (tfd >= 0) close(tfd);
kp_close(&kp_keys);
kp_close(&kp_power);
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
g_np_cover_path[0] = 0;
g_np_cc_valid = 0; /* 批5:退出清取色缓存,下次进入重新取 */
g_np_cc_path[0] = 0;
}
+106
View File
@@ -0,0 +1,106 @@
/* np_gesture.h — Now Playing 双页手势方向感知(header-only 纯逻辑,宿主可单测)。
*
* 定案(用户,批4):Now Playing 同一容器两页——播放页 + 歌词页,手势按当前页方向感知:
* - 播放页:右→左滑 → 进歌词页;任意右滑 → 回桌面(用户拍板保留任意右滑)。
* - 歌词页:左→右滑 → 回播放页(不是回桌面);左缘右滑不接桌面(避免误退)。
* 本头为 now_playing.c 与宿主单测(test_now_playing_widgets.c)共用的唯一手势决策,避免两端漂移。
*
* 依赖:仅 <stdint.h> + meow_notif_panel.h(mnp_top_swipe_down,纯函数)。左缘返回逻辑自
* (re)实现(镜像 meow_touch.c meow_edge_back 同口径,宿主编译无需链 meow_touch.c)。
*/
#ifndef NP_GESTURE_H
#define NP_GESTURE_H
#include <stdint.h>
#include "meow_notif_panel.h" /* mnp_top_swipe_down(顶部下滑→通知中心) */
/* ---- 页面 ---- */
#define NP_PAGE_PLAY 0
#define NP_PAGE_LYRIC 1
/* ---- 手势动作 ---- */
typedef enum {
NP_GEST_NONE = 0, /* 无(横向反向滑/纵向主导不匹配;调用方忽略,不误切页) */
NP_GEST_TOP, /* 顶部热区下滑 → 打开通知中心 */
NP_GEST_NEXT, /* 播放页 右→左滑 → 进入歌词页 */
NP_GEST_PREV, /* 歌词页 左→右滑 → 回到播放页 */
NP_GEST_EXIT, /* 播放页 任意右滑 → 返回桌面 */
NP_GEST_HOME, /* 批②:底部上滑 → 返回桌面(后台保留) */
NP_GEST_TAP, /* 近点按(按当前页分派:播放页=按钮/进度看点) */
} np_gest_t;
/* ---- 常量(对齐 launcher 同口径;#ifndef 防与 meow_touch.h 等重复定义冲突)---- */
#ifndef NP_SWIPE_THRESH
#define NP_SWIPE_THRESH 65 /* 顶部下滑触发位移阈值(对齐 launcher MENU_SWIPE_THRESH) */
#endif
#ifndef NP_PAGE_SWIPE
#define NP_PAGE_SWIPE 40 /* 横向切页最小位移(对齐 M6.26 np_anyright 的 dx>=40) */
#endif
#ifndef NP_TAP_MAX
#define NP_TAP_MAX 60 /* 点按位移上限(对齐现有 adx<60 && ady<60) */
#endif
#ifndef NP_TOP_HOTZONE
#define NP_TOP_HOTZONE 28 /* 顶部热区高(= launcher SB_BAR_H;通知中心/播放页同) */
#endif
#ifndef NP_EDGE_W
#define NP_EDGE_W 24 /* 左缘起点宽度(镜像 meow_touch.h MEOW_EDGE_W) */
#endif
#ifndef NP_EDGE_SWIPE
#define NP_EDGE_SWIPE 65 /* 左缘右滑位移阈值(镜像 meow_touch.h MEOW_EDGE_SWIPE) */
#endif
#ifndef NP_HOME_EDGE_H
#define NP_HOME_EDGE_H 28 /* 底部起滑区高(自屏底向上;镜像顶部热区 NP_TOP_HOTZONE 同为 28) */
#endif
#ifndef NP_HOME_SWIPE
#define NP_HOME_SWIPE 65 /* 底部上滑位移阈值(镜像 NP_SWIPE_THRESH / meow_edge_back 同口径) */
#endif
/* 左缘返回(镜像 meow_touch.c meow_edge_back:down_x∈[0,NP_EDGE_W) 且 dx>=NP_EDGE_SWIPE
* 且横向主导)。为宿主测试免链 meow_touch.c,此处 static inline 复刻同口径。 */
static inline int np_edge_back(int down_x, int dx, int dy)
{
int ady = dy < 0 ? -dy : dy;
if (down_x >= 0 && down_x < NP_EDGE_W && dx >= NP_EDGE_SWIPE && ady < dx)
return 1;
return 0;
}
/* 批② 底部上滑返回桌面(home,与左缘返回 np_edge_back 对称):原点在底部起滑区
* (down_y∈[fb_h-NP_HOME_EDGE_H, fb_h))且**不在左缘**(down_x>=NP_EDGE_W,与 np_edge_back 按
* 原点分界互斥——左缘 24px 列留给左缘返回)+ 向上位移≥NP_HOME_SWIPE 且竖向主导(adx<-dy)。
* fb_h=屏高(调用方喂 fb_yres,宿主单测亦可喂固定值)。其余(顶部/中部起滑、横向主导、
* 向下滑、未达阈值)一律 0,不与顶部下滑/横向切页/点按冲突。 */
static inline int np_home_gesture(int down_x, int down_y, int dx, int dy, int fb_h)
{
int adx = dx < 0 ? -dx : dx;
if (down_x >= NP_EDGE_W && down_y >= fb_h - NP_HOME_EDGE_H && down_y >= 0 &&
dy <= -NP_HOME_SWIPE && adx < -dy)
return 1;
return 0;
}
/* 按当前页把手势几何 → 动作。down_x/down_y=抬手原点;dx/dy=抬手位移(释放-原点,即 tlx-tdx)。
* 优先级:顶部下滑 > 横向主导切页/返回 > 点按。横向反向或无主导 → NONE(调用方忽略,不误切)。 */
static inline np_gest_t np_gesture_decide(int page, int down_x, int down_y, int dx, int dy)
{
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
/* 顶部热区下滑 → 通知中心(与主界面同语义;mnp_top_swipe_down 内部排除左缘起点) */
if (mnp_top_swipe_down(down_x, down_y, dx, dy, NP_TOP_HOTZONE, NP_SWIPE_THRESH))
return NP_GEST_TOP;
/* 横向主导(adx>=NP_PAGE_SWIPE 且横向位移>竖向)→ 按当前页分流切页/返回 */
if (adx >= NP_PAGE_SWIPE && adx > ady) {
if (page == NP_PAGE_PLAY) {
if (dx >= NP_PAGE_SWIPE) return NP_GEST_EXIT; /* 任意右滑 → 回桌面(含左缘/非左缘;用户拍板保留任意右滑) */
if (dx <= -NP_PAGE_SWIPE) return NP_GEST_NEXT; /* 右→左滑 → 进歌词页 */
return NP_GEST_NONE; /* 其余忽略 */
} else { /* NP_PAGE_LYRIC */
if (dx >= NP_PAGE_SWIPE) return NP_GEST_PREV; /* 左→右滑 → 回播放页(含左缘,不接桌面) */
return NP_GEST_NONE; /* 右→左滑=歌词页是末页,忽略 */
}
}
/* 非横向主导:近点按 */
if (adx < NP_TAP_MAX && ady < NP_TAP_MAX) return NP_GEST_TAP;
return NP_GEST_NONE;
}
#endif /* NP_GESTURE_H */
+94
View File
@@ -0,0 +1,94 @@
/* overlay_gate.h — SIGSTOP 叠加会话(通知中心 §10.5 / 锁屏 M2.x)关闭与死亡回退的**纯决策**。
*
* 宿主可单测(无信号/无设备/无文件系统依赖,全部 static inline);app_launcher.c 的两处叠加
* 关闭路径(run_app_notif_overlay / run_app_lock_overlay)调用,避免把「排空窗口」「中止条件」
* 这类时序/判定散落内联难验证。三个 bug 的定案语义都收敛到这里:
*
* ① 输入穿透(S1 根因):App 被 SIGSTOP 冻结过 → 关闭时必须在 /tmp/meow_notif_active 仍存在
* 时先 SIGCONT,让 SDK 输入线程(meow_ui_input.c overlay_input_suppressed)排空冻结期缓冲的
* evdev 事件副本 + 对齐 EVIOCGKEY,再等 OVERLAY_DRAIN_WINDOW_MS 后把标记内容更新为前台归属
* (notif_active_set(a->id),批①内容化标记取代旧 unlink;清标记/置空会连后台常驻 App 一起放行)。
* 若反序(清标记->SIGCONT,改动前写法),App 在无隔离态恢复 → 解冻瞬间把积累的触摸回放成
* App 内点击(锁屏穿透 /「启动奇怪界面」/ 解锁后状态栏消失的触发源)。
* ② 状态栏恢复(S2):解锁后强制 g_sb_mode_last=-1 让 app 循环立即重叠加,不等 1s 慢刷。
* ③ 死亡重绘(S3):锁屏叠加中 App 崩溃(/proc/<pid>/stat==Z)→ 中止锁屏走统一退出路径,
* 由 waitpid 循环回收 + run_menu dirty=1 重绘主屏(含状态栏)。
*/
#ifndef MEOW_OVERLAY_GATE_H
#define MEOW_OVERLAY_GATE_H
/* SDK overlay_input_suppressed() 的标记存在性节流窗是 200ms;排空窗须 > 该值(留 100ms 余量),
* 保证 App 恢复后 SDK 输入线程确实重新 stat 到标记再整批丢弃缓冲事件。 */
#define OVERLAY_DRAIN_WINDOW_MS 300
/* ① 排空是否必要:App 被 SIGSTOP 冻结过(非保活)才需要「SIGCONT 保持隔离 → 排窗 → unlink」;
* 媒体保活(keep_running,从未 STOP)直接「恢复帧上屏 → unlink」(自抑随标记消失自然解除)。 */
static inline int overlay_drain_required(int keep_running)
{
return !keep_running ? 1 : 0;
}
/* ③ 锁屏叠加中 App 崩溃是否触发「统一退出路径」(= 中止锁屏、交回 waitpid 循环回收、run_menu 重绘)。
* proc_zombie=1(/proc/<pid>/stat==Z)→ 走退出(返回 1);否则正常恢复(返回 0)。 */
static inline int lock_overlay_app_died_exit(int proc_zombie)
{
return proc_zombie ? 1 : 0;
}
/* ② 解锁后是否需要强制状态栏重同步(返回 1):始终需要——把 g_sb_mode_last 拨回 -1,
* 使 run_menu / app 循环的「模式变化→立即重叠加」分支命中(-1 != m 恒真除非 m==-1,
* 但 m==-1 表示无模式文件=展开态,同样被 app_overlay_statusbar 兜底到展开整条)。 */
static inline int overlay_force_sb_resync(void)
{
return 1;
}
/* 未冻结(媒体保活,keep_running)App 是否**需要 EVIOCGRAB 独占隔离**(返回 1)。
* §310 问题1真根:媒体保活 App 从不 SIGSTOP(保 PCM),SDK 输入线程**实时读 evdev**(不走缓冲),
* 标记(/tmp/meow_notif_active)让 SDK 排空只在有该新 SDK 的 App 上生效;launcher 侧可**无条件**
* 用 EVIOCGRAB 独占触控设备,使包括保活 App 在内的**任何**前台进程在锁屏期收不到触摸(且抓取期
* 事件不为未抓取 fd 缓冲/回放)。keep_alive=1 → 隔离靠 grab(overlay_drain_required==0,无
* SIGCONT 排空语义);冻结 App(keep_running=0)→ 隔离靠 SIGSTOP+SIGCONT 排空(drain_required==1)。 */
static inline int overlay_keepalive_needs_grab(int keep_running)
{
return keep_running ? 1 : 0;
}
/* 锁屏进场后「重铺全屏不透明底」的阈值(ms)。SDK present 抑制标记节流窗(MEOW_OVERLAY_CHK_MS)
* 是 200ms——媒体保活 App 不冻结、锁定瞬间还会进 1-2 帧(部分脏区盖到锁屏上)。阈值取 300ms
* (> 200ms),保证覆盖到抑制窗内 App 的残帧;App 此后被抑制不再上屏,锁屏稳定遮盖。
* 由 lock_screen_ui.c 的 run_lock_screen 重铺逻辑使用(单测同源断言)。 */
#define OVERLAY_LOCK_COVER_MS 300
/* 唤醒是否应触发锁屏(§310 M6.5 问题1):超时息屏(meow_auto_slept)与电源键手动息屏(watchdog
* 直接 blank 面板、背光 0 → launcher 检测得 app_panel_manual_off)两类唤醒都须走 lck_wake_should_lock
* 语义——真机实测「App 内电源键手动息屏→亮屏无锁屏」(手动路径丢了锁屏触发、超时路径接了)。 */
static inline int overlay_wake_should_lock(int auto_slept, int manual_panel_off)
{
return (auto_slept || manual_panel_off) ? 1 : 0;
}
/* 锁屏页自睡阈值(秒,§310 M6.7,294 遗留):min(系统 screen_off_timeout, 30s)。
* 系统未设/设 0(常亮)/超 30 → 锁屏页封顶 30s(锁屏页不应常亮耗电);系统设更短 → 尊重更短。
* 返回正数秒。lock_screen_ui.c 的 run_lock_screen 空闲超时用它(单测同源断言)。 */
static inline int overlay_lock_idle_off_sec(int screen_off_timeout)
{
return (screen_off_timeout > 0 && screen_off_timeout < 30)
? screen_off_timeout : 30;
}
/* 锁屏页是否应自睡(§310 M6.7):屏幕亮(meow_screen_on)且在本循环 && 空闲(now-last_act) ≥ 阈值。
* 返回 1=超时灭屏(锁态保持)。 */
static inline int overlay_lock_should_sleep(int screen_on, unsigned long long idle_ms,
unsigned long long off_ms)
{
return (screen_on && idle_ms >= off_ms) ? 1 : 0;
}
/* 锁屏自睡后应否唤醒回锁屏页:电源键按下边沿 或 触摸活动。返回 1=唤醒(meow_panel_on+重画锁屏)。 */
static inline int overlay_lock_wake_now(int pwr_edge, int touch_ready)
{
return (pwr_edge || touch_ready) ? 1 : 0;
}
#endif /* MEOW_OVERLAY_GATE_H */
+61 -1
View File
@@ -59,11 +59,71 @@ static const unsigned char PM_ICON_CANCEL[3136]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static const pm_icon_t pm_icons[5] = {
static const unsigned char PM_ICON_LOCK[3136]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const pm_icon_t pm_icons[6] = {
{ PM_ICON_POWER },
{ PM_ICON_RESTART },
{ PM_ICON_BACK },
{ PM_ICON_UPGRADE },
{ PM_ICON_LOCK },
{ PM_ICON_CANCEL },
};
+250 -43
View File
@@ -2,11 +2,13 @@
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530 }; /* 圆心 y(行距 140) */
/* M2.x 锁屏:电源菜单增「锁定」手动锁。网格 2 列×3 行:0=关机 1=重启 2=回HiBy 3=升级
* 4=锁定 5=取消;取消仍是安全默认项(PM_NUM-1),电源键确认/导航不会误触发关机。 */
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530, 530 }; /* 圆心 y(行距 140) */
static const char *pm_labels_cn[PM_NUM] = { "关机", "重启", "回HiBy", "升级", "取消" };
static const char *pm_labels_cn[PM_NUM] = { "关机", "重启", "回HiBy", "升级", "锁定", "取消" };
static const char *pm_labels_en[PM_NUM] = { "POWER OFF", "RESTART", "HIBYOS", "UPGRADE", "CANCEL" };
static const char *pm_labels_en[PM_NUM] = { "POWER OFF", "RESTART", "HIBYOS", "UPGRADE", "LOCK", "CANCEL" };
/* 电源菜单圆钮图标:从 pm_icons.h 的 56×56 alpha 位图 blit 到 fb(以圆钮中心 cx,cy 为锚居中)。
@@ -39,6 +41,10 @@ static void blit_pm_icon(int idx, int cx, int cy, uint16_t col)
}
#if 0 /* ================================================================
* M7.0b 退役:电源菜单圆钮改走 SDK 控件(meow_card/meow_iconbtn/自定义画布),
* 命中/选中/按压态全由 SDK 接管;本块保留作回退位(删除 #if 0 行恢复旧手绘)。
* ================================================================ */
/* 绘制电源菜单圆钮 i(MD3 圆形:外描边+内填充+圆内 Material 图标;选中高亮)。
* 圆内放 Material Symbols 位图,文字标签放圆下(Android 12 式:icon 上、label 下)。 */
static void draw_pm_button(int i, int selected)
@@ -69,6 +75,186 @@ static void draw_pm_button(int i, int selected)
draw_text(cx - lw / 2, ly + 1, lab, 1, col);
}
}
#endif /* M7.0b 退役:电源菜单旧手绘圆钮 */
/* ==================================================================
* M7.0b 电源菜单控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
* - 圆角卡 → meow_card(SDK 主题 surfaceContainerLow);
* - 六个圆钮 → meow_widget_custom_create(pm_icons 专有位图:关机/重启/回HiBy/升级/锁定/取消,
* 均 56×56 1:1 blit,无放大)。meow_iconbtn 分支保留(回HiBy/取消曾改走 sb_icons,因放大锯齿
* 定案回退 pm_icons,见 pm_sdk_icon[] 注释;后续 SDK 出更高清图标可再启用);
* - 命中/选中/按压态全走 SDK(meow_widget_hit/set_state/draw);
* - 主题 = 读 SDK meow_theme(单一权威),后台遮罩仍 dim_screen(会话骨架不动)。
* 控件 lazy 创建一次(owner=PM_SENTINEL_ACT 哨兵 activity),跨多次进入复用;
* show_power_menu() 进入时 push 哨兵、退出时 pop(owner 闸门使控件命中生效)。
* ================================================================== */
#define PM_SENTINEL_ACT 0x50 /* 电源菜单哨兵 activity(不与 launcher 其它页冲突) */
#define PM_BTN_SIZE (PM_R * 2) /* 圆钮 = 外接正方形边长(直径) */
/* 哪些 pm 图标走 SDK sb_icons 表(可用 meow_iconbtn 直接出图);-1=不在 → 自定义画布 blit pm_icons。
* M7.0b 曾把 2=回HiBy / 5=取消 走 meow_iconbtn(SB_ARROW_BACK / SB_CLOSE),但 meow_iconbtn 按
* 0.6*size 把 32×32 sb_icons 位图放大到 ~60px(>位图原生墨高),最近邻放大导致明显锯齿;且此尺寸
* 大于 M6.8 及更早电源菜单圆钮图标(pm_icons 56×56,1:1 无放大)。定案:这两钮也回退 pm_icons 专有
* 位图(PM_ICON_BACK/PM_ICON_CANCEL,同为 Material Symbols 56×56),统一其余四钮画法,恢复旧尺寸。
* pm_icons[] 全部专用 → meow_iconbtn 分支保留(后续 SDK 出更高清图标可再启用)。 */
static const int pm_sdk_icon[PM_NUM] = {
-1, /* 0 关机:pm_icons 专有(SDK 无 power 位图) */
-1, /* 1 重启:pm_icons 专有 */
-1, /* 2 回HiBy:pm_icons 专有(恢复 M6.8 56×56,避免 meow_iconbtn 放大锯齿) */
-1, /* 3 升级:pm_icons 专有 */
-1, /* 4 锁定:pm_icons 专有(SDK 无 lock 位图) */
-1, /* 5 取消:pm_icons 专有(同 2,恢复旧尺寸) */
};
/* 圆钮文案 i18n id(meow_str)。common.cancel 内嵌已有;power.* 需 TF 覆盖表/后续并入 SDK 内嵌。 */
static const char *pm_str_id[PM_NUM] = {
"power.off", "power.restart", "power.back_hiby",
"power.upgrade", "power.lock", "common.cancel",
};
static meow_widget *g_pm_card = NULL;
static meow_widget *g_pm_btns[PM_NUM] = {NULL};
static int g_pm_init = 0;
/* 按主题混合(pct 0..100:base 向 top 混 pct%,24-bit 每通道;近似 SDK state_tint/blend)。 */
static uint32_t pm_blend(uint32_t base, uint32_t top, int pct)
{
int br = (base >> 16) & 0xFF, bg = (base >> 8) & 0xFF, bb = base & 0xFF;
int tr = (top >> 16) & 0xFF, tg = (top >> 8) & 0xFF, tb = top & 0xFF;
int r = (br * (100 - pct) + tr * pct) / 100;
int g = (bg * (100 - pct) + tg * pct) / 100;
int b = (bb * (100 - pct) + tb * pct) / 100;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
/* 自定义画布:绘制 pm_icons 专有圆钮(图标不在 SDK sb_icons 表时用)。
* SDK 已把绘制 clip 设为控件矩形;圆形底色按 selected 取主题容器色,图标墨色随之;
* 禁用/按压态由本回调自叠(SDK 不替 W_CUSTOM 自动叠,meow_widget_draw_state_layer 铺方矩形,
* 圆形钮此处手工按圆域混合,更贴合)。 */
static void pm_render_cb(meow_widget *w, const meow_theme *th, void *ctx)
{
int i = (int)(intptr_t)ctx;
int rc[4]; meow_widget_rect(w, rc); /* 绝对坐标 x,y,w,h(launcher 侧 meow_widget 不透明) */
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
unsigned st = meow_widget_state(w);
int sel = (st & MEOW_WS_SELECTED) != 0;
int disabled = (st & MEOW_WS_ENABLED) == 0;
int pressed = (st & MEOW_WS_PRESSED) != 0;
uint32_t base = sel ? th->primaryContainer : th->surfaceContainer;
uint32_t fg = sel ? th->onPrimaryContainer : th->onSurface;
if (disabled) base = pm_blend(base, th->onSurface, 38); /* 禁用变暗(向灰) */
else if (pressed) base = pm_blend(base, th->onSurface, sel ? 24 : 12); /* 按压状态层 */
meow_gfx_fill_circle(cx, cy, r, base);
blit_pm_icon(i, cx, cy, rgb565(fg));
}
/* 自定义画布命中:圆形(圆心=控件中心,半径=size/2)。 */
static int pm_hit_cb(meow_widget *w, int x, int y, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
int dx = x - cx, dy = y - cy;
return dx * dx + dy * dy <= r * r;
}
/* 创建卡片 + 六钮(owner 钉 = PM_SENTINEL_ACT,仅首进创建一次,复用)。 */
static void pm_widgets_create(void)
{
meow_ui_activity_push(PM_SENTINEL_ACT);
g_pm_card = meow_card_create(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H);
for (int i = 0; i < PM_NUM; i++) {
int sz = PM_BTN_SIZE;
int x = pm_cx[i] - sz / 2, y = pm_cy[i] - sz / 2;
if (pm_sdk_icon[i] >= 0)
g_pm_btns[i] = meow_iconbtn_create(x, y, sz, pm_sdk_icon[i]);
else
g_pm_btns[i] = meow_widget_custom_create(x, y, sz, sz,
pm_render_cb, pm_hit_cb,
(void *)(intptr_t)i);
}
meow_ui_activity_pop(); /* 回主 activity(控件 owner 已钉为哨兵) */
g_pm_init = 1;
}
/* 主题刷新:iconbtn 底色=surfaceContainer(与卡片 surfaceContainerLow 拉开对比)。
* custom 钮由 render_cb 读 th 自带;此处只补 iconbtn 的 opts.bg(跨开放切主题也跟手)。 */
static void pm_refresh_theme(void)
{
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->surfaceContainer;
for (int i = 0; i < PM_NUM; i++)
if (g_pm_btns[i] && pm_sdk_icon[i] >= 0)
meow_widget_set_opts(g_pm_btns[i], &op);
}
/* i18n 文案(meow_str_zh:未知 id → 中文兜底)。TF 覆盖表/后续 SDK 内嵌并入后自动换语。 */
static const char *pm_label(int i)
{
return meow_str_zh(pm_str_id[i], pm_labels_cn[i]);
}
/* 绘制单个圆钮(SDK 控件)+ 圆下标签(主题色,选中亮/未选次色)。 */
static void pm_draw_button(int i, int selected)
{
meow_widget *w = g_pm_btns[i];
if (!w) return;
meow_widget_set_selected(w, selected);
meow_widget_draw(w);
const meow_ui_theme_t *th = s_ui_theme();
int cx = pm_cx[i], cy = pm_cy[i];
int ly = cy + PM_R + 8;
uint32_t col = selected ? th->on_surface : th->on_surface_variant;
if (mfont_ready) {
const char *lab = pm_label(i);
int lw = meow_font_width_text(lab, 18);
meow_font_drawtext((uint16_t *)fb, cx - lw / 2, ly, lab, rgb565(col), 18);
} else {
const char *lab = pm_labels_en[i];
draw_text(cx - (int)strlen(lab) * 5 / 2, ly + 1, lab, 1, col);
}
}
/* 标脏一个圆钮的整块区域(圆外接框 + 圆下标签),供局部 flush。 */
static void pm_button_dirty(int i)
{
int sz = PM_BTN_SIZE, cy = pm_cy[i];
meow_dirty_add(pm_cx[i] - sz / 2 - 4, cy - sz / 2 - 4, sz + 8, sz + PM_R + 44);
}
/* 命中检测:返回命中的钮下标(0..PM_NUM-1),否则 -1。 */
static int pm_hit_button(int x, int y)
{
for (int i = 0; i < PM_NUM; i++)
if (g_pm_btns[i] && meow_widget_hit(g_pm_btns[i], x, y)) return i;
return -1;
}
/* 绘制卡片 + 标题 + 底部提示 + 全部圆钮(进入/全刷时调用一次)。 */
static void pm_draw_card(void)
{
pm_refresh_theme();
if (g_pm_card) meow_widget_draw(g_pm_card);
const meow_ui_theme_t *th = s_ui_theme();
const char *title = "POWER MENU";
int title_y = PM_CARD_Y0 + 26;
if (mfont_ready)
meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2,
title_y, title, rgb565(th->on_surface), 20);
else
draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, th->on_surface);
const char *hint = meow_str_zh("power.hint_nav", "音量导航 电源确认");
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
if (mfont_ready)
meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(hint, 18)) / 2,
hint_y, hint, rgb565(th->on_surface_variant), 18);
else
draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2,
hint_y + 1, "VOLUME NAV POWER OK", 1, th->on_surface_variant);
}
/* ---- 电源管理服务 IPC 客户端(SYSTEM_FRAMEWORK_SERVICES §3.2:launcher 经服务触发电源动作)----
* launcher 不再直连 reboot(2)syscall/写 mtd5/写 boot 默认 —— 统一经 meow_svc_power 的
@@ -189,19 +375,12 @@ int show_power_menu(void)
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
int sel = PM_NUM - 1; /* 默认取消(安全) */
/* M2.30 MD3:居中圆角卡片背景 + 标题居中 + 圆钮网格。卡片白色柔和描边/深色底。 */
fill_round_rect(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H, PM_CARD_R, 0x141824);
fill_round_rect(PM_CARD_X0 + 2, PM_CARD_Y0 + 2, PM_CARD_W - 4, PM_CARD_H - 4, PM_CARD_R - 2, 0x1C2438);
/* 卡片顶部标题,水平居中 */
char title[32];
snprintf(title, sizeof title, "POWER MENU");
int title_y = PM_CARD_Y0 + 26; /* 标题在卡片内顶部(卡片顶 115,标题基线 ~141) */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2, title_y, title, rgb565(0xFFFFFF), 20);
else draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, 0xFFFFFF);
/* 底部提示行(卡片下方,非圆钮所需) */
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text("音量导航 电源确认", 18)) / 2, hint_y, "音量导航 电源确认", rgb565(0x777799), 18);
else draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2, hint_y + 1, "VOLUME NAV POWER OK", 1, 0x777799);
/* M7.0b 控件化:进入 push 哨兵(控件 owner 闸门使 meow_widget_hit 生效)+ lazy 建卡/钮。
* 卡片=meow_card(SDK 主题 surfaceContainerLow),圆钮=meow_iconbtn/自定义画布,命中/选中/
* 按压态全走 SDK;背景遮罩仍 dim_screen(会话骨架不动)。 */
meow_ui_activity_push(PM_SENTINEL_ACT);
if (!g_pm_init) pm_widgets_create();
pm_draw_card(); /* 卡片 + 标题 POWER MENU + 底部提示(SDK 主题色,i18n 文案) */
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
/* 触摸状态(按下→抬起):统一 meow_touch 接口(goodix 时序免疫,原点由首个有效 ABS 锁) */
@@ -228,19 +407,18 @@ int show_power_menu(void)
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
* 8s=强关」。*/
int pm_interacted = 0;
int pm_press_idx = -1; /* M7.0b:当前按下钮(按压态走 SDK 状态机位) */
for (;;) {
/* dirty 绘制:首次全画;此后仅选中变化才重画受影响两钮(消频闪)→ 标脏 + present。 */
if (!pm_init) {
for (int i = 0; i < PM_NUM; i++) draw_pm_button(i, i == sel);
for (int i = 0; i < PM_NUM; i++) pm_draw_button(i, i == sel);
pm_init = 1; last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present(vsync+帧率门控+blit 脏=全帧,含压暗列表+标题+按钮) */
} else if (sel != last_sel) {
draw_pm_button(last_sel, 0);
draw_pm_button(sel, 1);
/* M2.30 圆形:dirty 区 = 圆(圆心±半径)+ 圆下文字(半径+偏移+行高),四周留 10px */
int dr = (PM_R + 6) + 10, dtxt = PM_R + 26 + 10;
meow_dirty_add(pm_cx[last_sel] - dr, pm_cy[last_sel] - dr, dr * 2, dr + dtxt);
meow_dirty_add(pm_cx[sel] - dr, pm_cy[sel] - dr, dr * 2, dr + dtxt);
pm_draw_button(last_sel, 0);
pm_draw_button(sel, 1);
pm_button_dirty(last_sel);
pm_button_dirty(sel);
last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present,blit 两钮区 */
}
@@ -278,19 +456,31 @@ int show_power_menu(void)
mt_down(&mt);
pm_interacted = 1;
pm_last_interact = now_ms();
/* M7.0b 按压态:按下命中钮 → 置 PRESSED 并重绘(SDK 状态层,实时) */
pm_press_idx = pm_hit_button(mt.down_x, mt.down_y);
if (pm_press_idx >= 0) {
meow_widget_set_state(g_pm_btns[pm_press_idx], MEOW_WS_PRESSED, 1);
meow_widget_draw(g_pm_btns[pm_press_idx]);
pm_button_dirty(pm_press_idx);
meow_ui_flush();
}
} else if (!val && mt.down) {
pm_interacted = 1;
pm_last_interact = now_ms();
if (pm_press_idx >= 0) { /* 清按压态(SDK 状态层随 PRESSED 位清除) */
meow_widget_set_state(g_pm_btns[pm_press_idx], MEOW_WS_PRESSED, 0);
meow_widget_draw(g_pm_btns[pm_press_idx]);
pm_button_dirty(pm_press_idx);
pm_press_idx = -1;
meow_ui_flush();
}
int dx, dy;
/* M2.25:抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动(返回0),
* 不做确认;否则有效 tap,按下原点命中的按钮即确认。 */
if (mt_up(&mt, &dx, &dy)) {
/* M2.30 圆形命中:按下原点距圆心 <= PM_R+6 即命中 */
for (int i = 0; i < PM_NUM; i++) {
int ddx = mt.down_x - pm_cx[i], ddy = mt.down_y - pm_cy[i];
int rr = (PM_R + 6) * (PM_R + 6);
if (ddx * ddx + ddy * ddy <= rr) { sel = i; act = i; break; }
}
/* M7.0b 命中走 SDK(owner==哨兵 + enabled + 圆形命中) */
int hit = pm_hit_button(mt.down_x, mt.down_y);
if (hit >= 0) { sel = hit; act = hit; }
}
}
}
@@ -301,12 +491,13 @@ int show_power_menu(void)
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); return 1; }
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); meow_ui_activity_pop(); return 1; }
logmsg("pm: POWER HOLD 8s -> force poweroff");
if (meow_power_action(POWER_SHUTDOWN) != MEOW_OK) {
logmsg("pm: power service unavailable, abort force-off (menu closed, no action)");
return 0;
meow_ui_activity_pop(); return 0;
}
meow_ui_activity_pop();
return 1;
}
@@ -318,7 +509,7 @@ int show_power_menu(void)
logmsg("pm: idle %ds -> close menu + auto screen-off (act=cancel)", screen_off_timeout);
meow_panel_off();
meow_auto_slept = 1;
act = 4; /* 取消(关菜单) */
act = PM_NUM - 1; /* 取消(关菜单) */
break;
}
@@ -326,14 +517,22 @@ int show_power_menu(void)
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
meow_ui_activity_pop(); /* M7.0b:退出电源菜单,回主 activity(控件 owner 仍钉哨兵,复用) */
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与电源动作逐项核对:
* act==4(取消) -> 只关菜单,绝不关机(return 0,永不到电源动作);
* act==PM_NUM-1(取消) -> 只关菜单,绝不关机(return 0,永不到电源动作);
* act==4(锁定) -> 手动锁(meow_lock_screen_engage,未配 PIN 则 no-op),关菜单;
* act==0(关机)/act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才经服务 IPC 执行电源动作。
* 只有 act 为 0/1/2/3 才可能发电源动作;power 键本身绝不触发关机。 */
logmsg("pm: act=%d -> %s", act,
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式" : "取消");
if (act == 4) { logmsg("pm: cancel (close menu only)"); return 0; }
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式"
: act == 4 ? "锁定" : "取消");
if (act == PM_NUM - 1) { logmsg("pm: cancel (close menu only)"); return 0; }
if (act == 4) {
logmsg("pm: LOCK SCREEN");
meow_lock_screen_engage();
return 0; /* 关菜单;run_menu 继续(锁屏页已阻塞到解锁或电源息屏) */
}
/* 动作统一经电源管理服务 IPC(meow_svc_power);launcher 不再直连 poweroff/reboot(2)/写 mtd5/
* 写 boot 默认——关机/重启/升级/回HiBy 的动作原语都在服务内实现(§3.2 单一动作链路)。 */
/* 公共服务:执行动作;服务不可达 → 按「取消」返回(launcher 存活,绝不误报已动作卡死)。
@@ -380,15 +579,20 @@ int show_power_menu(void)
* 不随主循环每 10ms 全刷(原实现每帧重画同一内容,与 LCD 刷新竞争产生频闪)。 */
static void draw_cr_button(int sel, int i)
{
const meow_theme *mt = meow_theme_get(); /* M7.5:主题语义色(深/浅×MD3/MIUIx 单一权威) */
int x0 = (i == 0) ? 80 : 280, y0 = 420, w = 120, h = 60;
fill_round_rect(x0, y0, w, h, 12, (i == sel) ? 0x2A3A6A : 0x1A1A2A);
const char *lbl = (i == 0) ? "是" : "否";
uint32_t bg = (i == sel) ? mt->primary : mt->surfaceContainer;
fill_round_rect(x0, y0, w, h, 12, bg);
/* M7.0b i18n:是/否 经 meow_str 抽取(未知 id 回退中文) */
const char *lbl = (i == 0) ? meow_str_zh("power.confirm_yes", "是")
: meow_str_zh("power.confirm_no", "否");
uint32_t fg = (i == sel) ? mt->onPrimary : mt->onSurface;
if (mfont_ready) {
int lw = meow_font_width_text(lbl, 20);
meow_font_drawtext((uint16_t *)fb, x0 + w / 2 - lw / 2, y0 + 30 - meow_font_line_height(20) / 2,
lbl, rgb565((i == sel) ? 0xFFFFFF : 0xC8D0E8), 20);
lbl, rgb565(fg), 20);
} else {
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, (i == sel) ? 0xFFFFFF : 0xC8D0E8);
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, fg);
}
}
@@ -399,8 +603,10 @@ int confirm_reboot_player(void)
/* M2.8 Bug 3-residual:确认框同电源菜单 —— 若打开前刚经历息屏/blank,LCDC 扫描地址可能
* 非 buffer0;强制 re-pan 让「压暗列表 + 是/否」落在被显示的缓冲。 */
fb_pan_buf0_force();
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320, "返回播放器需重启,现在重启?", rgb565(0xFFFFFF), 20);
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, 0xFFFFFF);
const meow_theme *mt0 = meow_theme_get(); /* M7.5:标题颜色走主题语义色 */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320,
meow_str_zh("power.reboot_confirm", "返回播放器需重启,现在重启?"), rgb565(mt0->onSurface), 20);
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, mt0->onSurface);
meow_touch mt;
mt_reset(&mt);
@@ -419,8 +625,9 @@ int confirm_reboot_player(void)
int cr_init = 0, cr_last_sel = -1; /* M2.9 Bug 4:dirty——仅 sel 变化才重画两钮 */
/* 标题/按键提示:打开时画一次(原实现把它们放主循环每 10ms 全刷,与 LCD 刷新竞争 →
* 「是/否」钮频闪;改为只在打开/状态变化时绘制)。标题在前面已画一次,这里画提示行。 */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560, "音量切换 电源取消", rgb565(0x666688), 20);
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, 0x666688);
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560,
meow_str_zh("power.confirm_hint", "音量切换 电源取消"), rgb565(mt0->onSurfaceVariant), 20);
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, mt0->onSurfaceVariant);
meow_dirty_full(); /* M2.16:压暗列表+标题+提示作为一整帧呈现 */
for (;;) {
/* dirty 绘制:首次全画两钮;此后仅 sel 变化才重画(消频闪)→ 标脏 + present。 */
+483 -99
View File
@@ -23,6 +23,26 @@ static const char *const out_presets[] = { "hw:0,0" };
static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" };
/* i18n:SC_PRESET 选项中文标签键(meow_str_zh:未知 id → 中文兜底,行为/显示不变)。
* 字体行(font.file/font.class_*)用运行时扫描表 font_presets/font_preset_labels,无键表,原样。 */
static const char *const bg_preset_i18n[] = { "settings.bg_black", "settings.bg_blue", "settings.bg_green" };
static const char *const hk_preset_i18n[] = { "settings.hk_powervol_pr", "settings.hk_sequence" };
static const char *const seq_preset_i18n[] = { "settings.seq_volup3", "settings.seq_voldown3" };
static const char *const out_preset_i18n[] = { "settings.out_cs43198" };
/* SC_PRESET 行选项标签本地化:按该行 presets 表匹配对应键表(字体行用运行时动态表,无键 → 原样)。 */
static const char *s_preset_label(const srow_t *r, int i)
{
const char *const *keys = NULL;
if (r->presets == bg_presets) keys = bg_preset_i18n;
else if (r->presets == hk_presets) keys = hk_preset_i18n;
else if (r->presets == seq_presets) keys = seq_preset_i18n;
else if (r->presets == out_presets) keys = out_preset_i18n;
const char *lbl = (r->preset_labels && r->preset_labels[i]) ? r->preset_labels[i] : "";
if (keys && keys[i]) return meow_str_zh(keys[i], lbl);
return lbl;
}
/* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s,
* 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按
@@ -115,17 +135,94 @@ static const srow_t SMENU[] = {
{ "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0,0 },
{ "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0,0 },
{ "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N,0 },
/* M6.x 锁屏:安全组「锁屏 PIN」独立子页面(状态/设置/修改/清除/唤醒锁开关,见 lock_settings.c) */
{ "安全", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "锁屏 PIN", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_PIN_PAGE },
/* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */
{ "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0,0 },
{ "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT },
};
/* 设置行标题 i18n 键:与 SMENU 一一对应(含分组标题 / SC_LIST 导航行),settings.* 域。
* 默认值取 SMENU 行 label 原中文,行为/显示不变;TF 覆盖表/后续 SDK 内嵌并入后自动换语。 */
static const char *const s_row_i18n[SMENU_N] = {
"settings.sec_display", /* 显示 */
"settings.brightness", /* 亮度 */
"settings.screen_off_mode", /* 息屏模式 */
"settings.screen_off_timeout",/* 息屏时间 */
"settings.refresh_rate", /* 刷新率 */
"settings.refresh_always_max",/* 最高刷新率 */
"settings.theme_mode", /* 主题 */
"settings.theme", /* 主题深浅 */
"settings.theme_dark_start", /* 深色时段开始 */
"settings.theme_dark_end", /* 深色时段结束 */
"settings.background", /* 背景 */
"settings.bg_overlay_alpha", /* 背景叠加强度 */
"settings.home_layout", /* 主界面布局 */
"settings.sec_network", /* 网络 */
"settings.wifi", /* WiFi */
"settings.bt", /* 蓝牙 */
"settings.sec_power", /* 电源 */
"settings.longpress_menu", /* 长按弹菜单 */
"settings.forceoff", /* 长按强关 */
"settings.defeat_hotkey", /* 退出热键 */
"settings.sec_boot", /* 开机 */
"settings.boot_default", /* 默认系统 */
"settings.boot_menu", /* 显示选单 */
"settings.boot_timeout", /* 选单超时 */
"settings.sec_input", /* 输入 */
"settings.hotkey_seq", /* 热键序列 */
"settings.hotkey_window", /* 热键窗口 */
"settings.touch_arm", /* 触摸布防 */
"settings.swipe_invert", /* 滑动方向 */
"settings.sec_font", /* 字体主题 */
"settings.font_file", /* 字体文件 */
"settings.font_load_full", /* 加载大字库 */
"settings.multi_font", /* 多字体 */
"settings.font_cjk", /* 中文(CJK) */
"settings.font_cjk_sym", /* 中文标点 */
"settings.font_num", /* 数字 */
"settings.font_latin", /* 拉丁字母 */
"settings.font_latin_sym", /* 半角标点 */
"settings.font_other", /* 其他字符 */
"settings.font_title_size", /* 标题字号 */
"settings.font_body_size", /* 正文字号 */
"settings.ui_style", /* UI风格 */
"settings.sec_notify", /* 通知 */
"settings.notify_center", /* 通知中心 */
"settings.notify_app_quota", /* 应用配额 */
"settings.sec_log", /* 日志 */
"settings.log_level", /* 日志级别 */
"settings.log_to_card", /* 写入TF卡 */
"settings.sec_audio", /* 音频 */
"settings.volume", /* 音量 */
"settings.volume_max", /* 音量上限 */
"settings.volume_step", /* 音量跳步 */
"settings.audio_output", /* 输出设备 */
"settings.sec_security", /* 安全 */
"settings.pin", /* 锁屏 PIN */
"settings.sec_about", /* 关于 */
"settings.about", /* 关于本机 */
};
/* SMENU 行标题本地化:能定位到 SMENU 行 → 按对应键取文案(默认=原中文);否则原样返回。 */
static const char *s_row_label(const srow_t *r)
{
for (int i = 0; i < SMENU_N; i++) {
if (&SMENU[i] == r) return meow_str_zh(s_row_i18n[i], r->label);
}
return meow_str_zh("settings.unknown", r->label);
}
/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由
* 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */
#if 0 /* M7.2 回退位:旧「当前选中滑条行 track y」随旧手绘 draw_settings_page 退役
* (新路径在触摸按下时由 meow_listview hit 直接算 track 中心,无需全局记录)。 */
/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量,
* 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */
static int s_sel_track_y = -1;
#endif
/* ---- 行高/位置/导航辅助 ---- */
@@ -203,6 +300,9 @@ static const char *const *s_row_presets(const srow_t *r, int *n)
}
#if 0 /* M7.2 回退位:旧「纯范围/选中可见滚动钳制 + 内容 y→行号」随旧手绘 draw_settings_page 退役
* (新路径滚动走 meow_scroll 包裹 meow_listview,钳制/命中/可见性由 SDK 接管)。
* 删除 #if 0 行并切回旧 draw_settings_page 路径即可恢复。 */
/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */
static int s_clamp_scroll_range(int scroll)
{
@@ -237,15 +337,16 @@ static int s_row_at_content_y(int cy)
if (pos >= s_vis_count()) return -1;
return s_vis_at(pos);
}
#endif /* M7.2 回退位:旧滚动钳制 + 内容 y→行号 */
/* ---- 值格式化/展示 ---- */
static void s_fmt_time(int sec, char *buf, int len)
{
if (sec <= 0) { snprintf(buf, len, "常亮"); return; }
if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; }
if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; }
snprintf(buf, len, "%d 时", sec / 3600);
if (sec <= 0) { snprintf(buf, len, "%s", meow_str_zh("settings.always_on", "常亮")); return; }
if (sec < 60) { snprintf(buf, len, meow_str_zh("settings.time_sec", "%d 秒"), sec); return; }
if (sec < 3600) { snprintf(buf, len, meow_str_zh("settings.time_min", "%d 分"), sec / 60); return; }
snprintf(buf, len, meow_str_zh("settings.time_hr", "%d 时"), sec / 3600);
}
/* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */
@@ -288,14 +389,14 @@ static void s_fmt_value(const srow_t *r, char *buf, int len)
int s2 = 0, e2 = 0;
meow_settings_get_int("display.theme_dark_start", &s2);
meow_settings_get_int("display.theme_dark_end", &e2);
if (s2 == e2) { snprintf(buf, len, "未设置"); return; }
if (s2 == e2) { snprintf(buf, len, "%s", meow_str_zh("settings.unset", "未设置")); return; }
s_fmt_minutes(v, buf, len);
return;
}
/* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */
if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) {
if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000);
else snprintf(buf, len, "%.1f 秒", v / 1000.0);
if (v % 1000 == 0) snprintf(buf, len, meow_str_zh("settings.sec_val", "%d 秒"), v / 1000);
else snprintf(buf, len, meow_str_zh("settings.sec_val_f", "%.1f 秒"), v / 1000.0);
return;
}
snprintf(buf, len, "%d", v);
@@ -314,14 +415,14 @@ static void s_display_value(const srow_t *r, char *buf, int len)
if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; }
/* M2.5x 多字体:class 行空值 = 跟随全局 font.file */
if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) {
snprintf(buf, len, "跟随全局");
snprintf(buf, len, "%s", meow_str_zh("settings.follow_global", "跟随全局"));
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]) ? s_preset_label(r, i) : cur);
return;
}
}
@@ -373,7 +474,7 @@ static void s_commit(const srow_t *r, int save_now)
* 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。
* 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */
int v; if (meow_settings_get_int("display.brightness", &v) == 0) {
write_brightness(v);
write_brightness(v, "settings_page bright_slider");
g_sys.brightness = v;
paint_sb_right();
}
@@ -396,16 +497,35 @@ static void s_commit(const srow_t *r, int save_now)
* 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */
else if (!strcmp(r->key, "display.background")) {
char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg);
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
else menu_bg = 0x000000; /* black(默认) */
/* 状态栏(menu_bg)与主界面背景都会用到 menu_bg;退出设置页回主界面时重绘即可。 */
int mstyle = 0; if (meow_settings_get_enum("ui.style", &mstyle) != 0) mstyle = 0;
if (mstyle == MEOW_STYLE_MIUIX) { /* MIUIx 暖色族(与 main_menu menu_bg_prepare 同色) */
if (!strcmp(bg, "blue")) menu_bg = 0x101A2E;
else if (!strcmp(bg, "green")) menu_bg = 0x14251A;
else menu_bg = 0x181310;
} else { /* MD3(既有) */
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
else menu_bg = 0x000000; /* black(默认) */
}
/* menu_bg 仅主界面背景用(状态栏走 SDK meow_theme bg);退出设置页回主界面时经
* menu_bg_sig(含 ui.style)重build 缓存。 */
}
/* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */
else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") ||
!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */
}
/* §8.1.2 一键切换闭环:ui.style(md3/miuix)顶级风格族切换 → 全系统即时换肤,无需重启。
* meow_ui_set_style() 设 SDK g_style(launcher 同一进程内立即生效)+ 广播(App/状态栏下一帧换肤);
* paint_sb_right() → sb_sdk_refresh() 逼 SDK 状态栏重读 settings(含 display.background 派生)跟手换肤;
* meow_dirty_full() 让 launcher 主界面/设置页整帧重绘(s_ui_theme() 已接 ui.style → 换族配色)。
* 主界面背景缓存经 menu_bg_sig(含 ui.style,见 main_menu.c)在回主界面时自动重建。 */
else if (!strcmp(r->key, "ui.style")) {
int st = 0; meow_settings_get_enum("ui.style", &st);
meow_ui_set_style(st);
paint_sb_right();
meow_dirty_full();
}
/* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。
* 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走
* font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */
@@ -475,6 +595,9 @@ static int s_slider_val_from_x(const srow_t *r, int px)
}
#if 0 /* M7.2 回退位:旧手绘设置页主列表(手绘分组列表 + s_draw_switch/s_draw_slider + 自算滚动/命中)。
* 新路径走 meow_listview + meow_switch + meow_scroll + meow_select_show;删除 #if 0 行并把
* 下方「M7.2 SDK 路径」的 set_paint_page 调用切回 draw_settings_page 即可一键回退。 */
/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */
static void draw_settings_page(int sel, int scroll_off)
{
@@ -574,6 +697,7 @@ static void draw_settings_page(int sel, int scroll_off)
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
}
}
#endif /* M7.2 回退位:旧手绘设置页主列表 */
/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */
@@ -622,21 +746,23 @@ static int s_dropdown(const srow_t *r)
n = base + cnt;
if (is_cls) {
names[0] = "";
snprintf(labels[0], sizeof labels[0], "跟随全局字体");
snprintf(labels[0], sizeof labels[0], "%s", meow_str_zh("settings.follow_global_font", "跟随全局字体"));
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]);
(r->preset_labels && r->preset_labels[i][0]) ? s_preset_label(r, i) : pps[i]);
item[base + i] = labels[base + i];
if (!strcmp(pps[i], cstr)) cur = base + i;
}
}
if (n <= 0) return -1;
if (cur < 0 || cur >= n) cur = 0;
return s_dd_modal(r->label, item, n, cur);
/* M7.2:枚举/预设选择器迁 SDK meow_select_show(BottomSheet+单选列表,批次2 新控件),
* 替换自绘 s_dd_modal(后者仍供 about/网络列表/字体两级子菜单用,勿删)。 */
return meow_select_show(s_row_label(r), item, n, cur);
}
@@ -654,8 +780,8 @@ static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int
int content_top, int content_bot, int dd_scroll)
{
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container);
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline);
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 28, th->surface_container); /* MD3 extraLarge 28(原 16 偏小) */
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 28, th->outline);
if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20);
else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface);
for (int i = 0; i < n; i++) {
@@ -664,7 +790,7 @@ static void dd_modal_paint(const meow_ui_theme_t *th, int W, int H, int d_x, int
int cur_sel = (i == sel);
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
cur_sel ? 0xFFFFFF : th->on_surface, 20);
cur_sel ? meow_theme_get()->onPrimary : th->on_surface, 20);
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
}
}
@@ -871,8 +997,9 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
char fam_slug[S_NET_MAX][64], fam_disp[S_NET_MAX][64];
int famn = 0;
if (is_cls) {
if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], "");
snprintf(fam_disp[famn], sizeof fam_disp[0], "跟随全局字体"); famn++; }
if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], "%s", "");
snprintf(fam_disp[famn], sizeof fam_disp[0], "%s",
meow_str_zh("settings.follow_global_font", "跟随全局字体")); famn++; }
}
for (int i = 0; i < font_presets_n && famn < S_NET_MAX; i++) {
const char *f = font_preset_fams[i] ? font_preset_fams[i] : "";
@@ -891,9 +1018,9 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
int cur_fam = is_cls ? 0 : -1;
for (int i = is_cls ? 1 : 0; i < famn; i++)
if (curfam[0] && !strcmp(fam_slug[i], curfam)) cur_fam = i;
int pick = s_dd_modal(r->label, fitem, famn, cur_fam);
int pick = s_dd_modal(s_row_label(r), fitem, famn, cur_fam);
if (pick < 0) return 0;
if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */
if (is_cls && pick == 0) { snprintf(out_val, vlen, "%s", ""); return 1; } /* 跟随全局 */
const char *fam = fam_slug[pick];
/* ---- 二级:该 family 的字重列表(去重:同 (family,weight) 只显一行)----
@@ -937,49 +1064,280 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
for (int i = 0; i < wn; i++)
if (cwt2[0] && !strcmp(wt_pick[i], cwt2)) { cur_wt = i; break; }
}
int wp = s_dd_modal(r->label, witem, wn, cur_wt);
int wp = s_dd_modal(s_row_label(r), witem, wn, cur_wt);
if (wp < 0) return 0;
snprintf(out_val, vlen, "%s", wt_val[wp]);
return 1;
}
/* ==================================================================
* M7.2 设置页渲染层迁移(主列表迁 SDK 控件):
* - 分组列表 → meow_listview(虚拟化 set_vsource 固定 item 池 + section 段头 + set_item_draw 自绘);
* - 开关行 → 行内真 meow_switch(池化,SDK 绘制 + 按压态);
* - 滑条行 → 行内嵌 meow_slider(自绘 track/thumb,保留既有 s_draw_slider 语义色;
* 「行内嵌自绘」,M7 定案允许)——拖动实时写 settings.conf(读改写单键,
* 经 meow_settings_set_*,勿整文件覆盖——音量存盘教训);
* - 枚举/预设 → SDK meow_select_show(BottomSheet+单选列表,替换自绘 s_dd_modal);
* - 信息/列表行 → 行 right 值 + chevron(SDK 主题语义色)。
* 滚动 → meow_scroll(跟手 track + 松手 fling,同 subpage.c M7.1a 语义;刻意不 set_snap)。
* 控件 lazy 创建一次(owner=SET_SENTINEL_ACT 哨兵),跨多次进入复用;数据/动作(s_commit、
* s_set_int_value、s_net_*、s_font_dropdown、s_dropdown)逻辑层不动,仅换渲染层 + 命中;
* run_settings_page 会话骨架(触摸/滚动/返回/IPC/电源键语义)不改。旧手绘 draw_settings_page
* + s_clamp_scroll + s_row_at_content_y 保留 #if 0 作回退位(M7 红线:一键回退)。
* ================================================================== */
#define SET_SENTINEL_ACT 0x53 /* 设置页哨兵 activity(不与 launcher 其它页冲突) */
#define SET_LIST_Y SET_CONTENT_Y /* 100:列表顶缘(顶栏/标题下) */
#define SET_FOOT_H 40 /* 底部返回/版本带高 */
#define SET_ITEM_H 68 /* MD3 行高(滑条行给 track 留位) */
#define SET_SLIDER_YOFF 45 /* 滑条行内 track 顶缘相对行顶(÷s_draw_slider h=6,中心=顶+3) */
#define SET_TRACK_CY 48 /* 滑条 track 中心相对行顶 y(=SET_SLIDER_YOFF+3,与绘制对齐) */
#define SET_SLIDER_LY 12 /* 滑条行 label 顶相对行顶 y(上移给 track 留位) */
#define SET_SWITCH_POOL 12
/* MD3 容器化(P0):设置页行=surface 上 surfaceContainer 圆角卡 + 行间空隙(去分隔线);
* 卡横向外边距=SET_CARD_X(8),卡在行高内上下对称内缩(行中心=卡中心,同主界面/ncm 歌单手法)。 */
#define SET_ROW_CARD_X SET_CARD_X /* 8:卡左缘 = 内容区左缘 */
#define SET_ROW_CARD_G 8 /* 卡纵向间隙(px) */
#define SET_ROW_CARD_R 12 /* 卡圆角(12dp) */
static meow_widget *g_set_lv = NULL; /* meow_listview(虚拟化) */
static meow_scroll *g_set_sc = NULL; /* 滚动物理 */
static meow_widget *g_set_sw[SET_SWITCH_POOL]; /* 行内开关池(真 meow_switch) */
static int g_set_sw_n = 0, g_set_sw_use = 0; /* 池创建数 / 本轮已用(每次整绘复位) */
static int g_set_lv_h = 0; /* 列表视口高(选中可见性 clamp 用) */
static int g_set_init = 0;
static void set_lv_rect(int *x, int *y, int *w, int *h)
{
if (x) *x = 0;
if (y) *y = SET_LIST_Y;
if (w) *w = fb_xres;
if (h) { int t = fb_yres - SET_LIST_Y - SET_FOOT_H; if (t < 0) t = 0; *h = t; }
}
static int set_lv_count(void *ctx) { (void)ctx; return s_vis_count(); }
/* 行填充(虚拟化 pool bind;段头行 section=1、enabled=1 供 SDK 命中/段头绘制) */
static void set_lv_bind(int idx, meow_lv_item *out, void *ctx)
{
(void)ctx;
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->enabled = 1;
if (idx < 0 || idx >= s_vis_count()) { out->title = ""; return; }
const srow_t *r = &SMENU[s_vis_at(idx)];
out->title = r->label ? s_row_label(r) : "";
out->section = (r->ctrl == SC_HEADER) ? 1 : 0;
if (r->ctrl == SC_SWITCH) {
int on = 0; meow_settings_get_switch(r->key, &on);
out->right = MEOW_LIST_RIGHT_SWITCH; out->on = on;
}
}
/* 行内开关池:取下一个可用槽(每帧 set_lv_draw 顺序取用,整绘前复位 g_set_sw_use) */
static meow_widget *set_sw_take(void)
{
if (g_set_sw_use < g_set_sw_n) return g_set_sw[g_set_sw_use++];
if (g_set_sw_n >= SET_SWITCH_POOL) return NULL;
meow_widget *w = meow_switch_create(0, 0, 0);
if (w) { g_set_sw[g_set_sw_n++] = w; return g_set_sw[g_set_sw_use++]; }
return NULL;
}
/* 整行自绘(set_item_draw 覆盖默认 renderer;x=列表左缘、y=行顶、w/h=行全宽高,
* state=SELECTED/PRESSED/DISABLED 组合;颜色全取 meow_theme 语义色,无散落硬编码) */
static void set_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)ctx; (void)h;
const meow_theme *mt = meow_theme_get();
if (idx < 0) return;
int si = s_vis_at(idx);
const srow_t *r = &SMENU[si];
int sel = (state & MEOW_LISTVIEW_ST_SELECTED);
int press = (state & MEOW_LISTVIEW_ST_PRESSED);
int dis = (state & MEOW_LISTVIEW_ST_DISABLED);
int thh = meow_ui_text_height(mt->font_body);
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
/* MD3 容器化(P0):行=surface 上 surfaceContainer 圆角卡;段头=surface 上的 primary 段标签(无卡)。 */
int cx = x + SET_ROW_CARD_X; /* 卡左缘 */
int cw = w - 2 * SET_ROW_CARD_X; /* 卡宽 */
int cy = y + SET_ROW_CARD_G / 2; /* 卡顶(行内对称内缩) */
int ch = SET_ITEM_H - SET_ROW_CARD_G; /* 卡高 */
if (r->ctrl == SC_HEADER) {
int ty = y + (SET_ITEM_H - thh) / 2;
if (ty >= 0) meow_ui_text(cx + 16, ty, s_row_label(r), mt->font_body, mt->primary);
return;
}
meow_gfx_fill_round(cx, cy, cw, ch, SET_ROW_CARD_R, mt->surfaceContainer);
if (sel) meow_gfx_fill_round(cx, cy, cw, ch, SET_ROW_CARD_R, mt->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, ch, SET_ROW_CARD_R, mt->primary, 26);
int ly = y + (SET_ITEM_H - thh) / 2; /* label 单行垂直居中 */
int rgt = cx + cw - 16; /* 卡内右缘(内容右对齐锚点) */
switch (r->ctrl) {
case SC_SWITCH: {
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
meow_widget *sw = set_sw_take();
if (sw) {
int on = 0; meow_settings_get_switch(r->key, &on);
meow_widget_set_rect(sw, rgt - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
meow_switch_set(sw, on);
meow_widget_draw(sw);
}
break;
}
case SC_SLIDER: {
/* label 上移一行,track 放行下方(中心=SET_SLIDER_YOFF+3=48),与拖动命中带对齐 */
meow_ui_text(cx + 16, y + SET_SLIDER_LY, s_row_label(r), mt->font_body, textcol);
char vb[32]; s_fmt_value(r, vb, sizeof vb);
meow_ui_text(rgt - meow_ui_text_width(vb, mt->font_body), y + SET_SLIDER_LY, vb, mt->font_body, subcol);
int v = 0; meow_settings_get_int(r->key, &v);
/* 息屏时间为档位索引型(秒→索引 等距画);其余 clamp 到 [min,max] */
if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v);
else { if (v < r->min) v = r->min; if (v > r->max) v = r->max; }
s_draw_slider(S_SLIDER_X, y + SET_SLIDER_YOFF, S_SLIDER_W, v, r->min, r->max, s_ui_theme());
break;
}
case SC_INFO: {
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
char vb[48]; s_net_get(r, vb, sizeof vb);
meow_ui_text(rgt - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
break;
}
case SC_LIST: {
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
char vb[48]; s_net_get(r, vb, sizeof vb);
int vx = rgt - 12 - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
s_draw_chevron(rgt, y + SET_ITEM_H / 2, subcol);
break;
}
default: { /* SC_ENUM / SC_PRESET */
meow_ui_text(cx + 16, ly, s_row_label(r), mt->font_body, textcol);
char vb[40]; s_display_value(r, vb, sizeof vb);
int vx = rgt - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
s_draw_chevron(vx - 8, y + SET_ITEM_H / 2, subcol);
break;
}
}
if (dis) meow_gfx_fill_round_alpha(cx, cy, cw, ch, SET_ROW_CARD_R, mt->onSurface, 90);
}
/* ---- meow_scroll 回调(包 listview)---- */
static void set_sc_set(void *ctx, int off) { (void)ctx; if (g_set_lv) meow_listview_set_scroll(g_set_lv, off); }
static int set_sc_max(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_max_scroll(g_set_lv) : 0; }
static int set_sc_get(void *ctx) { (void)ctx; return g_set_lv ? meow_listview_scroll(g_set_lv) : 0; }
/* 列表区重绘(含内容底 + listview),供 fling/滚动局部刷。 */
static void set_redraw_region(void *ctx)
{
(void)ctx;
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
if (lvh <= 0) return;
const meow_ui_theme_t *th = s_ui_theme();
meow_ui_fill_rect(lvx, lvy, lvw, lvh, th->surface); /* P0:内容底改 surface(卡在其上抬升) */
if (g_set_lv) { g_set_sw_use = 0; meow_widget_draw(g_set_lv); }
meow_dirty_add(lvx, lvy, lvw, lvh);
meow_ui_flush();
}
/* 选中行可见性:滚动到选中行不越界(仅靠 tap 触发,选中行即在视野内,防御性兜底)。 */
static void set_ensure_sel_visible(int sel)
{
if (!g_set_lv) return;
int p = s_vis_of(sel); if (p < 0) return;
int scroll = meow_listview_scroll(g_set_lv);
int top = p * SET_ITEM_H, bot = top + SET_ITEM_H;
if (top < scroll) meow_listview_set_scroll(g_set_lv, top);
else if (bot > scroll + g_set_lv_h) meow_listview_set_scroll(g_set_lv, bot - g_set_lv_h);
}
/* 创建设置页控件集(owner=SET_SENTINEL_ACT;仅首进创建一次,跨次进入复用)。 */
static void set_widgets_create(void)
{
meow_ui_activity_push(SET_SENTINEL_ACT);
int lvx, lvy, lvw, lvh; set_lv_rect(&lvx, &lvy, &lvw, &lvh);
g_set_lv_h = lvh;
g_set_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
if (g_set_lv) {
meow_listview_set_vsource(g_set_lv, set_lv_count, set_lv_bind, NULL);
meow_listview_set_item_draw(g_set_lv, set_lv_draw, NULL);
meow_listview_set_item_h(g_set_lv, SET_ITEM_H);
/* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标会往回拉 */
}
g_set_sc = meow_scroll_create(set_sc_set, set_sc_max, set_sc_get, set_redraw_region, NULL);
meow_ui_activity_pop();
g_set_init = 1;
}
/* 整页绘制(进 back_buf;调用方 meow_dirty_full/meow_dirty_add + flush)。sel=当前选中(SMENU 下标)。 */
static void set_paint_page(int sel)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
fill_rect(0, 0, W, H, th->surface);
draw_statusbar();
if (mfont_ready) ui_draw(24, 50, meow_str_zh("menu.settings", "系统设置"), th->on_surface, 28);
else draw_text(24, 30, "SETTINGS", 3, th->on_surface);
fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline);
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface); /* P0:内容底 surface,行用圆角卡抬升 */
if (g_set_lv) {
g_set_sw_use = 0;
int p = s_vis_of(sel); if (p >= 0) meow_listview_set_sel(g_set_lv, p);
meow_widget_draw(g_set_lv);
}
/* 底部:返回(左)+ 版本(右),复用应用列表底部对齐逻辑 */
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(20) - 8;
s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant);
ui_draw(SET_CARD_X + 24, foot_y, meow_str_zh("settings.back", "返回"), th->on_surface_variant, 20);
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
}
/* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */
void run_settings_page(void)
{
int sel = s_first_setting();
int scroll_off = 0;
int H = fb_yres;
unsigned long long t_sets = now_ms(); /* M3 取证:进设置页耗时分段(真机核对用) */
/* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,刷新「字体文件」下拉。
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */
meow_font_scan_presets();
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。
* M3:verify=0 —— 跳过逐套 sha256 校验(gbk 套 6.8MB 级,逐套双 sha256 真机耗数秒),
* 只枚举+读 font.json+落 index,入口不再被 sha256sum 拖住;完整性校验留给启动全量扫描。 */
meow_font_scan_presets(0);
logmsg("settings entry: font rescan %llums", now_ms() - t_sets);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
struct kd_state kd_play, kd_pwr;
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
int m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
key_router_rebase(); /* 进入设置页:音量基线对齐当前 raw(防残留边沿) */
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
unsigned long long last_tick = now_ms();
unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */
int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */
unsigned long long net_refresh_pump_at = 0; /* M3:异步网络刷新下一推进时刻(0=立即) */
/* 触摸拖动滑条状态 */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
int drag = -1; /* 被拖动的行索引;-1=无 */
/* M2.37 列表拖动滚动状态 */
int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */
int scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */
int drag_last_y = 0; /* M7.2:滚动跟手增量基准(meow_scroll_track 速度采样) */
int scroll_y0 = 0; /* 按下原点纵向坐标 */
int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */
int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */
@@ -992,10 +1350,17 @@ void run_settings_page(void)
int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */
/* 首次全帧绘制 */
s_net_sync_from_sys(); /* M2.39:WiFi/蓝牙开关行同步真实硬件态 + 刷新网络信息缓存 */
scroll_off = s_clamp_scroll(sel, scroll_off);
draw_settings_page(sel, scroll_off);
/* M2.39/M3:开关行同步真实硬件态(快)+ 异步 kick 网络刷新 —— 不再在入口同步跑
* wpa_cli/bluetoothctl 串行 popen(真机 ≥5s 卡顿根因);状态/列表由下方主循环逐步骤
* s_net_refresh_pump() 填充,draw 用缓存(g_net_*),数据变化即 need_paint。 */
s_net_sync_from_sys();
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
key_router_page_enter(SET_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
set_paint_page(sel);
meow_dirty_full(); meow_ui_flush();
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
for (;;) {
unsigned long long now = now_ms();
@@ -1031,48 +1396,51 @@ void run_settings_page(void)
meow_auto_slept = 0;
wake_suppress = 1;
last_act = now_ms();
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸。
* 音量防抖/基线上收 key_router:唤醒时重播音量基线;播放/电源仍本页自理。 */
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
logmsg("settings: auto screen-off -> input wake, stayed in settings page");
scroll_off = s_clamp_scroll_range(scroll_off);
draw_settings_page(sel, scroll_off);
set_paint_page(sel);
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
continue;
}
/* ---- M3:异步网络刷新(每 ~150ms 推进一步,每步一个有界 popen)----
* 入口已同步 draw(用 g_net_* 缓存),此处逐步填充 SSID/已保存/附近/蓝牙角色,
* 数据变化则 need_paint=1 让行值即时刷新,绝不在某一同步调用里连续跑数条 popen。 */
if (now >= net_refresh_pump_at) {
net_refresh_pump_at = now + 150;
if (s_net_refresh_pump()) need_paint = 1;
}
/* ---- 按键 ---- */
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
int play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
} else {
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
@@ -1108,14 +1476,11 @@ void run_settings_page(void)
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
need_paint = 1;
@@ -1142,14 +1507,14 @@ void run_settings_page(void)
}
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold;
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
m_prev_play = play; m_prev_pwr = pwr;
settings_keys_seeded:
;
@@ -1166,34 +1531,47 @@ void run_settings_page(void)
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */
/* 按下原点落在选中滑条 track 带内 → 开始拖动 */
if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 &&
tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) {
drag = sel;
const srow_t *r = &SMENU[sel];
int v = s_slider_val_from_x(r, tlx);
s_slider_set_from_track(r, v, 0);
need_paint = 1;
meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(行按压态渲染) */
scroll_y0 = tdy; /* 列表拖动基准:按下原点 */
/* 按下原点落在滑条行 track 带内 → 开始拖动(meow_listview hit 判行) */
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tlx, tly) : -1;
if (vp >= 0 && drag < 0) {
const srow_t *rr = &SMENU[s_vis_at(vp)];
if (rr->ctrl == SC_SLIDER) {
int track_cy = SET_LIST_Y + vp * SET_ITEM_H -
meow_listview_scroll(g_set_lv) + SET_TRACK_CY;
if (tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
tly >= track_cy - 14 && tly <= track_cy + 14) {
drag = s_vis_at(vp);
int v = s_slider_val_from_x(rr, tlx);
s_slider_set_from_track(rr, v, 0);
need_paint = 1;
}
}
}
}
/* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
* scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。
* M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项);
* 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */
/* M7.2 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
* 走 meow_scroll(跟手 track + 松手 fling),同 subpage.c M7.1a 语义;
* 方向经 input.swipe_invert 切换(默认手指上滑 → 内容下翻)。 */
if (td_ok && drag < 0 && !scroll_drag) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
scroll_base = scroll_off + (scroll_invert ? -ddy : ddy);
meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */
drag_last_y = tly;
}
}
if (scroll_drag && td_ok) {
int dydt = tly - scroll_y0;
scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt));
need_paint = 1;
int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */
drag_last_y = tly;
int scdy = scroll_invert ? dydt : -dydt;
if (g_set_sc && scdy) meow_scroll_track(g_set_sc, scdy, now);
/* 列表滚动不在此即时重绘:整批事件末尾由下方「if (scroll_drag && td_ok)
* set_redraw_region」合并为一次视口局部重绘。此处若 set_redraw_region 且再设
* need_paint=1,底部 set_paint_page 会再走一遍整页重绘 + dirty_add(0,42,...)
* 近整屏 flush —— 快滑每帧整页重绘 → 卡顿,违背「局部脏区不整屏重绘」铁律;
* 且一个 poll 多个移动事件各自 flush 会多次 FBIO_WAITFORVSYNC 阻塞放大延迟。 */
}
if (drag >= 0 && td_ok) {
const srow_t *r = &SMENU[drag];
@@ -1210,6 +1588,7 @@ void run_settings_page(void)
}
} else if (!val && td) {
td = 0;
meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */
if (drag >= 0) { /* 滑条拖动结束 → 落盘 */
const srow_t *r = &SMENU[drag];
s_commit(r, 1);
@@ -1217,7 +1596,8 @@ void run_settings_page(void)
need_paint = 1;
} else if (scroll_drag) {
scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */
scroll_base = 0; scroll_y0 = 0;
if (g_set_sc) meow_scroll_fling(g_set_sc); /* 松手 fling(惯性 ease-out) */
scroll_y0 = 0;
need_paint = 1;
} else if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
@@ -1236,8 +1616,10 @@ void run_settings_page(void)
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto done;
}
int cy = ty - SET_CONTENT_Y + scroll_off;
int idx = s_row_at_content_y(cy);
/* M7.2:行命中走 meow_listview(含滚动换算 + 禁用/越界行不响应);
* 段头行 enabled=1 故 hit 亦命中 → 下方按 ctrl==SC_HEADER 分流。 */
int vp = g_set_lv ? meow_listview_hit(g_set_lv, tx, ty) : -1;
int idx = (vp >= 0) ? s_vis_at(vp) : -1;
if (idx >= 0 && !s_setting_idx(idx)) {
/* 点分组标题行 → 仅选中下一设置行 */
int ns = s_next_setting(idx);
@@ -1255,8 +1637,9 @@ void run_settings_page(void)
* M2.76:WiFi/蓝牙升级为独立子页面 → 点开进 run_wifi_page/
* run_bt_page(阻塞,内部自开 fds;返回后仍须排空本页 tfd)。 */
after_modal = 1;
if (r->net_list == NET_LIST_WIFI_PAGE) { run_wifi_page(); s_net_refresh(); }
else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh(); }
if (r->net_list == NET_LIST_WIFI_PAGE) { run_wifi_page(); s_net_refresh_kick(); }
else if (r->net_list == NET_LIST_BT_PAGE) { run_bt_page(); s_net_refresh_kick(); }
else if (r->net_list == NET_LIST_PIN_PAGE) { run_lock_settings_page(); s_net_refresh_kick(); }
else s_net_open(r);
if (tfd >= 0) {
unsigned char e[16];
@@ -1265,17 +1648,13 @@ void run_settings_page(void)
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_base = 0; scroll_y0 = 0;
scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
key_router_rebase(); /* 音量基线重播 */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1;
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
@@ -1309,18 +1688,14 @@ void run_settings_page(void)
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_base = 0; scroll_y0 = 0;
scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
/* 重置设置页按键防抖基线,防残边 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
key_router_rebase(); /* 音量基线重播 */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1;
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
@@ -1335,14 +1710,18 @@ void run_settings_page(void)
}
}
}
/* 列表拖动滚动:本批连续触摸事件合并为一次视口局部重绘(只刷列表区,
* 最多一次 present),不再触发 need_paint 整页重绘。scroll_drag/td_ok 在
* 抬手处置 0(抬手那轮已走 meow_scroll_fling + need_paint 整页),故此处只兜活动滚动。 */
if (scroll_drag && td_ok) set_redraw_region(NULL);
}
/* ---- 滚动 + 重绘 ---- */
if (need_paint) {
last_act = now;
/* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */
if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; }
draw_settings_page(sel, scroll_off);
if (sel_changed) { set_ensure_sel_visible(sel); sel_changed = 0; }
set_paint_page(sel);
/* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化),
* 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。
* 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */
@@ -1367,9 +1746,14 @@ void run_settings_page(void)
fb_norm_buf0();
meow_ui_flush();
}
/* M7.2: 推进 fling/回弹动画(meow_scroll 惯性停;无动画零开销) */
meow_anim_tick();
usleep(POLL_MS * 1000);
}
done:
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
key_router_page_exit(SET_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
logmsg("settings page closed");
+602 -21
View File
@@ -1,9 +1,9 @@
/* subpage.c — MeowOS launcher 独立子页面(WiFi/蓝牙)统一脚手架(M2.76)。
/* subpage.c — MeowOS launcher 独立子页面(WiFi/蓝牙/锁屏 PIN)统一脚手架(M2.76)。
*
* 安卓风子页骨架:顶栏(标题 + 返回 + 右上开关)+ 分组列表(组标题/行/开关/动作)
* + 滚动(跟手,方向沿用设置页 input.swipe_invert 语义)+ 左缘左滑返回 + 底部返回。
* + 滚动 + 左缘左滑返回。
*
* 由页面(wifi_page.c / bt_page.c)通过 sub_page_t 提供:
* 由页面(wifi_page.c / bt_page.c / lock_settings.c)通过 sub_page_t 提供:
* - title 顶栏标题
* - top_sw 顶栏开关(0=不显示;1=显示并回写当前值)
* - top_sw_set 开关切换(写设置 + 生效 + 触发重扫)
@@ -12,7 +12,18 @@
* - detail 点普通行(如当前连接详情、网络/设备详情)
* - live 1=驻留时周期性(约2s)重扫行(蓝牙附近扫描等动态数据)
*
* 与设置页同主题(s_ui_theme,MD3 语义色,明暗自动);局部刷新不整屏重绘;
* ================================================================
* M7.1a 渲染层迁移:脚手架手绘(顶栏 + 分组列表 + 跟手滚动 + 手绘 switch/chevron)
* 改走 SDK 控件(单一 meow_theme 权威):
* - 顶栏 → meow_topappbar(标题 + 返回箭头 + 底部分隔线);
* - 顶部开关 → meow_switch(叠顶栏右缘,保留开/关视觉与命中);
* - 分组列表 → meow_listview(virtualized set_vsource 固定 item 池 + section 段头
* + right=SWITCH/ARROW 尾件 + 选中/按压态);
* - 滚动 → meow_scroll(跟手 track 采样松手 fling + snap 到行边界)。
* wifi/bt/lock_settings 的「数据 + 动作」逻辑(netmgr IPC / bluetoothctl / 设置契约)
* 全部保留在 launcher 逻辑层,仅换渲染。旧手绘路径整体保留 #if 0 作回退位(M7 红线)。
*
* 与设置页同主题(s_ui_theme → SDK meow_theme 单一权威,MD3/MIUIx×明暗自动);
* 模态弹窗交给 launcher 既有 s_dd_modal(点外关闭不穿透),本脚手架在回调返回后
* 彻底排空本页 tfd + 重置触摸基线(复刻 run_settings_page 后置排空,防穿透)。
*
@@ -22,16 +33,28 @@
#include "meow_launcher_internal.h"
/* ---- 布局常量(顶栏/内容区;全屏内容区延用设置页 surface_variant 整屏底)---- */
/* ---- 布局常量(顶栏/内容区;全屏内容区延用设置页 surface 整屏底)----
* note:SUB_CONT_TOP 与旧版一致(96),顶栏底缘分隔线由 SDK topappbar 绘制(y=95)。 */
#define SUB_TB_Y SB_BAR_H /* 28:顶栏顶缘(状态栏下) */
#define SUB_TB_H 68 /* 顶栏高 28..96 */
#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96:顶栏底缘 / 分隔线 */
#define SUB_CONT_TOP (SUB_TB_BOT + 6) /* 102:内容区顶缘 */
#define SUB_TB_BOT (SUB_TB_Y + SUB_TB_H) /* 96:顶栏底缘 */
#define SUB_CONT_TOP (SUB_TB_BOT) /* 96:内容区顶缘(topappbar 分隔线下) */
#define SUB_FOOT_H 44 /* 底部版本信息带(右对齐) */
#define SUB_ITEM_H 56 /* MD3 行高(双行富 item:主/副标题) */
#define SUB_PAD_X 16 /* 水平内边距(顶开关右缘留白用) */
#define SUB_SW_H 32 /* 顶开关高(meow_switch 固定 56×32) */
#if 0 /* ================================================================
* M7.1a 退役:子页旧手绘脚手架(顶栏 + 分组列表 + 跟手无惯性滚动 + 手绘
* s_draw_switch/s_draw_chevron + 自算行前缀和 + 自算命中)。改走 SDK 控件
* (meow_topappbar + meow_listview + meow_scroll),命中/布局/状态/滚动/主题
* 全由 SDK 接管。本块保留作回退位(删除 #if 0 行并删下方「M7.1a SDK 路径」段
* 即可恢复旧手绘)。以下全部内容与迁移前 subpage.c 逐字一致。
* ================================================================ */
#define SUB_ROW_H 58 /* 行高(对齐 SROW_H,便于触摸) */
#define SUB_HDR_H 40 /* 组标题行高 */
#define SUB_PAD_X 16 /* 水平内边距 */
#define SUB_SW_W 48 /* 开关宽(s_draw_switch) */
#define SUB_SW_H 28
#define SUB_BTN_X 8 /* 底部返回按钮左缘 */
#define SUB_BTN_W 120 /* 底部返回按钮宽 */
@@ -224,12 +247,11 @@ int meow_subpage_run(sub_page_t *p)
meow_auto_slept = 0;
wake_suppress = 1;
last_act = now;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
@@ -279,14 +301,12 @@ int meow_subpage_run(sub_page_t *p)
last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1;
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1;
need_paint = 1;
@@ -433,14 +453,12 @@ int meow_subpage_run(sub_page_t *p)
scroll_base = 0; scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1;
need_paint = 1;
@@ -502,3 +520,566 @@ subdone:
logmsg("subpage '%s' closed", p->title ? p->title : "");
return 0;
}
#endif /* M7.1a 退役:旧手绘子页脚手架回退位 */
/* ==================================================================
* M7.1a 子页渲染层迁移(SDK 控件化):
* - 顶栏 → meow_topappbar(标题 + 返回箭头 + 底部分隔线);
* - 顶部开关 → meow_switch(叠顶栏右缘,保留开/关视觉 + 命中);
* - 分组列表 → meow_listview(virtualized set_vsource 固定 item 池 + section 段头
* + right=SWITCH/ARROW 尾件 + 选中/按压态,SDK 语义色);
* - 滚动 → meow_scroll(跟手 meow_scroll_track 采样 + 松手 fling ease-out 惯性停;
* 刻意不 set_snap——SDK snap 以 fling 起点为吸附目标会往回拉,见 sp_widgets_create 注)。
* 控件 lazy 创建一次(owner=SUB_SENTINEL_ACT 哨兵),跨多次进入复用;meow_subpage_run()
* 进入 push 哨兵、退出 pop(owner 闸门使控件命中生效)。数据/动作回调(rows/act/detail/
* top_sw_*)仍由各子页提供,逻辑层不动,仅渲染层替换。
* ================================================================== */
#define SUB_SENTINEL_ACT 0x52 /* 子页哨兵 activity(不与 launcher 其它页冲突) */
static sub_page_t *g_sp; /* 当前运行子页(rows/act/detail/top_sw 回调入口) */
static sub_row_t g_rows[SUB_ROWS_MAX]; /* 行缓冲:rows() 一次性填满,listview bind 只读 */
static int g_nrows = 0; /* 当前行数(listview vsource count 实时读) */
static int g_top_sw_en = 0; /* 本页是否显示顶开关 */
static int g_top_on = 0; /* 顶开关当前值 */
static meow_widget *g_tb = NULL; /* topappbar */
static meow_widget *g_sw = NULL; /* 顶开关(仅在有顶开关的页创建/使用) */
static meow_widget *g_lv = NULL; /* listview */
static meow_scroll *g_sc = NULL; /* 滚动物理 */
static int g_sp_init = 0;
/* 列表视口矩形(内容区,去掉底部版本带) */
static void sp_lv_rect(int *x, int *y, int *w, int *h)
{
if (x) *x = 0;
if (y) *y = SUB_CONT_TOP;
if (w) *w = fb_xres;
if (h) { *h = fb_yres - SUB_CONT_TOP - SUB_FOOT_H; if (*h < 0) *h = 0; }
}
/* ---- 行缓冲 → meow_lv_item(虚拟化 bind;每行轻量填充,不做 IO §5.5)---- */
static int sp_lv_count(void *ctx) { (void)ctx; return g_nrows; }
static void sp_lv_bind(int idx, meow_lv_item *out, void *ctx)
{
(void)ctx;
memset(out, 0, sizeof *out);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->enabled = 1;
if (idx < 0 || idx >= g_nrows) { out->title = ""; return; }
const sub_row_t *r = &g_rows[idx];
switch (r->kind) {
case SUB_HDR: /* 段头占位行(非粘性):default renderer 特殊绘制 */
out->section = 1; out->title = r->label ? r->label : "";
out->right = MEOW_LIST_RIGHT_NONE; out->enabled = 0; break;
case SUB_ROW: /* 普通信息行 */
out->title = r->label ? r->label : "";
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_NONE; break;
case SUB_SW: /* 开关行:右尾件 SWITCH */
out->title = r->label ? r->label : "";
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_SWITCH; out->on = r->on; break;
case SUB_ACT: /* 动作行:右尾件 ARROW */
out->title = r->label ? r->label : "";
out->subtitle = r->sub; out->right = MEOW_LIST_RIGHT_ARROW; break;
}
}
/* ---- 行完全自绘(批③:页面经 sub_page_t.row_draw 定制整行;默认 renderer 被覆盖)---- */
static void sp_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
{
(void)ctx;
if (!g_sp || !g_sp->row_draw) return;
if (idx < 0 || idx >= g_nrows) return;
g_sp->row_draw(g_sp->ctx, idx, &g_rows[idx], x, y, w, h, state);
}
/* ---- 顶栏标题栏右侧动作按钮矩形(批③:sub_page_t.top_action)---- */
static void sp_top_action_rect(int *x, int *y, int *w, int *h)
{
if (x) *x = 0;
if (y) *y = 0;
if (w) *w = 0;
if (h) *h = 0;
if (!g_sp || !g_sp->top_action) return;
int tw = ui_width(g_sp->top_action, 20);
if (tw <= 0) return;
int bw = tw + 24, bh = 40; /* 含内边距的圆角按钮 */
*x = fb_xres - 12 - bw;
*y = SUB_TB_Y + (SUB_TB_H - bh) / 2;
*w = bw; *h = bh;
}
/* 绘制顶栏动作按钮(sp_paint_page 内调用)。 */
static void sp_paint_top_action(void)
{
if (!g_sp || !g_sp->top_action) return;
int x, y, w, h; sp_top_action_rect(&x, &y, &w, &h);
if (w <= 0 || h <= 0) return;
const meow_theme *mt = meow_theme_get();
uint32_t col = mt->primary;
meow_ui_draw_rect_round(x, y, w, h, h / 2, col); /* 全形=胶囊描边按钮(P1) */
int th = meow_ui_text_height(20);
if (th > h) th = h;
meow_ui_text_center(x + w / 2, y + (h - th) / 2, g_sp->top_action, 20, col);
}
/* ---- meow_scroll 回调(包 listview)---- */
static void sp_sc_set(void *ctx, int off) { (void)ctx; if (g_lv) meow_listview_set_scroll(g_lv, off); }
static int sp_sc_max(void *ctx) { (void)ctx; return g_lv ? meow_listview_max_scroll(g_lv) : 0; }
static int sp_sc_get(void *ctx) { (void)ctx; return g_lv ? meow_listview_scroll(g_lv) : 0; }
/* 列表区重绘(含背景 + listview),供 fling 每帧/滚动局部刷。ctx 为 meow_scroll 回调用。 */
static void sp_redraw_region(void *ctx)
{
(void)ctx;
int lvx, lvy, lvw, lvh;
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
if (lvh <= 0) return;
const meow_theme *mt = meow_theme_get();
meow_ui_fill_rect(lvx, lvy, lvw, lvh, mt->surface); /* 内容底 surface */
if (g_lv) meow_widget_draw(g_lv);
meow_dirty_add(lvx, lvy, lvw, lvh);
meow_ui_flush();
}
/* 创建子页控件集(owner=SUB_SENTINEL_ACT;仅首进创建一次,跨页复用)。 */
static void sp_widgets_create(void)
{
meow_ui_activity_push(SUB_SENTINEL_ACT);
g_tb = meow_topappbar_create(0, SUB_TB_Y, fb_xres, SUB_TB_H, "");
if (g_tb) meow_topappbar_set_back(g_tb, 1);
int lvx, lvy, lvw, lvh;
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
g_lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
if (g_lv) {
meow_listview_set_vsource(g_lv, sp_lv_count, sp_lv_bind, NULL);
/* 双行富 item + 无左图标 + 右尾件(SWITCH/ARROW);行高 56 */
meow_listview_set_item_renderer(g_lv, 2, 0, 1);
meow_listview_set_item_h(g_lv, SUB_ITEM_H);
/* MD3(P0):默认 renderer 容器模式——行=surfaceContainer 圆角卡+行间空隙,
* 选中/按压 tonal(primaryContainer/primary α26)、去冗余分隔线;段头=surface 上的 primary 段标签。 */
meow_listview_set_container(g_lv, 1);
}
/* 注:刻意不 set_snap —— SDK meow_scroll 的 snap 以「fling 起点」为吸附目标,快速滑动时会
* 从自然停点往回拉一整行(实测 697→336),UX 不佳;默认 ease-out 惯性停(无 snap)更顺滑。
* 若后续 SDK 修正 snap 到「自然停点」,可恢复 meow_scroll_set_snap(g_sc, SUB_ITEM_H)。 */
g_sc = meow_scroll_create(sp_sc_set, sp_sc_max, sp_sc_get, sp_redraw_region, NULL);
meow_ui_activity_pop();
g_sp_init = 1;
}
/* 逐页配置:topappbar 标题 + 顶开关(按需创建/置态)+ listview 行数。 */
static void sp_configure_page(const sub_page_t *p, int top_sw_en, int top_on)
{
if (p->title && g_tb) meow_topappbar_set_title(g_tb, p->title);
g_top_sw_en = top_sw_en;
g_top_on = top_on;
if (top_sw_en) {
if (!g_sw) {
meow_ui_activity_push(SUB_SENTINEL_ACT); /* owner 钉哨兵(命中闸门) */
int swx = fb_xres - SUB_PAD_X - 56; /* meow_switch 固定 56×32 */
int swy = SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2;
g_sw = meow_switch_create(swx, swy, top_on);
meow_ui_activity_pop();
} else if (g_sw) {
meow_switch_set(g_sw, top_on);
}
}
/* 批③:行完全自绘 on/off —— 页提供 row_draw 用自绘;否则清 NULL 回默认 renderer。 */
if (g_lv) meow_listview_set_item_draw(g_lv, p->row_draw ? sp_lv_draw : NULL, NULL);
}
/* 整页绘制(进 back_buf;调用方 meow_dirty_full + flush 上屏)。 */
static void sp_paint_page(void)
{
const meow_theme *mt = meow_theme_get();
int W = fb_xres, H = fb_yres;
meow_ui_fill_rect(0, 0, W, H, mt->surface); /* 全屏底 surface */
draw_statusbar();
if (g_tb) meow_widget_draw(g_tb); /* topappbar(含底部分隔线) */
if (g_sw && g_top_sw_en) meow_widget_draw(g_sw); /* 顶开关(叠顶栏右缘) */
sp_paint_top_action(); /* 批③:顶栏右侧动作按钮 */
int lvx, lvy, lvw, lvh;
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
meow_ui_fill_rect(lvx, lvy, lvw, lvh, mt->surface); /* 内容底 surface */
if (g_lv) meow_widget_draw(g_lv); /* 分组列表 */
/* 底部版本信息(右对齐 dim) */
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s", g_meowos_version);
ui_draw(W - 8 - ui_width(ver, 18), H - SUB_FOOT_H + 10, ver,
s_ui_theme()->outline, 18);
}
/* 顶部开关切换:写设置 + 生效 + 触发重扫 + 重填行。 */
static void sp_top_sw_toggle(void)
{
if (!g_sp || !g_sp->top_sw_set) return;
g_sp->top_sw_set(g_sp->ctx, !g_top_on);
g_top_on = !g_top_on;
if (g_sw) meow_switch_set(g_sw, g_top_on);
if (g_sp->rows) {
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
}
}
/* 动作后重扫行(act/detail 已变更数据):重填行并 clamp 滚动。 */
static void sp_refresh_rows(void)
{
if (g_sp && g_sp->rows) {
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
}
if (g_lv) {
int ms = meow_listview_max_scroll(g_lv);
if (meow_listview_scroll(g_lv) > ms) meow_listview_set_scroll(g_lv, ms);
}
}
/* ---- 运行子页(阻塞到返回;返回 0)---- */
int meow_subpage_run(sub_page_t *p)
{
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
struct kd_state kd_play, kd_pwr;
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
key_router_rebase(); /* 进入子页:音量基线对齐当前 raw(防残留边沿) */
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
unsigned long long last_tick = now_ms();
unsigned long long last_act = now_ms();
unsigned long long last_live = now_ms();
int after_modal = 0; /* 子页内模态关闭后:覆盖过状态栏,需整帧重绘还原 */
/* 触摸状态 */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
int scroll_drag = 0, scroll_invert = 0, drag_last_y = 0;
int wake_suppress = 0;
unsigned long long pwr_hold_at = 0;
int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0;
/* 顶栏开关初始值 */
int top_on = 0, top_sw_en = 0;
if (p->top_sw) top_sw_en = p->top_sw(p->ctx, &top_on);
/* 首填行 + lazy 建控件 + 配置 + 首绘 */
g_sp = p;
g_nrows = p->rows ? p->rows(p->ctx, g_rows, SUB_ROWS_MAX) : 0;
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
if (!g_sp_init) sp_widgets_create();
sp_configure_page(p, top_sw_en, top_on);
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
key_router_page_enter(SUB_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
sp_paint_page();
meow_dirty_full(); meow_ui_flush();
for (;;) {
unsigned long long now = now_ms();
if (sigterm_flag) break;
int need_paint = 0;
/* ---- 自动息屏态:留在子页,等待任意输入唤醒,恢复后重绘(不丢上下文)---- */
if (meow_auto_slept && !meow_screen_on) {
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int wake = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1;
}
if (!wake && screen_off_mode == 1 && tfd >= 0) {
struct pollfd spfd = { tfd, POLLIN, 0 };
if (poll(&spfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1;
}
}
}
if (wake) {
meow_panel_on();
meow_auto_slept = 0;
wake_suppress = 1;
last_act = now;
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
after_modal = 1;
sp_paint_page();
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
continue;
}
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
} else {
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
if (pwr && !m_prev_pwr) {
pwr_hold_at = now; pwr_menu_state = 0;
pwr_suppress_click = 0;
last_act = now;
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
}
if (pwr && pwr_hold_at) {
if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) {
if (sim_no_exec) { logmsg("subpage pm: [sim-no-exec] POWER HOLD -> force poweroff"); }
else { logmsg("subpage pm: POWER HOLD -> force poweroff"); meow_power_action(POWER_SHUTDOWN); }
} else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) {
pwr_menu_state = 1;
logmsg("subpage pm: POWER HOLD -> show power menu");
show_power_menu();
last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1;
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1;
need_paint = 1;
goto sub_keys_seeded;
}
}
if (!pwr && m_prev_pwr) {
if (pwr_suppress_click) {
logmsg("subpage pm: power release after menu -> no toggle");
} else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) &&
!pwr_menu_state && !pwr_woke) {
logmsg("subpage pm: power single-click -> toggle panel");
meow_toggle_panel();
meow_auto_slept = 0;
last_act = now_ms();
need_paint = 1;
} else if (pwr_woke) {
logmsg("subpage pm: power release after auto wake -> no toggle");
}
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 0;
}
}
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
(void)vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
}
m_prev_play = play; m_prev_pwr = pwr;
sub_keys_seeded:
;
/* ---- 触摸(SDK 控件命中:topappbar/顶开关/listview + meow_scroll 滚动)---- */
if (tfd >= 0 && meow_screen_on) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
meow_input_touch_set(1, tdx, tdy, 0); /* 喂 SDK 输入态(按压态渲染) */
drag_last_y = tdy;
if (g_lv && meow_widget_hit(g_lv, tlx, tly)) /* 按压态即时反馈 */
sp_redraw_region(NULL);
}
if (td_ok && !scroll_drag) {
int ddy = tly - drag_last_y;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
meow_input_touch_set(0, 0, 0, 0); /* 滚动中不压行(清按压态) */
drag_last_y = tly; /* 从此增量(跨 slop 无跳变,对齐旧版补偿) */
}
}
if (scroll_drag && td_ok) {
int dydt = tly - drag_last_y; /* 本次位移增量(跟手) */
drag_last_y = tly;
int scdy = scroll_invert ? dydt : -dydt;
if (g_sc && scdy) meow_scroll_track(g_sc, scdy, now);
sp_redraw_region(NULL); /* 列表区重绘(新 offset) */
need_paint = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() < touch_cooldown_until) { tlx = tly = -1; }
else { td = 1; td_ok = 0; tlx = -1; tly = -1; scroll_drag = 0;
meow_input_touch_set(0, 0, 0, 0); }
} else if (!val && td) {
td = 0;
meow_input_touch_set(0, 0, 0, 0); /* 抬手:清 SDK 按压态 */
if (scroll_drag) {
scroll_drag = 0;
if (g_sc) meow_scroll_fling(g_sc); /* 松手 fling(速度采样) */
} else if (td_ok) {
int dx = tlx - tdx, dyy = tly - tdy;
/* 左缘左滑返回(全局手势语义) */
if (meow_edge_back(tdx, dx, dyy)) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto subdone;
}
int adx = dx < 0 ? -dx : dx, ady = dyy < 0 ? -dyy : dyy;
if (adx < 60 && ady < 60) {
int tx = tlx, ty = tly;
/* 顶栏:返回箭头(topappbar hit)/ 顶开关(meow_switch hit) */
int tb_hit = g_tb ? meow_topappbar_hit(g_tb, tx, ty) : -1;
if (tb_hit == MEOW_TOPBAR_BACK) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto subdone;
}
/* 批③:顶栏标题栏右侧动作按钮(一键清除;先于顶开关判定,二者页间互斥) */
if (g_sp && g_sp->top_action) {
int ax, ay, aw, ah; sp_top_action_rect(&ax, &ay, &aw, &ah);
if (aw > 0 && tx >= ax && tx < ax + aw && ty >= ay && ty < ay + ah) {
if (g_sp->top_action_cb) g_sp->top_action_cb(g_sp->ctx);
last_act = now;
sp_refresh_rows(); /* 清后立即重扫行 */
need_paint = 1;
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
continue;
}
}
if (g_top_sw_en && g_sw && meow_widget_hit(g_sw, tx, ty)) {
sp_top_sw_toggle();
last_act = now;
need_paint = 1;
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
continue;
}
/* 列表行命中(SDK 命中:含滚动换算 + 越界/禁用行不响应) */
int row_hit = g_lv ? meow_listview_hit(g_lv, tx, ty) : -1;
if (row_hit >= 0) {
sub_kind_t kind = g_rows[row_hit].kind;
if (kind == SUB_ROW && g_sp->row_tap) {
/* 批③:行内自定义命中(行内杀按钮 vs 行体;页自判,返回 1=重绘) */
int rry = SUB_CONT_TOP + row_hit * SUB_ITEM_H -
(g_lv ? meow_listview_scroll(g_lv) : 0);
g_sp->row_tap(g_sp->ctx, row_hit, &g_rows[row_hit],
tx, ty, 0, rry, fb_xres, SUB_ITEM_H);
} else if (kind == SUB_SW || kind == SUB_ACT) {
if (g_sp->act) g_sp->act(g_sp->ctx, row_hit, g_rows[row_hit].act);
} else if (kind == SUB_ROW) {
if (g_sp->detail) g_sp->detail(g_sp->ctx, row_hit, g_rows[row_hit].act);
}
/* 动作后重扫行 + clamp 滚动 */
sp_refresh_rows();
}
/* 仅命中行(回调可能开模态)才做后置排空/触摸基线重置/整帧重绘
* (对齐旧版 idx>=0 分支;空白区点按不重绘不排空,置 td_ok=0 即可)。 */
if (row_hit >= 0) {
if (tfd >= 0) {
unsigned char e[16];
while (read(tfd, e, sizeof e) == 16) {}
usleep(40000);
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; scroll_drag = 0;
drag_last_y = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1;
need_paint = 1;
}
}
}
td_ok = 0;
}
}
}
}
}
/* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */
if (g_sp && g_sp->live && now - last_live >= 2000) {
last_live = now;
int new_n = g_sp->rows ? g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX) : 0;
if (new_n > SUB_ROWS_MAX) new_n = SUB_ROWS_MAX;
if (new_n != g_nrows) {
g_nrows = new_n;
int ms = g_lv ? meow_listview_max_scroll(g_lv) : 0;
if (g_lv && meow_listview_scroll(g_lv) > ms) meow_listview_set_scroll(g_lv, ms);
last_act = now;
need_paint = 1;
}
}
/* ---- 重绘(整帧/内容区局部;fling 由 meow_scroll redraw 回调自刷)---- */
if (need_paint) {
last_act = now;
sp_paint_page();
if (after_modal) { meow_dirty_full(); after_modal = 0; }
else meow_dirty_add(0, SUB_TB_Y, fb_xres, fb_yres - SUB_TB_Y); /* 含顶栏(开关/返回/标题) */
meow_ui_flush();
}
/* ---- 自动息屏(空闲超时关屏,留在子页,等输入唤醒)---- */
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept &&
(unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
wake_suppress = 1;
logmsg("subpage: idle %ds -> auto screen-off (stay)", screen_off_timeout);
}
/* ~1s 状态栏时钟刷新 */
if (now - last_tick >= 1000) {
last_tick = now;
draw_clock();
fb_norm_buf0();
meow_ui_flush();
}
/* M7.1a: 推进 fling/回弹/snap 动画(无动画零开销) */
meow_anim_tick();
usleep(POLL_MS * 1000);
}
subdone:
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
key_router_page_exit(SUB_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
logmsg("subpage '%s' closed", p->title ? p->title : "");
return 0;
}
+172
View File
@@ -0,0 +1,172 @@
/* test_ams_start.c — 宿主单测:MSG_AMS_START 拉起决策(meow_ams_start.c)。
*
* 覆盖(AMSD_DESIGN §4.3 缺口回归):
* 1) 合法 MSG_AMS_START(裸启动 act 空)→ 置 pending → consume → ops.launch(idx, NULL);
* 2) 合法 MSG_AMS_START + activity → resolve → consume → ops.launch(idx, chain);
* 3) 参数非法(pkg 空)→ meow_ams_pending_start_set 返回 MEOW_ERR_INVAL,不置位、不拉起、不崩;
* 4) 有前台 App(busy)→ set 返回 MEOW_ERR_BUSY(负错误码,根因修复),不拉起;
* 5) 列表未就绪(state!=0)→ consume 返回 2(pending 保留),不拉起;后就绪再 consume → 拉起;
* 6) pkg 不在应用表 → consume 返回 1,toast「NO APP」,不拉起;
* 7) activity 无路由 → consume 返回 1,toast「NO ROUTE」,不拉起;
* 8) 启动失败(ops.launch 返 <0)→ consume 返回 1,app_ran=0,idx_out 命中(供 launch_fail_toast);
* 9) picker 扩载荷:set_ex 武装 → take_picker 取回并清位。
* 只依赖 meow_ams_start.c + meow_ipc.h(纯 C),宿主 gcc 直编:
* gcc -Wall -Wextra -std=gnu99 test_ams_start.c meow_ams_start.c -o test_ams_start
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "meow_ams_start.h"
#include "meow_ipc.h"
static int g_fails = 0;
#define CHECK(cond, msg) do { \
if (cond) printf("PASS: %s\n", msg); \
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
/* ---- 假件 ---- */
static int g_launch_calls = 0; /* ops.launch 被调次数 */
static int g_launch_last_idx = -1;
static char g_launch_last_chain[MEOW_CHAIN_MAX];
static int g_launch_ret = 0; /* 可注入:0=成功 / <0=启动失败 */
static int g_resolve_calls = 0;
static int g_resolve_rc = MEOW_OK; /* 可注入:MEOW_OK / MEOW_ERR_NOENT */
static int g_log_calls = 0;
static int fake_launch(int idx, const char *chain)
{
g_launch_calls++;
g_launch_last_idx = idx;
if (chain) snprintf(g_launch_last_chain, sizeof g_launch_last_chain, "%s", chain);
else g_launch_last_chain[0] = 0;
return g_launch_ret;
}
static int fake_resolve(const char *app_id, const char *activity,
char *chain, int *depth)
{
(void)app_id;
g_resolve_calls++;
if (g_resolve_rc != MEOW_OK) return g_resolve_rc;
snprintf(chain, MEOW_CHAIN_MAX, "%s", activity); /* 简化:chain=activity */
if (depth) *depth = 1;
return MEOW_OK;
}
static void fake_log(const char *fmt, ...) { (void)fmt; g_log_calls++; }
static void reset_fakes(void)
{
g_launch_calls = 0; g_launch_last_idx = -1;
g_launch_last_chain[0] = 0; g_launch_ret = 0;
g_resolve_calls = 0; g_resolve_rc = MEOW_OK;
g_log_calls = 0;
meow_ams_set_busy(0);
}
/* 应用表:id 主键串 + 用于 idx 断言 */
static const char *g_ids[3] = { "dev.player.ncm", "os.meow.files", "os.meow.hello" };
int main(void)
{
static struct meow_ams_ops ops;
ops.launch = fake_launch;
ops.resolve = fake_resolve;
ops.log = fake_log;
meow_ams_ops_init(&ops);
char toast[64];
int app_ran = 0, idx = -1;
/* ---- 1) 合法裸启动(act 空)→ consume → ops.launch ---- */
reset_fakes();
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set(bare): 受理");
int r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 1 && g_launch_calls == 1, "consume(bare): 拉起链路被调");
CHECK(g_launch_last_idx == 0, "consume(bare): idx 命中 dev.player.ncm");
CHECK(g_launch_last_chain[0] == 0, "consume(bare): chain 空(裸启动)");
CHECK(app_ran == 1, "consume(bare): app_ran=1");
/* ---- 2) activity 链 → resolve → consume → ops.launch(chain) ---- */
reset_fakes();
CHECK(meow_ams_pending_start_set("os.meow.files", "pick") == 0, "set(act): 受理");
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 1 && g_resolve_calls == 1, "consume(act): resolve 被调");
CHECK(g_launch_calls == 1 && g_launch_last_idx == 1, "consume(act): ops.launch(idx=1)");
CHECK(g_launch_last_chain[0] && strcmp(g_launch_last_chain, "pick") == 0,
"consume(act): chain 透传 pick");
/* ---- 3) 参数非法(pkg 空)→ INVAL,不置位、不拉起 ---- */
reset_fakes();
CHECK(meow_ams_pending_start_set("", "") == MEOW_ERR_INVAL, "set(空 pkg): 返回负错误码 MEOW_ERR_INVAL");
CHECK(meow_ams_pending_start_set(NULL, NULL) == MEOW_ERR_INVAL, "set(NULL pkg): MEOW_ERR_INVAL");
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 0 && g_launch_calls == 0, "consume: 空 pkg 未置位 → 不拉起");
/* ---- 4) busy(有前台 App)→ BUSY,不拉起 ---- */
reset_fakes();
meow_ams_set_busy(1);
CHECK(meow_ams_pending_start_set("os.meow.hello", "") == MEOW_ERR_BUSY,
"set(busy): 返回负错误码 MEOW_ERR_BUSY(根因修复:旧实现返回 +8 被误判 OK)");
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 0 && g_launch_calls == 0, "consume(busy): 不拉起");
meow_ams_set_busy(0);
/* ---- 5) 列表未就绪(state!=0)→ 保留 pending,后就绪拉起 ---- */
reset_fakes();
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set: 受理");
r = meow_ams_start_consume(g_ids, 3, 2, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 2 && g_launch_calls == 0, "consume(state!=0): 返回 2 保留 pending,不拉起");
CHECK(toast[0] && strstr(toast, "not ready"), "consume(state!=0): toast=列表未就绪");
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 1 && g_launch_calls == 1 && g_launch_last_idx == 0,
"consume(后就绪): pending 保留后拉起成功");
/* ---- 6) pkg 不在应用表 → NO APP,不拉起 ---- */
reset_fakes();
CHECK(meow_ams_pending_start_set("no.such.app", "") == 0, "set(未知app): 受理");
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 1 && g_launch_calls == 0, "consume(未知app): 不拉起");
CHECK(toast[0] && strstr(toast, "NO APP"), "consume(未知app): toast=NO APP");
/* ---- 7) activity 无路由 → NO ROUTE,不拉起 ---- */
reset_fakes();
g_resolve_rc = MEOW_ERR_NOENT;
CHECK(meow_ams_pending_start_set("os.meow.files", "nope") == 0, "set(nope): 受理");
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 1 && g_launch_calls == 0, "consume(无路由): 不拉起");
CHECK(toast[0] && strstr(toast, "NO ROUTE"), "consume(无路由): toast=NO ROUTE");
g_resolve_rc = MEOW_OK;
/* ---- 8) 启动失败(ops.launch<0)→ app_ran=0,idx_out 命中 ---- */
reset_fakes();
g_launch_ret = -2; /* MEOW_ERR_NOENT */
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set: 受理");
r = meow_ams_start_consume(g_ids, 3, 0, toast, sizeof toast, &app_ran, &idx);
CHECK(r == 1 && g_launch_calls == 1, "consume(启动失败): 拉起被调(失败)");
CHECK(app_ran == 0 && idx == 0, "consume(启动失败): app_ran=0 且 idx_out=0(供 launch_fail_toast)");
g_launch_ret = 0;
/* ---- 9) picker 扩载荷:set_ex 武装 → take_picker 取回并清位 ---- */
reset_fakes();
CHECK(meow_ams_pending_start_set_ex("os.meow.files", "pick", "pick_file", 7, "dev.player.ncm") == 0,
"set_ex(picker): 受理");
char act[24] = "", caller[MEOW_APP_ID_MAX] = "";
uint32_t req = 0;
CHECK(meow_ams_take_picker(act, sizeof act, caller, sizeof caller, &req) == 1,
"take_picker: 有上下文");
CHECK(strcmp(act, "pick_file") == 0 && req == 7 && strcmp(caller, "dev.player.ncm") == 0,
"take_picker: action/req/caller 正确");
CHECK(meow_ams_take_picker(act, sizeof act, caller, sizeof caller, &req) == 0,
"take_picker: 二次取回=0(一次性清位)");
/* 非 picker 启动:set(非 set_ex)不武装 picker */
reset_fakes();
CHECK(meow_ams_pending_start_set("dev.player.ncm", "") == 0, "set(bare): 受理");
CHECK(meow_ams_take_picker(act, sizeof act, caller, sizeof caller, &req) == 0,
"take_picker: 普通启动无 picker 上下文");
printf("\n%s (%d fails)\n", g_fails ? "FAILED" : "ALL PASS", g_fails);
return g_fails ? 1 : 0;
}
+161
View File
@@ -0,0 +1,161 @@
/* test_bg_keep.c — 批①`background_keepalive` 声明层宿主单测。
*
* 验证 meow_parse_app_json 对 app.json 后台三位(background / background_audio /
* background_keepalive)的解析与 meow_acct_sync 透传,重点覆盖**前缀不串扰**:
* - "background_keepalive":true → a->background_keepalive=='1';且 background/background_audio
* 在该行不被误命中(分别是 0/0);
* - background:true → a->background=='1'(独立键匹配不受影响);
* - background_audio:true → a->background_audio=='1';
* - 一行三键并存:三者各归其位;
* - meow_acct_sync 透传 background_keepalive 到注册表。
* 实现手法同 test_fg_act.c:真实 include app_launcher.c,-ffunction-sections -Wl,--gc-sections
* 只保留 parseInt/acct_sync/upsert/find 触及函数;json_str/meow_parse_depends/logmsg 由本文件提供
* 忠实桩(json_str 复刻 util.c 语义,含键前缀独立 ident_ch + 整体 UTF-8 截断)。
*
* 运行(patch_v2/meow_launcherd 下):
* gcc -D_GNU_SOURCE -Wall -Wextra -ffunction-sections -Wl,--gc-sections \
* -I. -o test_bg_keep test_bg_keep.c && ./test_bg_keep
* 全 PASS -> exit 0。
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
/* ---- logmsg:app_launcher.c 依赖,minimal 桩 ---- */
void logmsg(const char *fmt, ...)
{
(void)fmt;
}
/* ---- json_str:复刻 util.c 语义(键前缀独立 + 值提取 + UTF-8 截断)---- */
static int ident_ch(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '.';
}
static void utf8_trim(char *s)
{
size_t n = strlen(s), i = 0, last_good = 0;
while (i < n) {
unsigned char c = (unsigned char)s[i];
int len = (c < 0x80) ? 1 : (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1;
if (i + (size_t)len > n) break;
i += (size_t)len;
last_good = i;
}
s[last_good] = '\0';
}
int json_str(const char *buf, const char *key, char *out, int olen)
{
out[0] = 0;
if (!buf || !key || olen <= 0) return 0;
const char *p = strstr(buf, key);
if (!p) return 0;
if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */
p += strlen(key);
if (*p == '"') p++; /* JSON 键自身右引号 */
while (*p && (*p == ':' || *p == '=')) p++;
while (*p == ' ' || *p == '\t') p++;
if (*p == '"') p++; /* 值左引号 */
int i = 0;
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1)
out[i++] = *p++;
out[i] = 0;
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
utf8_trim(out);
return (int)strlen(out);
}
#include "app_launcher.c" /* 真实实现:meow_parse_app_json / meow_acct_sync / meow_acct_upsert */
/* meow_parse_depends:本测试 app.json 无 depends 行,桩返回 0(struct meow_dep 此时已可见)。 */
int meow_parse_depends(const char *val, struct meow_dep *deps, int max)
{
(void)val; (void)deps; (void)max;
return 0;
}
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \
else fprintf(stderr, "ok: %s\n", msg); \
} while (0)
/* 用一段 app.json 内容解析出 struct app。返回 0=成功。 */
static int parse_json(const char *content, struct app *a)
{
FILE *f = tmpfile();
if (!f) return -1;
fputs(content, f);
rewind(f);
memset(a, 0, sizeof *a);
meow_parse_app_json(f, a);
fclose(f);
return 0;
}
int main(void)
{
/* ---- 1) 单独 background_keepalive:true ---- */
{
struct app a;
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_keepalive\":true\n}", &a);
CHECK(a.background_keepalive == '1', "background_keepalive:true -> '1'");
CHECK(a.background == 0, "keepalive 行不误命中 background (==0)");
CHECK(a.background_audio == 0, "keepalive 行不误命中 background_audio (==0)");
}
/* ---- 2) 单独 background:true ---- */
{
struct app a;
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background\":true\n}", &a);
CHECK(a.background == '1', "background:true -> '1'(独立键匹配不受影响)");
CHECK(a.background_keepalive == 0, "background 行不串 to keepalive (==0)");
CHECK(a.background_audio == 0, "background 行不串 to background_audio (==0)");
}
/* ---- 3) 单独 background_audio:true ---- */
{
struct app a;
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_audio\":true\n}", &a);
CHECK(a.background_audio == '1', "background_audio:true -> '1'");
CHECK(a.background == 0, "background_audio 行不误命中 background (==0)");
CHECK(a.background_keepalive == 0, "background_audio 行不串 to keepalive (==0)");
}
/* ---- 4) 三键并存:各归其位(逐行格式,值后跟 , 或换行,同真实 app.json) ---- */
{
struct app a;
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background\":true,\n"
"\"background_audio\":true,\n\"background_keepalive\":true\n}", &a);
CHECK(a.background == '1', "三键并存 background=='1'");
CHECK(a.background_audio == '1', "三键并存 background_audio=='1'");
CHECK(a.background_keepalive == '1', "三键并存 background_keepalive=='1'");
}
/* ---- 5) 假值 / 前导噪声不应置位 ---- */
{
struct app a;
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_keepalive\":false,\n"
"\"background_audio\":0\n}", &a);
CHECK(a.background_keepalive == 0, "background_keepalive:false -> 0");
CHECK(a.background_audio == 0, "background_audio:0 -> 0");
CHECK(a.background == 0, "无 background 键 -> 0");
}
/* ---- 6) meow_acct_sync 透传 ---- */
{
struct app a;
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n"
"\"background_keepalive\":true,\n\"background_audio\":true\n}", &a);
meow_acct_sync("dev.test.keep", &a); /* 以 app_id 建/更注册表项 */
struct meow_acct *ac = meow_acct_find("dev.test.keep");
CHECK(ac != NULL, "meow_acct_sync 建注册表项");
CHECK(ac && ac->background_keepalive == '1', "透传 background_keepalive=='1'");
CHECK(ac && ac->background_audio == '1', "透传 background_audio=='1'");
CHECK(ac && ac->background == 0, "透传 background==0");
}
if (g_fail) { fprintf(stderr, "\n== %d assertion(s) FAILED ==\n", g_fail); return 1; }
fprintf(stderr, "\n== test_bg_keep ALL PASS ==\n");
return 0;
}
+106
View File
@@ -0,0 +1,106 @@
/* meow_launcherd/test_bg_row_card.c — 宿主验证:后台管理页(bg_page)MD3 圆角卡行 + 行内杀按钮
* 的几何与命中(纯整数运算,不链 SDK / 不开设备):
* gcc -Wall -Wextra test_bg_row_card.c -o test_bg_row_card && ./test_bg_row_card
*
* 断言集(对应「相邻 item 顶之间 = item 高 + gap」的可见形式):
* ① 卡高 = 行高 - BGM_CARD_GAP;卡在其行高内上下对称内缩 gap/2(行中心=卡中心);
* ② 相邻卡顶间距 = SUB_ITEM_H(= 卡高 + gap;SDK 行距不变),卡片间可见外距 = gap;
* ③ 杀按钮垂直居中于卡(中心=卡中心=行中心),完整落卡内,右缘距行右 12px;
* ④ 命中不破:卡内缩后按钮几何不变(相对整行视口)——按钮区内点按命中(1),
* 行体/卡内空白点按不命中(0);命中矩形仍为整行距;
* ⑤ 滚动偏移只整体平移,不改变上述间距/对称/命中。
*
* 几何公式与 patch_v2/meow_launcherd/bg_page.c 的 bgm_row_card_y/bgm_row_card_h/bgm_btn_rect 同源。
*/
#include <stdio.h>
#include <assert.h>
#define SUB_ITEM_H 56 /* 行距(SDK listview item_h,subpage.c,不变) */
#define BGM_CARD_GAP 8
#define BGM_BTN_W 46
#define BGM_BTN_H 34
/* 与 bg_page.c bgm_row_card_y / bgm_row_card_h 同源(上下对称内缩)。 */
static int bgm_row_card_y(int row_top) { return row_top + BGM_CARD_GAP / 2; }
static int bgm_row_card_h(int h) { return h - BGM_CARD_GAP; }
/* 与 bg_page.c bgm_btn_rect 同源(按钮垂直居中于卡;卡中心=行中心 → 命中不破)。 */
static void bgm_btn_rect(int rx, int ry, int rw, int rh, int *bx, int *by, int *bw, int *bh)
{
*bw = BGM_BTN_W;
*bh = BGM_BTN_H;
*bx = rx + rw - 12 - BGM_BTN_W;
*by = bgm_row_card_y(ry) + (bgm_row_card_h(rh) - BGM_BTN_H) / 2;
}
/* 行顶 = list_y + idx*item - off(SDK listview 布局:行距=item)。 */
static int row_top(int idx, int item, int list_y, int off) { return list_y + idx * item - off; }
/* 复刻 bgm_row_tap 的按钮命中判定(仅几何):命中按钮区=1,行体/卡内空白=0。 */
static int btn_hit(int tx, int ty, int rx, int ry, int rw, int rh)
{
int bx, by, bw, bh; bgm_btn_rect(rx, ry, rw, rh, &bx, &by, &bw, &bh);
if (tx < bx || tx >= bx + bw || ty < by || ty >= by + bh) return 0;
return 1;
}
int main(void)
{
const int item = SUB_ITEM_H;
const int gap = BGM_CARD_GAP;
const int list_y = 100; /* 内容区顶缘(SUB_CONT_TOP 等值,任意值皆可) */
const int rw = 480; /* 行视口宽(fb_xres,480×720) */
const int rx = 0; /* 行视口左缘 */
int y0 = bgm_row_card_y(row_top(0, item, list_y, 0));
int y1 = bgm_row_card_y(row_top(1, item, list_y, 0));
int h = bgm_row_card_h(item);
/* ① 卡相对行顶对称内缩 gap/2;卡高=item-gap;卡高+gap=item */
assert(y0 == list_y + gap / 2);
assert(h == item - gap);
assert(h + gap == item);
/* ① 相邻卡顶间距 = item(顶到顶 = 卡高 + gap) */
assert(y1 - y0 == item);
/* ② 卡片间可见外距 = gap(卡 0 底 到 卡 1 顶) */
assert(y1 - (y0 + h) == gap);
/* ③ 卡中心 == 行中心(内容按行高居中 → 落卡中心) */
assert(y0 - gap / 2 + item / 2 == y0 + h / 2);
/* 下行底到卡底 = gap/2(下对称) */
assert((list_y + item) - (y0 + h) == gap / 2);
/* ③④ 杀按钮:相对整行视口定位,垂直居中于卡(中心=卡中心=行中心),完整落卡内,
* 右缘距行右 12px;滚动偏移只平移,不改变以上(③④⑤合并循环)。 */
for (int off = 0; off <= item; off += item / 2) {
int r0 = row_top(0, item, list_y, off);
int bx, by, bw, bh; bgm_btn_rect(rx, r0, rw, item, &bx, &by, &bw, &bh);
int card_top = bgm_row_card_y(r0);
int card_bot = card_top + bgm_row_card_h(item);
int row_cy = r0 + item / 2;
int card_cy = card_top + bgm_row_card_h(item) / 2;
/* 按钮完整落卡内(含圆角内边距;上下由卡限,左右由行限) */
assert(bx >= rx && bx + bw <= rx + rw);
assert(by >= card_top && by + bh <= card_bot);
/* 按钮中心 = 卡中心 = 行中心(垂直) */
assert(by + bh / 2 == card_cy);
assert(by + bh / 2 == row_cy);
/* 按钮右缘距行右 12px(与绘制一致) */
assert(rx + rw - (bx + bw) == 12);
/* ④ 命中:按钮中心 → 命中(1);卡内空白(左上圆角内) / 行体左缘 → 不命中(0) */
assert(btn_hit(bx + bw / 2, by + bh / 2, rx, r0, rw, item) == 1);
assert(btn_hit(card_top + 2, card_top + 2, rx, r0, rw, item) == 0);
assert(btn_hit(rx + 16, row_cy, rx, r0, rw, item) == 0);
/* 卡上/下外侧(gap 带内)点按 → 不命中(命中仍按整行距,含 gap 带) */
assert(btn_hit(rx + rw / 2, r0 + gap / 2 - 1, rx, r0, rw, item) == 0);
}
printf("PASS bg_row_card_gap=%d card_h=%d top2top=%d visible_gap=%d "
"btn_in_card=1 btn_center==row_center=1 hit_unbroken=1\n",
gap, h, item, gap);
return 0;
}
+186
View File
@@ -0,0 +1,186 @@
/* test_bgmgr.c — 批③ 后台管理页 逻辑层宿主单测(meow_bgmgr)。
*
* 验证:
* 1) bgmgr_collect:enumerate 后台表 → app_meta 补 name/icon/protected_app/is_fg → proc_sample 补
* pcpu/rss_kb;行 killable 预判正确;name 回退 app_id。
* 2) bgmgr_can_kill 门控矩阵:protected_app=1 → 0(**受保护 App 不 kill**);is_fg=1 → 0(当前前台不 kill);
* pid<=0 → 0(非后台/无进程);普通后台(非受保护/非前台/pid>0)→ 1。
* 3) bgmgr_kill_all 一键清:受保护 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。
* 3b) 回归:app.json framework=1(SDK 框架 App 标志,现有 App 全写 1)**不影响**可杀性。
* 4) bgmgr_status_str / bgmgr_mem_str 文案。
* 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编:
* gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "meow_bgmgr.h"
static int g_fails = 0;
static int g_checks = 0;
#define CHECK(cond, msg) do { \
g_checks++; \
if (cond) printf("PASS: %s\n", msg); \
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
/* ---- 假件(enumerate/app_meta/proc_sample 注入)---- */
static const char g_meta_app[][24] = {
"os.meow.hello", "os.meow.reader", "os.meow.sysguard", "os.meow.fgapp",
};
static int g_killed_n = 0;
static char g_killed_app[8][40];
static int g_meta_use_unknown = 0; /* 第 4 行 meta 返回 -1(未注册),测 name 回退 */
static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
{
(void)ctx;
if (max < 4) return 0;
/* 4 个后台行:hello=普通、reader=常驻、sysguard=受保护、fgapp=当前前台 */
memset(&rows[0], 0, sizeof rows[0]);
snprintf(rows[0].app_id, sizeof rows[0].app_id, "%s", g_meta_app[0]);
rows[0].pid = 1001; rows[0].keepalive = 0; rows[0].on_card = 0; rows[0].bg_since_ms = 1000;
memset(&rows[1], 0, sizeof rows[1]);
snprintf(rows[1].app_id, sizeof rows[1].app_id, "%s", g_meta_app[1]);
rows[1].pid = 1002; rows[1].keepalive = 1; rows[1].on_card = 0; rows[1].bg_since_ms = 2000;
memset(&rows[2], 0, sizeof rows[2]);
snprintf(rows[2].app_id, sizeof rows[2].app_id, "%s", g_meta_app[2]);
rows[2].pid = 1003; rows[2].keepalive = 0; rows[2].on_card = 0; rows[2].bg_since_ms = 3000;
memset(&rows[3], 0, sizeof rows[3]);
snprintf(rows[3].app_id, sizeof rows[3].app_id, "%s", g_meta_app[3]);
rows[3].pid = 1004; rows[3].keepalive = 0; rows[3].on_card = 0; rows[3].bg_since_ms = 4000;
return 4;
}
/* meta:hello/reader/sysguard/fgapp 各给名字;sysguard 标受保护;pid==1004 标前台。 */
static int fake_app_meta(void *ctx, const char *app_id, pid_t pid,
char *name, int nl, char *icon, int il,
char *protected_app, int *is_fg)
{
(void)ctx;
if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放");
else if (!strcmp(app_id, "os.meow.reader")) snprintf(name, nl, "阅读");
else if (!strcmp(app_id, "os.meow.sysguard")) snprintf(name, nl, "系统守护");
else if (!strcmp(app_id, "os.meow.fgapp")) snprintf(name, nl, "前台应用");
else return -1; /* 未注册 → name 回退 app_id */
snprintf(icon, il, "/tmp/icon_%s.png", app_id);
*protected_app = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0;
*is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */
return 0;
}
static int fake_proc_sample(void *ctx, pid_t pid, double *pcpu, long *rss_kb)
{
(void)ctx;
if (pid <= 0) return -1;
*pcpu = (double)(pid % 100) * 0.5; /* 1001->0.5% ... 1004->2.0% */
*rss_kb = (long)(pid % 10) * 2048; /* 1001->2048 ... 1004->8192 */
return 0;
}
static void fake_log(const char *fmt, ...) { (void)fmt; }
static void fake_kill(const char *app_id)
{
if (g_killed_n < 8) snprintf(g_killed_app[g_killed_n++], 40, "%s", app_id);
}
/* enumerate 只回一行:未注册 app_id,测 name 回退。 */
static int fake_enumerate_unknown(void *ctx, bgmgr_row_t *rows, int max)
{
(void)ctx;
if (max < 1) return 0;
(void)g_meta_use_unknown;
memset(&rows[0], 0, sizeof rows[0]);
snprintf(rows[0].app_id, sizeof rows[0].app_id, "os.meow.noreg");
rows[0].pid = 2001; rows[0].keepalive = 0;
return 1;
}
int main(void)
{
bgmgr_ops_t ops;
memset(&ops, 0, sizeof ops);
ops.enumerate = fake_enumerate;
ops.app_meta = fake_app_meta;
ops.proc_sample = fake_proc_sample;
ops.log = fake_log;
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
int n = bgmgr_collect(&ops, rows, MEOW_BGMR_ROW_MAX);
printf("=== 1) collect ===\n");
CHECK(n == 4, "collect returns 4 rows");
CHECK(!strcmp(rows[0].name, "播放") && rows[0].pid == 1001, "row0 hello name/pid");
CHECK(rows[0].protected_app == 0, "row0 hello not protected");
CHECK(!strcmp(rows[1].name, "阅读") && rows[1].keepalive == 1, "row1 reader keepalive");
CHECK(rows[2].protected_app == '1', "row2 sysguard protected");
CHECK(rows[3].is_fg == 1, "row3 fgapp is_fg");
CHECK(rows[0].pcpu > 0 && rows[0].rss_kb > 0, "row0 pcpu/rss populated");
CHECK(rows[0].killable == 1 && rows[2].killable == 0, "killable prefilled by can_kill");
CHECK(!strcmp(rows[2].icon, "/tmp/icon_os.meow.sysguard.png"), "icon populated");
/* name 回退:未注册 app_id(app_meta 返回 -1)→ name==app_id */
printf("=== 1b) name fallback ===\n");
{
bgmgr_ops_t o2 = ops; o2.enumerate = fake_enumerate_unknown;
bgmgr_row_t r1[1]; memset(r1, 0, sizeof r1);
int n2 = bgmgr_collect(&o2, r1, 1);
CHECK(n2 == 1, "unknown row collected");
CHECK(!strcmp(r1[0].name, "os.meow.noreg"), "name falls back to app_id");
CHECK(bgmgr_can_kill(&r1[0]) == 1, "unknown bg app still killable");
}
printf("=== 2) can_kill gating ===\n");
CHECK(bgmgr_can_kill(&rows[2]) == 0, "protected app NOT killable");
CHECK(bgmgr_can_kill(&rows[3]) == 0, "fg app NOT killable (is_fg)");
CHECK(bgmgr_can_kill(&rows[0]) == 1, "normal bg app killable");
CHECK(bgmgr_can_kill(&rows[1]) == 1, "keepalive bg app killable");
{
bgmgr_row_t dead; memset(&dead, 0, sizeof dead);
snprintf(dead.app_id, sizeof dead.app_id, "x");
dead.pid = 0;
CHECK(bgmgr_can_kill(&dead) == 0, "pid<=0 NOT killable");
CHECK(bgmgr_can_kill(NULL) == 0, "NULL NOT killable");
}
printf("=== 3) kill_all (one-key clear) ===\n");
g_killed_n = 0;
int k = bgmgr_kill_all(&ops, rows, n, fake_kill);
CHECK(k == 2, "kill_all kills 2 (hello+reader)");
CHECK(g_killed_n == 2, "kill_all actually invoked kill_fn 2x");
int killed_fw = 0, killed_fg = 0, killed_bg = 0;
for (int i = 0; i < g_killed_n; i++) {
if (!strcmp(g_killed_app[i], "os.meow.sysguard")) killed_fw = 1;
if (!strcmp(g_killed_app[i], "os.meow.fgapp")) killed_fg = 1;
if (!strcmp(g_killed_app[i], "os.meow.hello")) killed_bg = 1;
}
CHECK(killed_fw == 0, "kill_all did NOT kill protected app");
CHECK(killed_fg == 0, "kill_all did NOT kill fg app");
CHECK(killed_bg == 1, "kill_all killed a real bg app");
CHECK(bgmgr_kill_all(&ops, rows, n, NULL) == 0, "kill_all NULL kill_fn -> 0");
/* 回归(2026-11-04 真机):app.json framework=1 是「SDK 框架 App」标志(现有 App 全写 1),
* **不是**受保护位。旧实现把它当门控 → dev.player.ncm 等所有后台 App 都不可杀、按钮置灰。 */
printf("=== 3b) framework(SDK) ≠ 受保护 ===\n");
{
bgmgr_row_t sdk; memset(&sdk, 0, sizeof sdk);
snprintf(sdk.app_id, sizeof sdk.app_id, "dev.player.ncm");
sdk.pid = 7520; sdk.protected_app = 0;
CHECK(bgmgr_can_kill(&sdk) == 1, "SDK 框架 App(framework=1)后台仍可杀");
}
printf("=== 4) status/mem str ===\n");
CHECK(!strcmp(bgmgr_status_str(&rows[1]), "常驻"), "status_str keepalive=常驻");
CHECK(!strcmp(bgmgr_status_str(&rows[0]), "后台"), "status_str normal=后台");
CHECK(!strcmp(bgmgr_status_str(&rows[3]), "前台"), "status_str is_fg=前台");
CHECK(!strcmp(bgmgr_mem_str(512), "512KB"), "mem_str(<1MB)=KB");
CHECK(!strcmp(bgmgr_mem_str(2048), "2.0MB"), "mem_str(>=1MB)=MB");
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
else printf("\n%d FAILED\n", g_fails);
return g_fails ? 1 : 0;
}
+205
View File
@@ -0,0 +1,205 @@
/* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。
*
* 通过 #include bg_page.c 把其 FILE-STATIC 的 bgm_rows/bgm_btn_rect/bgm_row_tap/bgm_top_action
* 平移进本 TU;桩掉 launcher 后台表(meow_bg_count/at/terminate)+ 时钟 + meow_str/logmsg,
* 直接验证:
* P1 bgm_rows 行骨架:后台 App 行(act=i)+ 空态行(act=-1);name 经 app_meta 正确;
* **行副文本指向 ctx 持久缓冲且扛得住栈抖动**(曾指向 bgm_rows 栈帧 → 真机副标题乱码);
* P2 bgm_btn_rect 按钮几何:行右 46×34 圆角钮(rx+rw-12-46 定位);
* P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;
* **app.json framework=1(SDK 框架 App)照样可杀**(2026-11-04 真机回归);
* 受保护/前台行按钮点击 → 不 terminate(门控,留诊断日志);空态行不响应;
* P4 bgm_top_action 顶栏清除:kill_all 不杀受保护/前台、杀普通后台(含 framework=1 的);
* P5 门控矩阵:bgmgr_can_kill(protected_app/is_fg/pid<=0/普通)。
* 不调用行自绘 bgm_row_draw(纯逻辑),故无需桩 SDK 绘制;行自绘段经 -ffunction-sections/--gc-sections 剔除。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined \
* -ffunction-sections -fdata-sections test_bgpage.c meow_bgmgr.c \
* -Wl,--gc-sections -o test_bgpage
* 运行:./test_bgpage
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "meow_launcher_internal.h"
#include "bg_page.c" /* FILE-STATIC 函数平移进本 TU */
/* ---------- 桩:launcher 后台表(meow_bgplay 视角) ---------- */
static meow_bg_entry_t g_bg[MEOW_BG_MAX];
static int g_bg_n;
static int g_term_calls;
static char g_terminated[16][64];
int meow_bg_count(void) { return g_bg_n; }
const meow_bg_entry_t *meow_bg_at(int i)
{
if (i < 0 || i >= g_bg_n) return NULL;
return &g_bg[i];
}
int meow_bg_terminate(const char *app_id)
{
if (g_term_calls < 16) snprintf(g_terminated[g_term_calls++], 64, "%s", app_id);
return 0;
}
unsigned long long now_ms(void) { return 0; }
const char *meow_str(const char *id) { (void)id; return NULL; } /* 未知 id → meow_str_zh 回退 zh */
static int g_log_n;
void logmsg(const char *fmt, ...) { (void)fmt; g_log_n++; } /* 只计数:门控/未命中应留诊断日志 */
/* 栈抖动:bgm_rows 返回后调用,用来暴露「sub 指向栈帧」的悬垂指针。 */
static void stack_clobber(void)
{
volatile char buf[256];
memset((void *)buf, 'X', sizeof buf);
buf[0] = buf[0]; /* 防优化掉 */
}
/* ---------- 测试态:launcher 应用表(app_meta 用) ---------- */
static struct app g_apps[4];
static void set_app(int i, const char *id, const char *name, char fw)
{
memset(&g_apps[i], 0, sizeof g_apps[i]);
snprintf(g_apps[i].id, sizeof g_apps[i].id, "%s", id);
snprintf(g_apps[i].name, sizeof g_apps[i].name, "%s", name);
g_apps[i].framework = fw;
}
static void bg_reset(int n, const char *id0, pid_t pid0, const char *id1, pid_t pid1)
{
g_bg_n = n; g_term_calls = 0;
memset(g_bg, 0, sizeof g_bg);
if (n >= 1 && id0) {
g_bg[0].used = 1; snprintf(g_bg[0].app_id, sizeof g_bg[0].app_id, "%s", id0);
g_bg[0].pid = pid0; g_bg[0].keepalive = 0;
}
if (n >= 2 && id1) {
g_bg[1].used = 1; snprintf(g_bg[1].app_id, sizeof g_bg[1].app_id, "%s", id1);
g_bg[1].pid = pid1; g_bg[1].keepalive = 0;
}
}
static int g_fails = 0;
static int g_checks = 0;
#define CHECK(cond, msg) do { \
g_checks++; \
if (cond) printf("PASS: %s\n", msg); \
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
static int term_count_app(const char *app_id)
{
int c = 0;
for (int i = 0; i < g_term_calls; i++) if (!strcmp(g_terminated[i], app_id)) c++;
return c;
}
int main(void)
{
static bgm_ctx_t b;
memset(&b, 0, sizeof b);
b.apps = g_apps; b.napps = 3;
set_app(0, "os.meow.hello", "播放", 0);
set_app(1, "dev.player.ncm", "流媒体客户端", '1'); /* SDK 框架 App(真机 app.json framework:1) */
set_app(2, "os.meow.sysguard", "系统守护", 0);
static sub_row_t rows[SUB_ROWS_MAX];
printf("=== P1) bgm_rows 行骨架 + 副文本持久化 ===\n");
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
int rn = bgm_rows(&b, rows, SUB_ROWS_MAX);
CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)");
CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0");
CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)");
CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1");
CHECK(!strcmp(rows[1].label, "流媒体客户端"), "row1 label=流媒体客户端");
CHECK(rows[1].sub != NULL && !strncmp(rows[1].sub, "CPU ", 4), "row1 sub=CPU/内存/状态");
CHECK(b.n == 2, "bgm_ctx.n=2");
/* 悬垂指针回归:sub 必须落在 ctx 缓冲里,且栈被改写后内容不变 */
uintptr_t subp = (uintptr_t)rows[1].sub;
uintptr_t lob = (uintptr_t)(char *)&b;
uintptr_t hib = lob + sizeof b;
CHECK(subp >= lob && subp < hib, "sub 指向 ctx 持久缓冲(不是 bgm_rows 栈帧)");
char sub_before[80];
snprintf(sub_before, sizeof sub_before, "%s", rows[1].sub);
stack_clobber();
CHECK(!strcmp(rows[1].sub, sub_before), "栈抖动后 sub 内容不变(无悬垂)");
printf("=== P2) bgm_btn_rect 按钮几何(行右 46×34) ===\n");
{
int bx, by, bw, bh; bgm_btn_rect(0, 0, 480, 56, &bx, &by, &bw, &bh);
CHECK(bw == 46 && bh == 34, "按钮 46×34");
CHECK(bx == 480 - 12 - 46, "按钮左缘=行宽-12-46");
CHECK(by == (56 - 34) / 2, "按钮垂直居中");
}
printf("=== P3) bgm_row_tap 行内杀按钮命中(仅按钮区 kill,行区不误杀) ===\n");
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
bgm_rows(&b, rows, SUB_ROWS_MAX);
/* 按钮区命中:行(0,0,480,56),按钮 x 422-468, y 11-45 */
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 1, "按钮点击→返回1(重绘)");
CHECK(term_count_app("os.meow.hello") == 1, "按钮点击→terminate hello 一次");
/* 行体(按钮左/其下区)命中 → 不 kill */
CHECK(bgm_row_tap(&b, 0, &rows[0], 200, 28, 0, 0, 480, 56) == 0, "行体点击→返回0(不杀)");
CHECK(term_count_app("os.meow.hello") == 1, "行体点击→不误杀(仍1次)");
CHECK(bgm_row_tap(&b, 0, &rows[0], 410, 28, 0, 0, 480, 56) == 0, "按钮左侧缘(410)→不杀");
CHECK(bgm_row_tap(&b, 0, &rows[0], 475, 28, 0, 0, 480, 56) == 0, "按钮右侧(475)→不杀");
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀");
/* 回归(2026-11-04 真机「杀后台按钮按下去没反应」):dev.player.ncm 的 app.json 写 framework:1
* (= 用 SDK 框架,现有 App 全写 1),**必须照杀**——旧实现把它当受保护位,按钮置灰点不动。 */
CHECK(b.rows[1].killable == 1, "framework=1(SDK 框架)App 仍判可杀");
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "framework=1 App 按钮→返回1");
CHECK(term_count_app("dev.player.ncm") == 1, "framework=1 App 按钮→真 terminate(无反应 bug 回归)");
/* 真·受保护行(未来系统组件/前台):按钮命中但不 terminate,且留诊断日志 */
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
bgm_rows(&b, rows, SUB_ROWS_MAX);
b.rows[1].protected_app = '1';
g_log_n = 0;
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "受保护行按钮→返回1(重绘)");
CHECK(term_count_app("os.meow.sysguard") == 0, "受保护行按钮→不 terminate");
CHECK(g_log_n >= 1, "受保护行按钮→留一行诊断日志(可查「点了没反应」)");
printf("=== P3b) 空态行不响应 ===\n");
bg_reset(0, NULL, 0, NULL, 0);
int rn0 = bgm_rows(&b, rows, SUB_ROWS_MAX);
CHECK(rn0 == 1 && rows[0].act == -1, "0 后台 App → 1 空态行(act=-1)");
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 0, "空态行点击→返回0(不杀)");
g_term_calls = 0;
printf("=== P4) 顶栏清除(一键清除后台)不杀受保护/前台 ===\n");
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
bgm_rows(&b, rows, SUB_ROWS_MAX);
b.rows[1].protected_app = '1';
CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)");
CHECK(term_count_app("os.meow.hello") == 1, "清除→terminate 普通后台 hello");
CHECK(term_count_app("os.meow.sysguard") == 0, "清除→不杀受保护 sysguard");
/* 回归:framework=1 的 SDK 框架 App 也在「一键清」范围内 */
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
bgm_rows(&b, rows, SUB_ROWS_MAX);
g_term_calls = 0;
CHECK(bgm_top_action(&b) == 1, "framework=1 App:一键清返回 1");
CHECK(term_count_app("dev.player.ncm") == 1, "一键清→terminate framework=1 App(回归)");
/* 前台 App(is_fg)不杀:给 hello 标前台再清 */
bg_reset(1, "os.meow.hello", 1001, NULL, 0);
bgm_rows(&b, rows, SUB_ROWS_MAX);
for (int i = 0; i < b.n; i++) if (!strcmp(b.rows[i].app_id, "os.meow.hello")) b.rows[i].is_fg = 1;
g_term_calls = 0;
CHECK(bgm_top_action(&b) == 1, "前台 App 仍在:顶栏清除返回 1(重绘)");
CHECK(g_term_calls == 0, "前台 App → kill_all 不杀(0 terminate)");
CHECK(bgm_top_action(&b) == 1, "二次清除(仍前台/不可杀):返回 1");
CHECK(g_term_calls == 0, "二次清除 → 仍不 terminate");
printf("=== P5) 门控矩阵(bgmgr_can_kill) ===\n");
{
bgmgr_row_t r; memset(&r, 0, sizeof r);
r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀");
r.protected_app = '1'; CHECK(bgmgr_can_kill(&r) == 0, "protected_app 不可杀");
r.protected_app = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀");
r.is_fg = 0; r.pid = 0; CHECK(bgmgr_can_kill(&r) == 0, "pid<=0 不可杀");
}
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
else printf("\n%d FAILED\n", g_fails);
return g_fails ? 1 : 0;
}
+524
View File
@@ -0,0 +1,524 @@
/* test_bgplay.c — 宿主单测:meow_bgplay 后台播放保活 + 无头后台服务状态机(M4)。
*
* 验证:
* 1) meow_bg_should_keep 判定矩阵(background_audio × media_playing);
* 2) 后台播放保活全链路(真 fork 子进程):META PLAYING → should_keep=1 → bg_add →
* 进程存活 + PLAYCTL 经会话 fd 往返(后台化不断连)→ take 恢复 → META STOPPED →
* should_keep=0(停播后再退出=真退);
* 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽;
* 4) 卡拔例外:on_card=1 + card_present 假 0 → tick SIGTERM 回收(防 SIGBUS);
* 4b) meow_bg_terminate(后台管理页「杀」按钮 / `am kill` 共用通道):受理置 term_at_ms →
* tick SIGTERM 回收 + on_bg_dead;不在表/空 id 分别 NOENT/INVAL;
* 4b) 批①+宽限:未声明(keepalive=0)媒体不活跃超 MEOW_BG_GRACE_MS 回收(含恰等边界不回收);
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
* (不回收声明常驻);pid<=0 → MEOW_ERR_INVAL;重复 add → MEOW_ERR_EXISTS;
* 6) 服务注册:exec 不可执行 → NOENT;同名在跑 → EXISTS;
* 7) 退避重启 1s/2s + max_restart=2 熔断(假时钟注入):清通知回调 + 注册连接收到
* MSG_SYS{SVC_DEAD=2} + circuit 后不再拉起;重注册复位熔断重新拉起;
* 8) MSG_SERVICE_STOP:SIGTERM + stopping,不触发重启,槽释放;
* 9) kill_all:bg+svc 全回收 + 注册连接收到 MSG_SYS{SHUTDOWN=1}。
* 只依赖 meow_bgplay.c + meow_media.c + meow_ipc.h(纯 C),宿主 gcc 直编:
* gcc -Wall -Wextra -std=gnu99 test_bgplay.c meow_bgplay.c meow_media.c -o test_bgplay
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "meow_bgplay.h"
#include "meow_media.h"
#include "meow_ipc.h"
static int g_fails = 0;
#define CHECK(cond, msg) do { \
if (cond) printf("PASS: %s\n", msg); \
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
/* ---- 假件(ops 注入)---- */
static uint64_t g_ms = 100000; /* 假时钟(退避断言用,手动推进) */
static int g_card = 1; /* 卡在位 */
static int g_bg_dead = 0; /* on_bg_dead 计数 */
static int g_bg_dead_status = 0;
static char g_bg_dead_app[MEOW_APP_ID_MAX];
static int g_notif_cleared = 0; /* notif_clear_app 计数 */
static char g_notif_cleared_app[MEOW_APP_ID_MAX];
static uint64_t fake_now_ms(void) { return g_ms; }
static int fake_card(void) { return g_card; }
static void fake_bg_dead(const char *app_id, pid_t pid, int status)
{
(void)pid;
g_bg_dead++;
g_bg_dead_status = status;
snprintf(g_bg_dead_app, sizeof g_bg_dead_app, "%s", app_id);
}
static int fake_notif_clear(const char *app_id)
{
g_notif_cleared++;
snprintf(g_notif_cleared_app, sizeof g_notif_cleared_app, "%s", app_id);
return 1;
}
/* send_frame:整帧写到 fd(socketpair 一端),对端读回校验。 */
static size_t fake_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, rid = req_id, l = len;
uint16_t ver = MEOW_IPC_VERSION, t = type;
size_t off = 0;
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2);
memcpy(hdr + 8, &rid, 4); memcpy(hdr + 12, &l, 4);
if (write(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return 0;
off = sizeof hdr;
if (len && payload && write(fd, payload, len) > 0) off += len;
return off;
}
/* media ops.send 同 fake_send_frame(PLAYCTL 下发校验用) */
/* 从 socketpair 读一帧(poll 500ms 防挂)。返回负载字节数,<0=超时/失败。 */
static int read_frame(int fd, uint16_t *type, void *pay, int paycap)
{
struct pollfd pf = { .fd = fd, .events = POLLIN, .revents = 0 };
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t len;
if (poll(&pf, 1, 500) <= 0) return -1;
if (read(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return -1;
memcpy(type, hdr + 6, 2);
memcpy(&len, hdr + 12, 4);
if (len > (uint32_t)paycap) return -1;
if (len && read(fd, pay, len) != (ssize_t)len) return -1;
return (int)len;
}
/* fork 一个长眠子进程(300s),测试末统一 SIGKILL 清理。 */
static pid_t spawn_sleeper(void)
{
pid_t pid = fork();
if (pid == 0) { sleep(300); _exit(0); }
return pid;
}
/* 泵 tick 直到 cond 或超时(~2s):子进程死亡到 WNOHANG 可见有调度延迟。 */
static int pump_bg_until(int (*cond)(void *), void *ctx)
{
for (int i = 0; i < 100; i++) {
meow_bg_tick();
if (cond(ctx)) return 1;
usleep(20000);
}
return 0;
}
static int pump_svc_until(int (*cond)(void *), void *ctx)
{
for (int i = 0; i < 100; i++) {
meow_svc_tick();
if (cond(ctx)) return 1;
usleep(20000);
}
return 0;
}
static int cond_bg_dead(void *ctx) { (void)ctx; return g_bg_dead > 0; }
static int cond_svc_reaped(void *ctx)
{
const meow_svc_rt_t *s;
(void)ctx;
s = meow_svc_find("dev.test.svc", "daemon");
return s && s->pid == 0;
}
static int cond_svc_gone(void *ctx)
{
(void)ctx;
return meow_svc_find("dev.test.svc", "daemon") == NULL;
}
/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。
* 路径取 $TMPDIR(Termux/Android 主机没有 /tmp),缺省回退 /tmp。 */
static char g_svc_path[256];
#define TEST_SVC_PATH g_svc_path
static void write_test_svc(void)
{
const char *tmp = getenv("TMPDIR");
if (!tmp || !tmp[0]) tmp = "/tmp";
snprintf(g_svc_path, sizeof g_svc_path, "%s/meow_bgplay_test_svc.sh", tmp);
FILE *f = fopen(TEST_SVC_PATH, "w");
if (!f) { printf("FAIL: 无法创建测试脚本 %s\n", TEST_SVC_PATH); exit(1); }
fputs("#!/bin/sh\nexec sleep 300\n", f);
fclose(f);
chmod(TEST_SVC_PATH, 0755);
}
int main(void)
{
meow_bgplay_ops ops;
meow_media_ops mops;
memset(&ops, 0, sizeof ops);
ops.now_ms = fake_now_ms;
ops.card_present = fake_card;
ops.on_bg_dead = fake_bg_dead;
ops.media_resident = meow_media_app_resident; /* 批①+宽限:媒体活跃判定(宽限回收/槽满LRU) */
ops.notif_clear_app = fake_notif_clear;
ops.send_frame = fake_send_frame;
ops.log_dir = NULL;
meow_bgplay_init(&ops);
memset(&mops, 0, sizeof mops);
mops.send = fake_send_frame;
meow_media_init(&mops);
/* ---- 1) should_keep 判定矩阵(批①三参:bg_audio, media_resident, keepalive) ---- */
CHECK(meow_bg_should_keep(0, 0, 0) == 0, "should_keep(未声明,未活跃,非常驻)=0");
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(声明bg_audio,无会话)=0");
CHECK(meow_bg_should_keep(0, 1, 0) == 0, "should_keep(未声明bg_audio,活跃)=0");
CHECK(meow_bg_should_keep(1, 1, 0) == 1, "should_keep(bg_audio+活跃)=1");
CHECK(meow_bg_should_keep(0, 0, 1) == 1, "should_keep(keepalive恒保)=1");
CHECK(meow_bg_should_keep(0, 1, 1) == 1, "should_keep(keepalive恒保,忽略media)=1");
CHECK(meow_bg_should_keep(1, 0, 1) == 1, "should_keep(keepalive恒保,无会话)=1");
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(bg_audio+resident=0 真退)=0");
/* ---- 2) 后台播放保活全链路 ---- */
int sp[2];
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp) == 0, "socketpair 建立");
{
m_meta_t meta;
memset(&meta, 0, sizeof meta);
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1");
meta.state = MEOW_MEDIA_PLAYING;
meta.dur_ms = 200000;
CHECK(meow_media_meta(&meta, sp[0]) == 0, "META 上报 PLAYING 注册媒体会话");
}
CHECK(meow_media_app_playing("dev.test.bg1") == 1, "app_playing(bg1)=1");
CHECK(meow_media_app_resident("dev.test.bg1") == 1, "app_resident(bg1)=1");
CHECK(meow_bg_should_keep(1, meow_media_app_resident("dev.test.bg1"), 0) == 1,
"播放中退出 → 应保活");
pid_t kid = spawn_sleeper();
CHECK(kid > 0, "fork 假 App 子进程");
CHECK(meow_bg_add("dev.test.bg1", kid, sp[0], 0, 0) == 0, "bg_add 转后台保活");
CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 1, "is_backgrounded=1");
CHECK(meow_bg_count() == 1, "bg count=1");
{
const meow_bg_entry_t *be = meow_bg_at(0);
CHECK(be && be->bg_since_ms > 0, "bg 槽记 bg_since_ms(转后台时刻)");
CHECK(be && be->keepalive == 0, "bg_audio 类 keepalive=0");
}
CHECK(kill(kid, 0) == 0, "后台化后进程仍存活(未 SIGTERM/SIGSTOP)");
/* PLAYCTL 经会话 fd 往返(后台化不断连:进程/fd 均保留) */
CHECK(meow_media_playctl_to("dev.test.bg1", 6, 0) == 0, "PLAYCTL 下发到后台 App 会话");
{
uint16_t t = 0;
m_playctl_t pc;
int n = read_frame(sp[1], &t, &pc, sizeof pc);
CHECK(n == (int)sizeof pc && t == MSG_PLAYCTL && pc.cmd == 6,
"对端收到 MSG_PLAYCTL cmd=6(控制通道后台不断连)");
}
{
int rfd = -1;
pid_t rpid = 0;
CHECK(meow_bg_take("dev.test.bg1", &rfd, &rpid) == 0 && rpid == kid && rfd == sp[0],
"bg_take 回前台(pid/fd 原样归还)");
}
CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 0 && meow_bg_count() == 0,
"take 后清槽");
meow_media_clear("dev.test.bg1");
{
m_meta_t meta;
memset(&meta, 0, sizeof meta);
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1");
meta.state = MEOW_MEDIA_STOPPED;
meow_media_meta(&meta, sp[0]);
}
CHECK(meow_bg_should_keep(1, meow_media_app_resident("dev.test.bg1"), 0) == 0,
"停播后再退出 → should_keep=0(真退 SIGTERM)");
kill(kid, SIGKILL); waitpid(kid, NULL, 0);
close(sp[0]); close(sp[1]);
meow_media_clear("dev.test.bg1");
/* ---- 2b) keepalive 类(background_keepalive)入槽标记 ---- */
{
pid_t kk = spawn_sleeper();
CHECK(meow_bg_add("dev.test.keep", kk, -1, 0, 1) == 0, "bg_add(keepalive=1)");
const meow_bg_entry_t *bk = meow_bg_at(0);
CHECK(bk && bk->keepalive == 1, "keepalive 类槽 keepalive=1");
CHECK(bk && bk->bg_since_ms > 0, "keepalive 类槽 bg_since_ms>0");
meow_bg_take("dev.test.keep", NULL, NULL);
kill(kk, SIGKILL); waitpid(kk, NULL, 0);
CHECK(meow_bg_count() == 0, "keepalive 槽 take 后清槽");
}
/* ---- 3) 后台 App 崩溃照收 ---- */
g_bg_dead = 0;
kid = spawn_sleeper();
CHECK(meow_bg_add("dev.test.crash", kid, -1, 0, 0) == 0, "bg_add(crash)");
kill(kid, SIGKILL);
{
int ok = pump_bg_until(cond_bg_dead, NULL);
(void)ok;
}
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.crash") == 0 &&
WIFSIGNALED(g_bg_dead_status) && WTERMSIG(g_bg_dead_status) == SIGKILL,
"崩溃子进程被 tick 回收 + on_bg_dead(SIGKILL) 回调");
CHECK(meow_bg_count() == 0, "崩溃回收后清槽");
/* ---- 4) 卡拔例外:on_card=1 + 卡不在 → SIGTERM 回收 ---- */
g_bg_dead = 0;
kid = spawn_sleeper();
CHECK(meow_bg_add("dev.test.card", kid, -1, 1, 0) == 0, "bg_add(on_card=1)");
g_card = 0;
pump_bg_until(cond_bg_dead, NULL);
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.card") == 0,
"卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)");
g_card = 1;
/* ---- 4b) meow_bg_terminate:后台管理页「杀」按钮 / `am kill` 共用的终止通道 ---- */
{
g_bg_dead = 0;
pid_t tk = spawn_sleeper();
CHECK(meow_bg_add("dev.test.term", tk, -1, 0, 0) == 0, "bg_add(term)");
CHECK(meow_bg_terminate("") == MEOW_ERR_INVAL, "terminate 空 app_id → INVAL");
CHECK(meow_bg_terminate("dev.test.nothere") == MEOW_ERR_NOENT, "terminate 不在后台表 → NOENT");
CHECK(meow_bg_terminate("dev.test.term") == 0, "terminate 受理(在后台表且 pid>0)");
{
int found = 0;
for (int i = 0; i < MEOW_BG_MAX; i++) {
const meow_bg_entry_t *be = meow_bg_at(i);
if (be && !strcmp(be->app_id, "dev.test.term")) {
CHECK(be->term_at_ms > 0, "terminate 置 term_at_ms(bgm_enumerate 据此隐藏行)");
found = 1;
}
}
CHECK(found == 1, "terminate 后槽仍在(等 tick 回收)");
}
pump_bg_until(cond_bg_dead, NULL);
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.term") == 0,
"terminate → tick SIGTERM 回收 + on_bg_dead 上报 amsd");
CHECK(meow_bg_count() == 0, "terminate 回收后清槽");
}
/* ---- 5) 表满:LRU 回收最久未激活的未声明槽 / 全常驻满→NOMEM / 重复 / 参数 ---- */
{
pid_t k[MEOW_BG_MAX];
char id[MEOW_APP_ID_MAX];
int i;
pid_t gextra = 0;
for (i = 0; i < MEOW_BG_MAX; i++) {
k[i] = spawn_sleeper();
snprintf(id, sizeof id, "dev.test.lru%d", i);
CHECK(meow_bg_add(id, k[i], -1, 0, 0) == 0, "lru 填表(未声明 keepalive=0)");
}
/* 5.1 表满 + 全未声明 → 新增 LRU 回收最旧(lru0,bg_since_ms 最小),新 App 入表成功 */
{
pid_t e2 = spawn_sleeper();
gextra = e2;
CHECK(meow_bg_add("dev.test.lrunew", e2, -1, 0, 0) == 0,
"表满全未声明 → LRU 回收最旧槽并成功新增");
CHECK(meow_bg_count() == MEOW_BG_MAX, "回收后仍满 4 槽");
CHECK(meow_bg_is_backgrounded("dev.test.lrunew") == 1, "新增 App 在后台表");
CHECK(meow_bg_is_backgrounded("dev.test.lru0") == 0, "最久未激活未声明 lru0 被回收摘表");
CHECK(kill(k[0], 0) < 0, "lru0 进程已终止(SIGTERM 真退回收)");
}
/* 5.2 重复 add → EXISTS;pid<=0 → INVAL(lru0 已回收,表内为 lrunew/lru1/lru2/lru3) */
CHECK(meow_bg_add("dev.test.lrunew", 999999, -1, 0, 0) == MEOW_ERR_EXISTS, "重复 app_id → EXISTS");
CHECK(meow_bg_add("dev.test.lrunew", 0, -1, 0, 0) == MEOW_ERR_INVAL, "pid<=0 → INVAL");
/* 5.3 清空剩余(take + 杀进程防泄漏) */
for (i = 1; i < MEOW_BG_MAX; i++) {
snprintf(id, sizeof id, "dev.test.lru%d", i);
meow_bg_take(id, NULL, NULL);
kill(k[i], SIGKILL); waitpid(k[i], NULL, 0);
}
meow_bg_take("dev.test.lrunew", NULL, NULL);
kill(gextra, SIGKILL); waitpid(gextra, NULL, 0);
CHECK(meow_bg_count() == 0, "清空后 bg 表空(未声明)");
}
/* ---- 5b) 表满 + 全为声明常驻(keepalive=1)→ 拒绝新入 NOMEM(不回收声明常驻,保安全) ---- */
{
pid_t d[MEOW_BG_MAX];
char id[MEOW_APP_ID_MAX];
int i;
for (i = 0; i < MEOW_BG_MAX; i++) {
d[i] = spawn_sleeper();
snprintf(id, sizeof id, "dev.test.dec%d", i);
CHECK(meow_bg_add(id, d[i], -1, 0, 1) == 0, "声明常驻填表(keepalive=1)");
}
{
pid_t e2 = spawn_sleeper();
CHECK(meow_bg_add("dev.test.decnew", e2, -1, 0, 0) == MEOW_ERR_NOMEM,
"全声明常驻满 → 新增 NOMEM(不回退真退,保安全)");
kill(e2, SIGKILL); waitpid(e2, NULL, 0); /* 未入表,就地清理 */
}
for (i = 0; i < MEOW_BG_MAX; i++) {
snprintf(id, sizeof id, "dev.test.dec%d", i);
meow_bg_take(id, NULL, NULL);
kill(d[i], SIGKILL); waitpid(d[i], NULL, 0);
}
CHECK(meow_bg_count() == 0, "声明常驻清空后 bg 表空");
}
/* ================= 批①+宽限:宽限超时回收 / 声明常驻与媒体活跃不清 ================= */
g_ms = 100000; /* 本组从干净时间基准起(后续 svc 段用相对 g_ms,不受影响) */
/* 5c) 未声明 + 媒体不活跃:grace 内保活;恰等 MEOW_BG_GRACE_MS 不回收(严格 >);超界回收 */
{
pid_t s1 = spawn_sleeper();
g_bg_dead = 0;
CHECK(meow_bg_add("dev.test.grace1", s1, -1, 0, 0) == 0, "grace1 入表(未声明 keepalive=0)");
g_ms = 100000u + MEOW_BG_GRACE_MS; /* 恰好等于宽限(非严格大于) */
meow_bg_tick();
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 1 && g_bg_dead == 0,
"grace 边界:恰等 MEOW_BG_GRACE_MS 不回收(严格 > 才回收)");
g_ms = 100000u + MEOW_BG_GRACE_MS + 1; /* 超界 1ms */
pump_bg_until(cond_bg_dead, NULL);
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 0 && meow_bg_count() == 0,
"超 grace 未再启动 → 回收摘表");
CHECK(g_bg_dead == 1, "宽限回收触发 on_bg_dead(通知 amsd)+ 进程经 SIGTERM 退出");
}
/* 5d) 声明常驻(keepalive=1)超 grace 不清(无限期) */
{
pid_t k1 = spawn_sleeper();
CHECK(meow_bg_add("dev.test.gracekeep", k1, -1, 0, 1) == 0, "gracekeep 入表(keepalive=1)");
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
meow_bg_tick();
CHECK(meow_bg_is_backgrounded("dev.test.gracekeep") == 1,
"声明常驻 keepalive=1 超 grace 不清(无限期)");
meow_bg_take("dev.test.gracekeep", NULL, NULL);
kill(k1, SIGKILL); waitpid(k1, NULL, 0);
}
/* 5e) 媒体活跃(META PLAYING,keepalive=0)超 grace 不清(resident 保护) */
{
int sp2[2];
pid_t m1;
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp2) == 0, "grace media socketpair");
{
m_meta_t meta;
memset(&meta, 0, sizeof meta);
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.gracemedia");
meta.state = MEOW_MEDIA_PLAYING;
meow_media_meta(&meta, sp2[0]);
}
m1 = spawn_sleeper();
CHECK(meow_bg_add("dev.test.gracemedia", m1, -1, 0, 0) == 0,
"gracemedia 入表(keepalive=0 但媒体活跃)");
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
meow_bg_tick();
CHECK(meow_media_app_resident("dev.test.gracemedia") == 1, "gracemedia media_resident=1");
CHECK(meow_bg_is_backgrounded("dev.test.gracemedia") == 1,
"媒体活跃超 grace 不清(resident 保护)");
meow_bg_take("dev.test.gracemedia", NULL, NULL);
kill(m1, SIGKILL); waitpid(m1, NULL, 0);
meow_media_clear("dev.test.gracemedia");
close(sp2[0]); close(sp2[1]);
}
/* ================= 无头后台服务 ================= */
write_test_svc();
/* ---- 6) 注册校验 ---- */
CHECK(meow_svc_register("dev.test.svc", "daemon", "/nonexistent/x", 1, 2, 60000, 0, -1)
== MEOW_ERR_NOENT, "exec 不可执行 → NOENT(不留尸槽)");
CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "NOENT 后无槽残留");
int sp_svc[2];
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp_svc) == 0, "svc socketpair 建立");
int pid1 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH,
1, 2, 60000, 0, sp_svc[0]);
CHECK(pid1 > 0, "register 拉起服务");
CHECK(meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, 1, 2, 60000, 0, sp_svc[0])
== MEOW_ERR_EXISTS, "同名服务在跑 → EXISTS");
/* ---- 7) 退避 1s/2s + max=2 熔断 ---- */
const meow_svc_rt_t *sl;
kill(pid1, SIGKILL);
pump_svc_until(cond_svc_reaped, NULL);
sl = meow_svc_find("dev.test.svc", "daemon");
CHECK(sl && sl->pid == 0 && sl->restart_count == 1 &&
sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_1,
"第 1 次死 → 1s 退避排程(restart_count=1)");
g_ms += MEOW_SVC_BACKOFF_1;
meow_svc_tick();
sl = meow_svc_find("dev.test.svc", "daemon");
CHECK(sl && sl->pid > 0 && sl->pid != pid1, "到点重启:新 pid");
pid_t pid2 = sl->pid;
kill(pid2, SIGKILL);
pump_svc_until(cond_svc_reaped, NULL);
sl = meow_svc_find("dev.test.svc", "daemon");
CHECK(sl && sl->pid == 0 && sl->restart_count == 2 &&
sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_2,
"第 2 次死 → 2s 退避排程(restart_count=2)");
g_ms += MEOW_SVC_BACKOFF_2;
meow_svc_tick();
sl = meow_svc_find("dev.test.svc", "daemon");
CHECK(sl && sl->pid > 0, "第 2 次重启拉起");
g_notif_cleared = 0;
kill(sl->pid, SIGKILL);
pump_svc_until(cond_svc_reaped, NULL);
sl = meow_svc_find("dev.test.svc", "daemon");
CHECK(sl && sl->pid == 0 && sl->circuit == 1 && sl->restart_count == 3,
"第 3 次死超 max_restart=2 → 熔断 circuit=1");
CHECK(g_notif_cleared == 1 && strcmp(g_notif_cleared_app, "dev.test.svc") == 0,
"熔断清该 App 通知(notif_clear_app 回调)");
{
uint16_t t = 0;
m_sys_t m;
int n = read_frame(sp_svc[1], &t, &m, sizeof m);
CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SVC_DEAD &&
strcmp(m.svc, "daemon") == 0,
"注册连接收到 MSG_SYS{SVC_DEAD=2} 熔断广播");
}
g_ms += 60000;
meow_svc_tick();
sl = meow_svc_find("dev.test.svc", "daemon");
CHECK(sl && sl->pid == 0, "熔断后不再拉起");
/* 重注册复位熔断 */
int pid4 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH,
1, 2, 60000, 0, sp_svc[0]);
CHECK(pid4 > 0, "熔断槽重注册 → 复位重新拉起");
sl = meow_svc_find("dev.test.svc", "daemon");
CHECK(sl && sl->circuit == 0 && sl->restart_count == 0, "熔断复位(circuit/count 清零)");
/* ---- 8) SERVICE_STOP:不重启 ---- */
CHECK(meow_svc_stop("dev.test.svc", "daemon") == 0, "svc_stop 受理");
pump_svc_until(cond_svc_gone, NULL);
CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "stop 后槽释放(不重启)");
CHECK(meow_svc_stop("dev.test.svc", "daemon") == MEOW_ERR_NOENT, "重复 stop → NOENT");
close(sp_svc[0]); close(sp_svc[1]);
/* ---- 9) kill_all:全回收 + SHUTDOWN 广播 ---- */
{
int sp2[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sp2);
int spid = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH,
1, 3, 60000, 0, sp2[0]);
pid_t bgk = spawn_sleeper();
meow_bg_add("dev.test.bg9", bgk, -1, 0, 0);
CHECK(spid > 0 && meow_bg_count() == 1, "kill_all 前置:1 svc + 1 bg");
meow_bgplay_kill_all();
CHECK(meow_bg_count() == 0 && meow_svc_find("dev.test.svc", "daemon") == NULL,
"kill_all 后 bg/svc 全回收");
CHECK(kill(spid, 0) < 0 && kill(bgk, 0) < 0, "子进程确已终止");
uint16_t t = 0;
m_sys_t m;
int n = read_frame(sp2[1], &t, &m, sizeof m);
CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SHUTDOWN,
"注册连接收到 MSG_SYS{SHUTDOWN=1}(关机前回收广播)");
close(sp2[0]); close(sp2[1]);
}
unlink(TEST_SVC_PATH);
printf(g_fails ? "== FAILURES: %d ==\n" : "== ALL PASS ==\n", g_fails);
return g_fails ? 1 : 0;
}
Loaded 100 of 576 files, more files were not shown because too many files have changed in this diff. Show more