Compare commits
134
Commits
b364b6371a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a06a8cc1a | ||
|
|
d36a3a02a6 | ||
|
|
234b4d13b9 | ||
|
|
6e67c74fe6 | ||
|
|
6e7c0d00c0 | ||
|
|
33bef36d16 | ||
|
|
ca042aa79c | ||
|
|
239cebae01 | ||
|
|
370c290160 | ||
|
|
00e7271395 | ||
|
|
1ad2346d34 | ||
|
|
7a32b0690e | ||
|
|
884d4c4891 | ||
|
|
96c15a06ec | ||
|
|
a9281c2d64 | ||
|
|
8cd6e21cad | ||
|
|
bc7bafa5dc | ||
|
|
63644985a6 | ||
|
|
f49585dd62 | ||
|
|
e4eda3a06f | ||
|
|
514cc0e5d0 | ||
|
|
b20e3c43e9 | ||
|
|
3f22158fc2 | ||
|
|
58cdb73bf8 | ||
|
|
ebd64d07e1 | ||
|
|
bd6fc8da3c | ||
|
|
2de47154bf | ||
|
|
a364932145 | ||
|
|
340c41afb7 | ||
|
|
63b278e13e | ||
|
|
b1acc9096b | ||
|
|
6c6da79ad9 | ||
|
|
837cd47e7c | ||
|
|
46278fd051 | ||
|
|
cf5703b32b | ||
|
|
829d4475e8 | ||
|
|
8f748b137d | ||
|
|
365c8f756b | ||
|
|
e21cbc7865 | ||
|
|
2881ce908a | ||
|
|
190041cd67 | ||
|
|
58f2d9bff4 | ||
|
|
e0dfab95c3 | ||
|
|
3f09f7c137 | ||
|
|
dc9bfdbdaf | ||
|
|
0b30cdbafb | ||
|
|
a9638fa59c | ||
|
|
dc559aa1ac | ||
|
|
bece9848e7 | ||
|
|
288c133efc | ||
|
|
e0a82a3d47 | ||
|
|
63ead3ec1e | ||
|
|
409fd1edf7 | ||
|
|
db672cbe8f | ||
|
|
281f4153ee | ||
|
|
ee5796747d | ||
|
|
479d93a3bf | ||
|
|
adaed09f94 | ||
|
|
0531603779 | ||
|
|
b52e6a08bc | ||
|
|
2a2e6988ae | ||
|
|
1f692ae2a7 | ||
|
|
c9f0cbaab6 | ||
|
|
2cfac55b4e | ||
|
|
f9c2ea42a4 | ||
|
|
0ab23161b6 | ||
|
|
316d9d42c7 | ||
|
|
dfde95b020 | ||
|
|
f2551a28a2 | ||
|
|
bcb11e012f | ||
|
|
62b3603ade | ||
|
|
72c6e9889d | ||
|
|
8d2c098deb | ||
|
|
9600f10352 | ||
|
|
e69b8efeeb | ||
|
|
c2635daf3f | ||
|
|
a7bd6c44d9 | ||
|
|
40ac1e40f0 | ||
|
|
0412994953 | ||
|
|
3b5ffece57 | ||
|
|
cfcb94b8d2 | ||
|
|
31c12892d6 | ||
|
|
4d9ba46a7b | ||
|
|
805cfe11cc | ||
|
|
c811913c06 | ||
|
|
278e7eaa93 | ||
|
|
d9950d75eb | ||
|
|
e23bdca26a | ||
|
|
22f9dab2ce | ||
|
|
98b9169b86 | ||
|
|
4e60f440f6 | ||
|
|
38be5e78bc | ||
|
|
8a115f6583 | ||
|
|
294ed6ffc7 | ||
|
|
7f4e18b0ed | ||
|
|
35a636ef42 | ||
|
|
aa225a1973 | ||
|
|
61ef0f6de9 | ||
|
|
31879f3302 | ||
|
|
b1d607d43f | ||
|
|
2ca386973b | ||
|
|
e0bd503261 | ||
|
|
707bbd8a02 | ||
|
|
8557da6fca | ||
|
|
03868d048a | ||
|
|
7d4906828d | ||
|
|
2c69070cc4 | ||
|
|
e60642217a | ||
|
|
68833cf642 | ||
|
|
30ffe469c8 | ||
|
|
132622cbc1 | ||
|
|
6b52740ff1 | ||
|
|
a73bcb9841 | ||
|
|
a9c94c7221 | ||
|
|
39465ea1ef | ||
|
|
f392ee5c48 | ||
|
|
184ebd0ac9 | ||
|
|
e56c837f4f | ||
|
|
8be4634a61 | ||
|
|
47c638d65b | ||
|
|
20f4727714 | ||
|
|
2ffb92c038 | ||
|
|
7c7d891570 | ||
|
|
e87ccdf061 | ||
|
|
f40abbd494 | ||
|
|
a64ae06eb6 | ||
|
|
68710d7ed8 | ||
|
|
5e67100231 | ||
|
|
06f872faf2 | ||
|
|
68f91f7de2 | ||
|
|
4ce7a268a8 | ||
|
|
ddc8d35eb9 | ||
|
|
626a87a30d | ||
|
|
2826c1e905 |
No files matched your search
@@ -302,12 +302,13 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
|
|||||||
|
|
||||||
**修复(`run_menu` + `menu_after_app_exit()`)**:
|
**修复(`run_menu` + `menu_after_app_exit()`)**:
|
||||||
1. **drain**:应用退出后立即读完并丢弃 `touch_fd` 的全部残留事件。
|
1. **drain**:应用退出后立即读完并丢弃 `touch_fd` 的全部残留事件。
|
||||||
2. **冷却**:应用**真正跑过**(`launch_app` 返回 0)时置 `menu_touch_cooldown_until = now + 2500ms`,冷却窗内忽略一切触摸按下边沿(与 watchdog 的同值 COOLDOWN_MS 对齐,覆盖实测幽灵窗 1.2-2.25s);启动失败(返回 -1)不置冷却,避免无谓锁菜单。
|
2. **短守卫**:应用**真正跑过**(`launch_app` 返回 0)时置 `menu_touch_cooldown_until = now + MENU_TOUCH_GUARD_MS`(**150ms**,M39 定值;M2.6 曾 2500ms、M2.22 缩到 1200ms),守卫窗内忽略触摸按下边沿;启动失败(返回 -1)不置守卫,避免无谓锁菜单。
|
||||||
|
**真正的防重放机制是第 1 条的 drain**:evdev 每个 fd 各收一份事件副本,App 运行期(launcher 不轮询 `touch_fd`)的全部事件都堆在本进程的 fd 里,drain 一次读空后已无历史事件可重放;**150ms 只兜面板重配后驱动补发的幽灵尾巴**。M39 起 drain 同时复位 `t_down/原点/坐标`——只清队列不清 pressed 边沿,会把「按下已被 drain、抬手稍后到」误判成一次 tap。旧注释写的「覆盖实测幽灵窗 1.2-2.25s」与实现自相矛盾(1200/2500ms 与 1.2–2.25s 的关系在 M2.22 注释里自己都写「实测略短」),故 M39 按「drain 是机制、时长只是尾巴守卫」定稿。
|
||||||
3. **菜单重绘**:v6.8 曾把 `last_redraw` 从绝对时间戳 `t` 改为相对量 `el`(使 500ms 安全重绘生效)以覆盖应用退出后的残帧(如 BYE)。**v6.9 起摒弃周期整屏重绘**(它在 500ms 定时把整个菜单重写进正在显示的 buffer0,造成列表 ~3fps 频闪):改为**仅 dirty 时整屏重绘**,应用退出/进入路径显式置 `dirty=1` 主动覆盖应用帧;时钟每秒用轻量 `draw_clock()` 只刷数字。
|
3. **菜单重绘**:v6.8 曾把 `last_redraw` 从绝对时间戳 `t` 改为相对量 `el`(使 500ms 安全重绘生效)以覆盖应用退出后的残帧(如 BYE)。**v6.9 起摒弃周期整屏重绘**(它在 500ms 定时把整个菜单重写进正在显示的 buffer0,造成列表 ~3fps 频闪):改为**仅 dirty 时整屏重绘**,应用退出/进入路径显式置 `dirty=1` 主动覆盖应用帧;时钟每秒用轻量 `draw_clock()` 只刷数字。
|
||||||
|
|
||||||
**Bug 3 根因与修复**:旧代码在所有 `launch_app` 调用点**无条件**置「CANNOT START: <name>」toast,即使应用**正常跑完**。修复:仅当 `launch_app` 返回 **-1(真启动失败)** 才置 toast;应用干净退出(返回 0)不显示。`launch_app` 用 **CLOEXEC 管道**检测 execve 失败(子进程写 errno → 父进程读 >0 判失败并返回 -1),不再把子进程 `_exit(127)` 误当「应用跑完」。
|
**Bug 3 根因与修复**:旧代码在所有 `launch_app` 调用点**无条件**置「CANNOT START: <name>」toast,即使应用**正常跑完**。修复:仅当 `launch_app` 返回 **-1(真启动失败)** 才置 toast;应用干净退出(返回 0)不显示。`launch_app` 用 **CLOEXEC 管道**检测 execve 失败(子进程写 errno → 父进程读 >0 判失败并返回 -1),不再把子进程 `_exit(127)` 误当「应用跑完」。
|
||||||
|
|
||||||
**副作用(已知取舍)**:应用退出后 2.5s 内菜单触摸不响应(返回键误触/退出后紧急再操作需等待;与 watchdog 相同值,实测幽灵窗 1.2-2.25s)。物理触摸的幽灵无法注入复现(§18.2 注入不投递),以上防抖逻辑在注入/sim 链路已验证,物理最终确认需用户。
|
**副作用(已知取舍)**:应用退出后 **150ms** 内菜单触摸不响应(M39 定值;M2.6 时是 2.5s,用户可见症状是「点一下没反应、要点两次」,M2.22 缩到 1.2s 仍会吞)。真正的不可控敞口是「面板在 drain 之后、队列之外新产生的硬件幽灵触摸」——这类幽灵 1.2s 与 150ms 都盖不住,只能靠真机日志收口:App 退出后观察 3–5s 内是否**自己重进 App / 闪回播放器**(Bug 2/4 原症状),并 `grep -c "menu touch dropped by cooldown"` 应为 0。物理触摸的幽灵无法注入复现(§18.2 注入不投递),宿主侧已由 `test_meow_ui_home_gesture` T4 覆盖「启动后立即点击生效 / 系统手势守卫内不触发、守卫后正好一次」。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1632,6 +1632,39 @@ void meow_lazygrid_draw(meow_lazygrid *g, const meow_theme *th);
|
|||||||
- **机制层新增**:`meow_ui_input.c` 主循环新增 `meow_anim_tick()` 调用(无动画零开销);`meow_ui_view.c` 新增布局登记表(未设布局的 view 行为不变);`meow_ui_widget.c` 新增 `meow_widget_set_rect`(只设字段不标脏,既有 `meow_layout_arrange` 不受影响)。
|
- **机制层新增**:`meow_ui_input.c` 主循环新增 `meow_anim_tick()` 调用(无动画零开销);`meow_ui_view.c` 新增布局登记表(未设布局的 view 行为不变);`meow_ui_widget.c` 新增 `meow_widget_set_rect`(只设字段不标脏,既有 `meow_layout_arrange` 不受影响)。
|
||||||
- **同步重编注意**:`meow_ui.a` 内容变化(新增 box/anim/scroll 三个 `.o`,`meow_ui_widget.o`/`meow_ui_view.o`/`meow_ui_input.o` 重编)→ 链 `meow_ui.a` 的 App 需**重链**(`build_sdk.sh` 一键重编 + 各 App `build_*.sh` 重链)。既有 App **不改源码**即可零回归。
|
- **同步重编注意**:`meow_ui.a` 内容变化(新增 box/anim/scroll 三个 `.o`,`meow_ui_widget.o`/`meow_ui_view.o`/`meow_ui_input.o` 重编)→ 链 `meow_ui.a` 的 App 需**重链**(`build_sdk.sh` 一键重编 + 各 App `build_*.sh` 重链)。既有 App **不改源码**即可零回归。
|
||||||
|
|
||||||
|
### 12.6 SDK 默认列表/容器控件默认 MD3(`meow_list_set_container`)+ 全局 App 内底部上滑回桌面(M6.37–M6.39 批)
|
||||||
|
|
||||||
|
> **性质**:M6.37–M6.39 批,两条默认行为定案。
|
||||||
|
> ① **SDK 默认列表/容器控件默认 MD3**——`meow_list_create` / `meow_lazycolumn_create` / `meow_lazyrow_create`(皆 `W_LIST`)与
|
||||||
|
> `meow_listview` 容器控件**默认即容器模式**:每项套 `surfaceContainer` 圆角卡 + 项间空隙 + 去分隔线 + 卡内 tonal 选中/按压,
|
||||||
|
> 用默认 renderer 的 App **开箱即 MD3**(此前默认扁平)。需扁平/满幅分隔线的宿主可显式 `meow_list_set_container(w,0)` **回退**。
|
||||||
|
> ② **全局 App 内「底部上滑回桌面」为系统保留手势**——任意 App 内屏底边缘上滑即回桌面(后台保活、进程保留转后台),
|
||||||
|
> App **勿在 `on_swipe` 拦截上滑**,否则该手势不生效。
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* MD3 容器模式(W_LIST 共用,§5.5/§5.8.6):0=扁平满宽项+分隔线(回退);1=每项外套 surfaceContainer
|
||||||
|
* 圆角卡+项间空隙(去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal)。
|
||||||
|
* 覆盖 meow_list_create / meow_lazycolumn_create / meow_lazyrow_create。返回 0=成功;无效=负值。 */
|
||||||
|
int meow_list_set_container(meow_widget *w, int on);
|
||||||
|
|
||||||
|
/* 底部热区上滑 → 回桌面(home)判定(镜像 launcher np_gesture.h 同口径):屏底热区(HOME_EDGE_H)起滑 +
|
||||||
|
* 上移 ≥HOME_SWIPE + 竖向主导(|dx|<|dy|)+ 避开左缘返回列(down_x≥EDGE_W)。1=命中(系统保留手势);0=否。
|
||||||
|
* 供宿主单测直调(判据与触摸状态机 touch_release 共用)。 */
|
||||||
|
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **MD3 默认**:两者 `container` 字段默认 1;`meow_list_set_container(w,0)` 回退至「满幅扁平行 + 分隔线」旧观感(宿主测试
|
||||||
|
`test_meow_ui_widgets`/`test_meow_ui_listvirt` 用 set_container(0) 锁扁平基线)。卡内未选中行以 `surfaceContainer` 呈现、
|
||||||
|
选中 `primaryContainer`、按压 `primary α26`;容器模式下分隔线去除,回退时以 `onSurfaceVariant` 分隔线呈现。
|
||||||
|
- **业务上滑不冲突**:`meow_ui_input_is_home_swipe` 只认「**屏底热区 + 竖向主导上滑**」;**中部上滑/滚动
|
||||||
|
(LazyColumn/LazyGrid/页面滚动)不受影响**,仍交 App 处理;底部热区的横向滑仍走左缘返回/横向逻辑。
|
||||||
|
- **App 勿拦截**:命中后 SDK 作为**系统保留手势**优先于 App `on_swipe`/视图树(否则已注册 `on_swipe` 的 App 会接管,
|
||||||
|
导致 App 内上滑不回桌面),路由 `meow_ui_nav_request_exit()`(MSG_NAV EXIT → launcher 批①后台保活,进程保留转后台、
|
||||||
|
present 自抑);**不**调 `meow_ui_request_exit()`(那会 `exit(0)` 断送保活)。IME 软键盘覆盖底部热区时手势让位 IME
|
||||||
|
(防输入时误退)。故 App 无需也不应在 `on_swipe` 处理「上滑回桌面」——交给 SDK 兜底。
|
||||||
|
- **系统边界**:与「顶部下拉通知中心」(§6)同理为系统保留手势;流媒体客户端/媒体 App 的列表、播放页照常用**中部**
|
||||||
|
手势(滚动/翻页),不外溢到屏底热区拦截回桌面。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
|
## 13. 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ def build_charset(tier="common", extra=""):
|
|||||||
add(ord(bytes([hi, lo]).decode("gb2312")))
|
add(ord(bytes([hi, lo]).decode("gb2312")))
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
pass
|
pass
|
||||||
|
# 显式补 U+00B7(MIDDLE DOT):Python 的 gb2312 codec 把 GB2312 A1A4 解成
|
||||||
|
# U+30FB(片假名中点),而 MiSans 无 U+30FB 字形 → 该槽位是空条目,U+00B7 从未进库,
|
||||||
|
# 真机渲染成 tofu。format 副题「Books/ · 595 本」与「· 读」徽标依赖此码位。
|
||||||
|
add(0x00B7)
|
||||||
if tier in ("gbk", "full"):
|
if tier in ("gbk", "full"):
|
||||||
# CJK 统一表意(含常用简繁/生僻):U+4E00-9FFF ≈ 2万+
|
# CJK 统一表意(含常用简繁/生僻):U+4E00-9FFF ≈ 2万+
|
||||||
for cp in range(0x4E00, 0xA000):
|
for cp in range(0x4E00, 0xA000):
|
||||||
|
|||||||
Executable
+102
@@ -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
|
||||||
Executable
+124
@@ -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
|
||||||
@@ -6,6 +6,10 @@ set -e
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
CC="$HOME/Project/R3ProII/.tools/zig-mipsel-cc"
|
CC="$HOME/Project/R3ProII/.tools/zig-mipsel-cc"
|
||||||
MEOW_TOUCH_DIR="../meow_launcherd"
|
MEOW_TOUCH_DIR="../meow_launcherd"
|
||||||
"$CC" -Os -static -I "$MEOW_TOUCH_DIR" meow_bootmenu.c "$MEOW_TOUCH_DIR/meow_touch.c" -o meow_bootmenu
|
# M43: bootmenu 也不链 meow_ui.a,单独链 time32 修取时/睡眠 ABI。只改语义正确的取时来源,
|
||||||
|
# boot_timeout/按键长按/守候计时行为不变(修复后才是真实毫秒;squashfs /usr/bin,改坏影响
|
||||||
|
# 开机选单/自动启动,但不是 initramfs 级不可恢复)。
|
||||||
|
"$CC" -Os -static -I "$MEOW_TOUCH_DIR" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_bootmenu.c "$MEOW_TOUCH_DIR/meow_touch.c" ../sdk/meow_time32.c -o meow_bootmenu
|
||||||
ls -la meow_bootmenu
|
ls -la meow_bootmenu
|
||||||
file meow_bootmenu
|
file meow_bootmenu
|
||||||
Binary file not shown.
@@ -32,6 +32,26 @@
|
|||||||
* 说明:/proc/mtd 只反映分区表不变读,无法观察内容变化,故用轻量读 /dev/mtd5 轮询。
|
* 说明:/proc/mtd 只反映分区表不变读,无法观察内容变化,故用轻量读 /dev/mtd5 轮询。
|
||||||
* 两根都写 bm_log(supervisor 接管 shutdown/upgrade)。
|
* 两根都写 bm_log(supervisor 接管 shutdown/upgrade)。
|
||||||
*
|
*
|
||||||
|
* M23 亮度单一权威源(2026-11-11,MEOW_BOOTMENU_VER 0.1.0-m2.30-bri-src):
|
||||||
|
* 真机症状(0.8.46)「开机菜单亮度=第一屏的 60」「HibyOS 亮/灭屏瞬间爆亮」。根因两处:
|
||||||
|
* - 开机菜单:M6.25 的「显示前按 settings.conf 写用户亮度」(本文件 main 末尾)只改了源码,
|
||||||
|
* **二进制没重编**(fw_content.conf 钉的是 8/31 的旧二进制,strings 无 display.brightness_min)
|
||||||
|
* → 0.8.46/0.8.49 上选单仍继承面板值(0.8.46 = S11 写的 60)。本版重编即带上。
|
||||||
|
* - HibyOS 亮/灭屏:supervisor 起始把「启动瞬间面板读数」当保底亮度,且 >60 一律钉 60;
|
||||||
|
* 扫描窗保底只要读到亮度<=0 就 400ms 内写回该值 —— 用户主动熄屏(播放器写 0)会被
|
||||||
|
* 强行点亮成 60。现改为:settings.conf 的 display.brightness 为唯一权威(与 watchdog
|
||||||
|
* wd_bri_decide / launcher 同源同 clamp);旧的 `>60 → 60` 改为 clamp 进
|
||||||
|
* [display.brightness_min, max_brightness](为什么不会重新引入 M2.9 的开机闪屏见
|
||||||
|
* bm_bri_pick_saved 注释)。settings 不可读的降级路径**不采信面板瞬时值**,取
|
||||||
|
* min(面板, BM_BRI_SAFE_LOW=10)(与 watchdog 的 WD_BRI_SAFE_LOW 对齐)。
|
||||||
|
* 扫描窗保底恢复 pre-M23 的「面板暗」触发(不依赖 /proc 的 D 态,避免媒体扫描在子线程时
|
||||||
|
* Bug 4 防护失效);不抢用户熄屏靠**电源键锁存**(窗内按过电源键则本次窗口内不再补),
|
||||||
|
* bl_power>0 仅作纯保险(实测本机播放器熄屏后 bl_power 仍为 0);窗口结束彻底停手;
|
||||||
|
* 日志同原因 ≥2s 一条。
|
||||||
|
* 决策全部抽成纯函数(bm_bri_pick_saved / bm_bri_floor_decide / bm_bri_hotkey_saved),
|
||||||
|
* 宿主单测见 test_bm_fw.c。亮度判定改为 bm_log 落卡(stderr 原行保留)。
|
||||||
|
* 本单一律**只交源码**:二进制由 M43/M51 统一重编并回写 fw_content.conf 钉值。
|
||||||
|
*
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
* WHY (设计见 BOOT_SELECTOR_DESIGN.md §3)
|
* WHY (设计见 BOOT_SELECTOR_DESIGN.md §3)
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
@@ -81,7 +101,7 @@
|
|||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/poll.h>
|
#include <poll.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/syscall.h> /* M2.29:supervisor 接管关机/升级,直连 reboot(2) syscall */
|
#include <sys/syscall.h> /* M2.29:supervisor 接管关机/升级,直连 reboot(2) syscall */
|
||||||
@@ -111,6 +131,7 @@
|
|||||||
#define FBIOBLANK 0x4611
|
#define FBIOBLANK 0x4611
|
||||||
#endif
|
#endif
|
||||||
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
|
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
|
||||||
|
#define BACKLIGHT_MAX "/sys/class/backlight/backlight_pwm0/max_brightness"
|
||||||
#define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power"
|
#define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power"
|
||||||
|
|
||||||
/* ---- 热键切换(HibyOS→MeowOS):按住电源 + 窗口内单击音量减 ---- */
|
/* ---- 热键切换(HibyOS→MeowOS):按住电源 + 窗口内单击音量减 ---- */
|
||||||
@@ -161,7 +182,7 @@
|
|||||||
#define POLL_MS 10
|
#define POLL_MS 10
|
||||||
#define SIM_SELECT_DELAY_MS 2000 /* --sim-select 延迟后模拟选 */
|
#define SIM_SELECT_DELAY_MS 2000 /* --sim-select 延迟后模拟选 */
|
||||||
#define GHOST_COOLDOWN_MS 800 /* 入口 drain + 短暂冷却防幽灵触摸(M2.1:缩短,不吞首次有效输入) */
|
#define GHOST_COOLDOWN_MS 800 /* 入口 drain + 短暂冷却防幽灵触摸(M2.1:缩短,不吞首次有效输入) */
|
||||||
#define MEOW_BOOTMENU_VER "0.1.0-m2.29-takeover" /* M2.29:supervisor 接管关机(poweroff)/升级(reboot) */
|
#define MEOW_BOOTMENU_VER "0.1.0-m2.30-bri-src" /* M23:亮度单一权威源(settings.conf),二进制随源码重编 */
|
||||||
|
|
||||||
/* ---- 版式 ---- */
|
/* ---- 版式 ---- */
|
||||||
#define STATUS_H 40
|
#define STATUS_H 40
|
||||||
@@ -329,7 +350,7 @@ static int fb_open(void)
|
|||||||
fb_bpp = *(uint32_t *)(vinfo + 24);
|
fb_bpp = *(uint32_t *)(vinfo + 24);
|
||||||
fb_line = *(uint32_t *)(finfo + 44);
|
fb_line = *(uint32_t *)(finfo + 44);
|
||||||
if (!fb_line) fb_line = fb_xres * (fb_bpp / 8);
|
if (!fb_line) fb_line = fb_xres * (fb_bpp / 8);
|
||||||
if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* 单缓冲兜底 */
|
if (fb_yres_virtual < (unsigned)fb_yres) fb_yres_virtual = fb_yres; /* 单缓冲兜底 */
|
||||||
fb = mmap(NULL, (size_t)fb_line * fb_yres_virtual,
|
fb = mmap(NULL, (size_t)fb_line * fb_yres_virtual,
|
||||||
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||||
if (fb == MAP_FAILED) {
|
if (fb == MAP_FAILED) {
|
||||||
@@ -345,7 +366,7 @@ static int fb_open(void)
|
|||||||
/* 选单进入前快照「当前显示缓冲」一帧(播放器事件驱动不主动重绘,须恢复) */
|
/* 选单进入前快照「当前显示缓冲」一帧(播放器事件驱动不主动重绘,须恢复) */
|
||||||
static void fb_snapshot_player(void)
|
static void fb_snapshot_player(void)
|
||||||
{
|
{
|
||||||
if (fb_yres_virtual <= fb_yres) { fb_orig_yoffset = -1; return; }
|
if (fb_yres_virtual <= (unsigned)fb_yres) { fb_orig_yoffset = -1; return; }
|
||||||
if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0;
|
if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0;
|
||||||
fb_orig_yoffset = (int)fb_active_yoffset;
|
fb_orig_yoffset = (int)fb_active_yoffset;
|
||||||
fb_snapshot_len = (size_t)fb_line * fb_yres;
|
fb_snapshot_len = (size_t)fb_line * fb_yres;
|
||||||
@@ -357,7 +378,7 @@ static void fb_snapshot_player(void)
|
|||||||
/* 画前 FBIOPAN_DISPLAY pan 到 yoffset=0(buffer0,选单/app 共同绘制目标) */
|
/* 画前 FBIOPAN_DISPLAY pan 到 yoffset=0(buffer0,选单/app 共同绘制目标) */
|
||||||
static void fb_pan_buf0(void)
|
static void fb_pan_buf0(void)
|
||||||
{
|
{
|
||||||
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
|
if (fb_fd < 0 || fb_yres_virtual <= (unsigned)fb_yres) { fb_active_yoffset = 0; return; }
|
||||||
if (fb_active_yoffset == 0) return; /* 已显示 buffer0 */
|
if (fb_active_yoffset == 0) return; /* 已显示 buffer0 */
|
||||||
unsigned char v[160];
|
unsigned char v[160];
|
||||||
memset(v, 0, sizeof v);
|
memset(v, 0, sizeof v);
|
||||||
@@ -436,6 +457,7 @@ static int amixer_vol(int numid)
|
|||||||
/* 28px 状态栏:行1=时钟(左) 电量% WIFI BT(右),行2=VOL nn%(左),y=28 分隔线(40→28,用户定) */
|
/* 28px 状态栏:行1=时钟(左) 电量% WIFI BT(右),行2=VOL nn%(左),y=28 分隔线(40→28,用户定) */
|
||||||
static void draw_statusbar(uint32_t bg)
|
static void draw_statusbar(uint32_t bg)
|
||||||
{
|
{
|
||||||
|
(void)bg; /* 传参保留(调用点已传 cfg_bg),本版状态栏用固定配色 */
|
||||||
char timebuf[16], right[48];
|
char timebuf[16], right[48];
|
||||||
time_t now = time(NULL);
|
time_t now = time(NULL);
|
||||||
struct tm tm;
|
struct tm tm;
|
||||||
@@ -814,9 +836,23 @@ static void draw_menu(int cursor, long remaining_ms)
|
|||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
/* 面板/背光 helper(复用 meow_launcherd 语义,§18.21.5 / §红线) */
|
/* 面板/背光 helper(复用 meow_launcherd 语义,§18.21.5 / §红线) */
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
|
/* 路径可被 env 覆盖(宿主测试用,与 meow_watchdog 的 MEOW_BACKLIGHT /
|
||||||
|
* MEOW_SETTINGS_FILE 同款钩子);真机 init 环境无这些变量 → 走宏默认路径。 */
|
||||||
|
static const char *bm_backlight_path(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("MEOW_BACKLIGHT");
|
||||||
|
return (e && e[0]) ? e : BACKLIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *bm_settings_path(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("MEOW_SETTINGS_FILE");
|
||||||
|
return (e && e[0]) ? e : MEOW_SETTINGS_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
static int read_brightness(void)
|
static int read_brightness(void)
|
||||||
{
|
{
|
||||||
int fd = open(BACKLIGHT, O_RDONLY);
|
int fd = open(bm_backlight_path(), O_RDONLY);
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
char buf[16];
|
char buf[16];
|
||||||
int n = read(fd, buf, sizeof buf - 1);
|
int n = read(fd, buf, sizeof buf - 1);
|
||||||
@@ -839,24 +875,46 @@ static void write_brightness(int val)
|
|||||||
{
|
{
|
||||||
char buf[8];
|
char buf[8];
|
||||||
int l = snprintf(buf, sizeof buf, "%d\n", val);
|
int l = snprintf(buf, sizeof buf, "%d\n", val);
|
||||||
int fd = open(BACKLIGHT, O_WRONLY);
|
int fd = open(bm_backlight_path(), O_WRONLY);
|
||||||
if (fd < 0) { fprintf(stderr, "bootmenu: backlight open fail: %s\n", strerror(errno)); return; }
|
if (fd < 0) { fprintf(stderr, "bootmenu: backlight open fail: %s\n", strerror(errno)); return; }
|
||||||
if (write(fd, buf, l) < 0) fprintf(stderr, "bootmenu: backlight write %d: %s\n", val, strerror(errno));
|
if (write(fd, buf, l) < 0) fprintf(stderr, "bootmenu: backlight write %d: %s\n", val, strerror(errno));
|
||||||
close(fd);
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 面板背光上限(本机 101)。读不到返回 0 = 未知,调用方按「无上限信息」降级处理。 */
|
||||||
|
static int read_max_brightness(void)
|
||||||
|
{
|
||||||
|
int v = read_file_int(BACKLIGHT_MAX);
|
||||||
|
return v > 0 ? v : 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* M6.25:开机选单亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness)。
|
/* M6.25:开机选单亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness)。
|
||||||
* 根因:选单显示路径从不写背光,开机菜单/第二屏亮度纯继承引导早期面板高值;M2.83 起 launcher
|
* 根因:选单显示路径从不写背光,开机菜单/第二屏亮度纯继承引导早期面板高值;M2.83 起 launcher
|
||||||
* 「开机选单期不抢屏」怠速,移除了 launcher 提前写用户亮度那一步 → 菜单亮在引导高值上。
|
* 「开机选单期不抢屏」怠速,移除了 launcher 提前写用户亮度那一步 → 菜单亮在引导高值上。
|
||||||
* 与 meow_watchdog wd_settings_brightness() 同源解析(同文件、同 clamp:下限 display.brightness_min
|
* 与 meow_watchdog wd_settings_brightness() 同源解析(同文件、同 clamp:下限 display.brightness_min
|
||||||
* 默认 1=极暗但可见、上限 100=防闪屏),自包含零依赖,不链 launcher meow_settings。返回 clamped
|
* 默认 1=极暗但可见、上限 100=防闪屏),自包含零依赖,不链 launcher meow_settings。返回 clamped
|
||||||
* 值;settings 文件不可读 → 返回 -1,调用方**不动**(退化为现状,保安全——选单阶段只写 brightness、
|
* 值;settings 文件不可读 → 返回 -1,调用方**不动**(退化为现状,保安全——选单阶段只写 brightness、
|
||||||
* 绝不碰 FBIOBLANK/blank,§18.24 纪律)。 */
|
* 绝不碰 FBIOBLANK/blank,§18.24 纪律)。
|
||||||
static int bootmenu_settings_brightness(void)
|
* M23:fopen 失败做 BM_SETTINGS_TRIES 次短重试,覆盖 /usr/data 刚挂载 / 原子替换的瞬时窗口
|
||||||
|
* (与 M20 的 wd_settings_brightness 同策略);只重试 open(rename 原子,已打开的 fd 内容完整)。
|
||||||
|
* M23 评审 r2:文件存在但**缺 display.brightness 键**同样返回 -1(不可信),不再按 launcher
|
||||||
|
* 设置表的默认 101→clamp 100。理由:bootmenu 的用途是「永不写高于用户值」,把「文件里没有
|
||||||
|
* 这个键」当成 100 会在扫描窗待机时补到 100(比老 bug 的 60 更亮)。launcher/watchdog 的 100
|
||||||
|
* 默认是「设置表缺省值」语义,与本进程的「授权写亮度」语义不同,这里有意更严格。
|
||||||
|
* (注:键存在但值非数字 → atoi=0 → 夹到 brightness_min,与旧行为一致。) */
|
||||||
|
#define BM_SETTINGS_TRIES 3
|
||||||
|
#define BM_SETTINGS_GAP_MS 10
|
||||||
|
|
||||||
|
static int bootmenu_settings_brightness(int *min_out)
|
||||||
{
|
{
|
||||||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
FILE *f = NULL;
|
||||||
|
for (int i = 0; i < BM_SETTINGS_TRIES; i++) {
|
||||||
|
f = fopen(bm_settings_path(), "r");
|
||||||
|
if (f) break;
|
||||||
|
if (i + 1 < BM_SETTINGS_TRIES) usleep(BM_SETTINGS_GAP_MS * 1000);
|
||||||
|
}
|
||||||
if (!f) return -1;
|
if (!f) return -1;
|
||||||
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
|
int b = -1; /* -1 = 文件里没看到 display.brightness(视为不可信 → 返回 -1) */
|
||||||
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
||||||
char buf[128];
|
char buf[128];
|
||||||
while (fgets(buf, sizeof buf, f)) {
|
while (fgets(buf, sizeof buf, f)) {
|
||||||
@@ -871,11 +929,99 @@ static int bootmenu_settings_brightness(void)
|
|||||||
else if (!strcmp(k, "display.brightness")) { b = iv; }
|
else if (!strcmp(k, "display.brightness")) { b = iv; }
|
||||||
}
|
}
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
if (min_out) *min_out = mn;
|
||||||
|
if (b < 0) return -1;
|
||||||
if (b < mn) b = mn;
|
if (b < mn) b = mn;
|
||||||
if (b > 100) b = 100;
|
if (b > 100) b = 100;
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- M23:亮度决策(纯函数,宿主单测见 test_bm_fw.c)----
|
||||||
|
* 方向同 meow_watchdog wd_bri_decide()(settings 是唯一权威,任何分支都不再写「固定 60」),
|
||||||
|
* 但 bootmenu **没有运行期亮度快照**(supervisor 起始只有一次读取),所以降级时转而走
|
||||||
|
* 安全下限,而不是 M20 的「运行期快照 → cur>0 不写」那条链。 */
|
||||||
|
#define BM_BRI_MIN_DEFAULT 1 /* display.brightness_min 缺省(与 launcher/watchdog 同值:极暗但可见) */
|
||||||
|
#define BM_BRI_SAFE_LOW 10 /* 与 meow_watchdog 的 WD_BRI_SAFE_LOW 对齐(同一系统同一个安全下限) */
|
||||||
|
|
||||||
|
/* 把任意亮度读数夹进面板合法区间 [mn, max_brightness](M23:取代旧 `>60 → 60` 的硬钉)。
|
||||||
|
* mn < 1 时按 1(防黑屏:亮着总比黑屏好,但也不伪造「比用户下限更亮」);
|
||||||
|
* maxb <= 0(sysfs 读不到)时只夹下限,不臆造上限。
|
||||||
|
* 为什么这样不会重新引入 M2.9 要压的开机闪屏:见 bm_bri_pick_saved 注释。 */
|
||||||
|
static int bm_bri_clamp(int v, int mn, int maxb)
|
||||||
|
{
|
||||||
|
if (mn < BM_BRI_MIN_DEFAULT) mn = BM_BRI_MIN_DEFAULT;
|
||||||
|
if (v < mn) v = mn;
|
||||||
|
if (maxb > 0 && v > maxb) v = maxb;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* supervisor 起始「保底亮度」取值。
|
||||||
|
* settings(已 clamp 到 [brightness_min,100])→ min(面板读数, SAFE_LOW) → SAFE_LOW;
|
||||||
|
* 后两者再夹进 [brightness_min, max_brightness]。
|
||||||
|
* *src 回填来源,原样进日志(settings / panel-capped / safe-low)。
|
||||||
|
* 真机症状来源:旧实现把「启动瞬间面板读数」当权威、且 >60 一律钉 60,用户值 1 被 60 顶掉。
|
||||||
|
*
|
||||||
|
* 降级路径(settings 不可读)**不再原样采信面板读数**(P2-1 评审):启动瞬间面板上可能是
|
||||||
|
* 播放器启动画面的 60/101,若原值保存,后续扫描窗保底/确认框 unblank 就会把 60 写回去 ——
|
||||||
|
* 正是 M20 把「当前面板值」排除出权威源的原因。bootmenu 也没有运行期快照可退,所以取
|
||||||
|
* min(cur, SAFE_LOW=10):宁可暗一点,也不把瞬时值当用户亮度回放(这就是 M2.9 闪屏防护的正确形态)。
|
||||||
|
* HibyOS 运行期由用户改的亮度**不走这条路径**,而走热键 `panel-now`(明确用户动作,见
|
||||||
|
* bm_bri_hotkey_saved),所以这里不跟随面板不会丢用户意图。
|
||||||
|
*
|
||||||
|
* 为什么 clamp[min,max] 不会重新引入 M2.9 Bug 3 的「开机闪一下最高亮度」:
|
||||||
|
* M2.9 的前置条件是「supervisor 启动时面板上是播放器启动画面的瞬时值、还没人写过用户值」。
|
||||||
|
* 现在:① 有 settings 时根本不看面板值,写回的就是用户设置本身;② M21/S22 之后 /usr/data
|
||||||
|
* 一挂载就写用户值,主路径上不存在「瞬时值当权威」;③ 写回只发生在面板已暗时,方向是
|
||||||
|
* 「暗 → 回到用户值」,而退化路径已被 SAFE_LOW 封顶,不会跳 max。 */
|
||||||
|
static int bm_bri_pick_saved(int user, int cur, int mn, int maxb, const char **src)
|
||||||
|
{
|
||||||
|
if (user > 0) { *src = "settings"; return bm_bri_clamp(user, mn, maxb); }
|
||||||
|
if (cur > 0) {
|
||||||
|
*src = "panel-capped";
|
||||||
|
return bm_bri_clamp((cur < BM_BRI_SAFE_LOW) ? cur : BM_BRI_SAFE_LOW, mn, maxb);
|
||||||
|
}
|
||||||
|
*src = "safe-low";
|
||||||
|
return bm_bri_clamp(BM_BRI_SAFE_LOW, mn, maxb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扫描窗保底(Bug 4)决策:返回 >0 = 要写的亮度,0 = 不动;*why 回填原因供日志。
|
||||||
|
* 触发条件**回到 pre-M23 的形态**(评审 r1 P2-2):窗口内 + 面板暗(bri<=0)→ 补。
|
||||||
|
* **不依赖 /proc 的 D 态**:媒体扫描若在子线程,主线程是 S,D 态门会让 Bug 4 的防护变成
|
||||||
|
* 空操作(黑屏几分钟回归),那比亮度偏高严重得多。
|
||||||
|
*
|
||||||
|
* 不抢用户熄屏靠 **电源键锁存**(评审 r2 P2-2):扫描窗内一旦看到电源键按下边沿,
|
||||||
|
* 本次窗口内此后一律不补(*why="power-key-latched")。理由:Bug 4 保护的是「**无人值守**时
|
||||||
|
* 媒体扫描导致黑屏几分钟」;用户已经按键介入,需要亮屏时自己再按一次即可。锁存覆盖两种残余:
|
||||||
|
* 播放器只写 brightness=0 不改 bl_power,以及 HibyOS 自动超时熄屏。
|
||||||
|
*
|
||||||
|
* bl_power 门**保留但只是纯保险**:据 patch_v2/meow_launcherd/backlight.c:90-95 的实测注释,
|
||||||
|
* 本机播放器熄屏(brightness 0 + FBIOBLANK 4)后 **bl_power 仍为 0**,所以这道门在「播放器
|
||||||
|
* 熄屏」场景**不生效**;保留它只是为了其它可能置位的面板下电状态。真正的门是上面的电源键锁存。
|
||||||
|
*
|
||||||
|
* 窗口结束 → *why="window-over",调用方彻底停手(并把锁存清零,逻辑上等价于「下一轮窗口从零开始」)。 */
|
||||||
|
static int bm_bri_floor_decide(int saved, int cur, int bl_power,
|
||||||
|
long long win_left_ms, int pwr_latched,
|
||||||
|
const char **why)
|
||||||
|
{
|
||||||
|
if (win_left_ms <= 0) { *why = "window-over"; return 0; }
|
||||||
|
if (cur > 0) { *why = "visible"; return 0; }
|
||||||
|
if (bl_power > 0) { *why = "panel-off"; return 0; }
|
||||||
|
if (pwr_latched) { *why = "power-key-latched"; return 0; }
|
||||||
|
*why = "scan-floor";
|
||||||
|
return saved > 0 ? saved : BM_BRI_SAFE_LOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 热键冻结播放器前刷新「熄屏恢复值」:面板当前有正亮度 = 用户此刻看到的真实值,以它为准
|
||||||
|
* (HibyOS 里改过亮度不写 settings.conf,这条路径必须跟随面板);否则沿用 saved(现在=用户设置);
|
||||||
|
* 都没有才 BM_BRI_SAFE_LOW。一律夹进 [brightness_min, max_brightness],防越界值回放。 */
|
||||||
|
static int bm_bri_hotkey_saved(int saved, int now_bri, int mn, int maxb, const char **src)
|
||||||
|
{
|
||||||
|
if (now_bri > 0) { *src = "panel-now"; return bm_bri_clamp(now_bri, mn, maxb); }
|
||||||
|
if (saved > 0) { *src = "saved"; return saved; }
|
||||||
|
*src = "safe-low";
|
||||||
|
return bm_bri_clamp(BM_BRI_SAFE_LOW, mn, maxb);
|
||||||
|
}
|
||||||
|
|
||||||
/* 面板 unblank(FBIOBLANK 0)。仅用于杀播放器后把屏点亮,以便 MeowOS 接管;
|
/* 面板 unblank(FBIOBLANK 0)。仅用于杀播放器后把屏点亮,以便 MeowOS 接管;
|
||||||
* 播放器已死,故不涉及「播放器 state machine」一致性(§18.11 关注点)。
|
* 播放器已死,故不涉及「播放器 state machine」一致性(§18.11 关注点)。
|
||||||
* bootmenu 选单/自检阶段绝不调用本函数(§18.24 纪律:不碰 panel blank)。 */
|
* bootmenu 选单/自检阶段绝不调用本函数(§18.24 纪律:不碰 panel blank)。 */
|
||||||
@@ -1192,14 +1338,19 @@ static int confirm_switch(void)
|
|||||||
/* ---- HibyOS 分支:父进程监督者/热键守护(M2)---- */
|
/* ---- HibyOS 分支:父进程监督者/热键守护(M2)---- */
|
||||||
static void bootmenu_supervisor(int player_pid)
|
static void bootmenu_supervisor(int player_pid)
|
||||||
{
|
{
|
||||||
/* 记录开机后的正常亮度,供「杀播放器后亮度==0 则恢复 saved/60」。
|
/* M23:保底亮度(saved_brightness)的权威源 = settings.conf 的 display.brightness
|
||||||
* 此刻播放器刚拉起,可能已写入其背光值;过早读到 0 则回退默认 60。 */
|
* (与 launcher / meow_watchdog 同源同 clamp),不再是「启动瞬间面板读数 + >60 钉 60」。
|
||||||
int saved_brightness = read_brightness();
|
* 旧写法把播放器启动瞬间写入的面板值(真机 60)当权威,用户设置 1 被顶掉,随后扫描窗
|
||||||
/* M2.9 Bug 3:开机初期播放器启动画面会把背光设为瞬时高值(本机 max=101),
|
* 保底 / 熄屏恢复都把 60 写回 → 「亮/灭屏瞬间爆亮」。
|
||||||
* 若直接以其为「保底亮度」,会在扫描窗首次写回时跳一次最高亮度(开机瞬间闪一下)。
|
* 取值优先级见 bm_bri_pick_saved:settings → 面板读数 → BM_BRI_SAFE_LOW,后两者夹进
|
||||||
* 只信任稳定读数:读到 0 或异常高(>稳定上限 60,适中非最大)一律回退稳定值 60,
|
* [brightness_min, max_brightness](取代旧的 >60→60 硬钉)。起始**不写**背光
|
||||||
* 确保扫描期写回平滑(只写目标一次、不跳 max)。 */
|
* (不抢播放器刚开始的写入)。 */
|
||||||
if (saved_brightness <= 0 || saved_brightness > 60) saved_brightness = 60;
|
const char *bri_src = "?";
|
||||||
|
int bri_min = BM_BRI_MIN_DEFAULT;
|
||||||
|
int bri_user = bootmenu_settings_brightness(&bri_min); /* >0=用户设置(clamped);-1=不可读 */
|
||||||
|
int bri_panel = read_brightness();
|
||||||
|
int bri_max = read_max_brightness();
|
||||||
|
int saved_brightness = bm_bri_pick_saved(bri_user, bri_panel, bri_min, bri_max, &bri_src);
|
||||||
fprintf(stderr, "supervisor: player pid=%d, saved brightness=%d, monitoring...\n",
|
fprintf(stderr, "supervisor: player pid=%d, saved brightness=%d, monitoring...\n",
|
||||||
player_pid, saved_brightness);
|
player_pid, saved_brightness);
|
||||||
/* 信号记录 hook(M2 诊断):先装 handler、start 一行写入 TF 卡日志。
|
/* 信号记录 hook(M2 诊断):先装 handler、start 一行写入 TF 卡日志。
|
||||||
@@ -1207,12 +1358,19 @@ static void bootmenu_supervisor(int player_pid)
|
|||||||
bm_sig_install();
|
bm_sig_install();
|
||||||
bm_log("supervisor: start pid=%d saved_brightness=%d mode=hiby ver=%s",
|
bm_log("supervisor: start pid=%d saved_brightness=%d mode=hiby ver=%s",
|
||||||
player_pid, saved_brightness, MEOW_BOOTMENU_VER);
|
player_pid, saved_brightness, MEOW_BOOTMENU_VER);
|
||||||
|
bm_log("supervisor: bri saved=%d src=%s user=%d panel=%d min=%d max=%d",
|
||||||
|
saved_brightness, bri_src, bri_user, bri_panel, bri_min, bri_max);
|
||||||
/* Bug 4:HibyOS 开机媒体扫描窗内做亮度保底。播放器忙(媒体扫描)时其背光
|
/* Bug 4:HibyOS 开机媒体扫描窗内做亮度保底。播放器忙(媒体扫描)时其背光
|
||||||
* 待机会把亮度降到 0、面板 blank → 黑屏几分钟。supervisor 在扫描窗内若读到
|
* 待机会把亮度降到 0、面板 blank → 黑屏几分钟。**只写 brightness、绝不碰 FBIOBLANK**
|
||||||
* 亮度 0,写回保底亮度(saved 或 60)。**只写 brightness、绝不碰 FBIOBLANK**
|
* (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。
|
||||||
* (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。 */
|
* M23:补的值改为用户设置(不再 60);触发回到 pre-M23 的「面板暗」,不抢用户熄屏靠
|
||||||
|
* 「电源键锁存」(见 bm_bri_floor_decide)。 */
|
||||||
unsigned long long scan_keep_until = now_ms() + SCAN_KEEP_MS;
|
unsigned long long scan_keep_until = now_ms() + SCAN_KEEP_MS;
|
||||||
unsigned long long last_bri_floor_at = 0; /* 亮度保底节流:≥400ms 才再写一次 */
|
unsigned long long last_bri_floor_at = 0; /* 亮度保底节流:≥400ms 才判定/写一次 */
|
||||||
|
const char *last_floor_why = "init"; /* 判定原因(日志只在跃迁或 ≥2s 时打) */
|
||||||
|
unsigned long long last_floor_log_at = 0; /* 上次 scan-window 日志时刻(同原因 ≥2s 一条) */
|
||||||
|
unsigned long long last_pwr_edge_at = 0; /* 最近一次电源键按下边沿(0=本次开机尚未按过) */
|
||||||
|
int scan_pwr_seen = 0; /* 扫描窗内见过电源键按下 → 本次窗口内不再补亮 */
|
||||||
|
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "supervisor: WARN no power dev\n");
|
if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "supervisor: WARN no power dev\n");
|
||||||
@@ -1268,19 +1426,10 @@ static void bootmenu_supervisor(int player_pid)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2a) Bug 4 亮度保底:扫描窗内、播放器存活且亮度被降到 0 → 写回保底亮度。
|
/* 2a) Bug 4 亮度保底(M23):**已移到主循环后半(按键采样之后)**,见下面
|
||||||
* (播放器扫描忙时无法响应唤醒,我们只把背光拉回,画面仍为播放器最后帧,
|
* 「2b) 亮度保底」—— 电源键边沿与播放器写 0 落在同一 poll 间隙时,先采键再判定
|
||||||
* 不再黑屏数分钟;不碰 blank。) */
|
* 才不会出现一轮「未看到电源键就补亮」的竞态(评审 r2 非阻塞建议)。
|
||||||
if (player_alive && now_ms() < scan_keep_until) {
|
*/
|
||||||
int bri = read_brightness();
|
|
||||||
if (bri <= 0 && now_ms() - last_bri_floor_at >= 400) {
|
|
||||||
int floor = saved_brightness > 0 ? saved_brightness : 60;
|
|
||||||
write_brightness(floor);
|
|
||||||
last_bri_floor_at = now_ms();
|
|
||||||
fprintf(stderr, "supervisor: scan-window brightness floor -> %d\n", floor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 2) EVIOCGKEY 采样(event0 电源 / event2 音量) */
|
/* 2) EVIOCGKEY 采样(event0 电源 / event2 音量) */
|
||||||
int pwr = 0, vold = 0;
|
int pwr = 0, vold = 0;
|
||||||
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
|
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
|
||||||
@@ -1294,12 +1443,54 @@ static void bootmenu_supervisor(int player_pid)
|
|||||||
if (se >= sim_hotkey_ms + 800 && se < sim_hotkey_ms + 950) vold = 1;
|
if (se >= sim_hotkey_ms + 800 && se < sim_hotkey_ms + 950) vold = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3) 热键:电源按下边沿 arming,窗口内音量减按下边沿触发 */
|
/* 3) 热键:电源按下边沿 arming,窗口内音量减按下边沿触发。
|
||||||
if (pwr && !prev_pwr) { pwr_hold_armed = 1; pwr_down_at = t; }
|
* 同一边沿也记到 last_pwr_edge_at,并**锁存**到本次扫描窗结束:亮度保底据此
|
||||||
|
* 不再与用户抢屏(用户已按键介入;需要亮屏时自己再按一次)。 */
|
||||||
|
if (pwr && !prev_pwr) {
|
||||||
|
pwr_hold_armed = 1; pwr_down_at = t; last_pwr_edge_at = t;
|
||||||
|
if (scan_keep_until) scan_pwr_seen = 1;
|
||||||
|
}
|
||||||
if (!pwr && prev_pwr) pwr_hold_armed = 0; /* 松开:disarm */
|
if (!pwr && prev_pwr) pwr_hold_armed = 0; /* 松开:disarm */
|
||||||
if (pwr_hold_armed && pwr && (t - pwr_down_at) > HK_PWRWIN_MAX)
|
if (pwr_hold_armed && pwr && (t - pwr_down_at) > HK_PWRWIN_MAX)
|
||||||
pwr_hold_armed = 0; /* 超窗:disarm */
|
pwr_hold_armed = 0; /* 超窗:disarm */
|
||||||
|
|
||||||
|
/* 2b) Bug 4 亮度保底(M23,放在按键采样之后以关掉「电源键与写 0 同间隙」竞态):
|
||||||
|
* 扫描窗内、面板暗(bri<=0)→ 写回用户设置亮度。触发不依赖 /proc 的 D 态;
|
||||||
|
* 不抢用户熄屏靠**电源键锁存**(why=power-key-latched);bl_power>0 只是纯保险
|
||||||
|
* (据 backlight.c:90-95 实测,本机播放器熄屏后 bl_power 仍为 0,这道门在该场景不生效)。
|
||||||
|
* 判定 400ms 节流;日志同一原因 ≥2s、跃迁 ≥1s(90s 窗口最坏 ~90 行)。 */
|
||||||
|
if (player_alive && scan_keep_until) {
|
||||||
|
unsigned long long now2 = now_ms();
|
||||||
|
if (now2 - last_bri_floor_at >= 400) {
|
||||||
|
long long win_left = (scan_keep_until > now2)
|
||||||
|
? (long long)(scan_keep_until - now2) : 0;
|
||||||
|
long long pwr_ago = (last_pwr_edge_at > 0)
|
||||||
|
? (long long)(now2 - last_pwr_edge_at) : -1;
|
||||||
|
int bri = read_brightness();
|
||||||
|
int blp = read_file_int(BACKLIGHT_POWER);
|
||||||
|
const char *why = "?";
|
||||||
|
int floor = bm_bri_floor_decide(saved_brightness, bri, blp, win_left,
|
||||||
|
scan_pwr_seen, &why);
|
||||||
|
last_bri_floor_at = now2;
|
||||||
|
if (floor > 0) {
|
||||||
|
write_brightness(floor);
|
||||||
|
/* 保留 M2 时代原有的 stderr 行(串口调试仍看它),另加一路 bm_log 落卡。 */
|
||||||
|
fprintf(stderr, "supervisor: scan-window brightness floor -> %d\n", floor);
|
||||||
|
}
|
||||||
|
unsigned long long gap = now2 - last_floor_log_at;
|
||||||
|
if ((strcmp(why, last_floor_why) != 0 && gap >= 1000) || gap >= 2000) {
|
||||||
|
last_floor_why = why;
|
||||||
|
last_floor_log_at = now2;
|
||||||
|
bm_log("supervisor: scan-window brightness %s -> %d (user=%d panel=%d bl_power=%d latched=%d pwr_ago=%lldms left=%lldms)",
|
||||||
|
(floor > 0) ? "floor" : "skip", floor, bri_user, bri, blp,
|
||||||
|
scan_pwr_seen, pwr_ago, win_left);
|
||||||
|
}
|
||||||
|
/* 窗口结束 → 彻底放手:此后不再读背光/bl_power;锁存清零(逻辑上等价于
|
||||||
|
* 「下一轮窗口从零开始」,当前实现每进程只有一轮窗口)。 */
|
||||||
|
if (win_left <= 0) { scan_keep_until = 0; scan_pwr_seen = 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int hotkey_fired = 0;
|
int hotkey_fired = 0;
|
||||||
if (pwr_hold_armed && pwr && vold && !prev_vold) {
|
if (pwr_hold_armed && pwr && vold && !prev_vold) {
|
||||||
long since = (long)(t - pwr_down_at);
|
long since = (long)(t - pwr_down_at);
|
||||||
@@ -1338,10 +1529,16 @@ static void bootmenu_supervisor(int player_pid)
|
|||||||
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/* 刷新 saved 亮度:若播放器当前亮度仍为正(如「背光常亮」设置),
|
/* 刷新 saved 亮度:若播放器当前亮度仍为正(如「背光常亮」设置 / 用户在 HibyOS 里
|
||||||
* 以它为准;若电源按下已触发灭屏(亮度 0),保留此前 saved(≥60)。 */
|
* 改过亮度但没写 settings.conf),以面板此刻的真实值为准(clamp 到
|
||||||
|
* [brightness_min, max_brightness]);若电源按下已触发灭屏(亮度 0),保留此前 saved
|
||||||
|
* —— M23 起 saved 来自 settings.conf 的用户亮度,不再回退 60。见 bm_bri_hotkey_saved。 */
|
||||||
int now_bri = read_brightness();
|
int now_bri = read_brightness();
|
||||||
if (now_bri > 0) saved_brightness = now_bri;
|
const char *hk_src = "?";
|
||||||
|
saved_brightness = bm_bri_hotkey_saved(saved_brightness, now_bri,
|
||||||
|
bri_min, bri_max, &hk_src);
|
||||||
|
bm_log("supervisor: hotkey saved brightness=%d src=%s (now=%d user=%d)",
|
||||||
|
saved_brightness, hk_src, now_bri, bri_user);
|
||||||
/* 先冻结播放器(SIGSTOP→T),防止其在确认框上重绘覆盖;
|
/* 先冻结播放器(SIGSTOP→T),防止其在确认框上重绘覆盖;
|
||||||
* 同时捕获面板是否已因电源按下而灭屏(was_blank),供较后恢复。 */
|
* 同时捕获面板是否已因电源按下而灭屏(was_blank),供较后恢复。 */
|
||||||
if (player_alive && !proc_is_stopped(player_pid)) {
|
if (player_alive && !proc_is_stopped(player_pid)) {
|
||||||
@@ -1354,7 +1551,10 @@ static void bootmenu_supervisor(int player_pid)
|
|||||||
|
|
||||||
/* 点亮面板以便看清确认框(播放器已冻结,不会 re-blank)。 */
|
/* 点亮面板以便看清确认框(播放器已冻结,不会 re-blank)。 */
|
||||||
unblank_panel();
|
unblank_panel();
|
||||||
if (was_blank) write_brightness(saved_brightness);
|
if (was_blank) {
|
||||||
|
write_brightness(saved_brightness);
|
||||||
|
bm_log("supervisor: confirm-box unblank brightness=%d (panel was blank)", saved_brightness);
|
||||||
|
}
|
||||||
if (read_file_int(BACKLIGHT_POWER) > 0) write_file_str(BACKLIGHT_POWER, "0\n");
|
if (read_file_int(BACKLIGHT_POWER) > 0) write_file_str(BACKLIGHT_POWER, "0\n");
|
||||||
|
|
||||||
int ans = confirm_switch();
|
int ans = confirm_switch();
|
||||||
@@ -1409,6 +1609,7 @@ static void bootmenu_supervisor(int player_pid)
|
|||||||
if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); }
|
if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); }
|
||||||
} else if (read_brightness() <= 0) {
|
} else if (read_brightness() <= 0) {
|
||||||
write_brightness(saved_brightness);
|
write_brightness(saved_brightness);
|
||||||
|
bm_log("supervisor: cancel restore brightness=%d (panel read 0)", saved_brightness);
|
||||||
}
|
}
|
||||||
fprintf(stderr, "supervisor: hotkey cancelled (stay HibyOS)\n");
|
fprintf(stderr, "supervisor: hotkey cancelled (stay HibyOS)\n");
|
||||||
if (player_alive && proc_is_stopped(player_pid)) {
|
if (player_alive && proc_is_stopped(player_pid)) {
|
||||||
@@ -1628,7 +1829,6 @@ static int run_menu(int sim_sel, int do_fork)
|
|||||||
/* 每秒刷时钟 */
|
/* 每秒刷时钟 */
|
||||||
if (el - last_clock_tick >= 1000) { last_clock_tick = el; draw_clock(cfg_bg); }
|
if (el - last_clock_tick >= 1000) { last_clock_tick = el; draw_clock(cfg_bg); }
|
||||||
/* 每秒刷倒计时(秒值变化才刷) */
|
/* 每秒刷倒计时(秒值变化才刷) */
|
||||||
long cd = (remaining + 999) / 1000;
|
|
||||||
if (el - last_cd_tick >= 1000) { last_cd_tick = el; draw_countdown(cfg_bg, remaining); }
|
if (el - last_cd_tick >= 1000) { last_cd_tick = el; draw_countdown(cfg_bg, remaining); }
|
||||||
|
|
||||||
usleep(POLL_MS * 1000);
|
usleep(POLL_MS * 1000);
|
||||||
@@ -1676,6 +1876,9 @@ static void usage(void)
|
|||||||
SIM_SELECT_DELAY_MS);
|
SIM_SELECT_DELAY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* M23:宿主单测(test_bm_fw.c)先 #define BM_NO_MAIN 再把本文件 include 进来,去 main 拿
|
||||||
|
* 静态亮度决策函数(与 meow_watchdog 的 -DWD_NO_MAIN 同款);交叉编译不定义该宏,main 照常。 */
|
||||||
|
#ifndef BM_NO_MAIN
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
long dumpkeys = 0, timeout_override = 0;
|
long dumpkeys = 0, timeout_override = 0;
|
||||||
@@ -1737,10 +1940,17 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* M6.25(方案A):选单显示前把背光按用户设置恢复(display.brightness,clamp [min||1,100])。
|
/* M6.25(方案A):选单显示前把背光按用户设置恢复(display.brightness,clamp [min||1,100])。
|
||||||
* settings 不可读/值非法 → 不动(退化为现状)。只在显示前写一次,不碰 FBIOBLANK/blank(§18.24)。 */
|
* settings 不可读/值非法 → 不动(退化为现状)。只在显示前写一次,不碰 FBIOBLANK/blank(§18.24)。
|
||||||
|
* M23:这句在 0.8.46/0.8.49 的**出货二进制里根本不存在**(源码改了但二进制没重编,见文件头),
|
||||||
|
* 所以选单一直亮在面板继承值上。现在补一行 bm_log:每次开机留下「选单亮度到底写了多少」。 */
|
||||||
{
|
{
|
||||||
int b = bootmenu_settings_brightness();
|
int b = bootmenu_settings_brightness(NULL);
|
||||||
if (b > 0) write_brightness(b);
|
if (b > 0) {
|
||||||
|
write_brightness(b);
|
||||||
|
bm_log("menu: brightness -> %d (src=settings.conf)", b);
|
||||||
|
} else {
|
||||||
|
bm_log("menu: brightness untouched, settings unreadable (panel=%d)", read_brightness());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fb_open() != 0) return 1;
|
if (fb_open() != 0) return 1;
|
||||||
@@ -1750,3 +1960,6 @@ int main(int argc, char **argv)
|
|||||||
run_menu(sim_sel, do_fork);
|
run_menu(sim_sel, do_fork);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* BM_NO_MAIN(宿主单测)到此为止 */
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
/* test_bm_fw.c — 宿主单测:meow_bootmenu 的亮度决策(M23 亮度单一权威源)。
|
||||||
|
*
|
||||||
|
* 覆盖真机 0.8.46 两个症状的判定矩阵:
|
||||||
|
* ① 开机菜单/选单亮度 = 面板继承值 60(用户设置 1)—— 选单路径 bootmenu_settings_brightness()
|
||||||
|
* ② HibyOS 亮/灭屏瞬间爆亮 —— supervisor 起始 saved、扫描窗保底、热键熄屏恢复三处决策
|
||||||
|
*
|
||||||
|
* 编译/运行(Termux 上 /tmp 不可写,**必须带 TMPDIR**,否则 mkstemp 两条用例会 FAIL):
|
||||||
|
* TMPDIR=$HOME/tmp/m23test mkdir -p "$HOME/tmp/m23test"
|
||||||
|
* gcc -Wall -Wextra -Os -I../meow_launcherd -Wno-unused-function \
|
||||||
|
* -Wno-unused-parameter -Wno-unused-variable \
|
||||||
|
* test_bm_fw.c ../meow_launcherd/meow_touch.c -o "$HOME/tmp/m23test/test_bm_fw"
|
||||||
|
* TMPDIR=$HOME/tmp/m23test "$HOME/tmp/m23test/test_bm_fw" # 全 PASS -> exit 0
|
||||||
|
* (TMPDIR 未设时本文件会自己退到可写的 /tmp 或 $HOME/tmp,但显式给最稳。)
|
||||||
|
* env MEOW_BACKLIGHT / MEOW_SETTINGS_FILE 把背光与 settings 指到临时文件(真机无这两个 env,
|
||||||
|
* 走 /sys 与 /usr/data 默认路径),因此这里的读写断言就是真机同一条代码路径。
|
||||||
|
*/
|
||||||
|
#define BM_NO_MAIN
|
||||||
|
#include "meow_bootmenu.c"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
static int g_pass = 0, g_fail = 0;
|
||||||
|
|
||||||
|
static void check(const char *name, int got, int want)
|
||||||
|
{
|
||||||
|
if (got == want) { g_pass++; printf("PASS %s\n", name); }
|
||||||
|
else { g_fail++; printf("FAIL %s: got %d want %d\n", name, got, want); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 临时目录根(返回可写目录;结果缓存在静态缓冲里):
|
||||||
|
* ① $TMPDIR(Termux/Android 上 /tmp 属别的用户、本进程不可写)
|
||||||
|
* ② /tmp(Linux 构建容器)
|
||||||
|
* ③ $HOME/tmp/m23test(两者都不可写时的兜底,自动 mkdir)
|
||||||
|
* 评审 r1 反馈:只靠 $TMPDIR 会让没设环境变量的复跑出现 2 条 mkstemp FAIL,故这里自愈。 */
|
||||||
|
static const char *tmp_root(void)
|
||||||
|
{
|
||||||
|
static char buf[512];
|
||||||
|
if (buf[0]) return buf;
|
||||||
|
const char *t = getenv("TMPDIR");
|
||||||
|
if (t && t[0]) { snprintf(buf, sizeof buf, "%s", t); return buf; }
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
snprintf(buf, sizeof buf, "%s", "/tmp");
|
||||||
|
char probe[560];
|
||||||
|
snprintf(probe, sizeof probe, "%s/.bm_probeXXXXXX", buf);
|
||||||
|
int fd = mkstemp(probe);
|
||||||
|
if (fd >= 0) { close(fd); unlink(probe); return buf; }
|
||||||
|
if (home && home[0]) {
|
||||||
|
snprintf(buf, sizeof buf, "%s/tmp/m23test", home);
|
||||||
|
mkdir(buf, 0700);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
return buf; /* 实在不行也返回 /tmp,让失败信息可读 */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put_file(const char *path, const char *body)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (!f) { g_fail++; printf("FAIL cannot write %s\n", path); return; }
|
||||||
|
fputs(body, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_file_int(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
int v = -1;
|
||||||
|
if (!f) return -2;
|
||||||
|
if (fscanf(f, "%d", &v) != 1) v = -1;
|
||||||
|
fclose(f);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- A) supervisor 起始 saved_brightness:settings 是唯一权威 ----
|
||||||
|
* 真机 bug:settings display.brightness=1,面板被播放器写成 60 → 旧代码 saved=60(>60→60 的
|
||||||
|
* 硬钉同宗),随后扫描窗保底/熄屏恢复把 60 写回 → 亮/灭屏爆亮。新语义必须取 1。 */
|
||||||
|
static void test_pick_saved(void)
|
||||||
|
{
|
||||||
|
const char *src = NULL;
|
||||||
|
int v;
|
||||||
|
|
||||||
|
v = bm_bri_pick_saved(1, 60, 1, 101, &src); /* ← 真机症状本体 */
|
||||||
|
check("saved: settings 1 wins over panel 60", v == 1 && !strcmp(src, "settings"), 1);
|
||||||
|
v = bm_bri_pick_saved(1, 0, 1, 101, &src); /* 播放器已把面板写成 0 */
|
||||||
|
check("saved: settings 1 when panel dark", v == 1 && !strcmp(src, "settings"), 1);
|
||||||
|
v = bm_bri_pick_saved(7, 7, 1, 101, &src);
|
||||||
|
check("saved: settings 7", v == 7 && !strcmp(src, "settings"), 1);
|
||||||
|
|
||||||
|
/* settings 不可读(降级路径):**不**原样采信面板瞬时值,封顶 SAFE_LOW=10(P2-1 修复)。
|
||||||
|
* 这是 M2.9 闪屏防护的正确形态:宁可用安全低亮,也不把「启动瞬间的面板值」当用户亮度回放。 */
|
||||||
|
v = bm_bri_pick_saved(-1, 60, 1, 101, &src);
|
||||||
|
check("saved: fallback caps panel 60 -> SAFE_LOW 10",
|
||||||
|
v == BM_BRI_SAFE_LOW && v == 10 && !strcmp(src, "panel-capped"), 1);
|
||||||
|
v = bm_bri_pick_saved(-1, 80, 1, 101, &src); /* 用户可能真设 80,但降级时不敢信面板 → 10 */
|
||||||
|
check("saved: fallback caps 80 -> SAFE_LOW (old bug shape)",
|
||||||
|
v == BM_BRI_SAFE_LOW && !strcmp(src, "panel-capped"), 1);
|
||||||
|
v = bm_bri_pick_saved(-1, 200, 1, 101, &src); /* 越界(播放器瞬时 max)同样封顶 */
|
||||||
|
check("saved: fallback caps 200 -> SAFE_LOW",
|
||||||
|
v == BM_BRI_SAFE_LOW && !strcmp(src, "panel-capped"), 1);
|
||||||
|
v = bm_bri_pick_saved(-1, 90, 1, 0, &src); /* max 读不到也不抬高 */
|
||||||
|
check("saved: fallback caps 90 -> SAFE_LOW (unknown max)",
|
||||||
|
v == BM_BRI_SAFE_LOW && !strcmp(src, "panel-capped"), 1);
|
||||||
|
/* 比 SAFE_LOW 更暗的真实值保留(用户刻意调很暗,不要替用户调亮) */
|
||||||
|
v = bm_bri_pick_saved(-1, 3, 1, 101, &src);
|
||||||
|
check("saved: fallback keeps darker 3", v == 3 && !strcmp(src, "panel-capped"), 1);
|
||||||
|
v = bm_bri_pick_saved(-1, 2, 6, 101, &src); /* 低于 brightness_min 时抬到 min(防全黑) */
|
||||||
|
check("saved: fallback clamps up to brightness_min 6", v == 6 && !strcmp(src, "panel-capped"), 1);
|
||||||
|
v = bm_bri_pick_saved(0, 3, 1, 101, &src); /* 非正值按住「未知」处理 */
|
||||||
|
check("saved: user<=0 treated unknown", v == 3 && !strcmp(src, "panel-capped"), 1);
|
||||||
|
|
||||||
|
/* settings 与面板都不可知且面板黑:安全低亮,绝不再写 60 */
|
||||||
|
v = bm_bri_pick_saved(-1, 0, 1, 101, &src);
|
||||||
|
check("saved: dark + no settings -> safe-low", v == BM_BRI_SAFE_LOW && v != 60 &&
|
||||||
|
v > 0 && !strcmp(src, "safe-low"), 1);
|
||||||
|
v = bm_bri_pick_saved(-1, -1, 1, 0, &src); /* 读失败 + max 读不到 */
|
||||||
|
check("saved: read-fail -> safe-low", v == BM_BRI_SAFE_LOW && !strcmp(src, "safe-low"), 1);
|
||||||
|
/* 与 M20 watchdog 同一安全下限(同一系统不该有两个安全值) */
|
||||||
|
check("saved: SAFE_LOW aligned with WD_BRI_SAFE_LOW (10)", BM_BRI_SAFE_LOW, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- A2) clamp 语义单测:取代旧的 `>60 → 60` 硬钉 ---- */
|
||||||
|
static void test_clamp(void)
|
||||||
|
{
|
||||||
|
check("clamp: in-range untouched", bm_bri_clamp(37, 1, 101), 37);
|
||||||
|
check("clamp: above max -> max", bm_bri_clamp(200, 1, 101), 101);
|
||||||
|
check("clamp: 80 stays 80 (was pinned 60)", bm_bri_clamp(80, 1, 101), 80);
|
||||||
|
check("clamp: below min -> min", bm_bri_clamp(0, 3, 101), 3);
|
||||||
|
check("clamp: mn<1 -> 1", bm_bri_clamp(0, 0, 101), 1);
|
||||||
|
check("clamp: unknown max only floors", bm_bri_clamp(90, 1, 0), 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- B) 扫描窗保底:只在「播放器忙 + 面板暗 + 面板未下电」时补,补的是用户值 ---- */
|
||||||
|
static void test_floor_decide(void)
|
||||||
|
{
|
||||||
|
const char *why = NULL;
|
||||||
|
int v;
|
||||||
|
|
||||||
|
/* 触发条件回到 pre-M23:面板暗就补(不依赖 /proc 的 D 态 = 评审 r1 P2-2 硬要求) */
|
||||||
|
v = bm_bri_floor_decide(1, 0, 0, 5000, 0, &why); /* settings=1,播放器待机写 0 */
|
||||||
|
check("floor: writes user value 1 (not 60)", v == 1 && !strcmp(why, "scan-floor"), 1);
|
||||||
|
v = bm_bri_floor_decide(80, 0, 0, 5000, 0, &why); /* saved=80 只可能来自 settings */
|
||||||
|
check("floor: saved 80 (from settings) floors at 80", v == 80 && !strcmp(why, "scan-floor"), 1);
|
||||||
|
v = bm_bri_floor_decide(0, 0, 0, 5000, 0, &why); /* saved 未知 -> 安全低亮 */
|
||||||
|
check("floor: unknown saved -> safe-low", v == BM_BRI_SAFE_LOW && !strcmp(why, "scan-floor"), 1);
|
||||||
|
|
||||||
|
/* Bug 4 防退化回归用例(评审 r1 P2-2):面板暗就必须能补,不能有任何「播放器看起来不忙」
|
||||||
|
* 的前提 —— 旧 busy 门会让这条变成 no-write(黑屏几分钟)。 */
|
||||||
|
v = bm_bri_floor_decide(7, 0, 0, 89999, 0, &why);
|
||||||
|
check("floor: Bug4 regression — dark+unlatched always floors", v == 7 &&
|
||||||
|
!strcmp(why, "scan-floor"), 1);
|
||||||
|
|
||||||
|
v = bm_bri_floor_decide(1, 5, 0, 5000, 0, &why); /* 面板已亮:不动 */
|
||||||
|
check("floor: visible -> no write", v == 0 && !strcmp(why, "visible"), 1);
|
||||||
|
v = bm_bri_floor_decide(1, 0, 1, 5000, 0, &why); /* bl_power>0(纯保险):不动 */
|
||||||
|
check("floor: panel powered off -> no write", v == 0 && !strcmp(why, "panel-off"), 1);
|
||||||
|
|
||||||
|
/* 评审 r2 P2-2 核心:电源键**锁存**。窗内按过电源键 → 本次窗口内此后一律不补,
|
||||||
|
* 不再「3s 到期就把用户刚熄的屏点亮」。 */
|
||||||
|
v = bm_bri_floor_decide(1, 0, 0, 5000, 1, &why);
|
||||||
|
check("floor: power key latched -> no write", v == 0 && !strcmp(why, "power-key-latched"), 1);
|
||||||
|
/* 锁存不会过期:窗口剩 1ms 也不补(旧实现 3s 后就会补) */
|
||||||
|
v = bm_bri_floor_decide(1, 0, 0, 1, 1, &why);
|
||||||
|
check("floor: latch never expires inside window", v == 0 &&
|
||||||
|
!strcmp(why, "power-key-latched"), 1);
|
||||||
|
/* 下一轮窗口(锁存已清零)恢复正常补亮 */
|
||||||
|
v = bm_bri_floor_decide(1, 0, 0, 90000, 0, &why);
|
||||||
|
check("floor: new window (latch cleared) floors again", v == 1 &&
|
||||||
|
!strcmp(why, "scan-floor"), 1);
|
||||||
|
|
||||||
|
v = bm_bri_floor_decide(1, 0, 0, 0, 0, &why); /* 90s 窗口结束:放手 */
|
||||||
|
check("floor: window over -> no write", v == 0 && !strcmp(why, "window-over"), 1);
|
||||||
|
v = bm_bri_floor_decide(1, 0, 0, -1, 0, &why);
|
||||||
|
check("floor: negative window -> no write", v == 0 && !strcmp(why, "window-over"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- C) 热键冻结前的熄屏恢复值:面板真实值 > settings saved > 安全低亮 ---- */
|
||||||
|
static void test_hotkey_saved(void)
|
||||||
|
{
|
||||||
|
const char *src = NULL;
|
||||||
|
int v;
|
||||||
|
|
||||||
|
v = bm_bri_hotkey_saved(1, 0, 1, 101, &src); /* 电源已熄屏:恢复用户值 1,不再回 60 */
|
||||||
|
check("hotkey: blank -> saved (settings 1)", v == 1 && !strcmp(src, "saved"), 1);
|
||||||
|
v = bm_bri_hotkey_saved(60, 0, 1, 101, &src); /* saved=60 只可能来自 settings(降级已封顶 10) */
|
||||||
|
check("hotkey: blank -> saved 60 (settings value)", v == 60 && !strcmp(src, "saved"), 1);
|
||||||
|
v = bm_bri_hotkey_saved(1, 77, 1, 101, &src); /* 面板亮着:跟随面板(HibyOS 里改过亮度) */
|
||||||
|
check("hotkey: panel-now wins", v == 77 && !strcmp(src, "panel-now"), 1);
|
||||||
|
v = bm_bri_hotkey_saved(1, 200, 1, 101, &src); /* 越界值 clamp 到 max_brightness */
|
||||||
|
check("hotkey: panel-now clamped to max", v == 101 && !strcmp(src, "panel-now"), 1);
|
||||||
|
v = bm_bri_hotkey_saved(1, 1, 6, 101, &src); /* 低于 brightness_min 抬到 min */
|
||||||
|
check("hotkey: panel-now raised to min 6", v == 6 && !strcmp(src, "panel-now"), 1);
|
||||||
|
v = bm_bri_hotkey_saved(0, 0, 1, 101, &src);
|
||||||
|
check("hotkey: nothing known -> safe-low", v == BM_BRI_SAFE_LOW && !strcmp(src, "safe-low"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- D) settings.conf 解析(与 watchdog 同源同 clamp)+ 打开重试 ---- */
|
||||||
|
static void test_settings_parse(void)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
snprintf(path, sizeof path, "%s/bmsetXXXXXX", tmp_root());
|
||||||
|
int fd = mkstemp(path);
|
||||||
|
if (fd < 0) { g_fail++; printf("FAIL settings: mkstemp\n"); return; }
|
||||||
|
close(fd);
|
||||||
|
setenv("MEOW_SETTINGS_FILE", path, 1);
|
||||||
|
|
||||||
|
put_file(path, "# meow-settings 1\ndisplay.brightness = 1\ndisplay.brightness_min = 1\n"
|
||||||
|
"audio.volume = 10\n");
|
||||||
|
check("settings: device file -> 1", bootmenu_settings_brightness(NULL), 1);
|
||||||
|
|
||||||
|
put_file(path, "display.brightness = 0\ndisplay.brightness_min = 3\n");
|
||||||
|
int mn = 0;
|
||||||
|
check("settings: clamp up to min", bootmenu_settings_brightness(&mn), 3);
|
||||||
|
check("settings: brightness_min parsed", mn, 3);
|
||||||
|
|
||||||
|
put_file(path, "display.brightness = 200\ndisplay.brightness_min = 1\n");
|
||||||
|
check("settings: clamp down to 100", bootmenu_settings_brightness(NULL), 100);
|
||||||
|
|
||||||
|
put_file(path, "audio.volume = 10\n");
|
||||||
|
/* 评审 r2 非阻塞 1(采纳):「文件里没有这个键」与「文件读不到」同属不可信 →
|
||||||
|
* 返回 -1,而不是 launcher 设置表的默认 101→100(那会让扫描窗待机补到 100)。 */
|
||||||
|
int mn2 = 0;
|
||||||
|
check("settings: missing key -> -1 (untrusted)", bootmenu_settings_brightness(&mn2), -1);
|
||||||
|
check("settings: missing key still reports min", mn2, 1);
|
||||||
|
|
||||||
|
unlink(path);
|
||||||
|
unsigned long long t0 = now_ms();
|
||||||
|
int r = bootmenu_settings_brightness(NULL);
|
||||||
|
unsigned long long dt = now_ms() - t0;
|
||||||
|
check("settings: missing -> -1", r, -1);
|
||||||
|
check("settings: open retried (>=15ms)", dt >= 15, 1);
|
||||||
|
|
||||||
|
unsetenv("MEOW_SETTINGS_FILE");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- E) 端到端:真的读写一个「背光文件」,复现两个真机场景 ----
|
||||||
|
* 1) settings=1、播放器把面板写成 60 → supervisor 起始 saved 必须是 1,扫描窗保底写 1(不是 60)
|
||||||
|
* 2) 用户按电源熄屏(播放器不忙)→ 保底决策返回 0,面板保持 0(不被抢亮 = 熄屏不爆亮) */
|
||||||
|
static void test_e2e_backlight(void)
|
||||||
|
{
|
||||||
|
char bl[600], st[600];
|
||||||
|
snprintf(bl, sizeof bl, "%s/bmblXXXXXX", tmp_root());
|
||||||
|
snprintf(st, sizeof st, "%s/bmstXXXXXX", tmp_root());
|
||||||
|
int fd = mkstemp(bl);
|
||||||
|
if (fd < 0) { g_fail++; printf("FAIL e2e: mkstemp bl\n"); return; }
|
||||||
|
close(fd);
|
||||||
|
fd = mkstemp(st);
|
||||||
|
if (fd < 0) { g_fail++; printf("FAIL e2e: mkstemp st\n"); unlink(bl); return; }
|
||||||
|
close(fd);
|
||||||
|
setenv("MEOW_BACKLIGHT", bl, 1);
|
||||||
|
setenv("MEOW_SETTINGS_FILE", st, 1);
|
||||||
|
|
||||||
|
put_file(st, "display.brightness = 1\ndisplay.brightness_min = 1\n");
|
||||||
|
put_file(bl, "60\n"); /* 播放器启动画面写的 60 */
|
||||||
|
|
||||||
|
int mn_e2e = 1;
|
||||||
|
int user = bootmenu_settings_brightness(&mn_e2e);
|
||||||
|
int panel = read_brightness();
|
||||||
|
const char *src = "?";
|
||||||
|
int saved = bm_bri_pick_saved(user, panel, mn_e2e, 101, &src);
|
||||||
|
check("e2e: panel file read 60", panel, 60);
|
||||||
|
check("e2e: saved from settings = 1", saved, 1);
|
||||||
|
|
||||||
|
/* 扫描窗:播放器待机把背光写 0 → 保底写用户值 1 */
|
||||||
|
put_file(bl, "0\n");
|
||||||
|
const char *why = NULL;
|
||||||
|
int floor = bm_bri_floor_decide(saved, read_brightness(), 0, 5000, 0, &why);
|
||||||
|
if (floor > 0) write_brightness(floor);
|
||||||
|
check("e2e: scan floor writes 1 (not 60)", get_file_int(bl), 1);
|
||||||
|
|
||||||
|
/* bl_power>0(纯保险分支)→ 保底不写 */
|
||||||
|
put_file(bl, "0\n");
|
||||||
|
floor = bm_bri_floor_decide(saved, read_brightness(), 1, 5000, 0, &why);
|
||||||
|
if (floor > 0) write_brightness(floor);
|
||||||
|
check("e2e: user screen-off (panel-off) stays dark", get_file_int(bl), 0);
|
||||||
|
|
||||||
|
/* 评审 r2 核心场景:用户按电源熄屏(bl_power 仍 0)→ 锁存 → 本次窗口内**永久**不补,
|
||||||
|
* 不会再出现「3s 后自己亮回来」 */
|
||||||
|
put_file(bl, "0\n");
|
||||||
|
floor = bm_bri_floor_decide(saved, read_brightness(), 0, 5000, 1, &why);
|
||||||
|
if (floor > 0) write_brightness(floor);
|
||||||
|
check("e2e: user screen-off (power key latched) stays dark", get_file_int(bl), 0);
|
||||||
|
check("e2e: latched why", !strcmp(why, "power-key-latched"), 1);
|
||||||
|
|
||||||
|
/* 热键确认框:面板黑着 → 按 saved(用户值 1)点亮,不是 60 */
|
||||||
|
int hk = bm_bri_hotkey_saved(saved, read_brightness(), mn_e2e, 101, &src);
|
||||||
|
write_brightness(hk);
|
||||||
|
check("e2e: confirm box lights at 1", get_file_int(bl), 1);
|
||||||
|
|
||||||
|
/* settings 被改:saved 不再跟旧值(每次开机重读 settings) */
|
||||||
|
put_file(st, "display.brightness = 5\ndisplay.brightness_min = 1\n");
|
||||||
|
src = "?";
|
||||||
|
saved = bm_bri_pick_saved(bootmenu_settings_brightness(NULL), read_brightness(), mn_e2e, 101, &src);
|
||||||
|
check("e2e: saved follows new settings 5", saved, 5);
|
||||||
|
|
||||||
|
/* 降级路径回归(评审 P2-1):settings 不可读 + 面板瞬时 60(播放器启动画面)
|
||||||
|
* → saved 只能是 SAFE_LOW=10,绝不能原样 60,否则扫描窗/确认框会把 60 写回去。 */
|
||||||
|
unlink(st);
|
||||||
|
put_file(bl, "60\n");
|
||||||
|
int mn_d = 1;
|
||||||
|
src = "?";
|
||||||
|
int dsaved = bm_bri_pick_saved(bootmenu_settings_brightness(&mn_d), read_brightness(),
|
||||||
|
mn_d, 101, &src);
|
||||||
|
check("e2e: settings unreadable -> saved SAFE_LOW (not 60)", dsaved, BM_BRI_SAFE_LOW);
|
||||||
|
check("e2e: degraded src=panel-capped", !strcmp(src, "panel-capped"), 1);
|
||||||
|
|
||||||
|
/* 评审 r2 非阻塞 1(采纳):文件存在但缺 display.brightness 键 → 视同不可信 →
|
||||||
|
* saved 同样封顶 SAFE_LOW,不会补到 launcher 缺省的 100(比老 bug 的 60 更亮)。 */
|
||||||
|
put_file(st, "display.brightness_min = 1\n");
|
||||||
|
put_file(bl, "60\n");
|
||||||
|
src = "?";
|
||||||
|
int msaved = bm_bri_pick_saved(bootmenu_settings_brightness(&mn_d), read_brightness(),
|
||||||
|
mn_d, 101, &src);
|
||||||
|
check("e2e: missing brightness key -> SAFE_LOW (not 100)", msaved, BM_BRI_SAFE_LOW);
|
||||||
|
check("e2e: missing key src=panel-capped", !strcmp(src, "panel-capped"), 1);
|
||||||
|
|
||||||
|
unlink(bl); unlink(st);
|
||||||
|
unsetenv("MEOW_BACKLIGHT");
|
||||||
|
unsetenv("MEOW_SETTINGS_FILE");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_pick_saved();
|
||||||
|
test_clamp();
|
||||||
|
test_floor_decide();
|
||||||
|
test_hotkey_saved();
|
||||||
|
test_settings_parse();
|
||||||
|
test_e2e_backlight();
|
||||||
|
printf("\n%d passed, %d failed\n", g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -22,7 +22,8 @@ patch_v2/files_app/
|
|||||||
│ ├── fsx.h # 纯业务接口(不依赖 meow_ui)
|
│ ├── fsx.h # 纯业务接口(不依赖 meow_ui)
|
||||||
│ └── fsx.c # 路径安全/格式化/枚举排序/虚拟化区间/写操作(可宿主测)
|
│ └── fsx.c # 路径安全/格式化/枚举排序/虚拟化区间/写操作(可宿主测)
|
||||||
└── test/
|
└── 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
|
cd patch_v2/files_app
|
||||||
gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
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
|
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`)、
|
覆盖:路径安全(`within_sd`/`join`/`sanitize`/`dirname`/`basename`)、展示格式化(`fmt_size`/`fmt_time`)、
|
||||||
目录枚举排序(`scan_dir`/`sort`)、虚拟化区间(`visible_range`/`max_scroll`)、写操作(`mkdir`/`rename`/
|
目录枚举排序(`scan_dir`/`sort`)、虚拟化区间(`visible_range`/`max_scroll`)、写操作(`mkdir`/`rename`/
|
||||||
`delete`,含卡外/越界拒绝)、非文本写拒绝。宿主全 PASS。
|
`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]);
|
- 触摸:行 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_ui_set_long_press_cb` 注册的回调带坐标交给本页;`browser_on_long_press`
|
||||||
用 `meow_listview_hit` 行命中打开 BottomSheet。App 层长按计时(`browser_tick`/`g_press_ms`)已删除,
|
用 `meow_listview_hit` 行命中打开 BottomSheet。App 层长按计时(`browser_tick`/`g_press_ms`)已删除,
|
||||||
其抬手 TAP 由 SDK 抑制(长按不再触发点击)。
|
其抬手 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 档会显示缺字豆腐。
|
2. **中文字形**:SDK 默认 `common` 字库(≈4k)不含 CJK 全量;exFAT 中文文件名/中文文本预览在 common 档会显示缺字豆腐。
|
||||||
预览页不声明 `fonts`(app.json 未加 `"fonts":"full"`/`"gbk"`),如需中文预览可在 app.json 声明大字号档。
|
预览页不声明 `fonts`(app.json 未加 `"fonts":"full"`/`"gbk"`),如需中文预览可在 app.json 声明大字号档。
|
||||||
3. **预览仅文本**:按扩展名白名单 + 首 512 字节 NUL 嗅探判文本;二进制/图片无预览(提示「not a text file」)。
|
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)。
|
- 编译:`-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):正常运行到事件循环,导出离屏帧,
|
- qemu-mipsel(无 /dev/fb0 → SDK 离屏 480×720):正常运行到事件循环,导出离屏帧,
|
||||||
确认 init/建页/topbar(返回+NEW)/空态(NO STORAGE CARD)渲染正确、SIGTERM 干净退出。
|
确认 init/建页/topbar(返回+NEW)/空态(NO STORAGE CARD)渲染正确、SIGTERM 干净退出。
|
||||||
- 实机目录列表渲染(有真实 TF 卡目录)需刷机/设备挂载验证——宿主模拟无法造出 `/usr/data/mnt/sd_0`。
|
- 实机目录列表渲染(有真实 TF 卡目录)需刷机/设备挂载验证——宿主模拟无法造出 `/usr/data/mnt/sd_0`。
|
||||||
Binary file not shown.
@@ -37,13 +37,38 @@ void picker_cancel(void); /* 取消:发 code=MEOW_ERR
|
|||||||
#define APP_W 480
|
#define APP_W 480
|
||||||
#define APP_H 720
|
#define APP_H 720
|
||||||
#define APP_TOPBAR_H 56
|
#define APP_TOPBAR_H 56
|
||||||
#define APP_ROW_H 56 /* 列表行高 */
|
#define APP_ROW_H 56 /* 列表行高下限(旧固定值;实际按字体行高派生,见 files_row_h) */
|
||||||
#define APP_PAD 16
|
#define APP_PAD 16
|
||||||
#define APP_ICON_W 26 /* 图标占宽 */
|
#define APP_ICON_W 26 /* 图标占宽 */
|
||||||
#define APP_OP_W 52 /* 右侧操作钮占宽(触达区) */
|
#define APP_OP_W 52 /* 右侧操作钮占宽(触达区) */
|
||||||
#define APP_FOOTER_H 26 /* 底部信息行高(列表/文本内容在其之上滚动) */
|
#define APP_FOOTER_H 26 /* 底部信息行高(列表/文本内容在其之上滚动) */
|
||||||
#define APP_READ_MAX (256 * 1024) /* 文本预览读取上限 */
|
#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 级复用矩形/几何 ---- */
|
/* ---- App 级复用矩形/几何 ---- */
|
||||||
typedef struct { int x, y, w, h; } app_rect_t;
|
typedef struct { int x, y, w, h; } app_rect_t;
|
||||||
#define AR(x,y,w,h) ((app_rect_t){ (x),(y),(w),(h) })
|
#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;
|
||||||
|
}
|
||||||
@@ -421,7 +421,7 @@ static void browser_page_create(meow_view *v, void *ctx)
|
|||||||
/* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */
|
/* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */
|
||||||
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
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);
|
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 自动重画) */
|
/* 空态/页脚注册为非控件绘制物(列表视口不含页脚;invalidate/paint_full 自动重画) */
|
||||||
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
|
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
|
||||||
browser_draw_empty, NULL);
|
browser_draw_empty, NULL);
|
||||||
|
|||||||
@@ -39,7 +39,27 @@ static int viewer_vp_h(void) { return r_content.h - APP_FOOTER_H; }
|
|||||||
/* 屏幕行高(px)。 */
|
/* 屏幕行高(px)。 */
|
||||||
static int viewer_rowh(const meow_theme *th) { return meow_ui_text_height(th->font_body) + 6; }
|
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)
|
static int viewer_wrap_line(int li, int intra, char *out, int outsz)
|
||||||
{
|
{
|
||||||
if (!g_buf || li < 0 || li >= g_nline) return 0;
|
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--;
|
while (len > 0 && (g_buf[start + len - 1] == '\n' || g_buf[start + len - 1] == '\r')) len--;
|
||||||
int colw = viewer_colw();
|
int colw = viewer_colw();
|
||||||
int col = 0, row = 0, outlen = 0;
|
int col = 0, row = 0, outlen = 0;
|
||||||
char ch[2]; ch[1] = '\0';
|
const unsigned char *s = (const unsigned char *)g_buf + start;
|
||||||
for (int i = 0; i < len; i++) {
|
for (int bi = 0; bi < len; ) {
|
||||||
ch[0] = g_buf[start + i];
|
int clen = viewer_cp_len(s, bi, len);
|
||||||
if (ch[0] == '\r') continue;
|
if (clen == 1 && s[bi] == '\r') { bi++; continue; }
|
||||||
int w = meow_ui_text_width(ch, 20);
|
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 (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;
|
col += w;
|
||||||
}
|
}
|
||||||
if (out) out[outlen] = '\0';
|
if (out) out[outlen] = '\0';
|
||||||
|
|||||||
Binary file not shown.
@@ -33,6 +33,31 @@ extern meow_sheet *g_sheet; /* BottomSheet(阻塞模态;main 创建
|
|||||||
typedef struct { int x, y, w, h; } rect_t;
|
typedef struct { int x, y, w, h; } rect_t;
|
||||||
#define RX(x,y,w,h) ((rect_t){ (x), (y), (w), (h) })
|
#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 绑定点)----
|
/* ---- 视图控制器(ctx = biz-UI 绑定点)----
|
||||||
* 本批控制器为薄壳:持 meow_view 回指针;页面细粒度视图态(g_ds/g_drag_* /scroll 等)留在各页
|
* 本批控制器为薄壳:持 meow_view 回指针;页面细粒度视图态(g_ds/g_drag_* /scroll 等)留在各页
|
||||||
* 模块作用域全局(设计定案 B:demo 视图态不迁 logic)。控制器未来可吸收这些状态。 */
|
* 模块作用域全局(设计定案 B:demo 视图态不迁 logic)。控制器未来可吸收这些状态。 */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -266,7 +266,7 @@ int sdk2_build_at(int yb_base, int ct, int scroll)
|
|||||||
g_lv = meow_listview_create(r_lv.x, r_lv.y, r_lv.w, r_lv.h, 0,
|
g_lv = meow_listview_create(r_lv.x, r_lv.y, r_lv.w, r_lv.h, 0,
|
||||||
NULL, NULL, MEOW_LIST_SEL_NONE);
|
NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||||
if (g_lv) {
|
if (g_lv) {
|
||||||
meow_listview_set_item_h(g_lv, 48);
|
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_vsource(g_lv, lv_count, lv_bind, NULL);
|
||||||
meow_listview_set_item_renderer(g_lv, 2, 1, 0); /* 双行、左图标、无右尾件 */
|
meow_listview_set_item_renderer(g_lv, 2, 1, 0); /* 双行、左图标、无右尾件 */
|
||||||
meow_listview_set_empty_text(g_lv, "All rows deleted");
|
meow_listview_set_empty_text(g_lv, "All rows deleted");
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ if [ "$1" = "am" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -I"$ROOT/patch_v2/meow_launcherd" -Os -Wall -Wextra -static meow_amsd.c -o meow_amsd
|
# meow_time32.c:独立 o32 time32 取时/睡眠(amsd 不链 meow_ui.a);同时修 now_ms 与
|
||||||
|
# usleep 退避。am CLI 无取时/睡眠调用,保持原样。
|
||||||
|
"$CC" -I"$ROOT/patch_v2/meow_launcherd" -Os -Wall -Wextra -static \
|
||||||
|
-DMEOW_TIME32_LIBC_OVERRIDES meow_amsd.c ../sdk/meow_time32.c -o meow_amsd
|
||||||
echo "== meow_amsd (mipsel static) =="
|
echo "== meow_amsd (mipsel static) =="
|
||||||
ls -la meow_amsd
|
ls -la meow_amsd
|
||||||
md5sum meow_amsd
|
md5sum meow_amsd
|
||||||
|
|||||||
Binary file not shown.
@@ -784,11 +784,35 @@ static void out_append(const char *pkg, pid_t pid, long epoch, long up, const ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- launcher IPC 转发(xchg 一帧,读回一帧) ---- */
|
/* ---- 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,
|
static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
||||||
uint8_t *rbuf, uint32_t rcap)
|
uint8_t *rbuf, uint32_t rcap)
|
||||||
{
|
{
|
||||||
|
g_ipc_err = IPC_ERR_NONE;
|
||||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
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 的阻塞
|
/* 收发加超时:launcher 刚刚重启/主循环未就绪(接受但不回包)时,amsd 的阻塞
|
||||||
* read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发
|
* read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发
|
||||||
* 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。
|
* 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。
|
||||||
@@ -802,7 +826,7 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
|||||||
memset(&a, 0, sizeof a);
|
memset(&a, 0, sizeof a);
|
||||||
a.sun_family = AF_UNIX;
|
a.sun_family = AF_UNIX;
|
||||||
strncpy(a.sun_path, g_ipc_sock, sizeof a.sun_path - 1);
|
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];
|
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||||
uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen;
|
uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen;
|
||||||
uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */
|
uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */
|
||||||
@@ -813,31 +837,35 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
|||||||
size_t off = 0;
|
size_t off = 0;
|
||||||
while (off < sizeof hdr) {
|
while (off < sizeof hdr) {
|
||||||
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
|
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;
|
off += (size_t)w;
|
||||||
}
|
}
|
||||||
if (len && payload) {
|
if (len && payload) {
|
||||||
size_t p = 0;
|
size_t p = 0;
|
||||||
while (p < len) {
|
while (p < len) {
|
||||||
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
|
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;
|
p += (size_t)w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
off = 0;
|
off = 0;
|
||||||
while (off < sizeof hdr) {
|
while (off < sizeof hdr) {
|
||||||
ssize_t r = read(fd, hdr + off, sizeof hdr - off);
|
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;
|
off += (size_t)r;
|
||||||
}
|
}
|
||||||
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2);
|
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2);
|
||||||
memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
|
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;
|
if (rlen > rcap) rlen = rcap;
|
||||||
off = 0;
|
off = 0;
|
||||||
while (off < rlen) {
|
while (off < rlen) {
|
||||||
ssize_t r = read(fd, rbuf + off, rlen - off);
|
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;
|
off += (size_t)r;
|
||||||
}
|
}
|
||||||
close(fd);
|
close(fd);
|
||||||
@@ -858,11 +886,12 @@ static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uin
|
|||||||
if (code) *code = a.code;
|
if (code) *code = a.code;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (rt == MSG_ERR) return MEOW_ERR_INTERNAL;
|
if (rt == MSG_ERR) { g_ipc_err = IPC_ERR_PROTO; return MEOW_ERR_INTERNAL; }
|
||||||
/* 一次连接失败/超时(launcher 未就绪)→ 短退避再试;仍失败则返回 -1。 */
|
/* 一次连接失败/超时(launcher 未就绪)→ 短退避再试;仍失败则返回 -1。 */
|
||||||
if (attempt + 1 < AMS_IPC_RETRY)
|
if (attempt + 1 < AMS_IPC_RETRY)
|
||||||
usleep((useconds_t)(AMS_IPC_BACKOFF_MS * (attempt + 1)) * 1000);
|
usleep((useconds_t)(AMS_IPC_BACKOFF_MS * (attempt + 1)) * 1000);
|
||||||
}
|
}
|
||||||
|
if (g_ipc_err == IPC_ERR_NONE) g_ipc_err = IPC_ERR_PROTO; /* 收到非 ACK/ERR 帧:不是「无回执」 */
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1228,9 +1257,14 @@ static int cmd_start(const char *spec, char *resp, int rn)
|
|||||||
/* launcher ACK ack.code 携带正幅度(busy=8,见 util.c `rc<0?-rc:MEOW_OK`),非二补。 */
|
/* launcher ACK ack.code 携带正幅度(busy=8,见 util.c `rc<0?-rc:MEOW_OK`),非二补。 */
|
||||||
snprintf(detail, sizeof detail, "busy (app running)");
|
snprintf(detail, sizeof detail, "busy (app running)");
|
||||||
emit_log("AMS start %s -> BUSY (app running)", pkg);
|
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 {
|
} else {
|
||||||
snprintf(detail, sizeof detail, "launcher offline");
|
/* rt!=0 只代表「这次没拿到回执」,不等于 launcher 死了(同 cmd_kill 注释)。
|
||||||
emit_log("AMS start %s -> launcher offline", pkg);
|
* 响应串保留 "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",
|
snprintf(resp, rn, "%s %s/%s\n", strstr(detail, "ok forwarded") ? "OK" : "ERR",
|
||||||
pkg, act[0] ? act : "(default)");
|
pkg, act[0] ? act : "(default)");
|
||||||
@@ -1251,14 +1285,21 @@ static int cmd_kill(const char *pkg, char *resp, int rn)
|
|||||||
uint32_t code = 0;
|
uint32_t code = 0;
|
||||||
int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code);
|
int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code);
|
||||||
if (rt != 0 && a->pid > 0) {
|
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);
|
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) {
|
} 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) {
|
} else if (code != MEOW_OK) {
|
||||||
emit_log("AMS kill %s -> launcher refused code=%u", pkg, code);
|
/* launcher 明确拒绝:不改判为直杀(pid 可能已被回收,盲杀会打错进程),如实回错。
|
||||||
return snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pkg, code);
|
* 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());
|
hist_push(a, "ams_kill", "terminate (via launcher)", (long)time(NULL), read_uptime_sec());
|
||||||
int m = snprintf(resp, rn, "OK terminate %s\n", pkg);
|
int m = snprintf(resp, rn, "OK terminate %s\n", pkg);
|
||||||
|
|||||||
+276
-96
@@ -1,18 +1,147 @@
|
|||||||
/* bt_dev.c — 蓝牙 A2DP 设备自识别(纯逻辑实现,见 bt_dev.h 契约)。
|
/* bt_dev.c — 蓝牙 A2DP 端点选择 + PCM 名构造 + 回落退避(纯逻辑实现,见 bt_dev.h 契约)。
|
||||||
*
|
*
|
||||||
* 纯 libc(string / stdlib),无任何 meow_* 调用、无 popen、无文件 I/O —— meow_audiod 负责
|
* 纯 libc(string / stdlib),无任何 meow_* 调用、无 popen、无文件 I/O —— meow_audiod 负责
|
||||||
* 读取 /usr/data/alsa.conf、执行 `bluealsa-cli list-pcms` 及写回。宿主单测零链路重。
|
* 执行 `bluealsa-cli list-pcms`、调用 libasound 及维护运行时状态。
|
||||||
*/
|
*/
|
||||||
#include "bt_dev.h"
|
#include "bt_dev.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <strings.h>
|
||||||
|
|
||||||
|
/* 是否 a2dpsrc / a2dp_source 端点行(bluealsa 4.1.1 list-pcms 的端点头)。
|
||||||
|
* ⚠️ 必须**行内**判定:直接对整串 strstr 会让「本行」借到「下一行」里的 a2dpsrc
|
||||||
|
* (真机 hfphf/source 行会被误判成 A2DP 源端点——M50 实测踩到)。 */
|
||||||
|
static int line_is_src(const char *ln)
|
||||||
|
{
|
||||||
|
const char *nl = strchr(ln, '\n');
|
||||||
|
size_t len = nl ? (size_t)(nl - ln) : strlen(ln);
|
||||||
|
char line[256];
|
||||||
|
if (len >= sizeof line) len = sizeof line - 1;
|
||||||
|
memcpy(line, ln, len);
|
||||||
|
line[len] = 0;
|
||||||
|
const char *p = line;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
return strstr(p, "a2dpsrc") != NULL || strstr(p, "a2dp_source") != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 从 `hcitool con` 输出收集**所有** ACL 连接的对端 BD_ADDR(按出现顺序去重)。
|
||||||
|
* 内核 ACL 是「设备是否真的连着」的权威(M45/M49 结论);bluealsa 的 list-pcms 只在
|
||||||
|
* profile 配置好后的**秒级窗口**里存在,不能当 MAC 的唯一来源(M50 真机实测)。
|
||||||
|
* ⚠️ 多条 ACL 是常态(手机/手柄/第二个耳机同时连着)——**不能只取第一条**:
|
||||||
|
* 取错设备会 snd_pcm_open 失败 → 有耳机却回落 DAC(M50 r1 评审 P2-3)。
|
||||||
|
* 行形如:`\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT`
|
||||||
|
* 返回找到的条数(0=无 ACL 连接)。macs 是 [max][18] 的二维数组,元素写成 NUL 结尾
|
||||||
|
* 17 字符大写冒号 MAC。 */
|
||||||
|
int bt_dev_acl_macs(const char *hcitool_con, char (*macs)[18], int max)
|
||||||
|
{
|
||||||
|
if (!hcitool_con || !macs || max <= 0) return 0;
|
||||||
|
for (int i = 0; i < max; i++) macs[i][0] = 0;
|
||||||
|
int n = 0;
|
||||||
|
const char *ln = hcitool_con;
|
||||||
|
while (ln && *ln && n < max) {
|
||||||
|
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;
|
||||||
|
if (strstr(line, "ACL")) {
|
||||||
|
for (const char *p = line; *p && n < max; p++) {
|
||||||
|
int ok = 1;
|
||||||
|
for (int i = 0; i < 17 && ok; i++) {
|
||||||
|
if ((i % 3) == 2) { if (p[i] != ':') ok = 0; }
|
||||||
|
else {
|
||||||
|
char c = p[i];
|
||||||
|
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) ok = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ok ||
|
||||||
|
!(p == line || !(p[-1] == ':' || (p[-1] >= '0' && p[-1] <= '9') ||
|
||||||
|
(p[-1] >= 'A' && p[-1] <= 'F') || (p[-1] >= 'a' && p[-1] <= 'f'))) ||
|
||||||
|
(p[17] == ':' || (p[17] >= '0' && p[17] <= '9') ||
|
||||||
|
(p[17] >= 'A' && p[17] <= 'F') || (p[17] >= 'a' && p[17] <= 'f')))
|
||||||
|
continue;
|
||||||
|
char mac[18];
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
char c = p[i];
|
||||||
|
mac[i] = (c >= 'a' && c <= 'f') ? (char)(c - 'a' + 'A') : c;
|
||||||
|
}
|
||||||
|
mac[17] = 0;
|
||||||
|
int dup = 0; /* 同一设备可能在多行/多 profile 出现 */
|
||||||
|
for (int i = 0; i < n; i++) if (strcmp(macs[i], mac) == 0) { dup = 1; break; }
|
||||||
|
if (!dup) memcpy(macs[n++], mac, 18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!nl) break;
|
||||||
|
ln = nl + 1;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 从 D-Bus 对象路径取 MAC。bluealsa 4.1.1 的 PCM 路径是
|
||||||
|
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink
|
||||||
|
* (dev_%.2X_%.2X_%.2X_%.2X_%.2X_%.2X,见 src/ba-transport.c / src/shared/dbus-client.c)。
|
||||||
|
* 返回 0=解析出 17 字符 MAC(大写、冒号分隔)。 */
|
||||||
|
static int parse_dev_seg(const char *line, char *mac, size_t macsz)
|
||||||
|
{
|
||||||
|
if (!line || !mac || macsz < 18) return -1;
|
||||||
|
const char *d = strstr(line, "dev_");
|
||||||
|
if (!d) return -1;
|
||||||
|
const char *p = d + 4;
|
||||||
|
char out[18];
|
||||||
|
size_t o = 0;
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
unsigned v = 0;
|
||||||
|
int digits = 0;
|
||||||
|
for (; digits < 2; digits++) {
|
||||||
|
char c = *p;
|
||||||
|
int h = -1;
|
||||||
|
if (c >= '0' && c <= '9') h = c - '0';
|
||||||
|
else if (c >= 'A' && c <= 'F') h = c - 'A' + 10;
|
||||||
|
else if (c >= 'a' && c <= 'f') h = c - 'a' + 10;
|
||||||
|
if (h < 0) break;
|
||||||
|
v = v * 16u + (unsigned)h;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (digits == 0) return -1;
|
||||||
|
o += (size_t)snprintf(out + o, sizeof out - o, (i == 0) ? "%02X" : ":%02X", v);
|
||||||
|
if (i < 5) {
|
||||||
|
if (*p != '_') return -1;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (o != 17) return -1;
|
||||||
|
memcpy(mac, out, 18);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 兜底:端点 token 的最后一段直接是 MAC(老式/简化输出 `a2dpsrc/AA:BB:...`)。 */
|
||||||
|
static int parse_tail_mac(const char *hit, char *mac, size_t macsz)
|
||||||
|
{
|
||||||
|
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) return -1;
|
||||||
|
const char *start = slash + 1;
|
||||||
|
size_t len = (size_t)(te - start);
|
||||||
|
if (len != 17 || len >= macsz) return -1;
|
||||||
|
int colons = 0;
|
||||||
|
for (size_t i = 0; i < len; i++) if (start[i] == ':') colons++;
|
||||||
|
if (colons != 5) return -1;
|
||||||
|
memcpy(mac, start, len);
|
||||||
|
mac[len] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
|
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
|
||||||
* 端点形如 `a2dpsrc/AA:BB:CC:DD:EE:FF`(MAC 在端点 token 内最后一个 '/' 之后到空白前)。
|
* 真实输出是 D-Bus 对象路径(每行一个):
|
||||||
|
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink
|
||||||
|
* MAC 取自 `dev_XX_XX_XX_XX_XX_XX` 段(下划线 → 冒号),另兼容简化形式 `a2dpsrc/AA:BB:...`。
|
||||||
* 逐个**有界行**匹配(strstr 必须在行内,避免跨换行误把后一行当本行端点——如行 1
|
* 逐个**有界行**匹配(strstr 必须在行内,避免跨换行误把后一行当本行端点——如行 1
|
||||||
* "profile/aptx" 与行 3 "a2dp_source/..." 的经典误判)。 */
|
* "hfphf/source" 与行 2 "a2dpsrc/sink" 的经典误判)。 */
|
||||||
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
||||||
{
|
{
|
||||||
if (!listpcms || !mac || macsz < 2) return -1;
|
if (!listpcms || !mac || macsz < 2) return -1;
|
||||||
@@ -27,22 +156,42 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
|||||||
memcpy(line, ln, linelen); line[linelen] = 0;
|
memcpy(line, ln, linelen); line[linelen] = 0;
|
||||||
const char *p = line;
|
const char *p = line;
|
||||||
while (*p == ' ' || *p == '\t') p++;
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
const char *hit = strstr(p, "a2dpsrc");
|
if (line_is_src(line)) {
|
||||||
if (!hit) hit = strstr(p, "a2dp_source");
|
if (parse_dev_seg(p, mac, macsz) == 0) return 0;
|
||||||
if (hit) {
|
const char *hit = strstr(p, "a2dpsrc");
|
||||||
const char *te = hit;
|
if (!hit) hit = strstr(p, "a2dp_source");
|
||||||
while (*te && *te != ' ' && *te != '\t' && *te != '\n') te++;
|
if (hit && parse_tail_mac(hit, mac, macsz) == 0) return 0;
|
||||||
const char *slash = NULL;
|
}
|
||||||
for (const char *q = hit; q < te; q++) if (*q == '/') slash = q;
|
if (!nl) break;
|
||||||
if (slash && (slash + 1) < te) {
|
ln = nl + 1;
|
||||||
const char *start = slash + 1;
|
}
|
||||||
size_t len = (size_t)(te - start);
|
mac[0] = 0;
|
||||||
if (len > 0 && len < macsz) {
|
return -1;
|
||||||
memcpy(mac, start, len);
|
}
|
||||||
mac[len] = 0;
|
|
||||||
return 0;
|
/* 在 list-pcms 里按「设备 MAC」精确挑端点路径(A→B 换耳机后不能拿 A 的路径去发 volume/info)。
|
||||||
}
|
* 返回 0=找到(写到 out),-1=列表里没有该 MAC 的端点。 */
|
||||||
}
|
int bt_dev_pick_endpoint(const char *listpcms, const char *mac, char *out, size_t outsz)
|
||||||
|
{
|
||||||
|
if (!listpcms || !mac || !mac[0] || !out || outsz < 2) return -1;
|
||||||
|
out[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], lmac[18];
|
||||||
|
if (linelen >= sizeof line) linelen = sizeof line - 1;
|
||||||
|
memcpy(line, ln, linelen); line[linelen] = 0;
|
||||||
|
if (line_is_src(line) && parse_dev_seg(line, lmac, sizeof lmac) == 0 &&
|
||||||
|
strcasecmp(lmac, mac) == 0) {
|
||||||
|
const char *p = line;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
size_t n = 0;
|
||||||
|
while (p[n] && p[n] != ' ' && p[n] != '\t' && p[n] != '\n' && p[n] != '\r' &&
|
||||||
|
n + 1 < outsz) { out[n] = p[n]; n++; }
|
||||||
|
out[n] = 0;
|
||||||
|
return n ? 0 : -1;
|
||||||
}
|
}
|
||||||
if (!nl) break;
|
if (!nl) break;
|
||||||
ln = nl + 1;
|
ln = nl + 1;
|
||||||
@@ -50,89 +199,120 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 在单行内把 anchor("DEV=" / "device")后的 MAC 值替换为 mac,保留引号风格。
|
/* 关闭决策(纯函数,宿主可单测):当前「设备」该关谁。
|
||||||
* 行未含 anchor → 返回 1(out=原样);含 anchor → 写回新行并返回 0。 */
|
* bt_dev=1 → 关 BT PCM(句柄在 audiod 的 g_bt_hdl),**不碰 fd**;
|
||||||
static int line_replace_mac(char *out, size_t outsz, const char *line,
|
* 否则 fd>2 才当设备 fd 关;**0/1/2 一律不动**——BT 打开成功时 dev_open() 返回 0,
|
||||||
const char *anchor, const char *mac)
|
* 任何「提前清 bt_dev」的路径都会落到 close(g_dev_fd) 上误关 stdin(M50 r1 评审 P2-1)。
|
||||||
|
* 返回 0=NONE / 1=BT / 2=FD。 */
|
||||||
|
int bt_dev_close_kind(int bt_dev, int fd)
|
||||||
{
|
{
|
||||||
const char *at = strstr(line, anchor);
|
if (bt_dev) return 1;
|
||||||
if (!at) { snprintf(out, outsz, "%s", line); return 1; }
|
return fd > 2 ? 2 : 0;
|
||||||
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。
|
/* 从 `bluealsa-cli info <path>` 输出取实际协商到的 codec("Selected codec: AAC" → "AAC")。
|
||||||
* 逐行处理;无 pcm.bluealsa / 无 DEV= / device token → -1;两端已一致 → 1;改了 → 0。 */
|
* 只认行首 "Selected codec:"("Available codecs: SBC AAC" 那行不能误匹配)。 */
|
||||||
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
|
int bt_dev_info_codec(const char *info_out, char *out, size_t outsz)
|
||||||
char **out, size_t *out_len)
|
|
||||||
{
|
{
|
||||||
if (!in || !mac || !mac[0] || !out || !out_len || in_len == 0) return -1;
|
if (!info_out || !out || outsz < 2) return -1;
|
||||||
if (!strstr(in, "pcm.bluealsa") ||
|
out[0] = 0;
|
||||||
(!strstr(in, "DEV=") && !strstr(in, "device"))) return -1;
|
const char *ln = info_out;
|
||||||
|
while (ln && *ln) {
|
||||||
size_t cap = in_len + 128;
|
const char *nl = strchr(ln, '\n');
|
||||||
char *res = malloc(cap ? cap : 1);
|
const char *p = ln;
|
||||||
if (!res) return -1;
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
size_t o = 0, changed = 0;
|
if (strncmp(p, "Selected codec:", 15) == 0) {
|
||||||
const char *p = in, *end = in + in_len;
|
p += 15;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
while (p < end) {
|
size_t n = 0;
|
||||||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
while (p[n] && p[n] != '\n' && p[n] != '\r' && n + 1 < outsz) { out[n] = p[n]; n++; }
|
||||||
const char *le = nl ? nl : end;
|
while (n > 0 && (out[n-1] == ' ' || out[n-1] == '\t')) n--;
|
||||||
size_t linelen = (size_t)(le - p);
|
out[n] = 0;
|
||||||
char line[512], newline[512];
|
return n ? 0 : -1;
|
||||||
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;
|
if (!nl) break;
|
||||||
p = nl + 1;
|
ln = nl + 1;
|
||||||
}
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
if (!changed) { free(res); return 1; }
|
/* bluealsa 4.1.1 src/shared/a2dp-codecs.c 的 canonical 名与别名(strcasecmp 匹配)。
|
||||||
res[o] = 0;
|
* 只列本机可达的音频 codec;auto/unchanged/未知 → unchanged(不干预协商)。 */
|
||||||
*out = res;
|
static const struct {
|
||||||
*out_len = o;
|
const char *canonical;
|
||||||
|
const char *aliases[4];
|
||||||
|
} codecs[] = {
|
||||||
|
{ "SBC", { "SBC", NULL } },
|
||||||
|
{ "MP3", { "MP3", "MPEG12", "MPEG", NULL } },
|
||||||
|
{ "AAC", { "AAC", "MPEG24", NULL } },
|
||||||
|
{ "ATRAC", { "ATRAC", NULL } },
|
||||||
|
{ "aptX", { "aptX", "apt-X", NULL } },
|
||||||
|
{ "aptX-AD", { "aptX-AD", "apt-X-AD", NULL } },
|
||||||
|
{ "aptX-HD", { "aptX-HD", "apt-X-HD", NULL } },
|
||||||
|
{ "aptX-LL", { "aptX-LL", "apt-X-LL", NULL } },
|
||||||
|
{ "aptX-TWS", { "aptX-TWS", "apt-X-TWS", NULL } },
|
||||||
|
{ "FastStream", { "FastStream", "FS", NULL } },
|
||||||
|
{ "LC3plus", { "LC3plus", NULL } },
|
||||||
|
{ "LDAC", { "LDAC", NULL } },
|
||||||
|
{ "LHDC", { "LHDC", NULL } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const char *bt_dev_codec_token(const char *setting, char *out, size_t outsz)
|
||||||
|
{
|
||||||
|
const char *auto_tok = "unchanged";
|
||||||
|
if (!out || outsz < 2) return auto_tok;
|
||||||
|
if (!setting || !setting[0]) { snprintf(out, outsz, "%s", auto_tok); return out; }
|
||||||
|
if (!strcasecmp(setting, "auto") || !strcasecmp(setting, "unchanged")) {
|
||||||
|
snprintf(out, outsz, "%s", auto_tok);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < sizeof codecs / sizeof codecs[0]; i++)
|
||||||
|
for (size_t n = 0; n < 4 && codecs[i].aliases[n]; n++)
|
||||||
|
if (!strcasecmp(codecs[i].aliases[n], setting)) {
|
||||||
|
snprintf(out, outsz, "%s", codecs[i].canonical);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
snprintf(out, outsz, "%s", auto_tok); /* 未知值:不干预协商(不把垃圾塞进 PCM 名) */
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MAC 合法性:6 组两位十六进制,'/' 或 ':' 分隔都不接受(只认冒号,bluealsa 约定)。 */
|
||||||
|
static int mac_valid(const char *mac)
|
||||||
|
{
|
||||||
|
if (!mac || strlen(mac) != 17) return 0;
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
if ((i % 3) == 2) { if (mac[i] != ':') return 0; }
|
||||||
|
else {
|
||||||
|
char c = mac[i];
|
||||||
|
int hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||||
|
if (!hex) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bt_dev_pcm_name(const char *mac, const char *codec_token, char *out, size_t outsz)
|
||||||
|
{
|
||||||
|
if (!out || outsz < 32 || !mac_valid(mac)) return -1;
|
||||||
|
const char *tok = (codec_token && codec_token[0]) ? codec_token : "unchanged";
|
||||||
|
int n = snprintf(out, outsz, "bluealsa:DEV=%s,PROFILE=a2dp,CODEC=%s", mac, tok);
|
||||||
|
if (n < 0 || (size_t)n >= outsz) { out[0] = 0; return -1; }
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsigned bt_dev_backoff_ms(unsigned fails)
|
||||||
|
{
|
||||||
|
if (fails == 0) return 0;
|
||||||
|
unsigned shift = fails - 1;
|
||||||
|
if (shift > 5) shift = 5;
|
||||||
|
unsigned long d = 2000UL << shift; /* 2s,4s,8s,16s,32s,64s */
|
||||||
|
if (d > 60000UL) d = 60000UL;
|
||||||
|
return (unsigned)d;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bt_dev_should_try(unsigned fails, long long since_ms)
|
||||||
|
{
|
||||||
|
if (fails == 0) return 1;
|
||||||
|
if (since_ms < 0) return 1;
|
||||||
|
return since_ms >= (long long)bt_dev_backoff_ms(fails) ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -1,27 +1,71 @@
|
|||||||
/* bt_dev.h — 蓝牙 A2DP 设备自识别(DEV 自动更新纯逻辑,宿主可单测)。
|
/* bt_dev.h — 蓝牙 A2DP 端点选择 + PCM 名构造 + 回落退避(纯逻辑,宿主可单测)。
|
||||||
*
|
*
|
||||||
* 背景:/usr/data/alsa.conf 的 `pcm.bluealsa`(plug→bluealsa)需把 DEV/device 钉在当前
|
* 背景(M50 真机定案):`/usr/data/alsa.conf` **不是** alsa 配置(是 HiBy 播放器的数据文件,
|
||||||
* 连接的 A2DP sink 设备 MAC 上才能正确出声。本模块提供两段纯逻辑(无 meow_* / popen / 文件
|
* libasound 从不加载),改它对实际解析零影响。真正生效的是 alsa-lib 自带的
|
||||||
* I/O —— meow_audiod 负责读文件与执行系统命令):
|
* `/etc/alsa/conf.d/20-bluealsa.conf`:
|
||||||
* - bt_dev_detect_mac:从 `bluealsa-cli list-pcms` 输出提取第一个 a2dpsrc 端点的设备 MAC;
|
*
|
||||||
* - bt_dev_rewrite:幂等改写 alsa.conf 文本,把 `pcm.bluealsa` 块内 `DEV=<mac>` 与
|
* pcm.bluealsa {
|
||||||
* `device <mac>` 两处换成目标 MAC(保留引号风格 / codec / 其余 token)。
|
* @args [ DEV PROFILE CODEC VOL SOFTVOL DELAY SRV ] ← 只有这 7 个名字
|
||||||
* 纯 libc(string / stdlib),独立编译,宿主零链接重。
|
* ... type plug / slave.pcm { type bluealsa ... }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 其 `@args` 均可从 PCM 名内联传入(`snd_pcm_open("bluealsa:DEV=<mac>,PROFILE=a2dp,
|
||||||
|
* CODEC=<token>")`),未声明的参数会被 alsa-lib 拒绝(parse_args: Unknown parameter)——
|
||||||
|
* 所以本模块只生成这 7 个里的 3 个,且**不改任何文件**。
|
||||||
|
*
|
||||||
|
* 纯 libc(string/stdlib),无 meow_* / popen / 文件 I/O —— meow_audiod 负责探测与打开。
|
||||||
*/
|
*/
|
||||||
#ifndef BT_DEV_H
|
#ifndef BT_DEV_H
|
||||||
#define BT_DEV_H
|
#define BT_DEV_H
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
|
/* 从 `hcitool con` 输出收集**所有** ACL 连接的对端 BD_ADDR(按出现顺序去重;MAC 主来源:
|
||||||
* 返回 0=找到并写入 mac(NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。macsz 建议 ≥18。 */
|
* 内核 ACL 是连接权威,list-pcms 只在 profile 配置后的秒级窗口内存在)。
|
||||||
|
* 多设备是常态,调用方要**逐个试开**而不是只取第一条(M50 r1 评审 P2-3)。
|
||||||
|
* macs 为 [max][18];返回条数(0=无 ACL)。 */
|
||||||
|
int bt_dev_acl_macs(const char *hcitool_con, char (*macs)[18], int max);
|
||||||
|
|
||||||
|
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC
|
||||||
|
* (**兜底来源**:list-pcms 可见时用)。
|
||||||
|
* 真实行为 D-Bus 对象路径(bluealsa 4.1.1):
|
||||||
|
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink
|
||||||
|
* 返回 0=找到并写入 mac(17 字符、大写冒号分隔,NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。
|
||||||
|
* macsz ≥ 18。 */
|
||||||
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz);
|
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 原样)。
|
/* 在 list-pcms 里按设备 MAC 精确挑端点路径(换耳机 A→B 后不能拿 A 的路径发 volume/info)。
|
||||||
* 返回 0=已改动(*out 与 *out_len 给出新文本,调用方负责 free);1=无需改动(两端已一致);
|
* 返回 0=找到,-1=该 MAC 无端点。 */
|
||||||
* -1=无 pcm.bluealsa / 无 DEV= 或 device token / 参数非法(不动)。 */
|
int bt_dev_pick_endpoint(const char *listpcms, const char *mac, char *out, size_t outsz);
|
||||||
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
|
|
||||||
char **out, size_t *out_len);
|
/* 关闭决策(纯函数):bt_dev=1 → 关 BT PCM;否则 fd>2 才关;0/1/2 一律不动(护 stdin)。
|
||||||
|
* 返回 0=NONE / 1=BT / 2=FD。 */
|
||||||
|
int bt_dev_close_kind(int bt_dev, int fd);
|
||||||
|
|
||||||
|
/* 从 `bluealsa-cli info` 输出取实际协商 codec:行首 "Selected codec: AAC" → "AAC"。
|
||||||
|
* 返回 0=找到,-1=无该行/参数非法。 */
|
||||||
|
int bt_dev_info_codec(const char *info_out, char *out, size_t outsz);
|
||||||
|
|
||||||
|
/* settings.conf `bt.codec` → bluealsa CODEC 参数 token。
|
||||||
|
* bluealsa 4.1.1 的 a2dp_codecs_codec_id_from_string() 用 strcasecmp 匹配 canonical 名与别名,
|
||||||
|
* 故大小写不敏感;本函数把它归一成 canonical 名(日志/STATE 用),未知值/auto/unchanged →
|
||||||
|
* "unchanged"(不改协商结果,等价 20-bluealsa.conf 的 defaults.bluealsa.codec)。
|
||||||
|
* 返回 out(调用方给的 buf)。outsz 建议 ≥16。 */
|
||||||
|
const char *bt_dev_codec_token(const char *setting, char *out, size_t outsz);
|
||||||
|
|
||||||
|
/* 组 snd_pcm_open 的 PCM 名:
|
||||||
|
* bluealsa:DEV=<mac>,PROFILE=a2dp,CODEC=<token>
|
||||||
|
* mac 非 6 组十六进制 → -1(不拼非法名字);token 为空 → 用 "unchanged"。
|
||||||
|
* 返回 0=写好(out NUL 结尾)。outsz 建议 ≥96。 */
|
||||||
|
int bt_dev_pcm_name(const char *mac, const char *codec_token, char *out, size_t outsz);
|
||||||
|
|
||||||
|
/* 回落退避:连续失败 fails 次后的重试间隔 ms(2s→4s→8s→16s→32s→60s 封顶;fails=0 → 0)。
|
||||||
|
* 防重试风暴(M10 教训):失败越多等越久,绝不每次 OPEN 都硬打一次 BT。 */
|
||||||
|
unsigned bt_dev_backoff_ms(unsigned fails);
|
||||||
|
|
||||||
|
/* 纯决策:此刻是否允许再尝试一次 BT。fails=0 → 允许;否则要求距上次失败 ≥ 退避间隔。
|
||||||
|
* since_ms<0(时钟异常)按「允许」处理,避免永久卡死。 */
|
||||||
|
int bt_dev_should_try(unsigned fails, long long since_ms);
|
||||||
|
|
||||||
#endif /* BT_DEV_H */
|
#endif /* BT_DEV_H */
|
||||||
@@ -17,18 +17,50 @@
|
|||||||
# ./build_audiod.sh # 交叉编译 → meow_audiod(mipsel glibc-dynamic,BT 直链 libasound)
|
# ./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 cli # 交叉编译 meow_audio CLI → meow_audio(mipsel musl-static)
|
||||||
# ./build_audiod.sh host # 宿主编译 *_host 并跑 test_audiod_host 回归(FILE sink)
|
# ./build_audiod.sh host # 宿主编译 *_host 并跑 test_audiod_host 回归(FILE sink)
|
||||||
|
#
|
||||||
|
# M50 蓝牙输出端到端:audiod 不再读写 /usr/data/alsa.conf(那是 HiBy 播放器的数据文件、语法非法、
|
||||||
|
# libasound 从不加载),改为 snd_pcm_open("bluealsa:DEV=<mac>,PROFILE=a2dp,CODEC=<token>")
|
||||||
|
# 内联参数(对应 /etc/alsa/conf.d/20-bluealsa.conf 的 pcm.bluealsa @args),失败必回落 DAC。
|
||||||
|
#
|
||||||
|
# 环境变量覆盖:ROOT / REPO 自动推导,ZIG / CC / LIBBT / ASOUND_SRC / CC_HOST 可显式指定。
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
ROOT="$HOME/Project/R3ProII"
|
# ---- 路径解析(M50:全部可被环境变量覆盖,不再硬绑某台机器的目录布局)----
|
||||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
# ROOT 只用于可选的历史路径(旧 .tools/zig-env、hiby_os_crack 解包树);缺省布局下不依赖它。
|
||||||
ZIG="$ROOT/.tools/zig-env/bin/zig"
|
ROOT="${ROOT:-$HOME/Project/R3ProII}"
|
||||||
INC="-I$ROOT/patch_v2/meow_launcherd"
|
REPO="$(cd "$(dirname "$0")/../.." && pwd)" # 仓库根(worktree 亦可)
|
||||||
|
# mipsel 静态 CC:显式 CC → 历史 zig 包装脚本 → PATH 里的 zig(裸 zig 要另带 target flags)。
|
||||||
|
# ⚠️ CC **只存单个命令词**:`"$CC"` 是按一个命令名展开的,把 "zig cc --target=..." 整串存进去
|
||||||
|
# 会变成「找不到名叫 zig cc --target=... 的命令」(评审 P2-1,M51 在 worktree 里必踩)。
|
||||||
|
# 多词 flags 走 CC_EXTRA(未加引号展开,故意按空白分词),与下面 ZIG 的写法保持一致。
|
||||||
|
CC="${CC:-}"
|
||||||
|
CC_EXTRA=""
|
||||||
|
if [ -z "$CC" ]; then
|
||||||
|
if [ -x "$ROOT/.tools/zig-mipsel-cc" ]; then
|
||||||
|
CC="$ROOT/.tools/zig-mipsel-cc" # 历史包装脚本(内部已带 --target)
|
||||||
|
elif command -v zig >/dev/null 2>&1; then
|
||||||
|
CC="$(command -v zig)"
|
||||||
|
CC_EXTRA="cc --target=mipsel-linux-musleabi"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# 交叉编译用的 zig:显式 ZIG → 历史 zig-env → PATH。
|
||||||
|
ZIG="${ZIG:-}"
|
||||||
|
if [ -z "$ZIG" ]; then
|
||||||
|
if [ -x "$ROOT/.tools/zig-env/bin/zig" ]; then ZIG="$ROOT/.tools/zig-env/bin/zig"; else ZIG="$(command -v zig || true)"; fi
|
||||||
|
fi
|
||||||
|
INC="-I$REPO/patch_v2/meow_launcherd" # 共享协议头(相对仓库根,不再绑 ROOT)
|
||||||
|
# 注:$ZIG 只有 audiod 目标用;guard 放在 audiod 段之前(见下),host/cli 不因缺 zig 而中断。
|
||||||
|
|
||||||
if [ "$1" = "host" ]; then
|
if [ "$1" = "host" ]; then
|
||||||
HOST_CC="${CC_HOST:-cc}"
|
HOST_CC="${CC_HOST:-cc}"
|
||||||
# BT 目标直链宿主 libasound.so.2(无 asoundlib.h,原型内联于 meow_audiod.c;-l: 精确到版本,
|
# BT 目标直链宿主 libasound(无 asoundlib.h,原型内联于 meow_audiod.c)。删 -ldl:BT 不再用 dlopen。
|
||||||
# 不依赖 unversioned dev 链接)。删 -ldl:BT 不再用 dlopen。
|
# 链接名先用「trivial 探针」决定:有 -l:libasound.so.2(精确版本)就用它,否则退 -lasound。
|
||||||
"$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread -l:libasound.so.2
|
# 用探针而**不是**「先编一次、失败再编一次 2>/dev/null」——后者会把成功路径的 warning 一起吞掉。
|
||||||
|
AS_LINK="-l:libasound.so.2"
|
||||||
|
if ! printf 'int main(void){return 0;}\n' | "$HOST_CC" -x c - -o /dev/null "$AS_LINK" 2>/dev/null; then
|
||||||
|
AS_LINK="-lasound"
|
||||||
|
fi
|
||||||
|
"$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread $AS_LINK
|
||||||
"$HOST_CC" $INC -Os -Wall -Wextra meow_audio.c -o meow_audio_host -lm
|
"$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" $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
|
"$HOST_CC" -Os -Wall -Wextra test_bt_dev.c bt_dev.c -o test_bt_dev -lm
|
||||||
@@ -43,7 +75,13 @@ if [ "$1" = "host" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$1" = "cli" ]; then
|
if [ "$1" = "cli" ]; then
|
||||||
"$CC" $INC -Os -Wall -Wextra -static meow_audio.c -o meow_audio -lm
|
[ -n "$CC" ] || {
|
||||||
|
echo "!! mipsel CC 未找到:设置 CC=/path/to/zig 或 CC=/path/to/zig-mipsel-cc(或把 zig 放进 PATH)" >&2
|
||||||
|
exit 1; }
|
||||||
|
# M43: meow_audio 是 musl-static(当前 zig 链),与其它系统组件同源受损 → 链 time32 修
|
||||||
|
# clock_gettime/usleep(monitor 的 100ms 窗口/轮询)。
|
||||||
|
"$CC" $CC_EXTRA $INC -Os -Wall -Wextra -static -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_audio.c ../sdk/meow_time32.c -o meow_audio -lm
|
||||||
echo "== meow_audio (mipsel static CLI) =="
|
echo "== meow_audio (mipsel static CLI) =="
|
||||||
ls -la meow_audio
|
ls -la meow_audio
|
||||||
md5sum meow_audio
|
md5sum meow_audio
|
||||||
@@ -52,15 +90,43 @@ if [ "$1" = "cli" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# meow_audiod:mipsel glibc-dynamic,BT 直链 libasound(-Llibbt -lasound)。-lpthread 供播放线程。
|
# meow_audiod:mipsel glibc-dynamic,BT 直链 libasound(-Llibbt -lasound)。-lpthread 供播放线程。
|
||||||
# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件镜像自拷贝(idempotent)。
|
# M43 例外:本目标走 glibc-2.22(time_t 32 位、timespec 8 字节)→ 无 musl time64 头/实现错位;
|
||||||
LIBBT="libbt"
|
# 当前钉在 fw_content.conf 的出货件则是旧工具链静态 musl,其 4403→ENOSYS→4263+宽度转换本身健康。
|
||||||
ASOUND_SRC="$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0"
|
# 两种形态都不需要 meow_time32.c(其 __clock_* 接管针对 zig musl 内部符号)。musl-static 的
|
||||||
|
# meow_audio CLI 已链 time32。
|
||||||
|
# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件树自拷贝(idempotent)。
|
||||||
|
# M50:LIBBT / ASOUND_SRC 可被环境变量覆盖;ASOUND_SRC 候选按「显式 → 历史解包树 → 仓内固件基线」
|
||||||
|
# 顺序取第一个存在的(旧值 $ROOT/hiby_os_crack/... 在多数 checkout 里不存在,set -e 下会直接卡住构建)。
|
||||||
|
# MAIN = 主 checkout 根(在 git worktree 里运行时,devdocs/ 与 fw_apps/ 这类未入库的固件树
|
||||||
|
# 只在主 checkout 里;git-common-dir 指向 <main>/.git,其父目录即主 checkout)。普通 checkout 下 == REPO。
|
||||||
|
MAIN=""
|
||||||
|
_gcd="$(git rev-parse --git-common-dir 2>/dev/null)" && MAIN="$(cd "$_gcd/.." 2>/dev/null && pwd)" || MAIN=""
|
||||||
|
[ -n "$ZIG" ] || {
|
||||||
|
echo "!! zig not found(audiod 目标需要它):设置 ZIG=/path/to/zig 或把 zig 放进 PATH" >&2
|
||||||
|
exit 1; }
|
||||||
|
LIBBT="${LIBBT:-libbt}"
|
||||||
|
if [ -z "$ASOUND_SRC" ]; then
|
||||||
|
tried=""
|
||||||
|
for c in "$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0" \
|
||||||
|
"$REPO/devdocs/fw-baseline/squashfs-root/usr/lib/libasound.so.2.0.0" \
|
||||||
|
"$REPO/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0" \
|
||||||
|
${MAIN:+"$MAIN/devdocs/fw-baseline/squashfs-root/usr/lib/libasound.so.2.0.0"} \
|
||||||
|
${MAIN:+"$MAIN/fw_apps/build/squashfs-root/usr/lib/libasound.so.2.0.0"}; do
|
||||||
|
[ -f "$c" ] && { ASOUND_SRC="$c"; break; }
|
||||||
|
tried="$tried
|
||||||
|
- $c"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[ -n "$ASOUND_SRC" ] && [ -f "$ASOUND_SRC" ] || {
|
||||||
|
echo "!! libasound.so.2.0.0 not found. 试过这些路径:$tried" >&2
|
||||||
|
echo " 覆盖方式:ASOUND_SRC=/path/to/libasound.so.2.0.0 $0" >&2
|
||||||
|
exit 1; }
|
||||||
mkdir -p "$LIBBT"
|
mkdir -p "$LIBBT"
|
||||||
if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi
|
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"
|
||||||
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2"
|
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 \
|
"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod \
|
||||||
-lpthread -Llibbt -lasound
|
-lpthread -L"$LIBBT" -lasound
|
||||||
echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) =="
|
echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) =="
|
||||||
ls -la meow_audiod
|
ls -la meow_audiod
|
||||||
md5sum meow_audiod
|
md5sum meow_audiod
|
||||||
|
|||||||
Binary file not shown.
+418
-183
@@ -8,9 +8,10 @@
|
|||||||
* 核心设计(用户定案,勿推翻):
|
* 核心设计(用户定案,勿推翻):
|
||||||
* - 独占 + 抢占(后开流赢,旧流收 MSG_AUDIO_PREEMPTED 并被置 draining,PCM 丢弃);
|
* - 独占 + 抢占(后开流赢,旧流收 MSG_AUDIO_PREEMPTED 并被置 draining,PCM 丢弃);
|
||||||
* 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。
|
* 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。
|
||||||
* - 单进程双线程(+路由线程):IO 线程(accept/读帧/解析/ring 写)、播放线程
|
* - 单进程四线程:IO 线程(accept/读帧/解析/ring 写)、播放线程
|
||||||
* (独占 ALSA 直开,从 ring 取数写 DAC,xrun PREPARE 恢复)、路由线程
|
* (独占 ALSA 直开,从 ring 取数写 DAC,xrun PREPARE 恢复)、路由线程
|
||||||
* (轮询 headset/balance 插孔 switch 状态 → amixer cset numid=9)。
|
* (轮询 headset/balance 插孔 switch 状态 → amixer cset numid=9)、
|
||||||
|
* 音量执行线程(MSG_AUDIO_VOLUME 异步落 amixer + 持久化,末值去抖,见事项三)。
|
||||||
* - 格式归一 = 协商:grant_rate/ch/fmt 回客户端应产出的格式(SDK 负责 mono→stereo
|
* - 格式归一 = 协商:grant_rate/ch/fmt 回客户端应产出的格式(SDK 负责 mono→stereo
|
||||||
* 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。
|
* 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。
|
||||||
* - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。
|
* - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include "meow_ipc.h"
|
#include "meow_ipc.h"
|
||||||
#include "bt_dev.h" /* P2:DEV 自识别(a2dpsrc 端点 → MAC → alsa.conf 幂等改写) */
|
#include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */
|
||||||
|
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
#include <sound/asound.h>
|
#include <sound/asound.h>
|
||||||
@@ -75,9 +76,12 @@
|
|||||||
#define SETTINGS_CONF "/usr/data/meow/settings.conf"
|
#define SETTINGS_CONF "/usr/data/meow/settings.conf"
|
||||||
#define DEFAULT_OUT_WAV "/tmp/meow_audiod.wav"
|
#define DEFAULT_OUT_WAV "/tmp/meow_audiod.wav"
|
||||||
|
|
||||||
/* ---- 蓝牙 A2DP source(P2 输出目标 OUT_BT) ---- */
|
/* ---- 蓝牙 A2DP source(P2 输出目标 OUT_BT)----
|
||||||
#define BT_PCM_DEFAULT "pcm.bluealsa" /* alsa.conf `pcm.bluealsa`(plug→bluealsa);aplay -D bluealsa 等价 */
|
* M50 真机定案:**不写任何配置文件**。设备真正生效的是 alsa-lib 自带的
|
||||||
#define BT_PCM_FALLBACK "bluealsa" /* 部分配置只定义别名节点,防打开失败再试一次 */
|
* /etc/alsa/conf.d/20-bluealsa.conf 里的 `pcm.bluealsa`(type plug → slave type bluealsa,
|
||||||
|
* @args = DEV PROFILE CODEC VOL SOFTVOL DELAY SRV)。DEV/CODEC 直接从 PCM 名内联传入,
|
||||||
|
* 一次 snd_pcm_open 即完成「钉设备 + 钉 codec」,无需 DEV 自识别改写文件。
|
||||||
|
* ⚠️ /usr/data/alsa.conf 是 HiBy 播放器的数据文件、语法非法、libasound 从不加载——不读也不写。 */
|
||||||
#define BT_CODEC_AUTO "auto"
|
#define BT_CODEC_AUTO "auto"
|
||||||
#define BT_VOL_FULL_PCM "/usr/bin/bluealsa-cli" /* 一键设 AVRCP 100%(best-effort,不存在则跳过) */
|
#define BT_VOL_FULL_PCM "/usr/bin/bluealsa-cli" /* 一键设 AVRCP 100%(best-effort,不存在则跳过) */
|
||||||
|
|
||||||
@@ -130,6 +134,7 @@ static void session_clean_locked(void);
|
|||||||
static void _audio_mark_reconfig_needed(void);
|
static void _audio_mark_reconfig_needed(void);
|
||||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static pthread_cond_t g_cond;
|
static pthread_cond_t g_cond;
|
||||||
|
static pthread_cond_t g_vol_cond; /* 音量异步执行线程等待(CLOCK_MONOTONIC,main 中初始化) */
|
||||||
|
|
||||||
struct client {
|
struct client {
|
||||||
int used;
|
int used;
|
||||||
@@ -137,10 +142,23 @@ struct client {
|
|||||||
char app_id[MEOW_APP_ID_MAX];
|
char app_id[MEOW_APP_ID_MAX];
|
||||||
uint32_t sid;
|
uint32_t sid;
|
||||||
int state; /* CLS_* */
|
int state; /* CLS_* */
|
||||||
|
uint32_t gen; /* 分配代际(音量异步 ACK 防 fd/槽复用竞态) */
|
||||||
|
int vol_ack; /* 音量请求已执行,待 IO 线程回 ACK(vol 线程写/IO 线程清) */
|
||||||
|
uint32_t vol_ack_req; /* 对应 req_id */
|
||||||
|
uint32_t vol_ack_status; /* ACK status(应用后 g_vol_pct) */
|
||||||
uint8_t rbuf[READ_BUF_MAX];
|
uint8_t rbuf[READ_BUF_MAX];
|
||||||
size_t rlen;
|
size_t rlen;
|
||||||
};
|
};
|
||||||
static struct client g_cli[CLI_MAX];
|
static struct client g_cli[CLI_MAX];
|
||||||
|
static uint32_t g_cli_gen = 0; /* 客户端分配代际计数(cli_alloc 递增) */
|
||||||
|
|
||||||
|
/* ---- 音量异步化(事项三):IO 线程只入队末值,专用 vol 线程落 amixer+持久化后
|
||||||
|
* 经 IO 线程回 ACK。驱动 wedge 时 3×popen≈4.5s 不再卡持锁的 dispatch/IO 循环。 ---- */
|
||||||
|
static int g_vol_pending = 0; /* 有待执行音量请求(g_mu 保护) */
|
||||||
|
static int g_vol_pending_pct = 0; /* 末值(0-100;连续请求只保留最新=去抖) */
|
||||||
|
static struct client *g_vol_pending_cli = NULL; /* 末次请求方(ACK 用;可能已回收,靠 gen 校验) */
|
||||||
|
static uint32_t g_vol_pending_req = 0; /* 末次请求 req_id */
|
||||||
|
static uint32_t g_vol_pending_gen = 0; /* 末次请求方 gen(防槽复用误 ACK) */
|
||||||
|
|
||||||
/* 当前授权流(喂养 DAC 的唯一流) */
|
/* 当前授权流(喂养 DAC 的唯一流) */
|
||||||
struct stream_info {
|
struct stream_info {
|
||||||
@@ -188,11 +206,27 @@ static int g_dev_dsd = 0; /* 当前打开设备为 DoP DSD(决
|
|||||||
static int g_reconfig_pending = 0;
|
static int g_reconfig_pending = 0;
|
||||||
|
|
||||||
/* ---- 蓝牙 A2DP source 运行时状态(g_mu 保护;仅 g_target==OUT_BT && g_sink==SINK_ALSA 时用) ----
|
/* ---- 蓝牙 A2DP source 运行时状态(g_mu 保护;仅 g_target==OUT_BT && g_sink==SINK_ALSA 时用) ----
|
||||||
* 直接调用 libasound 插件 PCM(pcm.bluealsa)。audiod 以动态 glibc 链接设备 libasound.so.2
|
* 直接调用 libasound 插件 PCM(见 bt_* 段注释),此处仅存 snd_pcm_t 不透明句柄。
|
||||||
* (见 bt_* 段注释),此处仅存 snd_pcm_t 不透明句柄。 */
|
*
|
||||||
|
* 失败状态机(M50 硬验收点:失败必回落 DAC,绝不整机静音):
|
||||||
|
* BT_ST_IDLE —— 尚未尝试(启动初值 / 刚切到 BT 未开流)
|
||||||
|
* BT_ST_OK —— snd_pcm_open 成功,音频走 A2DP
|
||||||
|
* BT_ST_FAIL —— 最近一次尝试失败(无端点/未连接/set_params 失败/写路径致命错),
|
||||||
|
* 音频已回落 DAC;重试按 bt_dev_backoff_ms() 指数退避(2s→60s),
|
||||||
|
* 且只在「开流/重配」边界触发,绝不在写循环里重试(防 M10 重试风暴)。 */
|
||||||
|
enum { BT_ST_IDLE = 0, BT_ST_OK = 1, BT_ST_FAIL = 2 };
|
||||||
static void *g_bt_hdl = NULL; /* snd_pcm_t *(libasound 不透明句柄) */
|
static void *g_bt_hdl = NULL; /* snd_pcm_t *(libasound 不透明句柄) */
|
||||||
static int g_bt_open = 0; /* BT PCM 已打开 */
|
static int g_bt_open = 0; /* BT PCM 已打开 */
|
||||||
static int g_bt_vol_full_done = 0; /* 已对 AVRCP 置 100%(幂等,一次即可) */
|
static int g_bt_vol_full_done = 0; /* 已对 AVRCP 置 100%(幂等,一次即可) */
|
||||||
|
static int g_bt_state = BT_ST_IDLE; /* BT_ST_*(见上) */
|
||||||
|
static unsigned g_bt_fail_n = 0; /* 连续失败次数(成功后归零) */
|
||||||
|
static long long g_bt_last_fail_ms = 0; /* 上次失败时刻(CLOCK_MONOTONIC ms) */
|
||||||
|
static char g_bt_mac[20] = ""; /* 已打开/最近探测到的 A2DP 端点 MAC */
|
||||||
|
static char g_bt_tok[16] = "unchanged"; /* 下发的 CODEC token(canonical) */
|
||||||
|
static char g_bt_used[16] = ""; /* bluealsa info 回报的实际协商 codec(AAC/SBC…) */
|
||||||
|
static char g_bt_pcm[128] = ""; /* 端点 D-Bus 路径(volume/info 用) */
|
||||||
|
static int g_bt_fail_req = 0; /* 写路径判死:请求播放线程下一拍重配(回落 DAC) */
|
||||||
|
static int g_bt_dev = 0; /* 当前「设备」是 BT PCM(dev_close 分流;g_mu 保护) */
|
||||||
|
|
||||||
/* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ----
|
/* ---- 数字域电平表(DAC 实时监控;CS43198 无模拟电平回读,播放线程看 PCM 样本) ----
|
||||||
* 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16),
|
* 播放线程对从 ring 取出的每块 PCM 做纯整数累加(归一到 16bit 域:S24/S32 >>16),
|
||||||
@@ -308,15 +342,16 @@ static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payloa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* cond 有界等待(g_cond 为 CLOCK_MONOTONIC;调用方已持 g_mu)。 */
|
/* cond 有界等待(g_cond 为 CLOCK_MONOTONIC;调用方已持 g_mu)。 */
|
||||||
static void cond_wait_tmo(int ms)
|
static void cond_wait_tmo_cv(pthread_cond_t *cv, int ms)
|
||||||
{
|
{
|
||||||
struct timespec ts;
|
struct timespec ts;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
ts.tv_sec += ms / 1000;
|
ts.tv_sec += ms / 1000;
|
||||||
ts.tv_nsec += (long)(ms % 1000) * 1000000L;
|
ts.tv_nsec += (long)(ms % 1000) * 1000000L;
|
||||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||||
pthread_cond_timedwait(&g_cond, &g_mu, &ts);
|
pthread_cond_timedwait(cv, &g_mu, &ts);
|
||||||
}
|
}
|
||||||
|
static void cond_wait_tmo(int ms) { cond_wait_tmo_cv(&g_cond, ms); }
|
||||||
|
|
||||||
/* ---- frame 大小/位深 ---- */
|
/* ---- frame 大小/位深 ---- */
|
||||||
static int fmt_bps(int fmt)
|
static int fmt_bps(int fmt)
|
||||||
@@ -362,10 +397,12 @@ static int target_grant_fmt(int native_fmt)
|
|||||||
return AUDIO_FMT_S16;
|
return AUDIO_FMT_S16;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。 */
|
/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。
|
||||||
|
* BT 回落 DAC(bt=fail)时实际由 DAC 控,故报 dac——面板据此显示「蓝牙(失败)」。 */
|
||||||
static const char *volctl_name(void)
|
static const char *volctl_name(void)
|
||||||
{
|
{
|
||||||
return (g_target == AUDIO_OUT_BT) ? "avrcp" : "dac";
|
if (g_target != AUDIO_OUT_BT) return "dac";
|
||||||
|
return (g_bt_state == BT_ST_FAIL) ? "dac" : "avrcp";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 整数开平方(uint64→uint32,逐位法;替代软浮点 sqrt) */
|
/* 整数开平方(uint64→uint32,逐位法;替代软浮点 sqrt) */
|
||||||
@@ -844,7 +881,8 @@ static int settings_get_output_target(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* P2 蓝牙:读 settings.conf `bt.codec`(auto/SBC/AAC/aptX/LDAC…)→ g_bt_codec。缺省 auto。
|
/* P2 蓝牙:读 settings.conf `bt.codec`(auto/SBC/AAC/aptX/LDAC…)→ g_bt_codec。缺省 auto。
|
||||||
* codec 经 /usr/data/alsa.conf 的 `pcm.bluealsa` codec 参数生效(audiod BT 打开前 best-effort sync)。 */
|
* 生效路径(M50):bt_dev_codec_token() 归一成 bluealsa canonical 名后,作为 CODEC 参数内联进
|
||||||
|
* PCM 名(bluealsa:DEV=…,PROFILE=a2dp,CODEC=…);auto/未知 → unchanged(不干预协商)。 */
|
||||||
static const char *settings_get_bt_codec(void)
|
static const char *settings_get_bt_codec(void)
|
||||||
{
|
{
|
||||||
char buf[24];
|
char buf[24];
|
||||||
@@ -907,137 +945,152 @@ int snd_pcm_drain(void *pcm);
|
|||||||
int snd_pcm_close(void *pcm);
|
int snd_pcm_close(void *pcm);
|
||||||
const char *snd_strerror(int errnum);
|
const char *snd_strerror(int errnum);
|
||||||
|
|
||||||
static void bt_close(void); /* 前向:bt_write 判死路径要用 */
|
static void bt_close(void); /* 前向:bt_write 判死路径要用 */
|
||||||
static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */
|
static void bt_vol_full(void); /* 前向:bt_open 成功时促成 AVRCP 100% */
|
||||||
|
static void bt_read_used_codec(void); /* 前向:bt_open 成功时读回实际协商 codec */
|
||||||
|
|
||||||
/* bt.codec 生效:bluealsa 的 codec 是 config-time 参数(/usr/data/alsa.conf `pcm.bluealsa` 块的
|
/* 单调时钟 ms(退避计时;CLOCK_MONOTONIC 不受系统时间跳变影响) */
|
||||||
* `codec "X"`)。audiod 在 BT 打开前做一次**幂等且外科式**的 token 替换:只把 bluealsa 块内的
|
static long long now_ms(void)
|
||||||
* `codec "..."` 改成 `codec "<g_bt_codec>"`(其余字符不动)。仅真机且文件可写时执行;
|
|
||||||
* 宿主/只读/找不到 codec token → 跳过(绝不破坏 alsa.conf——audiod 只调整既有 token,不新建结构)。 */
|
|
||||||
static void bt_apply_codec_conf(void)
|
|
||||||
{
|
{
|
||||||
if (!g_bt_codec[0] || !strcmp(g_bt_codec, BT_CODEC_AUTO) || !strcmp(g_bt_codec, "unchanged")) return;
|
struct timespec ts;
|
||||||
const char *path = "/usr/data/alsa.conf";
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
/* 先探测是否已有 codec token:无则不动(避免误建/无权写) */
|
return (long long)ts.tv_sec * 1000 + (long long)(ts.tv_nsec / 1000000);
|
||||||
{
|
|
||||||
FILE *pf = fopen(path, "r");
|
|
||||||
if (!pf) return;
|
|
||||||
int hit = 0; char l[160];
|
|
||||||
while (fgets(l, sizeof l, pf)) {
|
|
||||||
char *k = l; while (*k==' '||*k=='\t') k++;
|
|
||||||
if (strncmp(k, "codec", 5) == 0) { hit = 1; break; }
|
|
||||||
}
|
|
||||||
fclose(pf);
|
|
||||||
if (!hit) return;
|
|
||||||
}
|
|
||||||
FILE *f = fopen(path, "r");
|
|
||||||
if (!f) return;
|
|
||||||
char *data = NULL; size_t len = 0;
|
|
||||||
fseek(f, 0, SEEK_END); long sz = ftell(f);
|
|
||||||
if (sz <= 0 || sz > 64 * 1024) { fclose(f); return; }
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
data = malloc((size_t)sz + 1);
|
|
||||||
if (!data) { fclose(f); return; }
|
|
||||||
len = fread(data, 1, (size_t)sz, f); data[len] = 0;
|
|
||||||
fclose(f);
|
|
||||||
char *out = malloc(len + 64);
|
|
||||||
if (!out) { free(data); return; }
|
|
||||||
out[0] = 0; size_t o = 0; int changed = 0;
|
|
||||||
char *save = NULL;
|
|
||||||
char *ln = strtok_r(data, "\n", &save);
|
|
||||||
while (ln) {
|
|
||||||
char *k = ln; while (*k==' '||*k=='\t') k++;
|
|
||||||
int is_codec = (strncmp(k, "codec", 5) == 0);
|
|
||||||
if (is_codec && !changed) {
|
|
||||||
int n = snprintf(out + o, len + 64 - o, "codec \"%s\"\n", g_bt_codec);
|
|
||||||
if (n > 0) o += (size_t)n;
|
|
||||||
changed = 1;
|
|
||||||
} else {
|
|
||||||
int n = snprintf(out + o, len + 64 - o, "%s\n", ln);
|
|
||||||
if (n > 0) o += (size_t)n;
|
|
||||||
}
|
|
||||||
ln = strtok_r(NULL, "\n", &save);
|
|
||||||
}
|
|
||||||
free(data);
|
|
||||||
if (changed) {
|
|
||||||
int fd = open(path, O_WRONLY | O_TRUNC, 0644);
|
|
||||||
if (fd >= 0) { if (write(fd, out, o) < 0) { } close(fd); logmsg("bt: codec conf %s -> %s", path, g_bt_codec); }
|
|
||||||
else logmsg("bt: codec conf %s not writable, skip", path);
|
|
||||||
}
|
|
||||||
free(out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* P2 BT DEV 自识别:把 /usr/data/alsa.conf 的 `pcm.bluealsa` DEV/device 自动改为**当前连接的
|
/* 记录一次 BT 失败:状态置 FAIL(→ 回落 DAC)+ 失败计数递增(退避依据)+ 日志。
|
||||||
* A2DP sink 设备** MAC(bluealsa-cli list-pcms 的 a2dpsrc 端点)。幂等(设备未变不重写)、
|
* 只在「开流/重配」边界或写路径判死时调用;写循环内绝不重试(防 M10 重试风暴)。 */
|
||||||
* 无设备/失败跳过;只改 DEV/device 两处、保留 codec/其余 token。BT 打开前调用,实现
|
static void bt_note_fail(const char *why)
|
||||||
* 「连哪个设备就播哪个」。跟 bt_apply_codec_conf 同构(外科式 token 替换,绝不建结构)。 */
|
|
||||||
static void bt_apply_device_conf(void)
|
|
||||||
{
|
{
|
||||||
char mac[32] = "";
|
g_bt_state = BT_ST_FAIL;
|
||||||
|
g_bt_fail_n++;
|
||||||
|
g_bt_last_fail_ms = now_ms();
|
||||||
|
logmsg("bt: FAIL (%s) -> fallback DAC; fail#%u, next retry in %ums",
|
||||||
|
why ? why : "?", g_bt_fail_n, bt_dev_backoff_ms(g_bt_fail_n));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 成功:状态 OK、失败计数归零。 */
|
||||||
|
static void bt_note_ok(void)
|
||||||
|
{
|
||||||
|
g_bt_state = BT_ST_OK;
|
||||||
|
g_bt_fail_n = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收集「可以试着往它播」的设备 MAC 候选(按可信度排序,去重):
|
||||||
|
* ① list-pcms 里**已经存在端点**的设备 —— 那就是真的 A2DP 连接(最可信,窗口内命中时用它);
|
||||||
|
* ② `hcitool con` 的**全部** ACL 设备 —— 内核权威,覆盖「profile 还没配置好、端点还没出现」。
|
||||||
|
* ⚠️ 多条 ACL 是常态(手机/手柄/第二副耳机);只取第一条会挑错设备 → 耳机连着却回落 DAC
|
||||||
|
* (M50 r1 评审 P2-3)。候选交给 bt_try_open 逐个试开(上限 3 次尝试)。
|
||||||
|
* 返回候选数(0=当前没有可播的蓝牙设备)。 */
|
||||||
|
static int bt_collect_candidates(char (*cand)[18], int max)
|
||||||
|
{
|
||||||
|
if (!cand || max <= 0) return 0;
|
||||||
|
int n = 0;
|
||||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||||
if (!o) { logmsg("bt: dev conf skipped (no bluealsa-cli)"); return; }
|
if (o && o[0]) {
|
||||||
if (bt_dev_detect_mac(o, mac, sizeof mac) != 0) {
|
char m[18];
|
||||||
logmsg("bt: dev conf skipped (no a2dpsrc pcm)");
|
if (bt_dev_detect_mac(o, m, sizeof m) == 0) {
|
||||||
return;
|
memcpy(cand[n++], m, 18);
|
||||||
}
|
logmsg("bt: candidate#%d %s (source=list-pcms endpoint)", n, m);
|
||||||
logmsg("bt: current a2dp dev %s", mac);
|
|
||||||
const char *path = "/usr/data/alsa.conf";
|
|
||||||
FILE *f = fopen(path, "r");
|
|
||||||
if (!f) return;
|
|
||||||
fseek(f, 0, SEEK_END); long sz = ftell(f);
|
|
||||||
if (sz <= 0 || sz > 64 * 1024) { fclose(f); return; }
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
char *data = malloc((size_t)sz + 1);
|
|
||||||
if (!data) { fclose(f); return; }
|
|
||||||
size_t rl = fread(data, 1, (size_t)sz, f); data[rl] = 0;
|
|
||||||
fclose(f);
|
|
||||||
char *outdata = NULL; size_t outlen = 0;
|
|
||||||
int rc = bt_dev_rewrite(data, rl, mac, &outdata, &outlen);
|
|
||||||
free(data);
|
|
||||||
if (rc == 0) {
|
|
||||||
int fd = open(path, O_WRONLY | O_TRUNC, 0644);
|
|
||||||
if (fd >= 0) {
|
|
||||||
if (write(fd, outdata, outlen) < 0) { }
|
|
||||||
close(fd);
|
|
||||||
logmsg("bt: dev conf auto-updated %s -> %s", path, mac);
|
|
||||||
} else {
|
|
||||||
logmsg("bt: dev conf %s not writable, skip", path);
|
|
||||||
}
|
}
|
||||||
} else if (rc == 1) {
|
|
||||||
logmsg("bt: dev conf ok (already %s)", mac);
|
|
||||||
} else {
|
|
||||||
logmsg("bt: dev conf skip (no pcm.bluealsa/no DEV token)");
|
|
||||||
}
|
}
|
||||||
if (outdata) free(outdata);
|
o = popen_read_tmo(1200, "hcitool con 2>/dev/null");
|
||||||
|
if (o) {
|
||||||
|
char macs[4][18];
|
||||||
|
int k = bt_dev_acl_macs(o, macs, 4);
|
||||||
|
for (int i = 0; i < k && n < max; i++) {
|
||||||
|
int dup = 0;
|
||||||
|
for (int j = 0; j < n; j++) if (strcmp(cand[j], macs[i]) == 0) { dup = 1; break; }
|
||||||
|
if (dup) continue;
|
||||||
|
memcpy(cand[n++], macs[i], 18);
|
||||||
|
logmsg("bt: candidate#%d %s (source=hcitool con ACL)", n, macs[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BT 打开:libasound → snd_pcm_open(pcm.bluealsa/bluealsa, PLAYBACK, NONBLOCK) →
|
/* 用当前 g_bt_mac 精确刷新端点路径(PCM 已打开后 bluealsa 端点对象才稳定)。
|
||||||
* set_params(S16_LE, RW, ch, rate, soft_resample, 100ms)。返回 0=ok。失败置 g_bt_open=0。 */
|
* **必须按 MAC 匹配**:换耳机 A→B 后若沿用 A 的路径,volume/info 会打到旧设备上
|
||||||
static int bt_open(void)
|
* (M50 r1 评审 P2-2)。找不到本设备的端点就留空(宁可跳过 volume/info 也不串台)。 */
|
||||||
|
static void bt_refresh_endpoint(void)
|
||||||
|
{
|
||||||
|
g_bt_pcm[0] = 0;
|
||||||
|
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||||
|
if (!o) return;
|
||||||
|
char path[128];
|
||||||
|
if (bt_dev_pick_endpoint(o, g_bt_mac, path, sizeof path) == 0)
|
||||||
|
snprintf(g_bt_pcm, sizeof g_bt_pcm, "%s", path);
|
||||||
|
else
|
||||||
|
logmsg("bt: endpoint for %s not listed yet (volume/info skipped)", g_bt_mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按 PCM 名打开(libasound → NONBLOCK open + set_params S16/RW/ch/rate/soft_resample/100ms)。
|
||||||
|
* 返回 0=ok(g_bt_hdl/g_bt_open/g_dev_* 已置位),-1=失败(句柄已 close,未记账)。 */
|
||||||
|
static int bt_open_pcm(const char *name)
|
||||||
{
|
{
|
||||||
void *pcm = NULL;
|
void *pcm = NULL;
|
||||||
const char *names[2] = { BT_PCM_DEFAULT, BT_PCM_FALLBACK };
|
int r = snd_pcm_open(&pcm, name, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
||||||
int r = -1;
|
if (r != 0) {
|
||||||
for (int i = 0; i < 2; i++) {
|
logmsg("bt: snd_pcm_open %s: %s", name, snd_strerror(r));
|
||||||
r = snd_pcm_open(&pcm, names[i], SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
return -1;
|
||||||
if (r == 0) { logmsg("bt: opened %s", names[i]); break; }
|
|
||||||
logmsg("bt: snd_pcm_open %s: %s", names[i], snd_strerror(r));
|
|
||||||
pcm = NULL;
|
|
||||||
}
|
}
|
||||||
if (r != 0) return -1;
|
|
||||||
g_bt_hdl = pcm;
|
|
||||||
snd_pcm_nonblock(pcm, 1);
|
snd_pcm_nonblock(pcm, 1);
|
||||||
unsigned rate = g_st.rate ? g_st.rate : 44100;
|
unsigned rate = g_st.rate ? g_st.rate : 44100;
|
||||||
unsigned ch = g_st.ch ? g_st.ch : 2;
|
unsigned ch = g_st.ch ? g_st.ch : 2;
|
||||||
r = snd_pcm_set_params(pcm, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED,
|
r = snd_pcm_set_params(pcm, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||||
ch, rate, 1, 100000);
|
ch, rate, 1, 100000);
|
||||||
if (r < 0) {
|
if (r < 0) {
|
||||||
logmsg("bt: set_params %u/%u/S16: %s", rate, ch, snd_strerror(r));
|
logmsg("bt: set_params %u/%u/S16 (%s): %s", rate, ch, name, snd_strerror(r));
|
||||||
snd_pcm_close(pcm); g_bt_hdl = NULL; return -1;
|
snd_pcm_close(pcm);
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
g_bt_hdl = pcm;
|
||||||
g_bt_open = 1;
|
g_bt_open = 1;
|
||||||
logmsg("bt: PCM ready rate=%u ch=%u fmt=S16 codec=%s", rate, ch, g_bt_codec);
|
g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = AUDIO_FMT_S16;
|
||||||
bt_vol_full(); /* best-effort AVRCP 100%(一次性;失败仅日志) */
|
logmsg("bt: PCM ready name=%s rate=%u ch=%u fmt=S16 codec=%s", name, rate, ch, g_bt_tok);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 尝试把输出打开到 BT:退避门 → 端点探测(list-pcms)→ 内联 DEV/CODEC 打开 →
|
||||||
|
* 兜底裸 "bluealsa"(配置缺省 DEV 通配)→ 记账失败。
|
||||||
|
* 返回 0=BT 就绪(音频走 A2DP);-1=本次不可用(调用方负责回落 DAC)。
|
||||||
|
* ⚠️ 在 playback 线程持 g_mu 时执行(popen ≤1.5s + snd_pcm_open),所以只允许在开流/重配
|
||||||
|
* 边界调用;连续失败由退避门(2s→60s)挡住,不会形成重试风暴。 */
|
||||||
|
static int bt_try_open(void)
|
||||||
|
{
|
||||||
|
long long since = now_ms() - g_bt_last_fail_ms;
|
||||||
|
if (!bt_dev_should_try(g_bt_fail_n, since)) {
|
||||||
|
static unsigned s_skip = 0;
|
||||||
|
if ((s_skip++ % 16) == 0)
|
||||||
|
logmsg("bt: retry gated (fail#%u, need %ums, elapsed %lldms)",
|
||||||
|
g_bt_fail_n, bt_dev_backoff_ms(g_bt_fail_n), since);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char cand[3][18];
|
||||||
|
int ncand = bt_collect_candidates(cand, 3);
|
||||||
|
if (ncand <= 0) { bt_note_fail("no BT device (no a2dp endpoint, no ACL)"); return -1; }
|
||||||
|
bt_dev_codec_token(g_bt_codec, g_bt_tok, sizeof g_bt_tok);
|
||||||
|
/* 3 次尝试、每次间隔 300ms(ACL 刚建立时 BlueZ 可能还没配置完 A2DP profile,等一拍就通)。
|
||||||
|
* 多候选时轮流试:1 个候选→A,A,A(=r2 语义);2 个→A,B,A;3 个→A,B,C。
|
||||||
|
* 候选都来自「真的连着」的集合,每次尝试都有意义;总耗时上限 ≈600ms + open 时延。
|
||||||
|
* **不再用裸 "bluealsa" 兜底**:那会落到「最近连接」的设备上,多设备时等于挑错人。 */
|
||||||
|
const char *opened = NULL;
|
||||||
|
for (int i = 0; i < 3 && !opened; i++) {
|
||||||
|
const char *mac = cand[i % ncand];
|
||||||
|
if (i) { struct timespec ts = { 0, 300 * 1000000L }; nanosleep(&ts, NULL); }
|
||||||
|
char name[96];
|
||||||
|
if (bt_dev_pcm_name(mac, g_bt_tok, name, sizeof name) != 0) continue;
|
||||||
|
logmsg("bt: try#%d open %s (codec setting=%s -> token=%s)", i + 1, name, g_bt_codec, g_bt_tok);
|
||||||
|
if (bt_open_pcm(name) == 0) {
|
||||||
|
opened = name;
|
||||||
|
snprintf(g_bt_mac, sizeof g_bt_mac, "%s", mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!opened) { bt_note_fail("snd_pcm_open/set_params failed (3 tries)"); return -1; }
|
||||||
|
bt_note_ok();
|
||||||
|
bt_refresh_endpoint(); /* 端点对象此时稳定;按 MAC 精确取路径(换设备不串台) */
|
||||||
|
bt_read_used_codec();
|
||||||
|
g_bt_vol_full_done = 0; /* 新设备/PCM:允许再促成一次 AVRCP 满量程 */
|
||||||
|
bt_vol_full();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1068,13 +1121,18 @@ static void bt_write(uint8_t *buf, size_t n)
|
|||||||
if (pr == 0) { /* 无空间超时 → 停摆计数(镜像 DAC stall) */
|
if (pr == 0) { /* 无空间超时 → 停摆计数(镜像 DAC stall) */
|
||||||
g_stall++; stalls++;
|
g_stall++; stalls++;
|
||||||
if (stalls >= ALSA_STALL_MAX) {
|
if (stalls >= ALSA_STALL_MAX) {
|
||||||
logmsg("bt WEDGED: %d consecutive waits, close + drop until next OPEN", stalls);
|
logmsg("bt WEDGED: %d consecutive waits, close + fallback DAC", stalls);
|
||||||
bt_close(); g_bt_open = 0; g_dev_open = 0;
|
bt_close(); g_bt_open = 0; g_dev_open = 0;
|
||||||
|
g_bt_fail_req = 1; /* 播放线程下一拍重配 → 回落 DAC(不静音) */
|
||||||
g_played_frames += n / fs; return;
|
g_played_frames += n / fs; return;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (pr < 0 && errno != EINTR) { logmsg("bt wait err: %s", strerror(errno)); break; }
|
if (pr < 0 && errno != EINTR) {
|
||||||
|
logmsg("bt wait err: %s -> fallback DAC", strerror(errno));
|
||||||
|
bt_close(); g_bt_open = 0; g_dev_open = 0; g_bt_fail_req = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (err == EPIPE || err == ESTRPIPE) { /* xrun:recover 后继续 */
|
if (err == EPIPE || err == ESTRPIPE) { /* xrun:recover 后继续 */
|
||||||
@@ -1083,7 +1141,10 @@ static void bt_write(uint8_t *buf, size_t n)
|
|||||||
snd_pcm_recover(g_bt_hdl, err, 1);
|
snd_pcm_recover(g_bt_hdl, err, 1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
logmsg("bt writei err: %s", snd_strerror(err));
|
/* 端点消失/设备断开(ENODEV/ENXIO/EIO/ESHUTDOWN…):关 PCM 并请求回落 DAC。
|
||||||
|
* **不在这里重试**(写循环内重试 = M10 重试风暴);退避重试在下次开流/重配边界。 */
|
||||||
|
logmsg("bt writei err: %s -> fallback DAC", snd_strerror(err));
|
||||||
|
bt_close(); g_bt_open = 0; g_dev_open = 0; g_bt_fail_req = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (w == 0) break;
|
if (w == 0) break;
|
||||||
@@ -1103,44 +1164,78 @@ static void bt_close(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 音量隔离(PLAN §五):路由=BT 时**不下发 DAC 音量反转**(音量由 AVRCP/耳机侧控)。
|
/* 音量隔离(PLAN §五):路由=BT 时**不下发 DAC 音量反转**(音量由 AVRCP/耳机侧控)。
|
||||||
* 仅记录 DAC 音量(g_vol_pct 语义=本地 DAC 音量);BT 侧 best-effort 促成 AVRCP 100%
|
* BT 侧 best-effort 促成 AVRCP 最大音量(127 = bluealsa-cli volume 的满量程 [0,127];
|
||||||
* (避免"窃听音量误解"——耳机侧音量钉到最响,设备侧滑块即绝对控制)。 */
|
* 旧代码发 100 只到 79%,耳机偏轻的隐性原因之一),设备侧滑块即绝对控制。
|
||||||
|
* PCM-PATH 取 list-pcms 的整行(D-Bus 对象路径)——bluealsa-cli 要的就是它,不是 MAC。 */
|
||||||
static void bt_vol_full(void)
|
static void bt_vol_full(void)
|
||||||
{
|
{
|
||||||
if (g_bt_vol_full_done) return;
|
if (g_bt_vol_full_done) return;
|
||||||
g_bt_vol_full_done = 1;
|
g_bt_vol_full_done = 1;
|
||||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
/* 只用 bt_refresh_endpoint() 按 MAC 精确取到的路径;为空就**跳过**——
|
||||||
if (!o) { logmsg("bt: avrcp set-100 skipped (no bluealsa-cli)"); return; }
|
* 宁可这一次不顶音量,也不拿别的设备的端点发 volume(换耳机 A→B 串台,评审 P2-2)。 */
|
||||||
char pcm[128] = "";
|
char pcm[128];
|
||||||
const char *ln = o;
|
snprintf(pcm, sizeof pcm, "%s", g_bt_pcm);
|
||||||
while (ln && *ln) {
|
if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no endpoint path for %s)", g_bt_mac); return; }
|
||||||
const char *nl = strchr(ln, '\n');
|
|
||||||
const char *p = ln;
|
|
||||||
while (*p == ' ' || *p == '\t') p++;
|
|
||||||
if (strstr(p, "a2dpsrc") || strstr(p, "a2dp_source")) {
|
|
||||||
const char *tok = p;
|
|
||||||
while (*tok && *tok != ' ' && *tok != '\t') tok++;
|
|
||||||
size_t tlen = (size_t)(tok - p);
|
|
||||||
if (tlen && tlen < sizeof pcm) { memcpy(pcm, p, tlen); pcm[tlen] = 0; }
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!nl) break;
|
|
||||||
ln = nl + 1;
|
|
||||||
}
|
|
||||||
if (!pcm[0]) { logmsg("bt: avrcp set-100 skipped (no a2dpsrc pcm found)"); return; }
|
|
||||||
char cmd[256];
|
char cmd[256];
|
||||||
snprintf(cmd, sizeof cmd, "%s volume %s 100 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm);
|
snprintf(cmd, sizeof cmd, "%s volume %s 127 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm);
|
||||||
char *r = popen_read_tmo(1500, cmd);
|
char *r = popen_read_tmo(1500, cmd);
|
||||||
logmsg("bt: avrcp volume set 100%% (%s)%s", pcm, r ? " ok" : " (exec failed; AVRCP still controls)");
|
logmsg("bt: avrcp volume set max(127) (%s)%s", pcm, r ? " ok" : " (exec failed; AVRCP still controls)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 音量应用(含 BT 隔离):BT → 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、不持久化
|
/* 实际输出是否真的在蓝牙上(BT_ST_OK)。STATE/路由/音量都以「实际」而非「请求」为准判断,
|
||||||
* audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。 */
|
* 但 target= 仍报用户请求值(面板据此显示「输出=蓝牙(失败)」)。 */
|
||||||
static void vol_apply(int pct)
|
static int bt_effective_bt(void)
|
||||||
|
{
|
||||||
|
return g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA && g_bt_state == BT_ST_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 请求了 BT 但当前回落 DAC(STATE 的 bt=fail;此时 DAC 音量/路由语义生效)。 */
|
||||||
|
static int bt_fallback_active(void)
|
||||||
|
{
|
||||||
|
return g_target == AUDIO_OUT_BT && g_bt_state == BT_ST_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *bt_state_name(void)
|
||||||
|
{
|
||||||
|
switch (g_bt_state) {
|
||||||
|
case BT_ST_OK: return "ok";
|
||||||
|
case BT_ST_FAIL: return "fail";
|
||||||
|
default: return "idle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读回 bluealsa 实际协商到的 codec(`bluealsa-cli info <端点路径>` → "Selected codec:")。
|
||||||
|
* 为什么需要:耳机不一定支持 settings 里点名的 codec——真机 MOONDROP Pudding 只有 SBC/AAC,
|
||||||
|
* 我们请求 LDAC 时 bluealsa 接受参数但实际选中 AAC。STATE 报实际值(btacodec=)才不撒谎。
|
||||||
|
* 失败仅日志(best-effort,不影响播放)。 */
|
||||||
|
static void bt_read_used_codec(void)
|
||||||
|
{
|
||||||
|
g_bt_used[0] = 0;
|
||||||
|
if (!g_bt_pcm[0]) return;
|
||||||
|
char cmd[256];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s info %s 2>/dev/null", BT_VOL_FULL_PCM, g_bt_pcm);
|
||||||
|
char *o = popen_read_tmo(1500, cmd);
|
||||||
|
if (!o) return;
|
||||||
|
char codec[16];
|
||||||
|
if (bt_dev_info_codec(o, codec, sizeof codec) == 0) {
|
||||||
|
snprintf(g_bt_used, sizeof g_bt_used, "%s", codec);
|
||||||
|
if (strcasecmp(codec, g_bt_tok) != 0)
|
||||||
|
logmsg("bt: codec requested=%s but bluealsa selected=%s (device codec set)", g_bt_tok, codec);
|
||||||
|
else
|
||||||
|
logmsg("bt: codec %s confirmed by bluealsa", codec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 音量应用(含 BT 隔离):BT(真实走 A2DP)→ 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、
|
||||||
|
* 不持久化 audio.volume(那是 DAC 音量键);DAC 及「BT 回落 DAC」→ 原感知锥度反转 + 持久化
|
||||||
|
* (回落时声音实际从 DAC 出,音量必须由 DAC 控,否则面板拖动无感)。
|
||||||
|
* target 为调用方在持锁下快照的 g_target;本函数不持 g_mu 执行(g_vol_pct 为 plain int
|
||||||
|
* 松散写,同 route_thread 的 g_route_cur 语义,MIPS32 int 写天然原子)。 */
|
||||||
|
static void vol_apply(int pct, int target)
|
||||||
{
|
{
|
||||||
if (pct < 0) pct = 0;
|
if (pct < 0) pct = 0;
|
||||||
if (pct > 100) pct = 100;
|
if (pct > 100) pct = 100;
|
||||||
if (g_target == AUDIO_OUT_BT) {
|
if (target == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL) {
|
||||||
logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct);
|
logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1158,6 +1253,10 @@ static void target_set(int target)
|
|||||||
if (g_target == target) { logmsg("output: target already %d", target); return; }
|
if (g_target == target) { logmsg("output: target already %d", target); return; }
|
||||||
logmsg("output: target %d -> %d (%s)", g_target, target, target == AUDIO_OUT_BT ? "BT" : "DAC");
|
logmsg("output: target %d -> %d (%s)", g_target, target, target == AUDIO_OUT_BT ? "BT" : "DAC");
|
||||||
g_target = target;
|
g_target = target;
|
||||||
|
/* 用户显式切目标:清 BT 退避(切 BT 时立刻尝试一次;仍失败就按退避回落,不死缠蓝牙) */
|
||||||
|
g_bt_fail_n = 0;
|
||||||
|
g_bt_last_fail_ms = 0;
|
||||||
|
g_bt_state = BT_ST_IDLE;
|
||||||
if (g_st.active) g_reconfig_pending = 1;
|
if (g_st.active) g_reconfig_pending = 1;
|
||||||
pthread_cond_signal(&g_cond);
|
pthread_cond_signal(&g_cond);
|
||||||
}
|
}
|
||||||
@@ -1284,25 +1383,62 @@ static void wav_header(int fd, int rate, int ch, int bits, uint32_t data_len)
|
|||||||
/* ---- 播放线程设备管理(dev_open/close/write 皆在播放线程调用) ---- */
|
/* ---- 播放线程设备管理(dev_open/close/write 皆在播放线程调用) ---- */
|
||||||
static void dev_close(void); /* 前向:dev_write 的 wedge 判死路径要用 */
|
static void dev_close(void); /* 前向:dev_write 的 wedge 判死路径要用 */
|
||||||
|
|
||||||
static int dev_open(void)
|
/* DAC 回落前的端口武装(复制 route_thread 的 boot-arm 语义,但同步执行)。
|
||||||
|
* 为什么必须同步:hbc3000 FPGA 未烧录 + enable 时开 PCM 会在第二次 hbc3000_start 死锁整机
|
||||||
|
* (M2.91 事故)。BT→DAC 回落是**流内**切换,不能等路由线程 1s 节拍,否则 PCM 可能先开。 */
|
||||||
|
static void dac_fallback_arm(void)
|
||||||
{
|
{
|
||||||
/* 输出目标=BT 且走真机 ALSA:打开 libasound 的 pcm.bluealsa(不开 hw:0,0)。
|
int hb = read_switch("/sys/class/switch/headset/state");
|
||||||
* 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑,
|
int bal = read_switch("/sys/class/switch/balance/state");
|
||||||
* 真实 bluealsa 传输留给真机(见 bt_* 注释)。失败 → 降级丢帧(自感知兜底 §五)。 */
|
int port = (bal == 1) ? 3 : (hb == 1) ? 2 : 4;
|
||||||
if (g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA) {
|
int cur = amixer_get(9, 1);
|
||||||
bt_apply_device_conf(); /* P2:DEV 幂等自更新到当前连接 A2DP 设备(连哪台播哪台) */
|
if (cur == port) {
|
||||||
bt_apply_codec_conf();
|
g_route_cur = port;
|
||||||
if (bt_open() == 0) return 0; /* g_bt_open=1;返回 0=逻辑已开(真实句柄在 g_bt_hdl) */
|
logmsg("bt: fallback arm skipped (numid=9 already %d)", port);
|
||||||
logmsg("bt: open failed (degrade: drop PCM until next OPEN)");
|
return;
|
||||||
|
}
|
||||||
|
logmsg("bt: fallback arm numid=9 -> %d (was %d)", port, cur);
|
||||||
|
apply_route(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 把设备 fd 升到 ≥3:BT 打开成功时 dev_open() 返回 0,而 0/1/2 是 stdio——把设备 fd
|
||||||
|
* 钉在 0..2 上等于给「谁误清了 g_bt_dev」埋地雷(dev_close 的 close_kind 守卫会跳过 0..2)。
|
||||||
|
* open() 只在 stdio 被关掉时才可能返回 <3;这里 dup 一次让「设备 fd 恒 >2」成为不变式。
|
||||||
|
* dup 也失败(极端 EMFILE)时把原 fd 关掉并报失败——绝不留一个「在 stdio 区间又没人认领」
|
||||||
|
* 的设备 fd(那会变成打开的设备泄漏,比关一次 stdin 更糟)。 */
|
||||||
|
static int fd_high(int fd)
|
||||||
|
{
|
||||||
|
if (fd < 0 || fd > 2) return fd;
|
||||||
|
int nfd = fcntl(fd, F_DUPFD, 3);
|
||||||
|
if (nfd < 0) {
|
||||||
|
logmsg("fd %d in stdio range and dup failed: %s -> treat as open failure", fd, strerror(errno));
|
||||||
|
close(fd);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
close(fd);
|
||||||
|
return nfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int dev_open(void)
|
||||||
|
{
|
||||||
|
/* 输出目标=BT 且走真机 ALSA:打开 libasound 的 bluealsa PCM(内联 DEV/CODEC,不开 hw:0,0)。
|
||||||
|
* 宿主即 FILE/NULL sink 时不走此支——因此宿主可测「目标选择/音量隔离/格式归一」逻辑。
|
||||||
|
* **失败必回落 DAC**(M50 硬验收:没有 A2DP 端点时整机不许静音;同时区间由 bt_note_fail
|
||||||
|
* 记账退避,STATE 报 bt=fail 供面板显示「输出=蓝牙(失败)」)。 */
|
||||||
|
if (g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA) {
|
||||||
|
if (bt_try_open() == 0) { g_bt_dev = 1; return 0; } /* g_bt_open=1(真实句柄在 g_bt_hdl) */
|
||||||
|
logmsg("bt: open unavailable -> DAC fallback (audio continues)");
|
||||||
|
dac_fallback_arm();
|
||||||
|
/* 落到下面的 DAC 打开路径(grant fmt 已是 S16,DAC 支持) */
|
||||||
|
}
|
||||||
|
g_bt_dev = 0;
|
||||||
uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt;
|
uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt;
|
||||||
int bits = fmt_bps((int)fmt) * 8;
|
int bits = fmt_bps((int)fmt) * 8;
|
||||||
if (g_sink == SINK_ALSA) {
|
if (g_sink == SINK_ALSA) {
|
||||||
#ifdef AUDIOD_HAVE_ALSA
|
#ifdef AUDIOD_HAVE_ALSA
|
||||||
/* O_NONBLOCK:写路径走 poll(POLLOUT)+限时(dev_write),write 本身永不睡眠——
|
/* O_NONBLOCK:写路径走 poll(POLLOUT)+限时(dev_write),write 本身永不睡眠——
|
||||||
* 驱动 wedge 时绝不能把播放线程永久压进内核(真机长流卡死根因防线)。 */
|
* 驱动 wedge 时绝不能把播放线程永久压进内核(真机长流卡死根因防线)。 */
|
||||||
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
|
int fd = fd_high(open(DEV_PCM, O_RDWR | O_NONBLOCK));
|
||||||
if (fd < 0) { logmsg("dev open %s: %s", DEV_PCM, strerror(errno)); return -1; }
|
if (fd < 0) { logmsg("dev open %s: %s", DEV_PCM, strerror(errno)); return -1; }
|
||||||
if (alsa_hw_params(fd, rate, ch, alsa_fmt_to_snd((int)fmt)) != 0) {
|
if (alsa_hw_params(fd, rate, ch, alsa_fmt_to_snd((int)fmt)) != 0) {
|
||||||
logmsg("dev hw_params %u/%u/%s failed", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt));
|
logmsg("dev hw_params %u/%u/%s failed", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt));
|
||||||
@@ -1316,7 +1452,7 @@ static int dev_open(void)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
if (g_sink == SINK_FILE) {
|
if (g_sink == SINK_FILE) {
|
||||||
int fd = open(g_out_wav, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
int fd = fd_high(open(g_out_wav, O_WRONLY | O_CREAT | O_TRUNC, 0644));
|
||||||
if (fd < 0) { logmsg("dev open wav %s: %s", g_out_wav, strerror(errno)); return -1; }
|
if (fd < 0) { logmsg("dev open wav %s: %s", g_out_wav, strerror(errno)); return -1; }
|
||||||
wav_header(fd, (int)rate, (int)ch, bits, 0);
|
wav_header(fd, (int)rate, (int)ch, bits, 0);
|
||||||
return fd;
|
return fd;
|
||||||
@@ -1344,7 +1480,7 @@ static void dev_write(uint8_t *buf, size_t n)
|
|||||||
}
|
}
|
||||||
g_dev_bytes += n;
|
g_dev_bytes += n;
|
||||||
}
|
}
|
||||||
if (g_target == AUDIO_OUT_BT && g_sink == SINK_ALSA) { bt_write(buf, n); return; }
|
if (bt_effective_bt()) { bt_write(buf, n); return; }
|
||||||
if (g_sink == SINK_NULL) { g_played_frames += n / (size_t)frame_size(); return; }
|
if (g_sink == SINK_NULL) { g_played_frames += n / (size_t)frame_size(); return; }
|
||||||
if (g_sink == SINK_ALSA) {
|
if (g_sink == SINK_ALSA) {
|
||||||
#ifdef AUDIOD_HAVE_ALSA
|
#ifdef AUDIOD_HAVE_ALSA
|
||||||
@@ -1410,7 +1546,15 @@ static void dev_write(uint8_t *buf, size_t n)
|
|||||||
|
|
||||||
static void dev_close(void)
|
static void dev_close(void)
|
||||||
{
|
{
|
||||||
if (g_bt_open) { bt_close(); g_dev_fd = -1; g_dev_open = 0; return; }
|
/* 关设备的决策交给 bt_dev_close_kind()(纯函数,宿主可单测):
|
||||||
|
* - **BT 分支内自行清理**:认的是 g_bt_dev(不是 g_bt_open——写路径判死时 bt_write 已
|
||||||
|
* bt_close(),g_bt_open 已是 0),进来后把 g_bt_dev/g_dev_fd 一起复位;
|
||||||
|
* - ⚠️ **调用方不得提前清 g_bt_dev**:BT 打开成功时 dev_open() 返回 0,谁提前清 0,
|
||||||
|
* 这里就会落到下面 close(g_dev_fd) 分支上误关 stdin(M50 r1 评审 P2-1 就是这么复现的);
|
||||||
|
* - 非 BT 分支只关 fd>2 的真 fd(0/1/2 一律不动,第二道保险)。 */
|
||||||
|
int kind = bt_dev_close_kind(g_bt_dev, g_dev_fd);
|
||||||
|
if (kind == 1) { bt_close(); g_bt_dev = 0; g_dev_fd = -1; g_dev_open = 0; return; }
|
||||||
|
if (kind == 0) { g_dev_fd = -1; g_dev_open = 0; return; }
|
||||||
if (g_sink == SINK_FILE && g_dev_fd >= 0 && g_dev_fd != -2) {
|
if (g_sink == SINK_FILE && g_dev_fd >= 0 && g_dev_fd != -2) {
|
||||||
off_t end = lseek(g_dev_fd, 0, SEEK_END);
|
off_t end = lseek(g_dev_fd, 0, SEEK_END);
|
||||||
if (end > 44) {
|
if (end > 44) {
|
||||||
@@ -1466,7 +1610,8 @@ static void reconfigure_locked(void)
|
|||||||
g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = fmt;
|
g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = fmt;
|
||||||
if (g_dev_fd == -2) g_dev_fd = -1; /* NULL sink:无真实 fd */
|
if (g_dev_fd == -2) g_dev_fd = -1; /* NULL sink:无真实 fd */
|
||||||
g_dev_dsd = (do_dsd_seq && new_dsd);
|
g_dev_dsd = (do_dsd_seq && new_dsd);
|
||||||
if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原(含 DSD_VOLUME) */
|
if ((g_target == AUDIO_OUT_DAC || bt_fallback_active()) && g_sink == SINK_ALSA)
|
||||||
|
dac_vol_set(saved_vol); /* 复原(含 DSD_VOLUME);BT 回落 DAC 时同样要落 DAC 音量 */
|
||||||
logmsg("device open ok: %u/%u/%s sink=%d target=%s%s", (unsigned)rate, (unsigned)ch,
|
logmsg("device open ok: %u/%u/%s sink=%d target=%s%s", (unsigned)rate, (unsigned)ch,
|
||||||
fmt_name((int)fmt), g_sink, tgt, g_dev_dsd ? " dsd" : "");
|
fmt_name((int)fmt), g_sink, tgt, g_dev_dsd ? " dsd" : "");
|
||||||
} else {
|
} else {
|
||||||
@@ -1523,6 +1668,22 @@ static void *playback_thread(void *arg)
|
|||||||
pthread_mutex_unlock(&g_mu);
|
pthread_mutex_unlock(&g_mu);
|
||||||
if (n == 0) continue;
|
if (n == 0) continue;
|
||||||
dev_write(buf, n);
|
dev_write(buf, n);
|
||||||
|
/* BT 写路径判死(端点消失/耳机断电/wedge)→ 下一拍重配回落 DAC。
|
||||||
|
* 记账/状态在持锁下做;退避门保证这次重配不会立刻再打一次 BT(不形成重试风暴)。 */
|
||||||
|
if (g_bt_fail_req) {
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
if (g_bt_fail_req) {
|
||||||
|
g_bt_fail_req = 0;
|
||||||
|
/* **不要**在这里清 g_bt_dev:dev_close() 靠它认出「当前设备是 BT PCM」并走
|
||||||
|
* BT 分支清理;提前清 0 会让它落到 close(g_dev_fd),而 BT 打开时 g_dev_fd==0
|
||||||
|
* →误关 stdin(M50 r1 评审 P2-1)。g_dev_fd 同时置 -1 做第二道保险。 */
|
||||||
|
g_dev_fd = -1;
|
||||||
|
bt_note_fail("write path fatal (endpoint gone?)");
|
||||||
|
g_reconfig_pending = 1;
|
||||||
|
pthread_cond_signal(&g_cond);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pthread_mutex_lock(&g_mu);
|
pthread_mutex_lock(&g_mu);
|
||||||
if (g_dev_open) dev_close();
|
if (g_dev_open) dev_close();
|
||||||
@@ -1551,6 +1712,7 @@ static struct client *cli_alloc(int fd)
|
|||||||
freec->fd = fd;
|
freec->fd = fd;
|
||||||
freec->state = CLS_OPEN;
|
freec->state = CLS_OPEN;
|
||||||
freec->sid = 0;
|
freec->sid = 0;
|
||||||
|
freec->gen = ++g_cli_gen; /* 代际递增:音量异步 ACK 校验用,防槽复用误发 */
|
||||||
return freec;
|
return freec;
|
||||||
}
|
}
|
||||||
static void cli_free(struct client *c)
|
static void cli_free(struct client *c)
|
||||||
@@ -1687,9 +1849,9 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
|||||||
a.grant_fmt = g_st.fmt;
|
a.grant_fmt = g_st.fmt;
|
||||||
a.err = MEOW_OK;
|
a.err = MEOW_OK;
|
||||||
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
|
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
|
||||||
logmsg("OPEN grant: sid=%u app=%s rate=%u ch=%u fmt=%s target=%s codec=%s",
|
logmsg("OPEN grant: sid=%u app=%s rate=%u ch=%u fmt=%s target=%s bt=%s codec=%s",
|
||||||
(unsigned)sid, g_st.app_id, (unsigned)o.rate, (unsigned)g_st.ch,
|
(unsigned)sid, g_st.app_id, (unsigned)o.rate, (unsigned)g_st.ch,
|
||||||
fmt_name((int)g_st.fmt), target_name(), g_bt_codec);
|
fmt_name((int)g_st.fmt), target_name(), bt_state_name(), g_bt_codec);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MSG_AUDIO_WRITE: {
|
case MSG_AUDIO_WRITE: {
|
||||||
@@ -1730,15 +1892,19 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
|||||||
}
|
}
|
||||||
case MSG_AUDIO_QUERY: {
|
case MSG_AUDIO_QUERY: {
|
||||||
char txt[1024];
|
char txt[1024];
|
||||||
|
char bttok[16];
|
||||||
/* 路由口用缓存值 g_route_cur(路由线程维护),不在 IO 热路径 popen amixer——
|
/* 路由口用缓存值 g_route_cur(路由线程维护),不在 IO 热路径 popen amixer——
|
||||||
* 驱动 wedge 时 amixer 虽有界但仍会拖慢 IO 循环 1.5s/次。 */
|
* 驱动 wedge 时 amixer 虽有界但仍会拖慢 IO 循环 1.5s/次。 */
|
||||||
int rb = g_route_cur;
|
int rb = g_route_cur;
|
||||||
int vol = g_vol_pct;
|
int vol = g_vol_pct;
|
||||||
|
/* btcodec= 报「当前设置归一后的 token」(不是上次打开用的),便于面板/CLI 直接核对
|
||||||
|
* settings bt.codec 到底会被下发放什么;纯函数,无 I/O。 */
|
||||||
|
bt_dev_codec_token(g_bt_codec, bttok, sizeof bttok);
|
||||||
snprintf(txt, sizeof txt,
|
snprintf(txt, sizeof txt,
|
||||||
"active=%d app=%s sid=%u rate=%u ch=%u fmt=%s dsd=%d "
|
"active=%d app=%s sid=%u rate=%u ch=%u fmt=%s dsd=%d "
|
||||||
"written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d "
|
"written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d "
|
||||||
"route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u "
|
"route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u "
|
||||||
"target=%s codec=%s\n",
|
"target=%s bt=%s btdev=%s btcodec=%s btacodec=%s codec=%s\n",
|
||||||
g_st.active ? 1 : 0,
|
g_st.active ? 1 : 0,
|
||||||
g_st.active ? g_st.app_id : "-",
|
g_st.active ? g_st.app_id : "-",
|
||||||
(unsigned)g_st.sid, (unsigned)g_st.rate, (unsigned)g_st.ch,
|
(unsigned)g_st.sid, (unsigned)g_st.rate, (unsigned)g_st.ch,
|
||||||
@@ -1753,7 +1919,8 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
|||||||
(g_route_manual != -1 ? g_route_manual : rb),
|
(g_route_manual != -1 ? g_route_manual : rb),
|
||||||
vol, volctl_name(),
|
vol, volctl_name(),
|
||||||
g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq,
|
g_lvl_rms_db10, g_lvl_peak_db10, (unsigned)g_lvl_seq,
|
||||||
target_name(), g_bt_codec);
|
target_name(), bt_state_name(), g_bt_mac[0] ? g_bt_mac : "-",
|
||||||
|
bttok, g_bt_used[0] ? g_bt_used : "-", g_bt_codec);
|
||||||
send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1);
|
send_frame(c->fd, MSG_AUDIO_STATE, req_id, txt, (uint32_t)strlen(txt) + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1771,13 +1938,20 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
|||||||
int32_t pct = -1;
|
int32_t pct = -1;
|
||||||
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
|
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
|
||||||
if (pct >= 0) {
|
if (pct >= 0) {
|
||||||
/* 音量应用含 BT 隔离:target=BT 时绝不下发 DAC 反转(vol_apply 内判断) */
|
/* 异步化(事项三):只入队末值,**不在持 g_mu 下 popen amixer**——驱动 wedge 时
|
||||||
vol_apply((int)pct);
|
* 一次 set = 3×popen ≈ 4.5s,会把 OPEN/QUERY/喂流一起卡死。连续请求合并为末值
|
||||||
|
* (去抖),由 vol 线程应用+持久化后经 IO 线程回 ACK。 */
|
||||||
|
g_vol_pending = 1;
|
||||||
|
g_vol_pending_pct = (pct > 100) ? 100 : (int)pct;
|
||||||
|
g_vol_pending_cli = c;
|
||||||
|
g_vol_pending_req = req_id;
|
||||||
|
g_vol_pending_gen = c->gen;
|
||||||
|
pthread_cond_signal(&g_vol_cond);
|
||||||
} else {
|
} else {
|
||||||
g_vol_pct = settings_get_volume();
|
g_vol_pct = settings_get_volume(); /* 读当前:文件读非 popen,可同步 */
|
||||||
|
m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_vol_pct;
|
||||||
|
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
|
||||||
}
|
}
|
||||||
m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_vol_pct;
|
|
||||||
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MSG_AUDIO_OUTPUT: {
|
case MSG_AUDIO_OUTPUT: {
|
||||||
@@ -1911,6 +2085,14 @@ static void *io_thread(void *arg)
|
|||||||
if (snaps[i].revents & (POLLHUP | POLLERR | POLLNVAL)) { cli_free(c); continue; }
|
if (snaps[i].revents & (POLLHUP | POLLERR | POLLNVAL)) { cli_free(c); continue; }
|
||||||
if (snaps[i].revents & POLLIN) cx_read_frames(c);
|
if (snaps[i].revents & POLLIN) cx_read_frames(c);
|
||||||
}
|
}
|
||||||
|
/* 音量 ACK 回写(事项三):vol 线程执行完只置标记,由 IO 线程统一发送——客户端 fd
|
||||||
|
* 生命周期归 IO 线程独占,跨线程直接 send_frame 有 fd 复用竞态。 */
|
||||||
|
for (int i = 0; i < CLI_MAX; i++) {
|
||||||
|
if (!g_cli[i].used || !g_cli[i].vol_ack) continue;
|
||||||
|
m_ack_t va; va.code = MEOW_OK; va.status = g_cli[i].vol_ack_status;
|
||||||
|
send_frame(g_cli[i].fd, MSG_ACK, g_cli[i].vol_ack_req, &va, sizeof va);
|
||||||
|
g_cli[i].vol_ack = 0;
|
||||||
|
}
|
||||||
pthread_mutex_unlock(&g_mu);
|
pthread_mutex_unlock(&g_mu);
|
||||||
/* 路由线程与播放线程独立;此处不再做 */
|
/* 路由线程与播放线程独立;此处不再做 */
|
||||||
}
|
}
|
||||||
@@ -1921,6 +2103,40 @@ static void *io_thread(void *arg)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 音量执行线程(事项三):异步落 amixer + 持久化,不在 IO 线程持锁 popen ----
|
||||||
|
* 等待 g_vol_cond 取末值(连续请求合并=去抖),放锁后执行 dac_vol_set(3×popen,有界)
|
||||||
|
* + settings_set_volume(文件 IO),完成后只置 ACK 标记,由 IO 线程统一发送(防 fd 复用竞态)。
|
||||||
|
* BT 目标只记日志不动 DAC/不持久化(AVRCP 隔离,g_vol_pct 保持不变)。 */
|
||||||
|
static void *vol_thread(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
for (;;) {
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
while (!g_vol_pending && !g_stop)
|
||||||
|
cond_wait_tmo_cv(&g_vol_cond, COND_WAIT_MS);
|
||||||
|
if (g_stop) { pthread_mutex_unlock(&g_mu); break; }
|
||||||
|
int pct = g_vol_pending_pct;
|
||||||
|
int target = g_target; /* 持锁快照:vol_apply 不读全局 */
|
||||||
|
struct client *c = g_vol_pending_cli;
|
||||||
|
uint32_t req = g_vol_pending_req;
|
||||||
|
uint32_t cgen = g_vol_pending_gen;
|
||||||
|
g_vol_pending = 0;
|
||||||
|
g_vol_pending_cli = NULL;
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
|
||||||
|
vol_apply(pct, target);
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_mu);
|
||||||
|
if (c && c->used && c->gen == cgen) { /* 槽复用校验:只 ACK 原请求方 */
|
||||||
|
c->vol_ack = 1;
|
||||||
|
c->vol_ack_req = req;
|
||||||
|
c->vol_ack_status = (uint32_t)g_vol_pct;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_mu);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 路由线程主体 ---- */
|
/* ---- 路由线程主体 ---- */
|
||||||
static void *route_thread(void *arg)
|
static void *route_thread(void *arg)
|
||||||
{
|
{
|
||||||
@@ -1930,8 +2146,10 @@ static void *route_thread(void *arg)
|
|||||||
/* M2.91 挂死根治(AUDIO_HANG_INVESTIGATION.md):hbc3000 FPGA 未烧录+enable 时
|
/* M2.91 挂死根治(AUDIO_HANG_INVESTIGATION.md):hbc3000 FPGA 未烧录+enable 时
|
||||||
* 启动 PCM 会在第二次 hbc3000_start 死锁整机。写 Output Port Switch 是烧录触发器
|
* 启动 PCM 会在第二次 hbc3000_start 死锁整机。写 Output Port Switch 是烧录触发器
|
||||||
* (驱动 put:set_spdif_mode→program_ram→enabled)。开机端口恒 0=未烧,故启动时
|
* (驱动 put:set_spdif_mode→program_ram→enabled)。开机端口恒 0=未烧,故启动时
|
||||||
* 无条件写一次:4.4 在→3,3.5 在→2,都没→4(官方播放器拔插孔后的选择,保 FPGA 已烧)。
|
* 武装一次:4.4 在→3,3.5 在→2,都没→4(官方播放器拔插孔后的选择,保 FPGA 已烧)。
|
||||||
* 端口已是目标值(audiod 重启场景,驱动同值短路不写)→ 先写 5(disable) 强制重烧。
|
* 多余重烧收敛(AUDIO_HANG_INVESTIGATION):端口已是目标值(audiod 重启场景)时
|
||||||
|
* **不再**先写 5(disable) 强制重烧——每次开机做 5→目标 双写等于把 FPGA 烧录暴露翻倍,
|
||||||
|
* 正是驱动级非确定楔死触发面;改为只需读回一致即跳过,仅当未武装/端口不对时才写目标。
|
||||||
* ⚠️ P2 蓝牙:numnid=9/FPGA 武装只作用于 **DAC 物理路径**(plan §五解耦);target=BT 时
|
* ⚠️ P2 蓝牙:numnid=9/FPGA 武装只作用于 **DAC 物理路径**(plan §五解耦);target=BT 时
|
||||||
* 不开 hw:0,0,无 hbc3000 start 死锁风险,故 BT 期间跳过 numid=9/FPGA(切回 DAC 再武装)。 */
|
* 不开 hw:0,0,无 hbc3000 start 死锁风险,故 BT 期间跳过 numid=9/FPGA(切回 DAC 再武装)。 */
|
||||||
int need_arm = 1; /* 启动(DAC 起点)武装一次;BT 期间置 1(回 DAC 再武装) */
|
int need_arm = 1; /* 启动(DAC 起点)武装一次;BT 期间置 1(回 DAC 再武装) */
|
||||||
@@ -1939,8 +2157,11 @@ static void *route_thread(void *arg)
|
|||||||
pthread_mutex_lock(&g_mu);
|
pthread_mutex_lock(&g_mu);
|
||||||
int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */
|
int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */
|
||||||
int tgt = g_target; /* 输出目标快照(决定是否武装 FPGA) */
|
int tgt = g_target; /* 输出目标快照(决定是否武装 FPGA) */
|
||||||
|
/* M50:BT 失败回落 DAC 时 **必须武装**(否则 FPGA 未烧 + enable 下开 PCM 会楔死整机)。
|
||||||
|
* 只有「请求 BT 且 BT 真的在用」才跳过 numid=9。 */
|
||||||
|
int bt_hold = (tgt == AUDIO_OUT_BT && g_bt_state != BT_ST_FAIL);
|
||||||
pthread_mutex_unlock(&g_mu);
|
pthread_mutex_unlock(&g_mu);
|
||||||
if (tgt == AUDIO_OUT_BT) {
|
if (bt_hold) {
|
||||||
static int s_logged = 0;
|
static int s_logged = 0;
|
||||||
if (!s_logged) { s_logged = 1; logmsg("route: output=BT, numid=9/FPGA not armed (DAC dormant)"); }
|
if (!s_logged) { s_logged = 1; logmsg("route: output=BT, numid=9/FPGA not armed (DAC dormant)"); }
|
||||||
need_arm = 1; /* 回 DAC 后补一次武装 */
|
need_arm = 1; /* 回 DAC 后补一次武装 */
|
||||||
@@ -1951,8 +2172,19 @@ static void *route_thread(void *arg)
|
|||||||
int bal = read_switch("/sys/class/switch/balance/state");
|
int bal = read_switch("/sys/class/switch/balance/state");
|
||||||
int target = (bal == 1) ? 3 : (hb == 1) ? 2 : 4;
|
int target = (bal == 1) ? 3 : (hb == 1) ? 2 : 4;
|
||||||
int cur = amixer_get(9, 1);
|
int cur = amixer_get(9, 1);
|
||||||
if (cur == target) { logmsg("route: port already %d, force re-arm via 5", target); apply_route(5); }
|
if (cur == target) {
|
||||||
apply_route(target);
|
/* 端口已正确:跳过 5→target 双重强烧,只确认读回一致(cur==target 已由
|
||||||
|
* 上一拍 amixer_get 证实)。原逻辑「cur==target 时先写 5(disable) 再回
|
||||||
|
* target」是给 audiod 重启/同值短路场景强制重烧用的,但每次开机无条件做
|
||||||
|
* 等于把 FPGA 烧录暴露翻倍——正是驱动级非确定楔死的触发面,故收敛掉。 */
|
||||||
|
g_route_cur = target; /* 保持 QUERY/monitor 最近端口与读回一致 */
|
||||||
|
logmsg("route: port already %d, skip re-arm (no 5->%d re-burn)", target, target);
|
||||||
|
} else {
|
||||||
|
/* 未武装(cur=0/读失败)或端口不对:写目标端口。这是 M2.91「未武装则武装」
|
||||||
|
* 防挂死核心(FPGA 未烧+enable 时二次 hbc3000_start 死锁整机),绝不可裸奔;
|
||||||
|
* 直接写 target 即触发烧录,无需再绕写 5(同值短路场景已被上一分支跳过)。 */
|
||||||
|
apply_route(target);
|
||||||
|
}
|
||||||
last_hb = hb; last_bal = bal; last_target = (hb == 0 && bal == 0) ? -1 : target;
|
last_hb = hb; last_bal = bal; last_target = (hb == 0 && bal == 0) ? -1 : target;
|
||||||
logmsg("route: boot/DAC-return arm done (port %d %s)", target, port_name(target));
|
logmsg("route: boot/DAC-return arm done (port %d %s)", target, port_name(target));
|
||||||
need_arm = 0;
|
need_arm = 0;
|
||||||
@@ -2136,19 +2368,21 @@ int main(int argc, char **argv)
|
|||||||
logmsg("volume init = %d%% target=%s codec=%s volctl=%s",
|
logmsg("volume init = %d%% target=%s codec=%s volctl=%s",
|
||||||
g_vol_pct, target_name(), g_bt_codec, volctl_name());
|
g_vol_pct, target_name(), g_bt_codec, volctl_name());
|
||||||
|
|
||||||
/* g_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
|
/* g_cond/g_vol_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
|
||||||
{
|
{
|
||||||
pthread_condattr_t ca;
|
pthread_condattr_t ca;
|
||||||
pthread_condattr_init(&ca);
|
pthread_condattr_init(&ca);
|
||||||
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
|
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
|
||||||
pthread_cond_init(&g_cond, &ca);
|
pthread_cond_init(&g_cond, &ca);
|
||||||
|
pthread_cond_init(&g_vol_cond, &ca);
|
||||||
pthread_condattr_destroy(&ca);
|
pthread_condattr_destroy(&ca);
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_t ti, tp, tr;
|
pthread_t ti, tp, tr, tv;
|
||||||
pthread_create(&ti, NULL, io_thread, NULL);
|
pthread_create(&ti, NULL, io_thread, NULL);
|
||||||
pthread_create(&tp, NULL, playback_thread, NULL);
|
pthread_create(&tp, NULL, playback_thread, NULL);
|
||||||
pthread_create(&tr, NULL, route_thread, NULL);
|
pthread_create(&tr, NULL, route_thread, NULL);
|
||||||
|
pthread_create(&tv, NULL, vol_thread, NULL);
|
||||||
|
|
||||||
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
|
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
|
||||||
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
|
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
|
||||||
@@ -2178,6 +2412,7 @@ int main(int argc, char **argv)
|
|||||||
pthread_join(ti, NULL);
|
pthread_join(ti, NULL);
|
||||||
pthread_join(tp, NULL);
|
pthread_join(tp, NULL);
|
||||||
pthread_join(tr, NULL);
|
pthread_join(tr, NULL);
|
||||||
|
pthread_join(tv, NULL);
|
||||||
unlink(g_sock);
|
unlink(g_sock);
|
||||||
if (g_logfd >= 0) close(g_logfd);
|
if (g_logfd >= 0) close(g_logfd);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -187,6 +187,24 @@ static int start_audiod_trace(const char *sink, const char *settings_path, const
|
|||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 带假 amixer 的启动(事项三异步化验证:假 amixer 每次 cset 睡 1s,模拟慢 popen)。
|
||||||
|
* 用于断言「MSG_AUDIO_VOLUME 在 IO 线程只入队、不放锁 popen」——慢 amixer 期间 QUERY 仍即时。 */
|
||||||
|
static int start_audiod_fake_amixer(const char *sink, const char *settings_path, const char *amixer_script)
|
||||||
|
{
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
setenv("MEOW_AUDIO_SINK", sink, 1);
|
||||||
|
setenv("MEOW_AUDIO_OUT", TWAV, 1);
|
||||||
|
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
|
||||||
|
setenv("MEOW_AUDIO_FORCE", "1", 1);
|
||||||
|
if (settings_path) setenv("MEOW_AUDIO_SETTINGS", settings_path, 1);
|
||||||
|
if (amixer_script) setenv("MEOW_AUDIO_AMIXER", amixer_script, 1);
|
||||||
|
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
/* 直接客户端 open(带 flags:AUDIO_FLAG_DSD 用于 DoP) */
|
/* 直接客户端 open(带 flags:AUDIO_FLAG_DSD 用于 DoP) */
|
||||||
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, m_audio_open_ack_t *ack)
|
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, m_audio_open_ack_t *ack)
|
||||||
{
|
{
|
||||||
@@ -792,6 +810,11 @@ int main(void)
|
|||||||
check(strstr(q11, "codec=SBC") != NULL, "settings bt.codec=SBC -> codec=SBC");
|
check(strstr(q11, "codec=SBC") != NULL, "settings bt.codec=SBC -> codec=SBC");
|
||||||
check(strstr(q11, "volctl=avrcp") != NULL, "bt -> volctl=avrcp (AVRCP vol control)");
|
check(strstr(q11, "volctl=avrcp") != NULL, "bt -> volctl=avrcp (AVRCP vol control)");
|
||||||
check(strstr(q11, "fmt=S16") != NULL, "bt grant fmt=S16 (format normalize)");
|
check(strstr(q11, "fmt=S16") != NULL, "bt grant fmt=S16 (format normalize)");
|
||||||
|
/* M50:BT 运行时状态三键(面板据此显示「输出=蓝牙(失败)」)。FILE sink 从不尝试 BT 打开
|
||||||
|
* → bt=idle 且 btdev=-(无端点);token 由 settings bt.codec=SBC 归一(canonical 同名)。 */
|
||||||
|
check(strstr(q11, "bt=idle") != NULL, "bt=idle before any open attempt (FILE sink)");
|
||||||
|
check(strstr(q11, "btdev=-") != NULL, "btdev=- when no endpoint opened");
|
||||||
|
check(strstr(q11, "btcodec=SBC") != NULL, "btcodec=SBC (settings token normalized)");
|
||||||
/* BT/file sink 写一段流(应正常落盘到 WAV) */
|
/* BT/file sink 写一段流(应正常落盘到 WAV) */
|
||||||
do_write(&b11, pcm, (uint32_t)gen_sine(pcm, sizeof pcm, (int)(ack11.grant_fmt ? ack11.grant_fmt : AUDIO_FMT_S16), 600, 440, 44100));
|
do_write(&b11, pcm, (uint32_t)gen_sine(pcm, sizeof pcm, (int)(ack11.grant_fmt ? ack11.grant_fmt : AUDIO_FMT_S16), 600, 440, 44100));
|
||||||
usleep(150000);
|
usleep(150000);
|
||||||
@@ -1080,6 +1103,62 @@ int main(void)
|
|||||||
}
|
}
|
||||||
stop_audiod();
|
stop_audiod();
|
||||||
|
|
||||||
|
/* ---- T17:MSG_AUDIO_VOLUME 异步化(事项三)。假 amixer 每次 cset 睡 1s(一次 set=3×cset≈3s)。
|
||||||
|
* 断言:慢 amixer 期间 QUERY 即时返回(dispatch 不放锁 popen);应用后回 ACK + 双组写 + 持久化。 */
|
||||||
|
printf("[T17 volume async (dispatch not blocked by slow amixer)]\n");
|
||||||
|
unlink(TSOCK); unlink(TWAV);
|
||||||
|
const char *tconf17 = "/tmp/meow_test_settings17.conf";
|
||||||
|
FILE *tf17 = fopen(tconf17, "w");
|
||||||
|
if (tf17) { fprintf(tf17, "# meow-settings 1\naudio.volume = 60\n"); fclose(tf17); }
|
||||||
|
const char *fake17 = "/tmp/meow_fake_amixer17.sh";
|
||||||
|
const char *trace17 = "/tmp/meow_fake_amixer17_trace.txt";
|
||||||
|
unlink(trace17);
|
||||||
|
{
|
||||||
|
FILE *fs = fopen(fake17, "w");
|
||||||
|
if (fs) {
|
||||||
|
fprintf(fs, "#!/bin/sh\necho \"cset $4 $5\" >> %s\nsleep 1\nexit 0\n", trace17);
|
||||||
|
fclose(fs);
|
||||||
|
chmod(fake17, 0755);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_audiod_pid = start_audiod_fake_amixer("file", tconf17, fake17);
|
||||||
|
usleep(400000);
|
||||||
|
{
|
||||||
|
struct cli v17;
|
||||||
|
check(cli_connect(&v17) == 0, "connect V17");
|
||||||
|
int32_t pct = 40;
|
||||||
|
check(sf(v17.fd, MSG_AUDIO_VOLUME, 17, &pct, sizeof pct) == 0, "send async volume 40");
|
||||||
|
/* 慢 amixer 执行期间 QUERY 必须即时(<600ms),证明 IO 线程没被 popen 卡住 */
|
||||||
|
struct timespec t0, t1;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||||
|
char q17[1024];
|
||||||
|
int qok = do_query(&v17, q17, sizeof q17);
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||||
|
long qms = (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
|
||||||
|
check(qok == 0 && qms < 600, "QUERY immediate during slow amixer (async dispatch)");
|
||||||
|
if (qok == 0 && qms >= 600) printf(" (query took %ldms)\n", qms);
|
||||||
|
/* 应用后回 ACK(末值 40) */
|
||||||
|
uint8_t ab[64]; uint16_t at;
|
||||||
|
int rlen = rf_tmo(v17.fd, 4000, &at, ab, sizeof ab);
|
||||||
|
check(rlen >= 0 && at == MSG_ACK, "async volume ACK returned after apply");
|
||||||
|
usleep(100000);
|
||||||
|
{
|
||||||
|
FILE *chk = fopen(tconf17, "r"); int has40 = 0; char l[128];
|
||||||
|
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
|
||||||
|
check(has40, "async volume persisted = 40");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char trb[2048]; int trl = 0;
|
||||||
|
int fd = open(trace17, O_RDONLY);
|
||||||
|
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
|
||||||
|
check(trl > 0 && strstr(trb, "numid=1 ") && strstr(trb, "numid=2 ") && strstr(trb, "numid=11 "),
|
||||||
|
"async dual-group write (numid=1/2/11) via fake amixer");
|
||||||
|
}
|
||||||
|
close(v17.fd);
|
||||||
|
}
|
||||||
|
stop_audiod();
|
||||||
|
unlink(fake17); unlink(trace17);
|
||||||
|
|
||||||
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
/* test_bt_dev.c — 蓝牙 DEV 自识别宿主验证(纯逻辑,bt_dev.c)。
|
/* test_bt_dev.c — 蓝牙端点选择 / PCM 名构造 / 回落退避 宿主验证(纯逻辑,bt_dev.c)。
|
||||||
*
|
*
|
||||||
* 覆盖矩阵(对齐任务B 宿主测试要求):
|
* 输入全部取自**真机实测原文**(bluealsa v4.1.1;设备 /usr/bin/bluealsa 的
|
||||||
* D1 bt_dev_detect_mac:a2dpsrc/<MAC> 端点 → 提取 MAC(含 a2dp_source、多空行、多设备取第一个、
|
* `dev_%.2X_..` + src/ba-transport.c 的 `<dev>/<profile>/<source|sink>`):
|
||||||
* 无 a2dpsrc / 空输出 / NULL → -1)。
|
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink ← 实机(本机 a2dp-source)
|
||||||
* D2 bt_dev_rewrite:真实 alsa.conf 文本 → DEV=、device 两处 MAC 换成目标,保留 codec/其余 token;
|
* (同一格式下 /a2dpsrc/source 只是 PCM 模式不同,解析只认 dev_XX_.. 段)
|
||||||
* 同值 → 1(无需改动);无 pcm.bluealsa / 无 DEV= → -1;带引号风格保留。
|
*
|
||||||
|
* 覆盖矩阵:
|
||||||
|
* D1 bt_dev_detect_mac:真实 D-Bus 路径 → MAC;多端点/多设备取第一个 a2dpsrc;
|
||||||
|
* hfphf/a2dpsnk 不算;简化 `a2dpsrc/<MAC>` 兼容;空/无源端点/NULL/坏 dev_ 段 → -1。
|
||||||
|
|
||||||
|
* D3 bt_dev_codec_token:settings bt.codec → bluealsa canonical(4.1.1 别名表 strcasecmp);
|
||||||
|
* auto/空/unchanged/垃圾 → unchanged。
|
||||||
|
* D4 bt_dev_pcm_name:内联 DEV/PROFILE/CODEC;坏 MAC/小缓冲 → -1。
|
||||||
|
* D5 bt_dev_backoff_ms / should_try:2s→60s 封顶 + 门控(防重试风暴)。
|
||||||
|
* D7 bt_dev_info_codec:真机 `bluealsa-cli info` 原文 → Selected codec(不能误吃 Available 行)。
|
||||||
|
* D8 bt_dev_acl_macs:真机 `hcitool con` 原文 → **全部** ACL 对端 MAC(多设备一等公民:
|
||||||
|
* 只取第一条会在手机/手柄同时连着时挑错设备,导致有耳机却回落 DAC)。
|
||||||
|
* D9 bt_dev_pick_endpoint:按 MAC 精确挑端点(换耳机 A→B 后不能拿 A 的路径发 volume/info)。
|
||||||
|
* D10 bt_dev_close_kind:关闭决策(BT 分支 / 真 fd / **0-2 一律不动**=护 stdin,P2-1 回归)。
|
||||||
*
|
*
|
||||||
* 编译(在 patch_v2/meow_audiod 下):
|
* 编译(在 patch_v2/meow_audiod 下):
|
||||||
* cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c
|
* cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c
|
||||||
@@ -26,91 +39,250 @@ int main(void)
|
|||||||
{
|
{
|
||||||
setbuf(stdout, NULL);
|
setbuf(stdout, NULL);
|
||||||
|
|
||||||
/* ---- D1 设备列表 → 选中 a2dpsrc 连接设备 ---- */
|
/* ==== D1 设备列表 → 选中 a2dpsrc 连接设备(真实 D-Bus 路径格式) ==== */
|
||||||
{
|
{
|
||||||
char mac[32] = "";
|
char mac[32] = "";
|
||||||
int rc = bt_dev_detect_mac(
|
int rc = bt_dev_detect_mac(
|
||||||
"a2dpsrc/E0:42:6D:79:C5:98\na2dpsnk/E0:42:6D:79:C5:98\n", mac, sizeof mac);
|
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac);
|
||||||
check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1a a2dpsrc/<MAC> -> extract MAC");
|
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0,
|
||||||
|
"D1a real D-Bus path -> MAC (dev_ underscores -> colons)");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
char mac[32] = "";
|
char mac[32] = "";
|
||||||
int rc = bt_dev_detect_mac(
|
int rc = bt_dev_detect_mac(
|
||||||
"profile/aptx\n\na2dp_source/33:DE:2E:21:AD:30\n", mac, sizeof mac);
|
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n"
|
||||||
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1b a2dp_source/<MAC> -> extract MAC");
|
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsnk/sink\n"
|
||||||
|
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source\n", mac, sizeof mac);
|
||||||
|
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0,
|
||||||
|
"D1b hfphf/a2dpsnk skipped -> first a2dpsrc");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
char mac[32] = "";
|
char mac[32] = "";
|
||||||
int rc = bt_dev_detect_mac(
|
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);
|
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/source\n"
|
||||||
|
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/source\n", mac, sizeof mac);
|
||||||
check(rc == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, "D1c multi-device -> first a2dpsrc");
|
check(rc == 0 && strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0, "D1c multi-device -> first a2dpsrc");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
char mac[32] = "";
|
char mac[32] = "";
|
||||||
int rc = bt_dev_detect_mac("\n", mac, sizeof mac);
|
int rc = bt_dev_detect_mac("a2dpsrc/E0:42:6D:79:C5:98\n", mac, sizeof mac);
|
||||||
check(rc == -1, "D1d empty output -> -1");
|
check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1d legacy a2dpsrc/<MAC> still parsed");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
char mac[32] = "";
|
char mac[32] = "";
|
||||||
int rc = bt_dev_detect_mac("a2dpsnk/11:22:33:44:55:66\n", mac, sizeof mac);
|
int rc = bt_dev_detect_mac("", mac, sizeof mac);
|
||||||
check(rc == -1, "D1e only sink (no a2dpsrc) -> -1");
|
check(rc == -1, "D1e empty output -> -1");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char mac[32] = "";
|
||||||
|
int rc = bt_dev_detect_mac(
|
||||||
|
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsnk/sink\n", mac, sizeof mac);
|
||||||
|
check(rc == -1, "D1f only sink (no a2dpsrc) -> -1");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
char mac[32] = "";
|
char mac[32] = "";
|
||||||
int rc = bt_dev_detect_mac(NULL, mac, sizeof mac);
|
int rc = bt_dev_detect_mac(NULL, mac, sizeof mac);
|
||||||
check(rc == -1, "D1f NULL input -> -1");
|
check(rc == -1, "D1g NULL input -> -1");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char mac[32] = "";
|
||||||
|
int rc = bt_dev_detect_mac(
|
||||||
|
"/org/bluealsa/hci0/dev_ZZ_42_6D_79_C5_98/a2dpsrc/source\n", mac, sizeof mac);
|
||||||
|
check(rc == -1 && mac[0] == 0, "D1h malformed dev_ seg -> -1 (no garbage MAC)");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char mac[32] = "";
|
||||||
|
int rc = bt_dev_detect_mac(
|
||||||
|
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/source 1\n", mac, sizeof mac);
|
||||||
|
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1i trailing token tolerated");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- D2 DEV 串入 / 保留其它 token ---- */
|
/* ==== D3 settings bt.codec → bluealsa canonical token ==== */
|
||||||
{
|
{
|
||||||
const char *conf =
|
struct { const char *in; const char *want; const char *name; } t[] = {
|
||||||
"pcm.bluealsa:DEV=E0:42:6D:79:C5:98 {\n"
|
{ "LDAC", "LDAC", "D3a LDAC passthrough" },
|
||||||
" type plug\n"
|
{ "ldac", "LDAC", "D3b lowercase ldac -> canonical" },
|
||||||
" slave {\n"
|
{ "SBC", "SBC", "D3c SBC" },
|
||||||
" pcm {\n"
|
{ "AAC", "AAC", "D3d AAC" },
|
||||||
" type bluealsa\n"
|
{ "MPEG24", "AAC", "D3e MPEG24 alias -> AAC" },
|
||||||
" device E0:42:6D:79:C5:98\n"
|
{ "aptX", "aptX", "D3f aptX" },
|
||||||
" profile \"a2dp\"\n"
|
{ "apt-X", "aptX", "D3g apt-X alias -> aptX" },
|
||||||
" codec \"LDAC\"\n"
|
{ "aptx-hd", "aptX-HD", "D3h aptx-hd -> aptX-HD" },
|
||||||
" ldac_eqmid \"LDAC_ABR\"\n"
|
{ "MPEG", "MP3", "D3i MPEG alias -> MP3" },
|
||||||
" }\n"
|
{ "MP3", "MP3", "D3j MP3" },
|
||||||
" }\n"
|
{ "FastStream", "FastStream", "D3k FastStream" },
|
||||||
"}\n";
|
{ "FS", "FastStream", "D3l FS alias -> FastStream" },
|
||||||
char *out = NULL; size_t olen = 0;
|
{ "auto", "unchanged", "D3m auto -> unchanged (do not touch negotiation)" },
|
||||||
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
|
{ "", "unchanged", "D3n empty -> unchanged" },
|
||||||
check(rc == 0 && out != NULL, "D2a rewrite returns changed");
|
{ "unchanged", "unchanged", "D3o unchanged -> unchanged" },
|
||||||
check(out && strstr(out, ":DEV=33:DE:2E:21:AD:30 {"), "D2b DEV= updated");
|
{ "BOGUS", "unchanged", "D3p unknown -> unchanged (never inject junk)" },
|
||||||
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\""),
|
for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) {
|
||||||
"D2d codec/other tokens preserved");
|
char tok[16] = "";
|
||||||
/* 旧 MAC 不再出现 */
|
const char *r = bt_dev_codec_token(t[i].in, tok, sizeof tok);
|
||||||
check(out && !strstr(out, "E0:42:6D:79:C5:98"), "D2e old MAC gone");
|
check(r == tok && strcmp(tok, t[i].want) == 0, t[i].name);
|
||||||
free(out);
|
}
|
||||||
}
|
char tok[16] = "";
|
||||||
{
|
check(bt_dev_codec_token(NULL, tok, sizeof tok) == tok && !strcmp(tok, "unchanged"),
|
||||||
const char *conf =
|
"D3q NULL setting -> unchanged");
|
||||||
"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);
|
/* ==== D4 PCM 名构造(20-bluealsa.conf @args 只认 DEV/PROFILE/CODEC 这套名字) ==== */
|
||||||
return g_fail;
|
{
|
||||||
|
char n[96] = "";
|
||||||
|
int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, sizeof n);
|
||||||
|
check(rc == 0 && strcmp(n, "bluealsa:DEV=33:DE:2E:21:AD:30,PROFILE=a2dp,CODEC=LDAC") == 0,
|
||||||
|
"D4a name = bluealsa:DEV=<mac>,PROFILE=a2dp,CODEC=<token>");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char n[96] = "";
|
||||||
|
int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", "", n, sizeof n);
|
||||||
|
check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4b empty token -> CODEC=unchanged");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char n[96] = "";
|
||||||
|
int rc = bt_dev_pcm_name("33:DE:2E:21:AD:30", NULL, n, sizeof n);
|
||||||
|
check(rc == 0 && strstr(n, "CODEC=unchanged") != NULL, "D4c NULL token -> CODEC=unchanged");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char n[96] = "";
|
||||||
|
check(bt_dev_pcm_name("33:DE:2E:21:AD", "LDAC", n, sizeof n) == -1, "D4d short MAC -> -1");
|
||||||
|
check(bt_dev_pcm_name("GG:DE:2E:21:AD:30", "LDAC", n, sizeof n) == -1, "D4e non-hex MAC -> -1");
|
||||||
|
check(bt_dev_pcm_name(NULL, "LDAC", n, sizeof n) == -1, "D4f NULL MAC -> -1");
|
||||||
|
check(bt_dev_pcm_name("33:DE:2E:21:AD:30", "LDAC", n, 16) == -1, "D4g tiny buffer -> -1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==== D7 实际协商 codec(真机 info 原文:MOONDROP Pudding 只有 SBC/AAC,请求 LDAC 实际选 AAC) ==== */
|
||||||
|
{
|
||||||
|
const char *info =
|
||||||
|
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
|
||||||
|
"Sequence: 0\n"
|
||||||
|
"Transport: A2DP-source\n"
|
||||||
|
"Mode: sink\n"
|
||||||
|
"Running: true\n"
|
||||||
|
"Format: S16_LE\n"
|
||||||
|
"Channels: 2\n"
|
||||||
|
"Sampling: 48000 Hz\n"
|
||||||
|
"Available codecs: SBC AAC\n"
|
||||||
|
"Selected codec: AAC\n"
|
||||||
|
"Delay: 17.2 ms\n"
|
||||||
|
"SoftVolume: false\n"
|
||||||
|
"Volume: L: 127 R: 127\n"
|
||||||
|
"Muted: L: false R: false\n";
|
||||||
|
char codec[16] = "";
|
||||||
|
check(bt_dev_info_codec(info, codec, sizeof codec) == 0 && strcmp(codec, "AAC") == 0,
|
||||||
|
"D7a real info -> Selected codec=AAC (not the Available line)");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const char *info = "Available codecs: SBC AAC\nSelected codec: SBC\r\n";
|
||||||
|
char codec[16] = "";
|
||||||
|
check(bt_dev_info_codec(info, codec, sizeof codec) == 0 && strcmp(codec, "SBC") == 0,
|
||||||
|
"D7b CRLF trimmed");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char codec[16] = "";
|
||||||
|
check(bt_dev_info_codec("Running: false\n", codec, sizeof codec) == -1,
|
||||||
|
"D7c no Selected line -> -1");
|
||||||
|
check(bt_dev_info_codec(NULL, codec, sizeof codec) == -1, "D7d NULL -> -1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==== D8 hcitool con → 全部 ACL MAC(真机原文;MAC 主来源,多设备是常态) ==== */
|
||||||
|
{
|
||||||
|
const char *con =
|
||||||
|
"Connections:\n"
|
||||||
|
"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n";
|
||||||
|
char m[4][18];
|
||||||
|
check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0,
|
||||||
|
"D8a real hcitool con (state 1 AUTH ENCRYPT) -> 1 ACL MAC");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const char *con =
|
||||||
|
"Connections:\n"
|
||||||
|
"\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n";
|
||||||
|
char m[4][18];
|
||||||
|
check(bt_dev_acl_macs(con, m, 4) == 1 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0,
|
||||||
|
"D8b half-open ACL (state 5) still gives MAC (kernel is authority)");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
/* 多设备:第二副耳机 + 手机同时连着 —— 两条都要拿到,且顺序=输出顺序 */
|
||||||
|
const char *con =
|
||||||
|
"Connections:\n"
|
||||||
|
"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER AUTH ENCRYPT \n"
|
||||||
|
"\t< ACL 5C:FB:7C:11:22:33 handle 5 state 1 lm SLAVE \n";
|
||||||
|
char m[4][18];
|
||||||
|
int n = bt_dev_acl_macs(con, m, 4);
|
||||||
|
check(n == 2 && strcmp(m[0], "33:DE:2E:21:AD:30") == 0 && strcmp(m[1], "5C:FB:7C:11:22:33") == 0,
|
||||||
|
"D8c two ACLs -> both MACs in order (not just the first one)");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
/* 三设备 + 上限裁剪 + 去重(同一设备多 profile 行重复出现) */
|
||||||
|
const char *con =
|
||||||
|
"Connections:\n"
|
||||||
|
"\t< ACL AA:BB:CC:DD:EE:01 handle 1 state 1 lm MASTER \n"
|
||||||
|
"\t< ACL AA:BB:CC:DD:EE:01 handle 2 state 1 lm SLAVE \n"
|
||||||
|
"\t< ACL AA:BB:CC:DD:EE:02 handle 3 state 1 lm MASTER \n"
|
||||||
|
"\t< ACL AA:BB:CC:DD:EE:03 handle 4 state 1 lm MASTER \n";
|
||||||
|
char m[3][18];
|
||||||
|
int n = bt_dev_acl_macs(con, m, 3);
|
||||||
|
check(n == 3 && strcmp(m[0], "AA:BB:CC:DD:EE:01") == 0 &&
|
||||||
|
strcmp(m[1], "AA:BB:CC:DD:EE:02") == 0 && strcmp(m[2], "AA:BB:CC:DD:EE:03") == 0,
|
||||||
|
"D8d dedup + cap at max (3 distinct)");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char m[4][18];
|
||||||
|
check(bt_dev_acl_macs("Connections:\n", m, 4) == 0, "D8e no ACL -> 0");
|
||||||
|
check(bt_dev_acl_macs(NULL, m, 4) == 0, "D8f NULL -> 0");
|
||||||
|
check(bt_dev_acl_macs("\t< ACL aa:bb:cc:dd:ee:ff handle 3 state 1 lm SLAVE \n", m, 4) == 1 &&
|
||||||
|
strcmp(m[0], "AA:BB:CC:DD:EE:FF") == 0, "D8g lowercase -> uppercase canonical");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==== D9 按 MAC 精确挑端点(换耳机 A→B 不串台) ==== */
|
||||||
|
{
|
||||||
|
const char *pcms =
|
||||||
|
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink\n"
|
||||||
|
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink\n";
|
||||||
|
char path[128] = "";
|
||||||
|
check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:02", path, sizeof path) == 0 &&
|
||||||
|
strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_02/a2dpsrc/sink") == 0,
|
||||||
|
"D9a picks B's path when MAC=B (not the first endpoint)");
|
||||||
|
check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:01", path, sizeof path) == 0 &&
|
||||||
|
strcmp(path, "/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/a2dpsrc/sink") == 0,
|
||||||
|
"D9b picks A's path when MAC=A");
|
||||||
|
check(bt_dev_pick_endpoint(pcms, "AA:BB:CC:DD:EE:03", path, sizeof path) == -1 &&
|
||||||
|
path[0] == 0, "D9c unknown MAC -> -1 (never borrow another device's path)");
|
||||||
|
check(bt_dev_pick_endpoint("/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_01/hfphf/source\n",
|
||||||
|
"AA:BB:CC:DD:EE:01", path, sizeof path) == -1,
|
||||||
|
"D9d non-a2dpsrc endpoint -> -1");
|
||||||
|
check(bt_dev_pick_endpoint(NULL, "AA:BB:CC:DD:EE:01", path, sizeof path) == -1, "D9e NULL -> -1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==== D10 关闭决策(P2-1 回归:0/1/2 永不当设备 fd 关) ==== */
|
||||||
|
check(bt_dev_close_kind(1, 0) == 1, "D10a bt_dev=1 + fd=0 -> close BT only (never fd 0)");
|
||||||
|
check(bt_dev_close_kind(1, 5) == 1, "D10b bt_dev=1 + fd=5 -> close BT only");
|
||||||
|
check(bt_dev_close_kind(0, 0) == 0, "D10c bt_dev=0 + fd=0 -> NONE (stdin guard, P2-1)");
|
||||||
|
check(bt_dev_close_kind(0, 1) == 0, "D10d bt_dev=0 + fd=1 -> NONE (stdout guard)");
|
||||||
|
check(bt_dev_close_kind(0, 2) == 0, "D10e bt_dev=0 + fd=2 -> NONE (stderr guard)");
|
||||||
|
check(bt_dev_close_kind(0, 3) == 2, "D10f bt_dev=0 + fd=3 -> close FD");
|
||||||
|
check(bt_dev_close_kind(0, -1) == 0, "D10g bt_dev=0 + fd=-1 -> NONE");
|
||||||
|
check(bt_dev_close_kind(0, -2) == 0, "D10h bt_dev=0 + fd=-2 (NULL sink) -> NONE");
|
||||||
|
|
||||||
|
/* ==== D5 回落退避(防重试风暴:M10 教训) ==== */
|
||||||
|
check(bt_dev_backoff_ms(0) == 0, "D5a fails=0 -> 0ms (immediate)");
|
||||||
|
check(bt_dev_backoff_ms(1) == 2000, "D5b fails=1 -> 2s");
|
||||||
|
check(bt_dev_backoff_ms(2) == 4000, "D5c fails=2 -> 4s");
|
||||||
|
check(bt_dev_backoff_ms(3) == 8000, "D5d fails=3 -> 8s");
|
||||||
|
check(bt_dev_backoff_ms(4) == 16000, "D5e fails=4 -> 16s");
|
||||||
|
check(bt_dev_backoff_ms(5) == 32000, "D5f fails=5 -> 32s");
|
||||||
|
check(bt_dev_backoff_ms(6) == 60000, "D5g fails=6 -> 60s (capped)");
|
||||||
|
check(bt_dev_backoff_ms(99) == 60000, "D5h fails=99 -> 60s (capped)");
|
||||||
|
check(bt_dev_should_try(0, 0) == 1, "D5i never failed -> try now");
|
||||||
|
check(bt_dev_should_try(1, 1999) == 0, "D5j fail#1 @1999ms -> gated");
|
||||||
|
check(bt_dev_should_try(1, 2000) == 1, "D5k fail#1 @2000ms -> allowed");
|
||||||
|
check(bt_dev_should_try(3, 7999) == 0, "D5l fail#3 @7999ms -> gated");
|
||||||
|
check(bt_dev_should_try(3, 8000) == 1, "D5m fail#3 @8000ms -> allowed");
|
||||||
|
check(bt_dev_should_try(2, -1) == 1, "D5n clock anomaly (negative) -> allowed (no deadlock)");
|
||||||
|
|
||||||
|
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS (%d checks)\n",
|
||||||
|
g_fail ? g_fail : g_pass);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,9 @@ if [ "$1" = "host" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -Os -Wall -Wextra -static meow_crash.c -o meow_crash
|
# meow_time32.c:独立 o32 time32 取时/睡眠(crash 不链 meow_ui.a);扫描/清理节拍恢复真实毫秒。
|
||||||
|
"$CC" -Os -Wall -Wextra -static -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_crash.c ../sdk/meow_time32.c -o meow_crash
|
||||||
echo "== meow_crash (mipsel static) =="
|
echo "== meow_crash (mipsel static) =="
|
||||||
ls -la meow_crash
|
ls -la meow_crash
|
||||||
md5sum meow_crash
|
md5sum meow_crash
|
||||||
|
|||||||
Binary file not shown.
@@ -23,6 +23,28 @@ static void app_name_copy(char *dst, int cap, const char *src)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* M6.40 启动失败原因透传:launch_app/launch_activity(→launch_app_chain)返回 -1 前按实际
|
||||||
|
* 失败点填具体原因(fork OOM / exec 失败 / 缺依赖 / 路径不存在等),供 main_menu.c 的
|
||||||
|
* launch_fail_toast 显示「CANNOT START: %s(%s)」;success 路径恒为 ""。 */
|
||||||
|
static char g_launch_fail_reason[160] = "";
|
||||||
|
|
||||||
|
const char *launch_fail_reason(void)
|
||||||
|
{
|
||||||
|
return g_launch_fail_reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内部失败出口:设原因 + 记统一失败日志 + 返回 -1(各失败分支复用)。 */
|
||||||
|
static int launch_fail(struct app *a, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(g_launch_fail_reason, sizeof g_launch_fail_reason, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
logmsg("launch failed: %s reason=%s", a->name, g_launch_fail_reason);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ---- amsd 上报 / 控制挂点(AMSD_DESIGN §4):best-effort,amsd 不在不影响 launcher ---- */
|
/* ---- amsd 上报 / 控制挂点(AMSD_DESIGN §4):best-effort,amsd 不在不影响 launcher ---- */
|
||||||
#define MEOW_AMS_SOCK_DEFAULT "/usr/data/meow_ams.sock"
|
#define MEOW_AMS_SOCK_DEFAULT "/usr/data/meow_ams.sock"
|
||||||
|
|
||||||
@@ -1022,7 +1044,8 @@ void cont_player(void)
|
|||||||
|
|
||||||
|
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
/* M2.12 MeowAPPs 包数据目录 + .mpk 接口 */
|
/* M2.12 MeowAPPs 包数据目录 + .mpk 导出接口 */
|
||||||
|
/* 安装入口 meow_pkg_install M42 起移到 pkg_install.c(委托 nyai)。 */
|
||||||
/* exFAT 无符号链接 → 数据目录用环境变量(MEOW_DATA_DIR/CACHE_DIR) */
|
/* exFAT 无符号链接 → 数据目录用环境变量(MEOW_DATA_DIR/CACHE_DIR) */
|
||||||
/* 传址给应用,不做 symlink(APPS.md 附录 v2 修订)。 */
|
/* 传址给应用,不做 symlink(APPS.md 附录 v2 修订)。 */
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
@@ -1105,16 +1128,6 @@ static int pkg_json_field_mpk(const char *mpk, const char *key, char *out, int n
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 安装成功后,把原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名)。 */
|
|
||||||
static void pkg_save_src(const char *mpk, const char *pkg)
|
|
||||||
{
|
|
||||||
char dst[320], cmd[720];
|
|
||||||
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg);
|
|
||||||
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' 2>/dev/null && "
|
|
||||||
"cp '%s' '%s' 2>/dev/null", pkg, mpk, dst);
|
|
||||||
if (system(cmd) != 0) logmsg("pkg_install: save .mpk.src fail for %s", pkg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 导出保留签名:缓存 .mpk.src 存在 + 验签通过 + 其 app.json 版本 == 已装 → 直接复制(不二次签名)。
|
/* 导出保留签名:缓存 .mpk.src 存在 + 验签通过 + 其 app.json 版本 == 已装 → 直接复制(不二次签名)。
|
||||||
* 成功 0;否则 -1(调用方现场重打包,无签名)。 */
|
* 成功 0;否则 -1(调用方现场重打包,无签名)。 */
|
||||||
static int pkg_export_preserve_sig(const char *pkg, const char *dst)
|
static int pkg_export_preserve_sig(const char *pkg, const char *dst)
|
||||||
@@ -1135,97 +1148,6 @@ static int pkg_export_preserve_sig(const char *pkg, const char *dst)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs/<pkg>/。
|
|
||||||
* 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 <pkg>.mpk。
|
|
||||||
* 同名先备份;解压后校验 app.json;随后数据 init(MeowData/<pkg>/{data,cache})。
|
|
||||||
* 返回 0 ok / 负=错误。 */
|
|
||||||
int meow_pkg_install(const char *mpk_path)
|
|
||||||
{
|
|
||||||
if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; }
|
|
||||||
if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; }
|
|
||||||
if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; }
|
|
||||||
/* 包名 = 文件名茎(去掉 .mpk) */
|
|
||||||
const char *bn = strrchr(mpk_path, '/');
|
|
||||||
bn = bn ? bn + 1 : mpk_path;
|
|
||||||
const char *dot = strrchr(bn, '.');
|
|
||||||
char pkg[64];
|
|
||||||
if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need <pkg>.mpk"); return -1; }
|
|
||||||
int len = (int)(dot - bn);
|
|
||||||
if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; }
|
|
||||||
memcpy(pkg, bn, len); pkg[len] = 0;
|
|
||||||
/* 目标 MeowAPPs/<pkg>/;同名先备份(.bak_<pid>),再解压,再校验 app.json */
|
|
||||||
char dst[256], bak[256], cmd[760];
|
|
||||||
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
|
|
||||||
snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid());
|
|
||||||
int replacing = (access(dst, F_OK) == 0);
|
|
||||||
|
|
||||||
/* II 期:签名分级先于解包(MPK_SIGNATURE_DESIGN §5.2:先验签再解包,失败不落盘)。
|
|
||||||
* 有签名必须验签(失败拒装);os.meow.* 必须有有效签名;无签名第三方放行(告警)。 */
|
|
||||||
int vr = pkg_verify_sig(mpk_path, pkg);
|
|
||||||
int sysp = (strncmp(pkg, "os.meow.", 8) == 0);
|
|
||||||
if (vr == 1) { logmsg("pkg_install: sig invalid, reject %s", pkg); return -3; }
|
|
||||||
else if (vr == 2) {
|
|
||||||
if (sysp) { logmsg("pkg_install: sys pkg %s must be signed, rejected", pkg); return -3; }
|
|
||||||
logmsg("pkg_install: %s unsigned (3rd party allowed)", pkg);
|
|
||||||
}
|
|
||||||
else if (vr != 0) {
|
|
||||||
if (sysp) { logmsg("pkg_install: meow_verify unavailable rc=%d, sys pkg reject", vr); return -3; }
|
|
||||||
logmsg("pkg_install: meow_verify unavailable rc=%d, %s unverified (3rd party)", vr, pkg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 防降级门禁(与 nyai 同规范,APPS.md §3.8):仅按 version_code 比较。
|
|
||||||
* 新版 code < 已装 → 拒;相等 → 允许重装;任一侧缺 version_code → 拒。 */
|
|
||||||
if (replacing) {
|
|
||||||
char nc[32], oc[32];
|
|
||||||
long long ncode = -1, ocode = -1;
|
|
||||||
int nok = 0, ok2 = 0;
|
|
||||||
if (pkg_json_field_mpk(mpk_path, "version_code", nc, sizeof nc) == 0) {
|
|
||||||
char *ep; long long v = strtoll(nc, &ep, 10);
|
|
||||||
if (ep != nc && *ep == '\0') { ncode = v; nok = 1; }
|
|
||||||
}
|
|
||||||
char instj[280];
|
|
||||||
snprintf(instj, sizeof instj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
|
|
||||||
if (pkg_json_field(instj, "version_code", oc, sizeof oc) == 0) {
|
|
||||||
char *ep; long long v = strtoll(oc, &ep, 10);
|
|
||||||
if (ep != oc && *ep == '\0') { ocode = v; ok2 = 1; }
|
|
||||||
}
|
|
||||||
if (!nok || !ok2) { logmsg("pkg_install: %s version_code missing, reject", pkg); return -4; }
|
|
||||||
if (ncode < ocode) { logmsg("pkg_install: downgrade %s new %lld < old %lld, reject", pkg, ncode, ocode); return -4; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */
|
|
||||||
snprintf(cmd, sizeof cmd,
|
|
||||||
"rm -rf '%s' 2>/dev/null; "
|
|
||||||
"[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; "
|
|
||||||
"mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && "
|
|
||||||
"[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst);
|
|
||||||
if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; }
|
|
||||||
|
|
||||||
/* II 期:app.json.package 绑定(== 包名,禁改包名/目录改名;防第三方面具系统包)。 */
|
|
||||||
{
|
|
||||||
char appj[280], ph[64];
|
|
||||||
snprintf(appj, sizeof appj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
|
|
||||||
if (pkg_json_field(appj, "package", ph, sizeof ph) == 0 && strcmp(ph, pkg) != 0) {
|
|
||||||
logmsg("pkg_install: app.json.package %s != %s, reject", ph, pkg);
|
|
||||||
char rb[760];
|
|
||||||
snprintf(rb, sizeof rb, "rm -rf '%s' 2>/dev/null; [ -d '%s' ] && mv '%s' '%s' 2>/dev/null",
|
|
||||||
dst, bak, bak, dst);
|
|
||||||
system(rb);
|
|
||||||
return -3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 数据 init:MeowData/<pkg>/{data,cache} */
|
|
||||||
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
|
|
||||||
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
|
|
||||||
system(cmd);
|
|
||||||
/* 有效签 → 原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
|
|
||||||
if (vr == 0) pkg_save_src(mpk_path, pkg);
|
|
||||||
logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* .mpk 导出:打包 MeowAPPs/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
|
/* .mpk 导出:打包 MeowAPPs/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
|
||||||
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
|
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
|
||||||
int meow_pkg_export(const char *pkg, const char *out_path)
|
int meow_pkg_export(const char *pkg, const char *out_path)
|
||||||
@@ -1410,6 +1332,15 @@ static void notif_active_set(const char *owner)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* M40 F5/P5-P6:请求 App 在恢复前台后整帧重画一次(SDK present 层消费)。
|
||||||
|
* launcher 侧恢复的「底层帧」只能是快照;锁屏/菜单期间 App 若被冻结,它自己的 back_buf
|
||||||
|
* 仍是旧内容,只按脏区上屏会留下 PIN/菜单残影。写这个文件让 App dirty_full 一次。 */
|
||||||
|
static void meow_app_repaint_request(void)
|
||||||
|
{
|
||||||
|
int fd = open(MEOW_APP_REPAINT_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
if (fd >= 0) { (void)write(fd, "1", 1); close(fd); }
|
||||||
|
}
|
||||||
|
|
||||||
/* §10.5:App 前台顶部下滑 → 展开「由 launcher 渲染」的通知中心叠加会话。
|
/* §10.5:App 前台顶部下滑 → 展开「由 launcher 渲染」的通知中心叠加会话。
|
||||||
* 复用电源菜单对 App 的冻结模板(show_power_menu 的 SIGSTOP/读回/叠加/接管/恢复):
|
* 复用电源菜单对 App 的冻结模板(show_power_menu 的 SIGSTOP/读回/叠加/接管/恢复):
|
||||||
* ① SIGSTOP App → ② 写 /tmp/meow_notif_active(+ /tmp/meow_power_menu_active 让 watchdog 让位)
|
* ① SIGSTOP App → ② 写 /tmp/meow_notif_active(+ /tmp/meow_power_menu_active 让 watchdog 让位)
|
||||||
@@ -1548,6 +1479,9 @@ static int run_app_lock_overlay(struct app *a, pid_t pid)
|
|||||||
|
|
||||||
pm_capture_app_frame();
|
pm_capture_app_frame();
|
||||||
meow_ui_flush();
|
meow_ui_flush();
|
||||||
|
/* M40 F5/P5-P6:先写整帧重画请求,再 SIGCONT/放行——App 恢复后的第一次 present 会
|
||||||
|
* 消费它并 dirty_full(),用自身完整帧盖掉 launcher 快照恢复可能留下的任何残影。 */
|
||||||
|
meow_app_repaint_request();
|
||||||
if (!keep_running) {
|
if (!keep_running) {
|
||||||
/* §310 输入穿透定案(overlay_gate.h):App 被 SIGSTOP 冻结过 → 在 /tmp/meow_notif_active
|
/* §310 输入穿透定案(overlay_gate.h):App 被 SIGSTOP 冻结过 → 在 /tmp/meow_notif_active
|
||||||
* 仍未更新时**先 SIGCONT**,让 SDK 输入线程排空冻结期缓冲的 evdev 事件副本 + 对齐
|
* 仍未更新时**先 SIGCONT**,让 SDK 输入线程排空冻结期缓冲的 evdev 事件副本 + 对齐
|
||||||
@@ -1555,10 +1489,10 @@ static int run_app_lock_overlay(struct app *a, pid_t pid)
|
|||||||
* 防解锁瞬间把积累的触摸回放成 App 内点击(锁屏穿透→「启动奇怪界面」→ 状态栏消失的触发源)。 */
|
* 防解锁瞬间把积累的触摸回放成 App 内点击(锁屏穿透→「启动奇怪界面」→ 状态栏消失的触发源)。 */
|
||||||
kill(pid, SIGCONT);
|
kill(pid, SIGCONT);
|
||||||
usleep(OVERLAY_DRAIN_WINDOW_MS * 1000);
|
usleep(OVERLAY_DRAIN_WINDOW_MS * 1000);
|
||||||
logmsg("lock-overlay: closed (lr=%d) -> SIGCONT app, drain %dms, then mark fg",
|
logmsg("lock-overlay: closed (lr=%d) -> SIGCONT app, drain %dms, repaint req, then mark fg",
|
||||||
lr, OVERLAY_DRAIN_WINDOW_MS);
|
lr, OVERLAY_DRAIN_WINDOW_MS);
|
||||||
} else {
|
} else {
|
||||||
logmsg("lock-overlay: closed (lr=%d) -> app never stopped (media keep-alive), no SIGCONT", lr);
|
logmsg("lock-overlay: closed (lr=%d) -> app never stopped (media keep-alive), no SIGCONT, repaint req", lr);
|
||||||
}
|
}
|
||||||
/* 标记翻回前台归属(a->id):本 App 解除自抑恢复前台;后台常驻 App(如 ncm)随之继续自抑
|
/* 标记翻回前台归属(a->id):本 App 解除自抑恢复前台;后台常驻 App(如 ncm)随之继续自抑
|
||||||
* (不叠帧 / 不穿输入——修复「锁屏解锁后 ncm 歌单帧叠加在 Hello 上 + 交互双响应」)。 */
|
* (不叠帧 / 不穿输入——修复「锁屏解锁后 ncm 歌单帧叠加在 Hello 上 + 交互双响应」)。 */
|
||||||
@@ -1573,6 +1507,72 @@ static int run_app_lock_overlay(struct app *a, pid_t pid)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* M40 F2/P2:App 前台「电源菜单」会话——补齐 §10.5 协议。
|
||||||
|
* 旧实现(M2.24)只有 SIGSTOP → show_power_menu → SIGCONT:
|
||||||
|
* - 没有 notif_active 抑制标记(后台常驻 App 可能在这期间上屏/穿输入);
|
||||||
|
* - 没有帧快照/恢复(菜单把「压暗 App 帧 + 菜单卡」present 后,关闭时 launcher 又
|
||||||
|
* pm_capture_app_frame() 把**菜单帧**拍回 back_buf 再上屏 → 屏幕仍是菜单画面,
|
||||||
|
* 用户看到「按了没反应」,而 App 只按脏区重绘、永远不会重画整屏(P2 主因);
|
||||||
|
* - 没有 SIGCONT→300ms 排空→标记翻回前台 的输入隔离(冻结期触摸回放)。
|
||||||
|
* show_power_menu() 现在会在所有退出路径恢复进入前的背景帧(power_menu.c),所以这里
|
||||||
|
* 的 pm_capture_app_frame() 读到的是 App 帧而不是菜单帧。
|
||||||
|
* 返回:0=取消(继续 App);1=已执行动作(调用方退出循环);2=App 在菜单期死亡。 */
|
||||||
|
static int run_app_power_menu(struct app *a, pid_t pid)
|
||||||
|
{
|
||||||
|
int keep_running = meow_media_app_playing(a->id);
|
||||||
|
if (keep_running) {
|
||||||
|
logmsg("power-menu: open (app=%s pid=%d) PLAYING -> no SIGSTOP (SDK present suppressed)",
|
||||||
|
a->name, pid);
|
||||||
|
} else {
|
||||||
|
logmsg("power-menu: open (app=%s pid=%d) SIGSTOP", a->name, pid);
|
||||||
|
if (kill(pid, SIGSTOP) < 0) {
|
||||||
|
logmsg("power-menu: SIGSTOP %d fail: %s", pid, strerror(errno));
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
if (proc_is_stopped(pid)) break;
|
||||||
|
usleep(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 叠加期全抑制:菜单盖在前台 App + 后台常驻 App 上;同时写 POWMENU_ACTIVE 让 watchdog
|
||||||
|
* 对电源长按/强关让位(与通知中心/锁屏同款双标记)。 */
|
||||||
|
notif_active_set("");
|
||||||
|
int pfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
if (pfd >= 0) { write(pfd, "1", 1); close(pfd); }
|
||||||
|
|
||||||
|
meow_sys_light();
|
||||||
|
meow_theme_reload();
|
||||||
|
pm_capture_app_frame(); /* 背景 = App 帧(fb0 buffer0) */
|
||||||
|
|
||||||
|
int pm_ret = show_power_menu();
|
||||||
|
|
||||||
|
if (proc_is_zombie(pid)) {
|
||||||
|
/* App 在菜单期死亡:不 SIGCONT、不把菜单帧当 App 帧恢复;交回 waitpid 统一回收。 */
|
||||||
|
notif_active_set("");
|
||||||
|
unlink(POWMENU_ACTIVE);
|
||||||
|
logmsg("power-menu: app pid %d died during menu -> unified exit path", pid);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 恢复 App 帧(show_power_menu 已还原进入前背景;再拍一次兜底覆盖任何非标准退出路径),
|
||||||
|
* 然后按 §10.5:先请求整帧重画 → SIGCONT → 排空窗 → 标记翻回前台归属。 */
|
||||||
|
pm_capture_app_frame();
|
||||||
|
meow_ui_flush();
|
||||||
|
meow_app_repaint_request();
|
||||||
|
if (!keep_running) {
|
||||||
|
kill(pid, SIGCONT);
|
||||||
|
usleep(OVERLAY_DRAIN_WINDOW_MS * 1000);
|
||||||
|
logmsg("power-menu: closed (ret=%d) -> SIGCONT app, drain %dms, repaint req, then mark fg",
|
||||||
|
pm_ret, OVERLAY_DRAIN_WINDOW_MS);
|
||||||
|
} else {
|
||||||
|
logmsg("power-menu: closed (ret=%d) -> app never stopped (media keep-alive), no SIGCONT, repaint req",
|
||||||
|
pm_ret);
|
||||||
|
}
|
||||||
|
notif_active_set(a ? a->id : "");
|
||||||
|
unlink(POWMENU_ACTIVE);
|
||||||
|
return pm_ret;
|
||||||
|
}
|
||||||
|
|
||||||
/* launch_app_chain:launch_app / launch_activity 的共享核心。
|
/* launch_app_chain:launch_app / launch_activity 的共享核心。
|
||||||
* chain 非空 → 向子进程注入 MEOW_ACTIVITY/MEOW_CHAIN 并初始化 nav_stack(M3);空/NULL → 裸启动。 */
|
* chain 非空 → 向子进程注入 MEOW_ACTIVITY/MEOW_CHAIN 并初始化 nav_stack(M3);空/NULL → 裸启动。 */
|
||||||
static int launch_app_chain(struct app *a, const char *chain)
|
static int launch_app_chain(struct app *a, const char *chain)
|
||||||
@@ -1585,8 +1585,9 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
uint32_t pick_req = 0;
|
uint32_t pick_req = 0;
|
||||||
meow_ams_take_picker(pick_action, sizeof pick_action,
|
meow_ams_take_picker(pick_action, sizeof pick_action,
|
||||||
pick_caller, sizeof pick_caller, &pick_req);
|
pick_caller, sizeof pick_caller, &pick_req);
|
||||||
if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; }
|
|
||||||
meow_dep_clear_reason(); /* 每次 launch 入口清 stale 依赖阻断原因(防上一 app 残留) */
|
meow_dep_clear_reason(); /* 每次 launch 入口清 stale 依赖阻断原因(防上一 app 残留) */
|
||||||
|
g_launch_fail_reason[0] = 0; /* M6.40:同上,清 stale 启动失败原因 */
|
||||||
|
if (!a->ok) { logmsg("launch blocked: exec not ok"); return launch_fail(a, "no such file: %s", a->exec); }
|
||||||
|
|
||||||
/* M4 后台保活恢复:该 App 驻留后台(播放中经 nav-exit 转过)→ 原进程回前台,
|
/* M4 后台保活恢复:该 App 驻留后台(播放中经 nav-exit 转过)→ 原进程回前台,
|
||||||
* 不 fork/exec(进程仍存无法注入新 env:chain/picker 上下文忽略,记日志)。
|
* 不 fork/exec(进程仍存无法注入新 env:chain/picker 上下文忽略,记日志)。
|
||||||
@@ -1613,7 +1614,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
char dep_missing[32] = "";
|
char dep_missing[32] = "";
|
||||||
if (meow_check_app_depends(a, dep_missing, sizeof dep_missing) != 0) {
|
if (meow_check_app_depends(a, dep_missing, sizeof dep_missing) != 0) {
|
||||||
logmsg("launch blocked: dep '%s' missing (CANNOT START: %s)", dep_missing, a->name);
|
logmsg("launch blocked: dep '%s' missing (CANNOT START: %s)", dep_missing, a->name);
|
||||||
return -1;
|
return launch_fail(a, "missing dep: %s", dep_missing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* 冷启动:标记内容置为前台归属(a->id),让本 App 首帧即不被自抑(后台常驻 App 继续自抑)。
|
/* 冷启动:标记内容置为前台归属(a->id),让本 App 首帧即不被自抑(后台常驻 App 继续自抑)。
|
||||||
@@ -1641,7 +1642,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
* watchdog and waitpid — so an exec failure never leaves an orphan watchdog
|
* watchdog and waitpid — so an exec failure never leaves an orphan watchdog
|
||||||
* and never masquerades as "app ran and exited". */
|
* and never masquerades as "app ran and exited". */
|
||||||
int execpipe[2];
|
int execpipe[2];
|
||||||
if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return -1; }
|
if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return launch_fail(a, "exec pipe failed: %s", strerror(errno)); }
|
||||||
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
|
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
|
||||||
|
|
||||||
/* AMSD_DESIGN §5:stdout/stderr 管道捕获(logcat 思路)。fork 前建管道;子进程 dup2 到
|
/* AMSD_DESIGN §5:stdout/stderr 管道捕获(logcat 思路)。fork 前建管道;子进程 dup2 到
|
||||||
@@ -1661,7 +1662,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
close(execpipe[0]); close(execpipe[1]);
|
close(execpipe[0]); close(execpipe[1]);
|
||||||
if (out_pipe[0] >= 0) close(out_pipe[0]);
|
if (out_pipe[0] >= 0) close(out_pipe[0]);
|
||||||
if (out_pipe[1] >= 0) close(out_pipe[1]);
|
if (out_pipe[1] >= 0) close(out_pipe[1]);
|
||||||
logmsg("fork failed: %s", strerror(errno)); return -1;
|
logmsg("fork failed: %s", strerror(errno)); return launch_fail(a, "fork failed: %s", strerror(errno));
|
||||||
}
|
}
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
/* child: exec the app. M2.12 注入 MEOW_LOG_DIR + MEOW_DATA_DIR/MEOW_CACHE_DIR
|
/* child: exec the app. M2.12 注入 MEOW_LOG_DIR + MEOW_DATA_DIR/MEOW_CACHE_DIR
|
||||||
@@ -1850,7 +1851,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
int st = 0;
|
int st = 0;
|
||||||
reap_wait(pid, &st); /* reap the failed child(有界) */
|
reap_wait(pid, &st); /* reap the failed child(有界) */
|
||||||
if (out_fd >= 0) close(out_fd);
|
if (out_fd >= 0) close(out_fd);
|
||||||
return -1;
|
return launch_fail(a, "exec failed: %s (errno %d)", strerror(exe_err), exe_err);
|
||||||
}
|
}
|
||||||
} /* end if (!resumed):冷启动分支(prepare/fork/exec/确认) */
|
} /* end if (!resumed):冷启动分支(prepare/fork/exec/确认) */
|
||||||
|
|
||||||
@@ -1929,6 +1930,8 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
int nav_clean_exit = 0; /* M3:MSG_NAV{EXIT} 主动退出 → 不按崩溃上报 */
|
int nav_clean_exit = 0; /* M3:MSG_NAV{EXIT} 主动退出 → 不按崩溃上报 */
|
||||||
int to_background = 0; /* M4:nav-exit 转后台保活(进程保留,跳过退出尾) */
|
int to_background = 0; /* M4:nav-exit 转后台保活(进程保留,跳过退出尾) */
|
||||||
int app_prev_vold = 0;
|
int app_prev_vold = 0;
|
||||||
|
int app_prev_volu = 0; /* E:VOL_UP 上一边沿(翻页门下边沿判定) */
|
||||||
|
int fg_volpage = 0; /* E:前台 App 是否声明 volume_page(会话期恒定) */
|
||||||
int app_seq_hits = 0;
|
int app_seq_hits = 0;
|
||||||
unsigned long long app_seq_last = 0;
|
unsigned long long app_seq_last = 0;
|
||||||
struct vol_snap app_vol0 = { -1, -1 };
|
struct vol_snap app_vol0 = { -1, -1 };
|
||||||
@@ -1938,6 +1941,11 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
* 修法:本循环以「背光==0 且非超时」补判手动息屏,唤醒时按 meow_auto_slept||manual 一并触发
|
* 修法:本循环以「背光==0 且非超时」补判手动息屏,唤醒时按 meow_auto_slept||manual 一并触发
|
||||||
* lck_wake_should_lock 锁屏,两条路径对齐。 */
|
* lck_wake_should_lock 锁屏,两条路径对齐。 */
|
||||||
int app_panel_manual_off = 0;
|
int app_panel_manual_off = 0;
|
||||||
|
/* M40 F3/P3:叠加层/电源菜单返回后的排空窗。watchdog 在电源菜单期间让位,菜单关闭后
|
||||||
|
* 被 SIGCONT 的 App 会把冻结期缓存的触摸重放成 /tmp/meow_app_activity;若把它当用户
|
||||||
|
* 触摸,会立刻「亮屏 + 弹锁屏」(P3 的 ACTIVITY 唤醒路径)。窗口内的标记只消费不唤醒。 */
|
||||||
|
unsigned long long overlay_return_ms = now_ms();
|
||||||
|
int overlay_return_valid = 0;
|
||||||
/* M2.11 D:应用二进制/资源若在卡上,卡被拔可能 SIGBUS 崩。启动前记录,运行期监测卡移除。
|
/* M2.11 D:应用二进制/资源若在卡上,卡被拔可能 SIGBUS 崩。启动前记录,运行期监测卡移除。
|
||||||
* (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */
|
* (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */
|
||||||
int app_on_card = (strncmp(a->exec, MEOWAPPS, strlen(MEOWAPPS)) == 0);
|
int app_on_card = (strncmp(a->exec, MEOWAPPS, strlen(MEOWAPPS)) == 0);
|
||||||
@@ -1961,13 +1969,23 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
g_sb_mode_last = -1; /* 强制首帧同步(进入新 App 会话清零,避免沿用上一 App 的缓存态) */
|
g_sb_mode_last = -1; /* 强制首帧同步(进入新 App 会话清零,避免沿用上一 App 的缓存态) */
|
||||||
unlink(MEOW_SBAR_MODE_FILE); /* 清可能残留的上一 App 顶条模式(崩溃/被杀未清),防新 App 误收合 */
|
unlink(MEOW_SBAR_MODE_FILE); /* 清可能残留的上一 App 顶条模式(崩溃/被杀未清),防新 App 误收合 */
|
||||||
if (!a->fullscreen) app_overlay_statusbar();
|
if (!a->fullscreen) app_overlay_statusbar();
|
||||||
/* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。 */
|
/* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。
|
||||||
|
* M40 F3:同时清 /tmp/meow_app_activity——上一会话(或启动手势)遗留的标记不能在本会话
|
||||||
|
* 首圈被当成「用户触摸」而触发息屏计时重置/唤醒+锁屏。 */
|
||||||
unlink(POWMENU_MARKER);
|
unlink(POWMENU_MARKER);
|
||||||
unlink(POWMENU_ACTIVE);
|
unlink(POWMENU_ACTIVE);
|
||||||
|
unlink(MEOW_APP_ACTIVITY_FILE);
|
||||||
/* §10.5:进入 App 前台会话。置位让 ipc_dispatch 的 MSG_NOTIF_OPEN 可入队(否则回 ERR);
|
/* §10.5:进入 App 前台会话。置位让 ipc_dispatch 的 MSG_NOTIF_OPEN 可入队(否则回 ERR);
|
||||||
* 清残留 pending(防上一会话未消费的 OPEN 被本会话误展开)。 */
|
* 清残留 pending(防上一会话未消费的 OPEN 被本会话误展开)。 */
|
||||||
g_app_session_active = 1;
|
g_app_session_active = 1;
|
||||||
meow_notif_open_clear();
|
meow_notif_open_clear();
|
||||||
|
/* E:系统级音量翻页——前台 App 声明 volume_page 时武装 sink(fd 就绪即生效;断线重连由
|
||||||
|
* 本循环每圈重试 meow_volpage_sink 幂等更新,keys.c 门只消费 VOL 按下且已武装)。 */
|
||||||
|
fg_volpage = meow_volpage_app_allows(a->id) ? 1 : 0;
|
||||||
|
if (fg_volpage) {
|
||||||
|
int vfd = meow_ipc_app_fd(a->id);
|
||||||
|
if (vfd >= 0) meow_volpage_sink(a->id, vfd);
|
||||||
|
}
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int r = waitpid(pid, &status, WNOHANG);
|
int r = waitpid(pid, &status, WNOHANG);
|
||||||
if (r == pid) { ams_status_valid = 1; break; }
|
if (r == pid) { ams_status_valid = 1; break; }
|
||||||
@@ -2025,6 +2043,8 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
m_notif_open_t nopen;
|
m_notif_open_t nopen;
|
||||||
if (meow_notif_open_take(&nopen)) {
|
if (meow_notif_open_take(&nopen)) {
|
||||||
int nr = run_app_notif_overlay(a, pid);
|
int nr = run_app_notif_overlay(a, pid);
|
||||||
|
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点(activity 标记忽略窗) */
|
||||||
|
overlay_return_valid = 1;
|
||||||
/* 输入基线重播(与电源菜单同款):面板期间按键可能已变化,重播防误判 key_edge */
|
/* 输入基线重播(与电源菜单同款):面板期间按键可能已变化,重播防误判 key_edge */
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
@@ -2057,14 +2077,43 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
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_VOLUMEDOWN % 32))) vold = 1;
|
||||||
}
|
}
|
||||||
|
/* E:系统级音量翻页——前台 App 声明 volume_page 时,VOL_UP/VOL_DOWN 按下边沿 →
|
||||||
|
* 系统翻页信号(keys.c 门,已武装才消费下发;未武装返回 0 放行,音量/物理键语义全保留)。
|
||||||
|
* 每圈幂等重试武装(reader 持久连接 HELLO 后 fd 就绪;断线重连换 fd 自动更新)。 */
|
||||||
|
if (fg_volpage) {
|
||||||
|
int vfd = meow_ipc_app_fd(a->id);
|
||||||
|
if (vfd >= 0) meow_volpage_sink(a->id, vfd);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
int volu = 0;
|
||||||
|
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||||
|
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||||
|
if (volu && !app_prev_volu) (void)meow_volpage_key(KEY_VOLUMEUP, 1);
|
||||||
|
if (vold && !app_prev_vold) (void)meow_volpage_key(KEY_VOLUMEDOWN, 1);
|
||||||
|
app_prev_volu = volu;
|
||||||
|
}
|
||||||
/* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏;
|
/* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏;
|
||||||
* 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。
|
* 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。
|
||||||
* M6.5 §310 问题1:watchdog 电源键手动息屏(背光 0、不置 meow_auto_slept)→ 唤醒时按
|
* M6.5 §310 问题1:watchdog 电源键手动息屏(背光 0、不置 meow_auto_slept)→ 唤醒时按
|
||||||
* app_panel_manual_off 一并锁屏,与超时路径(meow_auto_slept)对齐。 */
|
* app_panel_manual_off 一并锁屏,与超时路径(meow_auto_slept)对齐。 */
|
||||||
{
|
{
|
||||||
/* 背光==0 => 面板已关(超时路径已置 auto_slept;手动路径靠此补判)。
|
/* M40 F3:去掉单向闩锁。旧实现只在 watchdog 手动息屏时置 1、只有本循环唤醒分支清 1;
|
||||||
* read_brightness() 失败返回 -1(≠0),不误置位。 */
|
* watchdog 之后把屏点亮(pwr_woke)闩锁仍是 1 → 任何 activity 标记都会再次
|
||||||
if (read_brightness() == 0) app_panel_manual_off = 1;
|
* 「亮屏+锁屏」(P3 放大器)。改为每圈按真实背光复位:
|
||||||
|
* - 背光==0 → 置 1(手动/超时熄屏都算);
|
||||||
|
* - 背光>0 且**没有任何键按住** → 复位 0(屏已被 watchdog 点亮,不再残留闩锁);
|
||||||
|
* - 背光>0 但电源键仍按住(本次唤醒按压)→ 保留一拍,让下面的 key-wake 分支仍能
|
||||||
|
* 按 lck_wake_should_lock 弹锁屏(不牺牲「手动息屏后电源唤醒要锁屏」语义)。
|
||||||
|
* read_brightness() 失败返回 -1(≠0),不误置/误清。 */
|
||||||
|
int bri_now = read_brightness();
|
||||||
|
if (bri_now == 0) {
|
||||||
|
app_panel_manual_off = 1;
|
||||||
|
} else if (bri_now > 0) {
|
||||||
|
int any_key_down = 0;
|
||||||
|
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||||
|
if (kp_power.cur[w] || kp_keys.cur[w]) { any_key_down = 1; break; }
|
||||||
|
if (!any_key_down) app_panel_manual_off = 0;
|
||||||
|
}
|
||||||
int key_edge = 0;
|
int key_edge = 0;
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||||
if (kp_power.cur[w] != save_pow[w] || kp_keys.cur[w] != save_keys[w]) { key_edge = 1; break; }
|
if (kp_power.cur[w] != save_pow[w] || kp_keys.cur[w] != save_keys[w]) { key_edge = 1; break; }
|
||||||
@@ -2094,6 +2143,8 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
* (launcher 不崩、run_menu 重绘主屏),同 run_app_notif_overlay 的 died 分支。 */
|
* (launcher 不崩、run_menu 重绘主屏),同 run_app_notif_overlay 的 died 分支。 */
|
||||||
if (lck_wake_should_lock()) {
|
if (lck_wake_should_lock()) {
|
||||||
int lr2 = run_app_lock_overlay(a, pid);
|
int lr2 = run_app_lock_overlay(a, pid);
|
||||||
|
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点 */
|
||||||
|
overlay_return_valid = 1;
|
||||||
if (lr2 != 0) {
|
if (lr2 != 0) {
|
||||||
reap_wait(pid, &status);
|
reap_wait(pid, &status);
|
||||||
ams_status_valid = 1;
|
ams_status_valid = 1;
|
||||||
@@ -2114,11 +2165,15 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* M2.71-bug5:App 前台触摸/交互(launcher 看不到应用内触摸,SDK 写标记)→ 重置自动息屏
|
/* M2.71-bug5:App 前台触摸/交互(launcher 看不到应用内触摸,SDK 写标记)→ 重置自动息屏
|
||||||
* 计时;若已自动息屏,按 key-wake 同款路径唤醒(面板上电 + 落 buffer0)。 */
|
* 计时;若已自动息屏,按 key-wake 同款路径唤醒(面板上电 + 落 buffer0)。
|
||||||
|
* M40 F3:叠加层/电源菜单刚返回的排空窗内,该标记是 App 被 SIGCONT 后重放的冻结期
|
||||||
|
* 输入,只消费不唤醒(否则「亮屏 + 弹 PIN」,P3)。 */
|
||||||
if (access(MEOW_APP_ACTIVITY_FILE, F_OK) == 0) {
|
if (access(MEOW_APP_ACTIVITY_FILE, F_OK) == 0) {
|
||||||
unlink(MEOW_APP_ACTIVITY_FILE);
|
unlink(MEOW_APP_ACTIVITY_FILE);
|
||||||
app_last_input = now_ms();
|
app_last_input = now_ms();
|
||||||
if (meow_auto_slept || app_panel_manual_off) {
|
if (overlay_activity_drain_active(overlay_return_valid, overlay_return_ms, now_ms())) {
|
||||||
|
logmsg("activity marker consumed in overlay drain window -> no wake");
|
||||||
|
} else if (meow_auto_slept || app_panel_manual_off) {
|
||||||
logmsg("screen-off (app): touch wake (auto=%d manual=%d)",
|
logmsg("screen-off (app): touch wake (auto=%d manual=%d)",
|
||||||
meow_auto_slept, app_panel_manual_off);
|
meow_auto_slept, app_panel_manual_off);
|
||||||
meow_panel_on();
|
meow_panel_on();
|
||||||
@@ -2129,6 +2184,8 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
* §310:叠加期 App 崩溃 → 回收僵尸走统一退出路径(launcher 不崩、run_menu 重绘主屏)。 */
|
* §310:叠加期 App 崩溃 → 回收僵尸走统一退出路径(launcher 不崩、run_menu 重绘主屏)。 */
|
||||||
if (lck_wake_should_lock()) {
|
if (lck_wake_should_lock()) {
|
||||||
int lr2 = run_app_lock_overlay(a, pid);
|
int lr2 = run_app_lock_overlay(a, pid);
|
||||||
|
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点 */
|
||||||
|
overlay_return_valid = 1;
|
||||||
if (lr2 != 0) {
|
if (lr2 != 0) {
|
||||||
reap_wait(pid, &status);
|
reap_wait(pid, &status);
|
||||||
ams_status_valid = 1;
|
ams_status_valid = 1;
|
||||||
@@ -2169,20 +2226,16 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
app_prev_vold = vold;
|
app_prev_vold = vold;
|
||||||
if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2)
|
if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2)
|
||||||
app_seq_hits = 0;
|
app_seq_hits = 0;
|
||||||
/* M2.24 全面按键接管:watchdog 检测到电源长按≥3s 写 /tmp/meow_power_menu → 此处消费:
|
/* M2.24/M40 全面按键接管:watchdog 检测到电源长按≥power.longpress_menu_ms 写
|
||||||
* SIGSTOP 应用冻结 → 写 powmenu_active 让 watchdog 让位 → 统一电源菜单(launcher 一处绘制,
|
* /tmp/meow_power_menu → 此处消费。M40 F2 起走 run_app_power_menu() 的完整 §10.5
|
||||||
* 复用 show_power_menu,含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)→ 菜单关 →
|
* 协议(SIGSTOP + 全抑制标记 + 帧恢复 + SIGCONT→排空→标记翻回前台 + 整帧重画请求),
|
||||||
* 清 active → SIGCONT 应用续跑。 */
|
* 不再只 SIGSTOP/show_power_menu/SIGCONT(旧实现关闭后屏幕仍是菜单帧 = P2 主因)。 */
|
||||||
if (access(POWMENU_MARKER, F_OK) == 0) {
|
if (access(POWMENU_MARKER, F_OK) == 0) {
|
||||||
unlink(POWMENU_MARKER);
|
unlink(POWMENU_MARKER);
|
||||||
logmsg("watchdog: power menu request (app %s) -> SIGSTOP + menu", a->name);
|
logmsg("watchdog: power menu request (app %s) -> overlay session", a->name);
|
||||||
kill(pid, SIGSTOP);
|
int pm_ret = run_app_power_menu(a, pid);
|
||||||
int mfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点(activity 标记忽略窗) */
|
||||||
if (mfd >= 0) { write(mfd, "1", 1); close(mfd); }
|
overlay_return_valid = 1;
|
||||||
int pm_ret = show_power_menu();
|
|
||||||
unlink(POWMENU_ACTIVE);
|
|
||||||
kill(pid, SIGCONT);
|
|
||||||
logmsg("watchdog: power menu closed (ret=%d) -> SIGCONT app", pm_ret);
|
|
||||||
/* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge
|
/* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge
|
||||||
* (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */
|
* (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
@@ -2190,6 +2243,11 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
memcpy(save_pow, kp_power.cur, sizeof save_pow);
|
memcpy(save_pow, kp_power.cur, sizeof save_pow);
|
||||||
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
|
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
|
||||||
app_last_input = now_ms(); /* M2.17:菜单交互即输入,重置应用会话自动息屏计时 */
|
app_last_input = now_ms(); /* M2.17:菜单交互即输入,重置应用会话自动息屏计时 */
|
||||||
|
if (pm_ret == 2) { /* App 在菜单期死亡:回收僵尸走统一退出路径 */
|
||||||
|
reap_wait(pid, &status);
|
||||||
|
ams_status_valid = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (pm_ret != 0) break; /* 关机/重启/回HibyOS:系统通常已 reboot,防御性退出 */
|
if (pm_ret != 0) break; /* 关机/重启/回HibyOS:系统通常已 reboot,防御性退出 */
|
||||||
}
|
}
|
||||||
/* 「同一个状态栏」闭环:常驻顶条叠加(agent-146 全屏收合接入)。
|
/* 「同一个状态栏」闭环:常驻顶条叠加(agent-146 全屏收合接入)。
|
||||||
@@ -2223,6 +2281,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
if (to_background) {
|
if (to_background) {
|
||||||
g_app_session_active = 0; /* 前台会话结束(ipc_dispatch 不再受理 NOTIF_OPEN) */
|
g_app_session_active = 0; /* 前台会话结束(ipc_dispatch 不再受理 NOTIF_OPEN) */
|
||||||
meow_notif_open_clear();
|
meow_notif_open_clear();
|
||||||
|
meow_volpage_sink_clear(); /* E:离开前台撤翻页 sink(音量键交还系统) */
|
||||||
if (wpid > 0) {
|
if (wpid > 0) {
|
||||||
/* 看门狗随前台会话结束回收(后台无手势监听;有界等防挂死) */
|
/* 看门狗随前台会话结束回收(后台无手势监听;有界等防挂死) */
|
||||||
kill(wpid, SIGTERM);
|
kill(wpid, SIGTERM);
|
||||||
@@ -2251,6 +2310,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
|||||||
/* §10.5:App 前台会话结束,清会话标记(ipc_dispatch 不再受理 MSG_NOTIF_OPEN) */
|
/* §10.5:App 前台会话结束,清会话标记(ipc_dispatch 不再受理 MSG_NOTIF_OPEN) */
|
||||||
g_app_session_active = 0;
|
g_app_session_active = 0;
|
||||||
meow_notif_open_clear();
|
meow_notif_open_clear();
|
||||||
|
meow_volpage_sink_clear(); /* E:离开前台撤翻页 sink(音量键交还系统) */
|
||||||
/* M2.17 兜底:App 会话结束(waitpid 回收/崩溃/被杀,回主界面)清 fg_act ——
|
/* M2.17 兜底:App 会话结束(waitpid 回收/崩溃/被杀,回主界面)清 fg_act ——
|
||||||
* App 在 keep_awake activity(如 ncm 播放页)被 OOM/崩溃杀死且未弹回 home 时,g_fg_act 会
|
* App 在 keep_awake activity(如 ncm 播放页)被 OOM/崩溃杀死且未弹回 home 时,g_fg_act 会
|
||||||
* 滞留 "<act>",使主页 fg_keep_awake()==1 不再自动息屏。此处兜底清空(App 已退出/死亡,
|
* 滞留 "<act>",使主页 fg_keep_awake()==1 不再自动息屏。此处兜底清空(App 已退出/死亡,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
*
|
*
|
||||||
* 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar:
|
* 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar:
|
||||||
* 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action,
|
* 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action,
|
||||||
* act=bgm_top_action → bgmgr_kill_all,**不杀框架/当前前台/非后台**,经 bgmgr_can_kill 门控)。
|
* act=bgm_top_action → bgmgr_kill_all,**不杀受保护/当前前台/非后台**,经 bgmgr_can_kill 门控)。
|
||||||
* 列表(live=1 约 2s 重扫,实时增删):
|
* 列表(live=1 约 2s 重扫,实时增删):
|
||||||
* - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮**
|
* - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮**
|
||||||
* (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。
|
* (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。
|
||||||
@@ -38,8 +38,9 @@ typedef struct {
|
|||||||
int napps;
|
int napps;
|
||||||
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
|
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
|
||||||
int n;
|
int n;
|
||||||
char toast[64];
|
/* 行副文本存储:sub_row_t.sub 被 SDK 行池/自绘在整页会话内持续读取,
|
||||||
long toast_ms;
|
* 不能指向 bgm_rows 的栈帧(曾用函数内 char sub[80] → 悬垂,页面副标题为乱码)。 */
|
||||||
|
char subs[MEOW_BGMR_ROW_MAX][80];
|
||||||
} bgm_ctx_t;
|
} bgm_ctx_t;
|
||||||
|
|
||||||
/* 找 struct app(launcher 应用表)by id */
|
/* 找 struct app(launcher 应用表)by id */
|
||||||
@@ -54,36 +55,44 @@ static struct app *bgm_app_find(struct app *apps, int napps, const char *app_id)
|
|||||||
/* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */
|
/* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */
|
||||||
static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid,
|
static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid,
|
||||||
char *name, int nl, char *icon, int il,
|
char *name, int nl, char *icon, int il,
|
||||||
char *framework, int *is_fg)
|
char *protected_app, int *is_fg)
|
||||||
{
|
{
|
||||||
(void)pid;
|
(void)pid;
|
||||||
bgm_ctx_t *b = ctx;
|
bgm_ctx_t *b = ctx;
|
||||||
struct app *a = bgm_app_find(b->apps, b->napps, app_id);
|
struct app *a = bgm_app_find(b->apps, b->napps, app_id);
|
||||||
*framework = 0;
|
*protected_app = 0;
|
||||||
*is_fg = 0;
|
*is_fg = 0;
|
||||||
if (!a) return -1; /* 未注册 → name 回退 app_id */
|
if (!a) return -1; /* 未注册 → name 回退 app_id */
|
||||||
if (a->name[0]) snprintf(name, nl, "%s", a->name);
|
if (a->name[0]) snprintf(name, nl, "%s", a->name);
|
||||||
if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : "");
|
if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : "");
|
||||||
*framework = (a->framework == '1') ? '1' : 0;
|
/* 刻意**不**用 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int bgm_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
static int bgm_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
int n = meow_bg_count();
|
/* 逐槽扫描并**压缩**(g_bg 可能有空洞),跳过已发出终止的条目:
|
||||||
if (n > max) n = max;
|
* meow_bg_terminate 只置 term_at_ms 发 SIGTERM,真正的 reap/摘表在 meow_bg_tick(launcher
|
||||||
for (int i = 0; i < n; i++) {
|
* 主循环),它在本页 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);
|
const meow_bg_entry_t *e = meow_bg_at(i);
|
||||||
if (!e || !e->used || !e->app_id[0]) continue;
|
if (!e || !e->used || !e->app_id[0]) continue;
|
||||||
memset(&rows[i], 0, sizeof rows[i]);
|
if (e->term_at_ms) continue; /* 已进入终止序列(kill/宽限回收/卡拔)→ 行不再显示 */
|
||||||
snprintf(rows[i].app_id, sizeof rows[i].app_id, "%s", e->app_id);
|
memset(&rows[k], 0, sizeof rows[k]);
|
||||||
rows[i].pid = e->pid;
|
snprintf(rows[k].app_id, sizeof rows[k].app_id, "%s", e->app_id);
|
||||||
rows[i].keepalive = (e->keepalive ? 1 : 0);
|
rows[k].pid = e->pid;
|
||||||
rows[i].on_card = (e->on_card ? 1 : 0);
|
rows[k].keepalive = (e->keepalive ? 1 : 0);
|
||||||
rows[i].bg_since_ms = e->bg_since_ms;
|
rows[k].on_card = (e->on_card ? 1 : 0);
|
||||||
|
rows[k].bg_since_ms = e->bg_since_ms;
|
||||||
|
k++;
|
||||||
}
|
}
|
||||||
return n;
|
return k;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* proc_sample:/proc/<pid>/stat(utime+stime 差分) + /proc/<pid>/status(VmRSS)。
|
/* proc_sample:/proc/<pid>/stat(utime+stime 差分) + /proc/<pid>/status(VmRSS)。
|
||||||
@@ -171,12 +180,13 @@ static int bgm_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < b->n && r < max; i++) {
|
for (int i = 0; i < b->n && r < max; i++) {
|
||||||
char sub[80];
|
/* 副文本写进 ctx 的持久缓冲(b->subs):sub_row_t.sub 会被 SDK 行池在整页会话内反复读取,
|
||||||
snprintf(sub, sizeof sub, "CPU %.1f%% · %s · %s", b->rows[i].pcpu,
|
* 指向本函数栈帧的 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]));
|
bgmgr_mem_str(b->rows[i].rss_kb), bgmgr_status_str(&b->rows[i]));
|
||||||
rbuf[r].kind = SUB_ROW;
|
rbuf[r].kind = SUB_ROW;
|
||||||
rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id;
|
rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id;
|
||||||
rbuf[r].sub = sub;
|
rbuf[r].sub = b->subs[i];
|
||||||
rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */
|
rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */
|
||||||
r++;
|
r++;
|
||||||
}
|
}
|
||||||
@@ -226,12 +236,16 @@ static void bgm_row_draw(void *ctx, int idx, sub_row_t *r,
|
|||||||
int ry = cy + (ch - (th + tsub + 4)) / 2; /* 内容在卡内垂直居中(行中心=卡中心) */
|
int ry = cy + (ch - (th + tsub + 4)) / 2; /* 内容在卡内垂直居中(行中心=卡中心) */
|
||||||
if (r->label) meow_ui_text(rx, ry, r->label, 20, mt->onSurface);
|
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);
|
if (r->sub) meow_ui_text(rx, ry + th + 4, r->sub, 16, mt->onSurfaceVariant);
|
||||||
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化 */
|
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化。
|
||||||
|
* 不可杀行(受保护/前台/无进程,bgmgr_can_kill=0)→ 置灰禁点外观,避免误导(按钮可点但不可杀)。 */
|
||||||
int bx, by, bw, bh; bgm_btn_rect(x, y, w, h, &bx, &by, &bw, &bh);
|
int bx, by, bw, bh; bgm_btn_rect(x, y, w, h, &bx, &by, &bw, &bh);
|
||||||
meow_ui_draw_rect_round(bx, by, bw, bh, bh / 2, mt->error);
|
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);
|
int bth = meow_ui_text_height(16);
|
||||||
if (bth > bh) bth = bh;
|
if (bth > bh) bth = bh;
|
||||||
meow_ui_text_center(bx + bw / 2, by + (bh - bth) / 2, "杀", 16, mt->error);
|
meow_ui_text_center(bx + bw / 2, by + (bh - bth) / 2, "杀", 16, btn_col);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 行内自定义命中(sub_page_t.row_tap;仅按钮区 kill,行体不响应/不误杀)---- */
|
/* ---- 行内自定义命中(sub_page_t.row_tap;仅按钮区 kill,行体不响应/不误杀)---- */
|
||||||
@@ -245,13 +259,16 @@ static int bgm_row_tap(void *ctx, int idx, sub_row_t *r, int x, int y,
|
|||||||
int bx, by, bw, bh; bgm_btn_rect(rx, ry, rw, rh, &bx, &by, &bw, &bh);
|
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 (x < bx || x >= bx + bw || y < by || y >= by + bh) return 0; /* 行体/按钮外 → 不杀 */
|
||||||
if (!bgmgr_can_kill(&b->rows[i])) {
|
if (!bgmgr_can_kill(&b->rows[i])) {
|
||||||
snprintf(b->toast, sizeof b->toast, "该 App 不可杀(框架/前台)");
|
/* 禁点:受保护/前台/无进程 → 按钮已置灰,不 kill(返回 1=重绘)。
|
||||||
b->toast_ms = (long)now_ms();
|
* 留一行日志:「按了没反应」当年因为没有这行日志,只能靠翻 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;
|
return 1;
|
||||||
}
|
}
|
||||||
meow_bg_terminate(b->rows[i].app_id);
|
int rc = meow_bg_terminate(b->rows[i].app_id); /* 置 term_at_ms → bgm_enumerate 下一跳行消失(可见反馈) */
|
||||||
snprintf(b->toast, sizeof b->toast, "已请求终止 %s", b->rows[i].app_id);
|
if (rc != 0)
|
||||||
b->toast_ms = (long)now_ms();
|
logmsg("bgpage: kill app=%s 未命中后台表 rc=%d(列表/后台表不同步)", b->rows[i].app_id, rc);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,11 +287,12 @@ static int bgm_top_action(void *ctx)
|
|||||||
memset(&ops, 0, sizeof ops);
|
memset(&ops, 0, sizeof ops);
|
||||||
ops.ctx = b;
|
ops.ctx = b;
|
||||||
ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */
|
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);
|
int k = bgmgr_kill_all(&ops, b->rows, b->n, bgm_kill_fn);
|
||||||
if (k > 0) snprintf(b->toast, sizeof b->toast, "已清 %d 个后台 App", k);
|
if (k < b->n)
|
||||||
else snprintf(b->toast, sizeof b->toast, "无后台 App 可清");
|
logmsg("bgpage: 一键清除 杀 %d/%d(其余为前台/无进程/受保护)", k, b->n);
|
||||||
b->toast_ms = (long)now_ms();
|
return 1; /* 重绘:重扫行(已清行消失) */
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 入口:运行后台管理页(阻塞到返回)---- */
|
/* ---- 入口:运行后台管理页(阻塞到返回)---- */
|
||||||
|
|||||||
@@ -17,8 +17,14 @@
|
|||||||
*
|
*
|
||||||
* 【扫描节流(§七)】进页 nm_bt_scan_on、退页/超时 nm_bt_scan_off;7s 节流由 netmgrd 统一控。
|
* 【扫描节流(§七)】进页 nm_bt_scan_on、退页/超时 nm_bt_scan_off;7s 节流由 netmgrd 统一控。
|
||||||
*
|
*
|
||||||
* 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。开关写 network.bluetooth_enabled +
|
* 【M47 开关可信化】顶栏开关的显示态只来自真实态查询(daemon `bt status` 的 powered,
|
||||||
* netmgrd on/off;连接/断开/配对经 netmgrd(netmgr_ipc.c nm_bt_* 封装)。
|
* 不可达时直连 hciconfig 探测),三态:-1 未知 / 0 关 / 1 开;探测失败=未知(画「?」,不当已关)。
|
||||||
|
* settings 的 network.bluetooth_enabled 降级为「意图持久化」,不再决定开关外观。点击后先出示
|
||||||
|
* 「正在开启/关闭…(等待设备确认)」,下发命令 → 有界轮询真实态收敛 → 用真实态定稿;失败/超时
|
||||||
|
* 回滚并弹窗告知(不许乐观显示成功)。
|
||||||
|
*
|
||||||
|
* 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。连接/断开/配对经 netmgrd
|
||||||
|
* (netmgr_ipc.c nm_bt_* 封装)。
|
||||||
*/
|
*/
|
||||||
#include "meow_launcher_internal.h"
|
#include "meow_launcher_internal.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -42,18 +48,40 @@ typedef struct {
|
|||||||
meow_net_t near[S_NET_MAX];
|
meow_net_t near[S_NET_MAX];
|
||||||
bt_ep_t a2dp[S_NET_MAX];
|
bt_ep_t a2dp[S_NET_MAX];
|
||||||
int conn_n, paired_n, near_n, a2dp_n;
|
int conn_n, paired_n, near_n, a2dp_n;
|
||||||
int bt_on;
|
int bt_on; /* 真实态三态(-1 未知 / 0 关 / 1 开)——只由 bt_state_now() 写 */
|
||||||
|
unsigned long long bt_on_ts; /* 上面这次查询的时刻(rows/top_sw 同一 tick 只查一次) */
|
||||||
int output_bt; /* 0=DAC/本地,1=蓝牙 */
|
int output_bt; /* 0=DAC/本地,1=蓝牙 */
|
||||||
int full; /* 1=本次 rows() 全量重扫(conn/paired/a2dp);0=仅附近(缓存)+角色(节流) */
|
int full; /* 1=本次 rows() 全量重扫(conn/paired/a2dp);0=仅附近(缓存)+角色(节流) */
|
||||||
char mode[48]; /* bt_role 中文描述(次行摘要) */
|
char mode[48]; /* bt_role 中文描述(次行摘要) */
|
||||||
/* 行副文本池:sub_page_t 行 label/sub 为 const char*,跨 rows() 返回后仍须有效(直到下次
|
/* 行副文本池:sub_page_t 行 label/sub 为 const char*,跨 rows() 返回后仍须有效(直到下次
|
||||||
* rows() 重填)。本地栈缓冲会悬垂(agent-396 根因同类),故格式化 sub 一律写进本池。 */
|
* rows() 重填)。本地栈缓冲会悬垂(agent-396 根因同类),故格式化 sub 一律写进本池。 */
|
||||||
char pool[512];
|
char pool[768];
|
||||||
int pool_off;
|
int pool_off;
|
||||||
} bt_ctx_t;
|
} bt_ctx_t;
|
||||||
|
|
||||||
|
/* M47:settings 里的网络开关布尔值降级为「用户意图的持久化」,不再参与显示判定;
|
||||||
|
* 显示态一律来自 nm_bt_power_state()。 */
|
||||||
|
extern int nm_bt_power_state(void); /* netmgr_ipc.c:daemon 权威 → 直连探测,三态 */
|
||||||
|
|
||||||
/* 格式化并拷入 ctx 行副文本池(返回稳定指针;溢出则绕回——512B 远超单页行数所需)。 */
|
/* 开关收敛参数(宿主测试可在 include 前改小,见 test_bt_page.c)。
|
||||||
|
* 上限从**点击时刻**起算(评审 r1 P2-3),故它约束的是「从点击到出结论」的总时长:
|
||||||
|
* - 快路径(daemon 正常,`bt status` 本地 IPC 几 ms):4s 内约 16 次探测,通常 1~2s 收敛;
|
||||||
|
* - 慢路径(netmgrd 卡住:on/off 读超时 1.5s → 直连 s_net_toggle_bt 的 hciconfig 0.3~2s;
|
||||||
|
* 之后一次探测 = status 1.5s 超时 + hciconfig 兜底 1.8s ≈ 3.3s):总阻塞最坏
|
||||||
|
* ≈3.5s(下发) + 3.3s(最后一次探测) ≈ **7s**,预算内只够 1 次探测。此时能确认的只是
|
||||||
|
* 「探测超时」,**不能**宣称「设备无响应」——弹窗文案按此区分(「无法确认当前状态」)。
|
||||||
|
* 250ms 只是快路径的轮询间隔(不 20ms 级忙轮询);慢路径由探测自身耗时决定。 */
|
||||||
|
#ifndef BT_TOGGLE_SETTLE_MS
|
||||||
|
#define BT_TOGGLE_SETTLE_MS 4000
|
||||||
|
#endif
|
||||||
|
#ifndef BT_TOGGLE_POLL_MS
|
||||||
|
#define BT_TOGGLE_POLL_MS 250
|
||||||
|
#endif
|
||||||
|
#define BT_STATE_TTL_MS 800 /* 真实态缓存:rows/top_sw 同一帧共用一次查询 */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 格式化并拷入 ctx 行副文本池(返回稳定指针;溢出则绕回——768B 远超单页行数所需)。 */
|
||||||
static char *bt_pool(bt_ctx_t *c, const char *fmt, ...)
|
static char *bt_pool(bt_ctx_t *c, const char *fmt, ...)
|
||||||
{
|
{
|
||||||
va_list ap; char tmp[128];
|
va_list ap; char tmp[128];
|
||||||
@@ -95,27 +123,127 @@ static const char *bt_conn_name(const bt_ctx_t *c, const char *mac)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 顶栏开关当前值 */
|
/* ===== M47:开关「意图 → 真实态」收敛判定(纯函数;无 I/O、无 UI、无全局态)=====
|
||||||
|
* 输入:intent(1=要开/0=要关)、last_real(下发前最后一次可信真实态,-1 未知)、
|
||||||
|
* dispatch_ok(下发命令是否成功)、probe(有界等待后的探测结果,-1 未知)、
|
||||||
|
* timed_out(是否已到等待上限)。
|
||||||
|
* 输出(四个访问器共用同一套判定,拆开只为调用处可读):
|
||||||
|
* meow_toggle_display() 应显示的态(-1 未知 / 0 / 1):**只认 probe**。probe 未知 → -1,
|
||||||
|
* 不许回退 last_real 冒充确定态(评审 r1 P2-2:从没被确认过的旧值
|
||||||
|
* 一旦显示成确定态,就与本单「探测失败=未知」的契约自相矛盾)。
|
||||||
|
* meow_toggle_pending() 1=还要继续等:下发成功、未超时、且 probe 未等于意图。
|
||||||
|
* 注意「probe 未知」也算没收敛——绝不因为 last_real 恰好等于意图就收工。
|
||||||
|
* meow_toggle_rollback() 1=意图未达成(显示态 != 意图)→ UI 必须回滚到真实态。
|
||||||
|
* meow_toggle_error() 1=收尾后仍未确认 → 必须给用户可见的错误提示(不许静默失败)。
|
||||||
|
* last_real 不参与状态判定:它只由调用方用于弹窗文案(「上次已知为开启,当前无法确认」),
|
||||||
|
* 以及后续异步化时判断「回滚到哪」;保留在接口里是因为它是本单约定的输入维度。
|
||||||
|
* 判定矩阵见 test_bt_page.c「收敛矩阵」;wifi_page.c 复用同一实现。 */
|
||||||
|
static void mtog_calc(int intent, int last_real, int dispatch_ok, int probe, int timed_out,
|
||||||
|
int *disp, int *pending, int *rollback, int *error)
|
||||||
|
{
|
||||||
|
int want = intent ? 1 : 0;
|
||||||
|
int d = (probe >= 0) ? (probe ? 1 : 0) : -1; /* 未确认 = 未知,绝不拿旧值顶替 */
|
||||||
|
int confirmed = (probe >= 0) && ((probe ? 1 : 0) == want);
|
||||||
|
(void)last_real; /* 仅弹窗文案用,见函数头注释 */
|
||||||
|
*disp = d;
|
||||||
|
*pending = (dispatch_ok && !timed_out && !confirmed) ? 1 : 0;
|
||||||
|
*rollback = (d == want) ? 0 : 1;
|
||||||
|
*error = (!confirmed && !*pending) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_toggle_display(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||||||
|
{
|
||||||
|
int d, p, r, e;
|
||||||
|
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
int meow_toggle_pending(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||||||
|
{
|
||||||
|
int d, p, r, e;
|
||||||
|
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
int meow_toggle_rollback(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||||||
|
{
|
||||||
|
int d, p, r, e;
|
||||||
|
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
int meow_toggle_error(int intent, int last_real, int dispatch_ok, int probe, int timed_out)
|
||||||
|
{
|
||||||
|
int d, p, r, e;
|
||||||
|
mtog_calc(intent, last_real, dispatch_ok, probe, timed_out, &d, &p, &r, &e);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 真实态(带 800ms 缓存:top_sw 与 rows 在同一帧各查一次,别为同一问题问 daemon 两遍)。 */
|
||||||
|
static int bt_state_now(bt_ctx_t *c, int force)
|
||||||
|
{
|
||||||
|
unsigned long long t = now_ms();
|
||||||
|
if (!force && c->bt_on_ts && t - c->bt_on_ts < BT_STATE_TTL_MS) return c->bt_on;
|
||||||
|
c->bt_on = nm_bt_power_state();
|
||||||
|
c->bt_on_ts = t;
|
||||||
|
return c->bt_on;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶栏开关当前值:只报真实态(-1=未知,subpage 画「?」第三态) */
|
||||||
static int bt_top_sw(void *ctx, int *on)
|
static int bt_top_sw(void *ctx, int *on)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
bt_ctx_t *c = ctx;
|
||||||
int v = 0;
|
*on = bt_state_now(c, 0);
|
||||||
if (meow_settings_get_bool("network.bluetooth_enabled", &v) != 0) v = 0;
|
|
||||||
*on = v;
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶栏开关切换:写设置 + on/off(netmgrd IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */
|
/* 顶栏开关切换:落盘意图 → 下发 → 有界等待真实态收敛 → 用真实态定稿/回滚 + 可见错误提示。
|
||||||
|
* 绝不在下发后就直接把 UI 设成用户想要的样子(那是「显示意图」而不是「显示状态」)。 */
|
||||||
static void bt_top_sw_set(void *ctx, int on)
|
static void bt_top_sw_set(void *ctx, int on)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
bt_ctx_t *c = ctx;
|
||||||
meow_settings_set_bool("network.bluetooth_enabled", on);
|
unsigned long long t_click = now_ms(); /* 上限从点击起算(含下发耗时,见参数注释) */
|
||||||
nm_bt_onoff(on);
|
int want = on ? 1 : 0;
|
||||||
g_sys.bt_on = on;
|
int last_real = bt_state_now(c, 0);
|
||||||
bt_role_refresh(1);
|
meow_settings_set_bool("network.bluetooth_enabled", want); /* 意图持久化 */
|
||||||
paint_sb_right();
|
|
||||||
meow_settings_save();
|
meow_settings_save();
|
||||||
if (on) nm_bt_scan_on();
|
int dispatch_ok = (nm_bt_onoff(want) == MEOW_OK);
|
||||||
|
unsigned long long deadline = t_click + BT_TOGGLE_SETTLE_MS;
|
||||||
|
int probe = bt_state_now(c, 1), timed_out = 0;
|
||||||
|
while (meow_toggle_pending(want, last_real, dispatch_ok, probe, timed_out)) {
|
||||||
|
if (now_ms() >= deadline) { timed_out = 1; break; }
|
||||||
|
usleep(BT_TOGGLE_POLL_MS * 1000);
|
||||||
|
probe = bt_state_now(c, 1);
|
||||||
|
}
|
||||||
|
int disp = meow_toggle_display(want, last_real, dispatch_ok, probe, timed_out);
|
||||||
|
int rb = meow_toggle_rollback(want, last_real, dispatch_ok, probe, timed_out);
|
||||||
|
int err = meow_toggle_error(want, last_real, dispatch_ok, probe, timed_out);
|
||||||
|
unsigned long long elapsed = now_ms() - t_click;
|
||||||
|
c->bt_on = disp; c->bt_on_ts = now_ms();
|
||||||
|
/* 回滚语义:只在拿到**真实态**时才改写 settings(确认态或确认的相反态)。
|
||||||
|
* disp == -1(探测未知)时保留用户原意图不动——本键被 notif_center / settings_page 当
|
||||||
|
* 显示态读、被 app_launcher 用来决定自动拉起 WiFi,写 0 会把「不知道」污染成「确定已关」
|
||||||
|
* (评审 r1 P2-1)。 */
|
||||||
|
if (disp >= 0) {
|
||||||
|
meow_settings_set_bool("network.bluetooth_enabled", disp == 1);
|
||||||
|
meow_settings_save();
|
||||||
|
}
|
||||||
|
bt_role_refresh(1); /* 先刷新角色(它会写 g_sys.bt_on),再用真实态覆盖 */
|
||||||
|
if (disp >= 0) g_sys.bt_on = disp;
|
||||||
|
paint_sb_right();
|
||||||
|
if (disp == 1) nm_bt_scan_on();
|
||||||
|
logmsg("bt_page: toggle want=%d ok=%d probe=%d to=%d disp=%d rollback=%d err=%d elapsed=%llums",
|
||||||
|
want, dispatch_ok, probe, timed_out, disp, rb, err, elapsed);
|
||||||
|
if (err) {
|
||||||
|
char line[128];
|
||||||
|
const char *txt[1]; txt[0] = line;
|
||||||
|
/* 文案必须与结论同义:probe 未知 = 「无法确认」(不冒充未生效/未响应);
|
||||||
|
* probe 已知且 != 意图 = 确认未生效。last_real 只在这里出现。 */
|
||||||
|
if (disp < 0)
|
||||||
|
snprintf(line, sizeof line, "无法确认当前状态(上次已知:%s)",
|
||||||
|
last_real < 0 ? "未知" : (last_real ? "开启" : "关闭"));
|
||||||
|
else
|
||||||
|
snprintf(line, sizeof line, "切换未生效:当前仍为%s", disp ? "开启" : "关闭");
|
||||||
|
s_dd_modal("蓝牙", txt, 1, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +254,7 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
bt_ctx_t *c = ctx;
|
bt_ctx_t *c = ctx;
|
||||||
int full = c->full; c->full = 0; /* 消费一次全量标记 */
|
int full = c->full; c->full = 0; /* 消费一次全量标记 */
|
||||||
c->pool_off = 0; /* 行副文本池绕回(上轮行字符串已被重填替换) */
|
c->pool_off = 0; /* 行副文本池绕回(上轮行字符串已被重填替换) */
|
||||||
c->bt_on = nm_bt_powered(); /* IPC status;回退直连 hci_powered() */
|
c->bt_on = bt_state_now(c, 0); /* 真实态三态(未知名 -1);见文件头 M47 说明 */
|
||||||
c->output_bt = 0;
|
c->output_bt = 0;
|
||||||
if (meow_settings_get_bool("audio.output_bt", &c->output_bt) != 0) 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(全权威) */
|
if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired/a2dp(全权威) */
|
||||||
@@ -148,6 +276,13 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
|
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
|
||||||
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
|
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
|
||||||
}
|
}
|
||||||
|
/* 第三态显式呈现:真实态未知 ≠ 已关。顶栏开关此时也画「?」(subpage.c),这里再给一行
|
||||||
|
* 文字说明,避免用户把「不知道」读成「关着」。 */
|
||||||
|
if (c->bt_on < 0 && n < max) {
|
||||||
|
rbuf[n].kind = SUB_ROW; rbuf[n].label = "蓝牙状态未知";
|
||||||
|
rbuf[n].sub = "无法读取开关状态(服务/控制器无响应)";
|
||||||
|
rbuf[n].on = 0; rbuf[n].act = -2; n++;
|
||||||
|
}
|
||||||
|
|
||||||
/* ② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC) */
|
/* ② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC) */
|
||||||
{
|
{
|
||||||
@@ -270,10 +405,14 @@ static int bt_act(void *ctx, int idx, int act)
|
|||||||
char mac[24] = "";
|
char mac[24] = "";
|
||||||
/* 输出目标/编解码器:与本页设备行 act 段(CONN/PAIRED/NEAR base)不冲突,先处理 */
|
/* 输出目标/编解码器:与本页设备行 act 段(CONN/PAIRED/NEAR base)不冲突,先处理 */
|
||||||
if (act == BT_ACT_OUTPUT) {
|
if (act == BT_ACT_OUTPUT) {
|
||||||
|
/* 注意返回值语义:return 1 = 「请重绘/重填行」,**不是**「切换已生效」。本分支只写
|
||||||
|
* settings(audiod 仅启动时读 audio.output_bt),在线切需要 MSG_AUDIO_OUTPUT,
|
||||||
|
* 属 M46 输出目标单的范围(M45 finding: 20261111-survey-m45-btctl-bug-bt-settings-audiod)。
|
||||||
|
* 这里不假装成功:不发 IPC、不写 g_sys,只落偏好。 */
|
||||||
c->output_bt = !c->output_bt;
|
c->output_bt = !c->output_bt;
|
||||||
meow_settings_set_bool("audio.output_bt", c->output_bt);
|
meow_settings_set_bool("audio.output_bt", c->output_bt);
|
||||||
meow_settings_save();
|
meow_settings_save();
|
||||||
logmsg("bt_page: output_target=%s", c->output_bt ? "bt" : "dac");
|
logmsg("bt_page: output_target=%s (preference only; audiod reads at start)", c->output_bt ? "bt" : "dac");
|
||||||
return 1; /* 重绘 + 重填行 */
|
return 1; /* 重绘 + 重填行 */
|
||||||
}
|
}
|
||||||
if (act == BT_ACT_CODEC) {
|
if (act == BT_ACT_CODEC) {
|
||||||
|
|||||||
@@ -11,22 +11,59 @@
|
|||||||
# - launcher 与 SDK 共用同一 meow_font.o(已打进 meow_ui.a)——不再单独链 meow_font.a。
|
# - launcher 与 SDK 共用同一 meow_font.o(已打进 meow_ui.a)——不再单独链 meow_font.a。
|
||||||
# - MEOWOS_VERSION 0.7.21 → 0.7.23(M2.28)。
|
# - MEOWOS_VERSION 0.7.21 → 0.7.23(M2.28)。
|
||||||
#
|
#
|
||||||
# Build(在 patch_v2/meow_launcherd 下):
|
# 用法(在 patch_v2/meow_launcherd 下或任意目录):
|
||||||
# zig-mipsel-cc -I../sdk -Os -static main_menu.c statusbar.c fb_compositor.c settings_page.c \
|
# bash build_launcher.sh # 交叉编译 meow_launcherd(mipsel 静态)
|
||||||
# power_menu.c notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
|
# bash build_launcher.sh test # 宿主 gcc 跑本目录 affected 单测(test_bt_page / test_netmgr_ipc)
|
||||||
# font_theme.c keys.c util.c meow_touch.c meow_settings.c meow_notif.c \
|
#
|
||||||
# ../sdk/meow_ui.a <sysroot>/lib/libz.a -lpthread -o meow_launcherd
|
# 工具链:优先环境变量 ZIGCC / MIPS_SYSROOT(M47 起),否则退回历史固定路径 $HOME/Project/R3ProII。
|
||||||
|
# export ZIGCC=$HOME/.tools/zig-mipsel-cc
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
ROOT="$HOME/Project/R3ProII"
|
|
||||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
CC="${ZIGCC:-$HOME/.tools/zig-mipsel-cc}"
|
||||||
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
[ -x "$CC" ] || CC="$HOME/Project/R3ProII/.tools/zig-mipsel-cc"
|
||||||
|
SYSROOT="${MIPS_SYSROOT:-$HOME/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf}"
|
||||||
|
[ -f "$SYSROOT/lib/libz.a" ] || SYSROOT="$HOME/Project/R3ProII/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||||
|
|
||||||
|
# ---------------- 宿主单测(M47 起提供入口;只跑本目录受影响的测试)----------------
|
||||||
|
run_tests() {
|
||||||
|
local HOSTCC="${HOSTCC:-gcc}"
|
||||||
|
local TMPD rc=0
|
||||||
|
TMPD="$(mktemp -d "${TMPDIR:-/tmp}/meow_launcher_test.XXXXXX")"
|
||||||
|
trap 'rm -rf "$TMPD"' EXIT
|
||||||
|
|
||||||
|
echo "== host test: test_bt_page =="
|
||||||
|
if "$HOSTCC" -I"../sdk" -I. -Wall -Wextra -fsanitize=undefined \
|
||||||
|
test_bt_page.c -o "$TMPD/test_bt_page"; then
|
||||||
|
"$TMPD/test_bt_page" || rc=1
|
||||||
|
else
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== host test: test_netmgr_ipc =="
|
||||||
|
if "$HOSTCC" -I"../sdk" -I. -Wall -Wextra -c netmgr_ipc.c -o "$TMPD/netmgr_ipc.test.o" \
|
||||||
|
&& "$HOSTCC" -I"../sdk" -I. -Wall -Wextra \
|
||||||
|
test_netmgr_ipc.c "$TMPD/netmgr_ipc.test.o" -lpthread -o "$TMPD/test_netmgr_ipc"; then
|
||||||
|
"$TMPD/test_netmgr_ipc" || rc=1
|
||||||
|
else
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then echo "== ALL TESTS PASS =="; else echo "!! TESTS FAILED"; fi
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$1" = "test" ]; then
|
||||||
|
run_tests
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
[ -f "../sdk/meow_ui.a" ] || { echo "!! 缺 ../sdk/meow_ui.a,先跑 sdk/build_sdk.sh"; exit 1; }
|
[ -f "../sdk/meow_ui.a" ] || { echo "!! 缺 ../sdk/meow_ui.a,先跑 sdk/build_sdk.sh"; exit 1; }
|
||||||
"$CC" -I"../sdk" -Os -static \
|
"$CC" -I"../sdk" -Os -static -Wall -Wextra \
|
||||||
main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \
|
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 \
|
notif_center.c app_launcher.c pkg_install.c audio.c backlight.c net.c about.c \
|
||||||
subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.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 \
|
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_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
|
||||||
meow_bgmgr.c \
|
meow_bgmgr.c \
|
||||||
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
@@ -212,3 +212,92 @@ void load_config(void)
|
|||||||
menu_bg, seq_len, seq_codes[0], seq_gap_ms);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -315,11 +315,12 @@ int run_lock_screen(void)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* M7.1 解锁恢复:锁屏是整帧不透明覆盖(lk_draw fill_rect 全屏),退出后底层界面不会自动重绘
|
/* M7.1/M40 F5 解锁/熄屏恢复:锁屏是整帧不透明覆盖(lk_draw fill_rect 全屏),退出后底层界面
|
||||||
* —— 只有 App 自绘的控件区域刷新,未刷新区残留锁屏 PIN 最后一帧(解锁后整屏脏区根因)。此处
|
* 不会自动重绘 —— 只有 App 自绘的控件区域刷新,未刷新区残留锁屏 PIN 最后一帧。
|
||||||
* 在锁屏首扑前把底层帧(SDK back_buf = 进入前的 App 帧/主界面帧)快照下来;解锁(unlocked=1)
|
* 此处把锁屏首扑前的底层帧(SDK back_buf = 进入前的 App 帧/主界面帧)快照下来;
|
||||||
* 时整帧回写 back_buf + present,强制底层界面完整重绘(app_launcher.c 的 lock-overlay 关闭路径
|
* **任何退出路径**(解锁 unlocked=1 或电源键熄屏 unlocked=0)都整帧回写 back_buf + present,
|
||||||
* 随后 pm_capture_app_frame 读 fb0 即得到正确 App 帧,不再读到锁帧)。 */
|
* 保证 app_launcher.c 的 lock-overlay 关闭路径随后 pm_capture_app_frame 读 fb0 得到的是
|
||||||
|
* 正确 App 帧,而不是 PIN 帧(后者会被下一次锁屏当成底层帧 → P5 残影链)。 */
|
||||||
unsigned char *lk_bg_snap = NULL;
|
unsigned char *lk_bg_snap = NULL;
|
||||||
size_t lk_bg_len = 0;
|
size_t lk_bg_len = 0;
|
||||||
if (back_buf_ok && fb && fb_line > 0 && fb_yres > 0) {
|
if (back_buf_ok && fb && fb_line > 0 && fb_yres > 0) {
|
||||||
@@ -400,7 +401,7 @@ int run_lock_screen(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 电源键/音量键轮询 + 输入活动检测(重置自睡计时) + 自睡/唤醒切换。
|
/* 电源键/音量键轮询 + 输入活动检测(重置自睡计时) + 自睡/唤醒切换。
|
||||||
* 活跃态:电源键按下 → 息屏并退出锁屏(回 App/主屏,下次唤醒再锁)——沿用原语义;
|
* 活跃态:电源键**按下边沿**(进场 ≥300ms)→ 息屏并退出锁屏(回 App/主屏,下次唤醒再锁);
|
||||||
* 空闲超 lk_off_ms → 自睡(asleep=1,锁态保持,仍在本循环)。
|
* 空闲超 lk_off_ms → 自睡(asleep=1,锁态保持,仍在本循环)。
|
||||||
* asleep:电源键按下边沿 / 触摸活动 → meow_panel_on + 重画锁屏页(本循环唤醒链回锁)。
|
* asleep:电源键按下边沿 / 触摸活动 → meow_panel_on + 重画锁屏页(本循环唤醒链回锁)。
|
||||||
* 音量键本页不动(仅视为输入活动,重置计时)。 */
|
* 音量键本页不动(仅视为输入活动,重置计时)。 */
|
||||||
@@ -429,9 +430,14 @@ int run_lock_screen(void)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* 电源键=息屏(允许再睡;下次唤醒按 lck_wake_should_lock 再锁) */
|
/* 电源键=息屏(允许再睡;下次唤醒按 lck_wake_should_lock 再锁)。
|
||||||
if (pwr_down) {
|
* M40 F4/P4:必须用**按下边沿**且进场 ≥300ms 才生效。旧实现用电平
|
||||||
logmsg("lock: power key -> screen off (stay locked)");
|
* (pwr_down):打开电源菜单的那次长按在 PIN 页出现时仍按着,页面被同一次
|
||||||
|
* 按住立刻熄灭(用户症状「手动锁屏不熄屏而是进 PIN,此时点电源才熄屏」)。
|
||||||
|
* 边沿判定在 overlay_gate.h(宿主单测同源)。 */
|
||||||
|
if (overlay_lock_power_off_now(pwr_edge, now_ms() - entry_ms)) {
|
||||||
|
logmsg("lock: power key edge (%llums after entry) -> screen off (stay locked)",
|
||||||
|
now_ms() - entry_ms);
|
||||||
meow_panel_off();
|
meow_panel_off();
|
||||||
break; /* return 0 */
|
break; /* return 0 */
|
||||||
}
|
}
|
||||||
@@ -569,13 +575,17 @@ int run_lock_screen(void)
|
|||||||
done:
|
done:
|
||||||
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
|
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
|
||||||
g_lk_down_key = LK_DOWN_NONE;
|
g_lk_down_key = LK_DOWN_NONE;
|
||||||
/* M7.1 解锁恢复:把锁屏首扑前快照的底层帧整帧回写 back_buf + present,强制底层 App/主界面
|
/* M7.1/M40 F5:把锁屏首扑前快照的底层帧整帧回写 back_buf + present。
|
||||||
* 完整重绘(消除「解锁后整屏 = PIN 最后一帧,仅 App 控件自绘区刷新」的脏区残留)。 */
|
* M40 起**不再限定 unlocked**:电源键熄屏退出(lr=0)也必须恢复,否则 fb0 会停留在
|
||||||
if (unlocked && lk_bg_snap && back_buf_ok && fb && lk_bg_len > 0) {
|
* PIN 最后一帧;launcher 的 lock-overlay 关闭路径紧接着 pm_capture_app_frame() 会把
|
||||||
|
* 这帧 PIN 抄进 back_buf 并上屏,下一次锁屏再把它当「底层帧」快照 —— P5 残影链的起点。
|
||||||
|
* 屏黑时 present 无害(面板已下电),解锁时则同时消除「整屏 = PIN」的脏区残留。
|
||||||
|
* 「解锁后 App 整帧重画」由 launcher 写 /tmp/meow_app_repaint(SDK present 消费)完成。 */
|
||||||
|
if (lk_bg_snap && back_buf_ok && fb && lk_bg_len > 0) {
|
||||||
memcpy(fb, lk_bg_snap, lk_bg_len);
|
memcpy(fb, lk_bg_snap, lk_bg_len);
|
||||||
meow_dirty_full();
|
meow_dirty_full();
|
||||||
meow_ui_flush();
|
meow_ui_flush();
|
||||||
logmsg("lock: unlock -> restore underlying frame (full repaint)");
|
logmsg("lock: exit (unlocked=%d) -> restore underlying frame (full repaint)", unlocked);
|
||||||
}
|
}
|
||||||
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
|
if (lk_bg_snap) { free(lk_bg_snap); lk_bg_snap = NULL; lk_bg_len = 0; }
|
||||||
if (tfd >= 0) {
|
if (tfd >= 0) {
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ int menu_opened_blank = 0;
|
|||||||
|
|
||||||
|
|
||||||
/* launch_app/launch_activity 返回 -1 时的 toast:缺二进制依赖(app.json depends)→ 「缺依赖: <name>」;
|
/* launch_app/launch_activity 返回 -1 时的 toast:缺二进制依赖(app.json depends)→ 「缺依赖: <name>」;
|
||||||
* 其余启动失败沿用「CANNOT START: <name>」。原因由 meow_dep.c 的 meow_dep_block_reason() 提供。 */
|
* 其余启动失败优先显示具体失败原因(app_launcher.c 的 launch_fail_reason():fork OOM / exec 失败 /
|
||||||
|
* 路径不存在等)→ 「CANNOT START: <name>(<reason>)」;无具体原因则回退「CANNOT START: <name>」。 */
|
||||||
static void launch_fail_toast(char *toast, size_t n, const char *name)
|
static void launch_fail_toast(char *toast, size_t n, const char *name)
|
||||||
{
|
{
|
||||||
|
const char *reason = launch_fail_reason(); /* M6.40:具体失败原因(成功/未失败时为 "") */
|
||||||
const char *bd = meow_dep_block_reason();
|
const char *bd = meow_dep_block_reason();
|
||||||
if (bd && bd[0]) snprintf(toast, n, meow_str_zh("menu.missing_dep", "缺依赖: %s"), bd);
|
if (bd && bd[0]) snprintf(toast, n, meow_str_zh("menu.missing_dep", "缺依赖: %s"), bd);
|
||||||
|
else if (reason && reason[0]) snprintf(toast, n, "CANNOT START: %s(%s)", name, reason);
|
||||||
else snprintf(toast, n, "CANNOT START: %s", name);
|
else snprintf(toast, n, "CANNOT START: %s", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,17 +56,28 @@ static int menu_live_open_pending = 0;
|
|||||||
* fresh menu taps. A replayed tap on an app row relaunches the app (symptom:
|
* fresh menu taps. A replayed tap on an app row relaunches the app (symptom:
|
||||||
* BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO
|
* BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO
|
||||||
* PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen
|
* PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen
|
||||||
* flashes back to the player — Bug 4). We drain the queue immediately after
|
* flashes back to the player — Bug 4).
|
||||||
* the app exits and ignore touches for a short cooldown that covers the
|
*
|
||||||
* measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */
|
* M39:真正的机制是「排空本进程 touch_fd」——evdev 每个 fd 各收一份事件副本,App 运行期间
|
||||||
|
* (launcher 不读)的全部事件都堆在这个 fd 里,app 一退出就会作为「新菜单 tap」重放。
|
||||||
|
* menu_drain_touch() 把它们全读掉(含按下沿)+ 复位手势状态机,历史上用于兜底的
|
||||||
|
* 1.2–2.25s 冷却窗口因此可以收窄到 MENU_TOUCH_GUARD_MS:那个窗口是从「App 退出」开始计时
|
||||||
|
* 的,用户看到菜单后伸手的第一下必然落在里面被吞(用户实测「首页点图标要点两次」根因之一)。
|
||||||
|
* 现在只留极短的驱动尾巴守卫,不再按时间吞真实输入。 */
|
||||||
static unsigned long long menu_touch_cooldown_until = 0;
|
static unsigned long long menu_touch_cooldown_until = 0;
|
||||||
|
|
||||||
|
/* 排空本进程触摸队列(非阻塞读到 EAGAIN)+ 复位手势状态机。
|
||||||
|
* 队列/down 边沿都要清:只清队列而保留 t_down,会让「按下已被 drain 掉、抬手稍后才到」的
|
||||||
|
* 这次手势被当成一次有效 tap(ghost touch 重放的另一半)。 */
|
||||||
static void menu_drain_touch(void)
|
static void menu_drain_touch(void)
|
||||||
{
|
{
|
||||||
if (touch_fd >= 0) {
|
if (touch_fd >= 0) {
|
||||||
unsigned char ev[16];
|
unsigned char ev[16];
|
||||||
while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ }
|
while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ }
|
||||||
}
|
}
|
||||||
|
t_down = 0; t_down_valid = 0;
|
||||||
|
t_down_x = t_down_y = -1;
|
||||||
|
t_last_x = t_last_y = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void menu_arm_touch(long ms)
|
static void menu_arm_touch(long ms)
|
||||||
@@ -73,18 +87,19 @@ static void menu_arm_touch(long ms)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Drain the touch queue + arm the post-app cooldown (call right after the
|
/* Drain the touch queue + arm the post-app guard (call right after the app
|
||||||
* app exits, before the menu resumes polling touch). The drain always runs so
|
* exits, before the menu resumes polling touch). The drain always runs so
|
||||||
* touches queued by the launch-attempt tail are cleared; the cooldown only
|
* touches queued by the launch-attempt tail are cleared; the guard only arms
|
||||||
* arms when an app actually ran (app_ran) so a failed launch doesn't lock the
|
* when an app actually ran (app_ran) so a failed launch doesn't lock the menu
|
||||||
* menu out for 2.5s. */
|
* out. */
|
||||||
static void menu_after_app_exit(int app_ran)
|
static void menu_after_app_exit(int app_ran)
|
||||||
{
|
{
|
||||||
menu_drain_touch();
|
menu_drain_touch();
|
||||||
/* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。
|
/* M39:M2.6 的 2500ms → M2.22 的 1200ms → 现在的 MENU_TOUCH_GUARD_MS(150ms)。
|
||||||
* 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短,
|
* 依据:重放的来源是本进程 fd 里 App 运行期的事件,menu_drain_touch() 已一次性清空
|
||||||
* 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */
|
* (含 t_down/原点,见上),窗口只兜「面板重配后驱动补发的幽灵尾巴」。1200ms 会稳定吞掉
|
||||||
if (app_ran) menu_touch_cooldown_until = now_ms() + 1200;
|
* 用户「返回桌面后立即点下一个图标」——正是要修的症状。 */
|
||||||
|
if (app_ran) menu_touch_cooldown_until = now_ms() + MENU_TOUCH_GUARD_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1121,7 +1136,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
unsigned long long t0 = now_ms();
|
unsigned long long t0 = now_ms();
|
||||||
int toast_ms = 0;
|
int toast_ms = 0;
|
||||||
char toast[64] = "";
|
char toast[256] = ""; /* M6.40:失败 toast 带具体原因(CANNOT START: <name>(<reason>))需更大缓冲 */
|
||||||
int want_exit = 0;
|
int want_exit = 0;
|
||||||
/* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。
|
/* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。
|
||||||
* meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */
|
* meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */
|
||||||
@@ -1204,17 +1219,19 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
/* touch: event reads (event1 is not grabbed) */
|
/* touch: event reads (event1 is not grabbed) */
|
||||||
touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno));
|
if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno));
|
||||||
/* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */
|
/* M39:菜单激活(启动)即 drain + 极短守卫(M2.6 原为 300ms;界面此时已整帧画完,
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
* 窗口只会吞用户看到首页后的第一下,故收窄到 MENU_TOUCH_GUARD_MS) */
|
||||||
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
|
|
||||||
/* key polling */
|
/* key polling */
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER);
|
kp_open(&kp_power, DEV_POWER);
|
||||||
kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_keys, DEV_KEYS);
|
||||||
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */
|
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动。
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
* 音量键防抖/基线已上收 key_router(进程唯一);本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||||
|
key_router_rebase(); /* 进入主菜单:音量基线对齐当前 raw(防残留边沿) */
|
||||||
|
|
||||||
logmsg("menu open: napps=%d state=%d cursor=%d",
|
logmsg("menu open: napps=%d state=%d cursor=%d",
|
||||||
napps, state, cursor);
|
napps, state, cursor);
|
||||||
@@ -1223,10 +1240,8 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* when the menu opens (e.g. the hotkey release tail) do not fire
|
* when the menu opens (e.g. the hotkey release tail) do not fire
|
||||||
* edges; kp.prev cannot be used for this because kp_poll() syncs it
|
* edges; kp.prev cannot be used for this because kp_poll() syncs it
|
||||||
* to cur before we compare */
|
* to cur before we compare */
|
||||||
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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||||
}
|
}
|
||||||
@@ -1333,7 +1348,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
sim_exit_tap_done = 1;
|
sim_exit_tap_done = 1;
|
||||||
if (meow_mode) {
|
if (meow_mode) {
|
||||||
int rb = confirm_reboot_player();
|
int rb = confirm_reboot_player();
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
dirty = 1;
|
dirty = 1;
|
||||||
logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb);
|
logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb);
|
||||||
} else {
|
} else {
|
||||||
@@ -1430,26 +1445,20 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* does not affect it) — the launcher must NOT touch brightness/panel
|
* does not affect it) — the launcher must NOT touch brightness/panel
|
||||||
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch
|
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch
|
||||||
* row (below) or sim-exit. */
|
* row (below) or sim-exit. */
|
||||||
|
/* 音量键走 key_router(进程唯一防抖/基线,产边沿/held/ramp);播放/电源键仍本页自理。 */
|
||||||
|
key_evt_t kev = key_router_pump();
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, 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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||||
}
|
}
|
||||||
if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */
|
if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */
|
||||||
/* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */
|
|
||||||
int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */
|
|
||||||
(void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */
|
|
||||||
/* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。
|
/* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。
|
||||||
* 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
|
* 稳定态 play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
|
||||||
(void)kd_tick(&kd_vold, vold);
|
|
||||||
(void)kd_tick(&kd_volu, volu);
|
|
||||||
(void)kd_tick(&kd_play, play);
|
(void)kd_tick(&kd_play, play);
|
||||||
(void)kd_tick(&kd_pwr, pwr);
|
(void)kd_tick(&kd_pwr, pwr);
|
||||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
|
||||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
|
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
|
||||||
* 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */
|
* 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */
|
||||||
@@ -1486,7 +1495,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||||
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */
|
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */
|
||||||
dirty = 1; /* 取消后重绘应用列表 */
|
dirty = 1; /* 取消后重绘应用列表 */
|
||||||
menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */
|
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫(原 M2.6 800ms) */
|
||||||
/* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。
|
/* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。
|
||||||
* 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」,
|
* 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」,
|
||||||
* 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at,
|
* 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at,
|
||||||
@@ -1494,15 +1503,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw
|
* 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw
|
||||||
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
|
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
|
||||||
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
|
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1527,15 +1534,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
if (lck_wake_should_lock()) {
|
if (lck_wake_should_lock()) {
|
||||||
int lr = run_lock_screen();
|
int lr = run_lock_screen();
|
||||||
/* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */
|
/* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
if (lr == 1)
|
if (lr == 1)
|
||||||
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
||||||
@@ -1564,11 +1569,11 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp),
|
* M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp),
|
||||||
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
|
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
|
||||||
if (meow_mode) {
|
if (meow_mode) {
|
||||||
if (vold && !m_prev_vold) {
|
if (kev.vol_dn_edge) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
|
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
|
||||||
logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
|
logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
|
||||||
} else if (volu && !m_prev_volu) {
|
} else if (kev.vol_up_edge) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
settings_volume_adj(+settings_volume_step);
|
settings_volume_adj(+settings_volume_step);
|
||||||
logmsg("volume UP <- ramp step=%d", settings_volume_step);
|
logmsg("volume UP <- ramp step=%d", settings_volume_step);
|
||||||
@@ -1576,22 +1581,23 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
} else {
|
} else {
|
||||||
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
|
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
|
||||||
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */
|
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */
|
||||||
if (vold && !m_prev_vold && napps > 0 && state == 0) {
|
if (kev.vol_dn_edge && napps > 0 && state == 0) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
cursor++;
|
cursor++;
|
||||||
if (cursor > napps) cursor = 0; /* napps=设置行 */
|
if (cursor > napps) cursor = 0; /* napps=设置行 */
|
||||||
logmsg("menu cursor -> %d", cursor);
|
logmsg("menu cursor -> %d", cursor);
|
||||||
}
|
}
|
||||||
if (volu && !m_prev_volu && napps > 0 && state == 0) {
|
if (kev.vol_up_edge && napps > 0 && state == 0) {
|
||||||
any_input = 1;
|
any_input = 1;
|
||||||
cursor--;
|
cursor--;
|
||||||
if (cursor < 0) cursor = napps; /* 回绕到设置行 */
|
if (cursor < 0) cursor = napps; /* 回绕到设置行 */
|
||||||
logmsg("menu cursor -> %d", cursor);
|
logmsg("menu cursor -> %d", cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */
|
/* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值)。
|
||||||
|
* 长按 raw held 取自 key_router(kev.vol_held_*,同现状 rvolu/rvold),松开立即停止。 */
|
||||||
if (meow_mode && vol_ramp_dir != 0) {
|
if (meow_mode && vol_ramp_dir != 0) {
|
||||||
int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */
|
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
|
||||||
int going = vol_ramp_tick(held);
|
int going = vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */
|
paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */
|
||||||
@@ -1615,7 +1621,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
logmsg("play key: media-reserved (no media source)");
|
logmsg("play key: media-reserved (no media source)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play;
|
m_prev_play = play;
|
||||||
|
|
||||||
/* touch (event reads) */
|
/* touch (event reads) */
|
||||||
if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */
|
if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */
|
||||||
@@ -1673,25 +1679,23 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
logmsg("menu: top-edge live drag-down -> notification center (dy=%d)",
|
logmsg("menu: top-edge live drag-down -> notification center (dy=%d)",
|
||||||
t_last_y - loy);
|
t_last_y - loy);
|
||||||
t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */
|
t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
notif_click_t click;
|
notif_click_t click;
|
||||||
memset(&click, 0, sizeof click);
|
memset(&click, 0, sizeof click);
|
||||||
click.app_idx = -1;
|
click.app_idx = -1;
|
||||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||||
run_notif_center(apps, napps, &click, 1, t_down_x, loy);
|
run_notif_center(apps, napps, &click, 1, t_down_x, loy);
|
||||||
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
|
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
dirty = 1;
|
dirty = 1;
|
||||||
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
|
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
|
||||||
if (click.status == NOTIF_CLICK_STATUS_OK &&
|
if (click.status == NOTIF_CLICK_STATUS_OK &&
|
||||||
@@ -1773,25 +1777,23 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
|
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
|
||||||
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
|
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
|
||||||
logmsg("menu: top-edge swipe-down -> notification center (release edge, fallback)");
|
logmsg("menu: top-edge swipe-down -> notification center (release edge, fallback)");
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
notif_click_t click;
|
notif_click_t click;
|
||||||
memset(&click, 0, sizeof click);
|
memset(&click, 0, sizeof click);
|
||||||
click.app_idx = -1;
|
click.app_idx = -1;
|
||||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||||
run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
|
run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
|
||||||
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
|
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
dirty = 1;
|
dirty = 1;
|
||||||
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
|
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
|
||||||
if (click.status == NOTIF_CLICK_STATUS_OK &&
|
if (click.status == NOTIF_CLICK_STATUS_OK &&
|
||||||
@@ -1829,21 +1831,19 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
if (dx <= -MENU_SWIPE_THRESH && adx > ady &&
|
if (dx <= -MENU_SWIPE_THRESH && adx > ady &&
|
||||||
tx >= MEOW_EDGE_W && ty >= SB_BAR_H) {
|
tx >= MEOW_EDGE_W && ty >= SB_BAR_H) {
|
||||||
logmsg("menu: left-swipe -> Now Playing (dx=%d,dy=%d)", dx, dy);
|
logmsg("menu: left-swipe -> Now Playing (dx=%d,dy=%d)", dx, dy);
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
run_now_playing(apps, napps);
|
run_now_playing(apps, napps);
|
||||||
/* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */
|
/* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
dirty = 1;
|
dirty = 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1853,20 +1853,18 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
/* 批③:顶栏居右「后台管理」按钮(最高优先级;阻挡非阻塞,返回后重画) */
|
/* 批③:顶栏居右「后台管理」按钮(最高优先级;阻挡非阻塞,返回后重画) */
|
||||||
if (menu_hit_bgm_btn(tx, ty)) {
|
if (menu_hit_bgm_btn(tx, ty)) {
|
||||||
logmsg("menu: touch topbar bg-manager button");
|
logmsg("menu: touch topbar bg-manager button");
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
|
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */
|
||||||
run_bg_page(apps, napps);
|
run_bg_page(apps, napps);
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms();
|
last_input_ms = now_ms();
|
||||||
menu_arm_touch(800);
|
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||||
dirty = 1;
|
dirty = 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1878,7 +1876,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */
|
* 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */
|
||||||
logmsg("meow mode: BACK TO PLAYER -> confirm reboot");
|
logmsg("meow mode: BACK TO PLAYER -> confirm reboot");
|
||||||
int rb = confirm_reboot_player();
|
int rb = confirm_reboot_player();
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
|
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */
|
||||||
dirty = 1;
|
dirty = 1;
|
||||||
if (rb) { logmsg("reboot-to-player: yes, rebooting"); }
|
if (rb) { logmsg("reboot-to-player: yes, rebooting"); }
|
||||||
/* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */
|
/* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */
|
||||||
@@ -1893,20 +1891,18 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
* 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */
|
* 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */
|
||||||
else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) {
|
else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) {
|
||||||
logmsg("menu: touch settings row -> settings page");
|
logmsg("menu: touch settings row -> settings page");
|
||||||
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */
|
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */
|
||||||
run_settings_page();
|
run_settings_page();
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
last_input_ms = now_ms(); /* 回主界面即视为输入 */
|
last_input_ms = now_ms(); /* 回主界面即视为输入 */
|
||||||
menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */
|
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫(原设置页返回 800ms) */
|
||||||
dirty = 1; /* 全帧重画应用列表 */
|
dirty = 1; /* 全帧重画应用列表 */
|
||||||
}
|
}
|
||||||
/* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与
|
/* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与
|
||||||
@@ -1972,15 +1968,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
|||||||
/* M2.x 锁屏:自动息屏唤醒后若需锁屏 → 弹阻塞式锁屏页;解锁才重绘,息屏留 dirty */
|
/* M2.x 锁屏:自动息屏唤醒后若需锁屏 → 弹阻塞式锁屏页;解锁才重绘,息屏留 dirty */
|
||||||
if (lck_wake_should_lock()) {
|
if (lck_wake_should_lock()) {
|
||||||
int lr = run_lock_screen();
|
int lr = run_lock_screen();
|
||||||
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
if (lr == 1)
|
if (lr == 1)
|
||||||
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
|
||||||
@@ -2138,7 +2132,7 @@ static void usage(void)
|
|||||||
" [--sim-exit-tap N] [--sim-power-hold N]\n"
|
" [--sim-exit-tap N] [--sim-power-hold N]\n"
|
||||||
" [--sim-power-pick N] [--sim-no-exec]\n"
|
" [--sim-power-pick N] [--sim-no-exec]\n"
|
||||||
" [--dumpkeys N] [--scan] [--sysdump]\n"
|
" [--dumpkeys N] [--scan] [--sysdump]\n"
|
||||||
" [--pkg-install <pkg.mpk>] [--pkg-export <pkg> <out.mpk>]\n"
|
" [--pkg-install <file>] [--pkg-export <pkg> <out.mpk>]\n"
|
||||||
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
|
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
|
||||||
" [--fontscan <card_root>]\n");
|
" [--fontscan <card_root>]\n");
|
||||||
}
|
}
|
||||||
@@ -2216,6 +2210,7 @@ int main(int argc, char **argv)
|
|||||||
load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per
|
load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per
|
||||||
* menu open so edits apply without a daemon restart) */
|
* menu open so edits apply without a daemon restart) */
|
||||||
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
|
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
|
||||||
|
key_router_init(); /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1)单开 kp_*/
|
||||||
|
|
||||||
/* M2.6:启动扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,填充「字体文件」下拉 preset。
|
/* M2.6:启动扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,填充「字体文件」下拉 preset。
|
||||||
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次(verify=0)。 */
|
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次(verify=0)。 */
|
||||||
@@ -2258,10 +2253,12 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pkg_install_mpk) {
|
if (pkg_install_mpk) {
|
||||||
/* M2.12 .mpk 安装接口(CLI 钩子;UI 后期)。安全、离开主循环。 */
|
/* M42:安装委托 nyai(唯一权威实现;包身份只看包内 app.json,文件名/后缀不参与)。
|
||||||
|
* 失败必须给可读原因(nyai stderr 尾行),不能静默;退出码统一 0/1。 */
|
||||||
int r = meow_pkg_install(pkg_install_mpk);
|
int r = meow_pkg_install(pkg_install_mpk);
|
||||||
printf("pkg_install %s -> %d\n", pkg_install_mpk, r);
|
printf("pkg_install %s -> %d\n", pkg_install_mpk, r);
|
||||||
return r;
|
if (r != 0) fprintf(stderr, "pkg_install failed: %s\n", meow_pkg_install_error());
|
||||||
|
return r == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
if (pkg_export_name && pkg_export_out) {
|
if (pkg_export_name && pkg_export_out) {
|
||||||
/* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */
|
/* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
|
|||||||
if (n > max) n = max;
|
if (n > max) n = max;
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
bgmgr_row_t *r = &rows[i];
|
bgmgr_row_t *r = &rows[i];
|
||||||
r->framework = 0;
|
r->protected_app = 0;
|
||||||
r->is_fg = 0;
|
r->is_fg = 0;
|
||||||
r->name[0] = 0;
|
r->name[0] = 0;
|
||||||
r->icon[0] = 0;
|
r->icon[0] = 0;
|
||||||
@@ -21,7 +21,7 @@ int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
|
|||||||
r->rss_kb = 0;
|
r->rss_kb = 0;
|
||||||
ops->app_meta(ops->ctx, r->app_id, r->pid,
|
ops->app_meta(ops->ctx, r->app_id, r->pid,
|
||||||
r->name, (int)sizeof r->name, r->icon, (int)sizeof r->icon,
|
r->name, (int)sizeof r->name, r->icon, (int)sizeof r->icon,
|
||||||
&r->framework, &r->is_fg);
|
&r->protected_app, &r->is_fg);
|
||||||
if (!r->name[0]) snprintf(r->name, sizeof r->name, "%s", r->app_id);
|
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) {
|
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->pcpu = 0.0; r->rss_kb = 0;
|
||||||
@@ -34,7 +34,7 @@ int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
|
|||||||
int bgmgr_can_kill(const bgmgr_row_t *r)
|
int bgmgr_can_kill(const bgmgr_row_t *r)
|
||||||
{
|
{
|
||||||
if (!r) return 0;
|
if (!r) return 0;
|
||||||
if (r->framework) return 0; /* 框架/系统 App 不 kill(铁律) */
|
if (r->protected_app) return 0; /* 系统级受保护 App 不 kill(铁律;framework 是 SDK 标志,不在此列) */
|
||||||
if (r->is_fg) return 0; /* 当前前台 App 不 kill */
|
if (r->is_fg) return 0; /* 当前前台 App 不 kill */
|
||||||
if (r->pid <= 0) return 0; /* 非后台/无进程 */
|
if (r->pid <= 0) return 0; /* 非后台/无进程 */
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
* 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法:
|
* 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法:
|
||||||
* UI 提供真实 enumerate/app_meta/proc_sample;单测用假件。**单一写者=launcher 主循环**,无锁。
|
* UI 提供真实 enumerate/app_meta/proc_sample;单测用假件。**单一写者=launcher 主循环**,无锁。
|
||||||
*
|
*
|
||||||
* 铁律遵循:kill/一键清 带 !framework 门控(防误杀框架/系统 App,对齐 app_launcher.c:2088 教训);
|
* 铁律遵循: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。
|
* 不新建 daemon;不引 GDBus;不碰 /data/recovery_all。
|
||||||
*/
|
*/
|
||||||
#ifndef MEOW_BGMGR_H
|
#ifndef MEOW_BGMGR_H
|
||||||
@@ -23,7 +26,9 @@ typedef struct bgmgr_row {
|
|||||||
char app_id[MEOW_APP_ID_MAX];
|
char app_id[MEOW_APP_ID_MAX];
|
||||||
char name[49]; /* display name(回退 app_id) */
|
char name[49]; /* display name(回退 app_id) */
|
||||||
char icon[129]; /* 图标 path(空=无,页面用首字/占位) */
|
char icon[129]; /* 图标 path(空=无,页面用首字/占位) */
|
||||||
char framework; /* '1'=framework=true(看门狗/系统 App,禁止 kill) */
|
char protected_app; /* '1'=系统级受保护 App(禁止 kill)。**非** app.json framework;
|
||||||
|
* 目前 launcher 自身组件(状态栏/设置页…)不在 App 表/后台表里,
|
||||||
|
* 故实际恒为 0,保留为将来受保护组件的显式标记位。 */
|
||||||
pid_t pid; /* >0 后台进程 */
|
pid_t pid; /* >0 后台进程 */
|
||||||
int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */
|
int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */
|
||||||
int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */
|
int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */
|
||||||
@@ -39,10 +44,10 @@ typedef struct bgmgr_ops {
|
|||||||
/* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms,
|
/* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms,
|
||||||
* 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */
|
* 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */
|
||||||
int (*enumerate)(void *ctx, bgmgr_row_t *rows, int max);
|
int (*enumerate)(void *ctx, bgmgr_row_t *rows, int max);
|
||||||
/* 按 app_id 补 App 元数据:name/icon/framework/is_fg。返回 0=找到;<0=未注册(name 回退 app_id)。 */
|
/* 按 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,
|
int (*app_meta)(void *ctx, const char *app_id, pid_t pid,
|
||||||
char *name, int nl, char *icon, int il,
|
char *name, int nl, char *icon, int il,
|
||||||
char *framework, int *is_fg);
|
char *protected_app, int *is_fg);
|
||||||
/* 每进程 CPU/RSS 取样:返回 0=ok(写 pcpu 与 rss_kb);<0=无数据(pcpu=0/rss=0)。 */
|
/* 每进程 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);
|
int (*proc_sample)(void *ctx, pid_t pid, double *pcpu, long *rss_kb);
|
||||||
void (*log)(const char *fmt, ...); /* 可空=静默 */
|
void (*log)(const char *fmt, ...); /* 可空=静默 */
|
||||||
@@ -53,11 +58,11 @@ typedef struct bgmgr_ops {
|
|||||||
* 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */
|
* 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */
|
||||||
int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max);
|
int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max);
|
||||||
|
|
||||||
/* !framework 门控 + 非前台 + pid>0:返回 1=可 kill;0=禁止(框架/非后台/当前前台/无进程)。 */
|
/* !protected_app 门控 + 非前台 + pid>0:返回 1=可 kill;0=禁止(受保护/当前前台/无进程)。 */
|
||||||
int bgmgr_can_kill(const bgmgr_row_t *r);
|
int bgmgr_can_kill(const bgmgr_row_t *r);
|
||||||
|
|
||||||
/* 一键清:遍历 rows,bgmgr_can_kill() 的条目逐一调 kill_fn(app_id)。返回发出 kill 数。
|
/* 一键清:遍历 rows,bgmgr_can_kill() 的条目逐一调 kill_fn(app_id)。返回发出 kill 数。
|
||||||
* **不杀框架 App/当前前台/非后台**;调用方负责对返回 0 弹「无后台 App 可清」之类提示。 */
|
* **不杀受保护 App/当前前台/非后台**;调用方负责对返回 0 弹「无后台 App 可清」之类提示。 */
|
||||||
int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n,
|
int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n,
|
||||||
void (*kill_fn)(const char *app_id));
|
void (*kill_fn)(const char *app_id));
|
||||||
|
|
||||||
|
|||||||
@@ -270,9 +270,11 @@ void meow_bg_tick(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 批③后台管理页:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。
|
/* 批③后台管理页 / `am kill`:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。
|
||||||
* 只置 term_at_ms(tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表),
|
* 只置 term_at_ms(tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表),
|
||||||
* 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。 */
|
* 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。
|
||||||
|
* 调用者:bg_page.c 行内「杀」/「一键清除」+ util.c MSG_AMS_KILL(`am kill`,无前台会话时)
|
||||||
|
* ——两个入口同一通道,故页面按钮与面板/CLI kill 行为对称。 */
|
||||||
int meow_bg_terminate(const char *app_id)
|
int meow_bg_terminate(const char *app_id)
|
||||||
{
|
{
|
||||||
if (!app_id || !app_id[0]) return MEOW_ERR_INVAL;
|
if (!app_id || !app_id[0]) return MEOW_ERR_INVAL;
|
||||||
@@ -281,7 +283,8 @@ int meow_bg_terminate(const char *app_id)
|
|||||||
meow_bg_entry_t *e = &g_bg[i];
|
meow_bg_entry_t *e = &g_bg[i];
|
||||||
if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) {
|
if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) {
|
||||||
if (e->term_at_ms == 0) {
|
if (e->term_at_ms == 0) {
|
||||||
bg_log("[bgplay] 后台管理页终止后台App: %s pid=%d", app_id, (int)e->pid);
|
bg_log("[bgplay] 用户终止后台App: %s pid=%d(后台管理页/am kill 同通道)",
|
||||||
|
app_id, (int)e->pid);
|
||||||
kill(e->pid, SIGTERM);
|
kill(e->pid, SIGTERM);
|
||||||
e->term_at_ms = bg_now_ms();
|
e->term_at_ms = bg_now_ms();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int kee
|
|||||||
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid);
|
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid);
|
||||||
/* 该 App 是否在后台保活。 */
|
/* 该 App 是否在后台保活。 */
|
||||||
int meow_bg_is_backgrounded(const char *app_id);
|
int meow_bg_is_backgrounded(const char *app_id);
|
||||||
/* 批③后台管理页:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。
|
/* 用户终止后台保活 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→摘表。
|
* 置 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 空。 */
|
* 返回 0=已受理(在后台表且 pid>0);MEOW_ERR_NOENT=不在后台/无进程;MEOW_ERR_INVAL=app_id 空。 */
|
||||||
int meow_bg_terminate(const char *app_id);
|
int meow_bg_terminate(const char *app_id);
|
||||||
|
|||||||
@@ -82,10 +82,19 @@ enum {
|
|||||||
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
||||||
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
||||||
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
|
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 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
||||||
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
||||||
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
|
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 ----
|
/* ---- audiod 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md §2):0x00C0-0x00CA ----
|
||||||
* 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。
|
* 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。
|
||||||
* 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */
|
* 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */
|
||||||
@@ -455,7 +464,9 @@ typedef struct m_ams_start {
|
|||||||
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
|
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
|
||||||
} m_ams_start_t; /* 160 B */
|
} 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 {
|
typedef struct m_ams_kill {
|
||||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||||
} m_ams_kill_t; /* 32 B */
|
} m_ams_kill_t; /* 32 B */
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ extern "C" {
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
|
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
|
||||||
#include <pthread.h> /* M0:IPC 线程 */
|
#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/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
|
||||||
#include <sys/socket.h> /* M0:AF_UNIX IPC */
|
#include <sys/socket.h> /* M0:AF_UNIX IPC */
|
||||||
#include <sys/un.h> /* M0:sockaddr_un */
|
#include <sys/un.h> /* M0:sockaddr_un */
|
||||||
@@ -177,7 +178,12 @@ static inline const char *meow_str_zh(const char *id, const char *zh)
|
|||||||
#define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */
|
#define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */
|
||||||
#define NCP_DRAG_ENTER_SLOP 16
|
#define NCP_DRAG_ENTER_SLOP 16
|
||||||
#define NCP_DRAG_THR_PCT 40
|
#define NCP_DRAG_THR_PCT 40
|
||||||
#define TAP_ARM_DELAY_MS 300
|
/* M39:launcher 触摸守卫窗口(取代 M2.6「菜单关闭后忽略触摸」800ms / M2.22 的 App 退出后
|
||||||
|
* 1200ms / M2.6 打开子页 300ms)。语义只剩「drain 队列后仍防面板重配尾巴的幽灵按下」,
|
||||||
|
* 不再吞用户看到界面后的第一下:窗口起点固定在 drain 之后,且只覆盖极短的驱动尾巴。
|
||||||
|
* 依据:每次 arm 前都先 menu_drain_touch() 清本进程 touch_fd 队列(evdev 每 fd 各收一份,
|
||||||
|
* App/子页运行期间的全量事件都在本 fd 里,drain 后已无历史事件可重放)。 */
|
||||||
|
#define MENU_TOUCH_GUARD_MS 150
|
||||||
#define PM_NUM 6
|
#define PM_NUM 6
|
||||||
#define PM_R 50 /* 圆钮半径(直径 100) */
|
#define PM_R 50 /* 圆钮半径(直径 100) */
|
||||||
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
|
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
|
||||||
@@ -262,6 +268,9 @@ static inline const char *meow_str_zh(const char *id, const char *zh)
|
|||||||
#define MEOW_SBAR_MODE_FILE "/tmp/meowapp_sbar"
|
#define MEOW_SBAR_MODE_FILE "/tmp/meowapp_sbar"
|
||||||
/* M2.71-bug5:App 前台触摸/交互 → SDK 写该标记;launcher 前台 app 循环消费(unlink + 重置息屏计时) */
|
/* M2.71-bug5:App 前台触摸/交互 → SDK 写该标记;launcher 前台 app 循环消费(unlink + 重置息屏计时) */
|
||||||
#define MEOW_APP_ACTIVITY_FILE "/tmp/meow_app_activity"
|
#define MEOW_APP_ACTIVITY_FILE "/tmp/meow_app_activity"
|
||||||
|
/* M40 F5/P5-P6:解锁/菜单关闭后强制 App 整帧重画的请求文件。launcher 在 SIGCONT 前写,
|
||||||
|
* SDK present 层(sdk/meow_ui_repaint.h,static inline)看到就 dirty_full() 一次并删除。 */
|
||||||
|
#define MEOW_APP_REPAINT_FILE "/tmp/meow_app_repaint"
|
||||||
|
|
||||||
/* ---- 共享结构体/枚举(来自 meow_launcherd.c,拆分后统一放置)---- */
|
/* ---- 共享结构体/枚举(来自 meow_launcherd.c,拆分后统一放置)---- */
|
||||||
struct kd_state { int raw, stable, down; int longpressed; unsigned long long since, down_at; };
|
struct kd_state { int raw, stable, down; int longpressed; unsigned long long since, down_at; };
|
||||||
@@ -675,6 +684,10 @@ int kp_open(struct keypoll *kp, const char *dev);
|
|||||||
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
|
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
|
||||||
void *ctx);
|
void *ctx);
|
||||||
int launch_app(struct app *a);
|
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)---- */
|
/* ---- app.json 二进制依赖(depends)解析与启动前检查(meow_dep.c)---- */
|
||||||
int meow_parse_depends(const char *val, struct meow_dep *deps, int max);
|
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);
|
int meow_check_app_depends(const struct app *a, char *missing, int mlen);
|
||||||
@@ -756,6 +769,15 @@ int meow_ipc_conn_alive(int fd);
|
|||||||
/* M4:ipc_send 公开包装(meow_bgplay send_frame 回调用;同 meow_media 的注入口径)。 */
|
/* M4:ipc_send 公开包装(meow_bgplay send_frame 回调用;同 meow_media 的注入口径)。 */
|
||||||
size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id,
|
size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id,
|
||||||
const void *payload, uint32_t len);
|
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 前台顶部下滑请求展开通知中心叠加会话)。
|
/* §10.5:读并清除待处理的 MSG_NOTIF_OPEN(App 前台顶部下滑请求展开通知中心叠加会话)。
|
||||||
* 调用方=launch_app waitpid 循环;返回 1 并填 *out(待处理),0=无。 */
|
* 调用方=launch_app waitpid 循环;返回 1 并填 *out(待处理),0=无。 */
|
||||||
int meow_notif_open_take(m_notif_open_t *out);
|
int meow_notif_open_take(m_notif_open_t *out);
|
||||||
@@ -765,7 +787,23 @@ void meow_panel_off(void);
|
|||||||
void meow_panel_on(void);
|
void meow_panel_on(void);
|
||||||
void meow_parse_app_json(FILE *f, struct app *a);
|
void meow_parse_app_json(FILE *f, struct app *a);
|
||||||
int meow_pkg_export(const char *pkg, const char *out_path);
|
int meow_pkg_export(const char *pkg, const char *out_path);
|
||||||
int meow_pkg_install(const char *mpk_path);
|
/* ---- M42:包安装(唯一实现 = /usr/bin/nyai,launcherd 只做入口预检 + 异步执行 + 错误文案)----
|
||||||
|
* 安装流的解包/清单/footer/验签(meow_verify)/防降级/部署/清理全在 nyai;本模块不解析包内容。
|
||||||
|
* nyai 路径 env MEOW_NYAI_BIN 覆写(缺省 /usr/bin/nyai,不依赖 PATH)。 */
|
||||||
|
#define MEOW_PKG_E_PATH (-1) /* 入口路径预检失败(文案见 meow_pkg_install_error) */
|
||||||
|
#define MEOW_PKG_E_NYAI (-2) /* nyai 不可执行 / pipe/fork 失败 */
|
||||||
|
#define MEOW_PKG_E_FAIL (-3) /* nyai 返回非 0 */
|
||||||
|
#define MEOW_PKG_E_TIMEOUT (-4) /* 超时并已终止子进程 */
|
||||||
|
#define MEOW_PKG_E_BUSY (-5) /* 已有一次安装在跑 */
|
||||||
|
#define MEOW_PKG_INSTALL_TIMEOUT_MS 180000
|
||||||
|
int meow_pkg_install(const char *path); /* 同步:0=成功;<0=MEOW_PKG_E_* */
|
||||||
|
int meow_pkg_install_start(const char *path); /* 异步启动:0=已启动;<0 同左 */
|
||||||
|
int meow_pkg_install_poll(int *rc); /* 1=运行中 0=结束(*rc) -1=无任务 */
|
||||||
|
int meow_pkg_install_busy(void); /* 1=有安装在跑(UI 据此禁双击) */
|
||||||
|
const char *meow_pkg_install_error(void); /* 最近一次失败的可读文案(永不 NULL) */
|
||||||
|
/* 纯逻辑(宿主单测 test_pkg_install.c):路径预检 / 退出码→可读文案 */
|
||||||
|
int meow_pkg_install_check_path(const char *path, char *err, int n);
|
||||||
|
void meow_pkg_install_error_text(int rc, const char *detail, char *out, int n);
|
||||||
void meow_sys_light(void);
|
void meow_sys_light(void);
|
||||||
void meow_sys_refresh(void);
|
void meow_sys_refresh(void);
|
||||||
void meow_toggle_panel(void);
|
void meow_toggle_panel(void);
|
||||||
@@ -834,6 +872,9 @@ int nm_wifi_saved(meow_net_t *out, int max);
|
|||||||
int nm_wifi_scan(meow_net_t *out, int max);
|
int nm_wifi_scan(meow_net_t *out, int max);
|
||||||
void nm_wifi_scan_trigger(void);
|
void nm_wifi_scan_trigger(void);
|
||||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */
|
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_disconnect(char *err, int elen);
|
||||||
int nm_wifi_toggle(int on);
|
int nm_wifi_toggle(int on);
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -128,6 +128,129 @@ char *popen_read_tmo(int tmo_ms, const char *cmd)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== M47:WiFi/BT「真实开/关」三态探测 =========================================
|
||||||
|
* 背景:设置页两个开关此前把 settings 布尔(用户意图)当显示态,读不到时 WiFi 还默认 1,
|
||||||
|
* 于是「开关长什么样」与硬件真实态无关(用户无法判断到底开没开)。M47 起 UI 只认真实态,
|
||||||
|
* 且真实态一律三态:-1=未知 / 0=关 / 1=开。探测失败/解析不出 = 未知,绝不用 0 冒充「已关」,
|
||||||
|
* 也不许用 1 冒充「已开」。netmgrd 可达时 BT 以 daemon 的 bt status 为准(见 netmgr_ipc.c);
|
||||||
|
* 下列函数是「daemon 不可达」时的直连回退,以及 WiFi 的权威探测(netmgrd 的 wifi on/off
|
||||||
|
* 就是 rfkill wlan block/unblock,而 wifi status 只报 wpa_state、不报射频态)。 */
|
||||||
|
|
||||||
|
/* 读一份 sysfs 字符串(去尾部换行);失败返回 -1,buf 置空。 */
|
||||||
|
static int net_read_sys(const char *path, char *buf, int n)
|
||||||
|
{
|
||||||
|
buf[0] = 0;
|
||||||
|
int fd = open(path, O_RDONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
ssize_t r = read(fd, buf, (size_t)n - 1);
|
||||||
|
close(fd);
|
||||||
|
if (r <= 0) { buf[0] = 0; return -1; }
|
||||||
|
buf[r] = 0;
|
||||||
|
while (r > 0 && (buf[r - 1] == '\n' || buf[r - 1] == '\r')) buf[--r] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按 rfkill type 读 soft/hard。返回 1=找到 / 0=无该类型 / -1=不可读。
|
||||||
|
* 本机实测有 2 个 wlan + 2 个 bluetooth 节点(rfkill0..3):同一 type 只要有一个被 block,
|
||||||
|
* 射频就不可用,故按「任一 block 即 block」汇总(保守方向:宁可报关,不假装开着)。 */
|
||||||
|
static int rfkill_state(const char *type, int *soft, int *hard)
|
||||||
|
{
|
||||||
|
DIR *d = opendir("/sys/class/rfkill");
|
||||||
|
if (!d) return -1;
|
||||||
|
int found = 0, s_any = 0, h_any = 0;
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(d))) {
|
||||||
|
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
|
||||||
|
char path[128], ty[32];
|
||||||
|
snprintf(path, sizeof path, "/sys/class/rfkill/%s/type", de->d_name);
|
||||||
|
if (net_read_sys(path, ty, sizeof ty) != 0) continue;
|
||||||
|
if (strcmp(ty, type) != 0) continue;
|
||||||
|
snprintf(path, sizeof path, "/sys/class/rfkill/%s/soft", de->d_name);
|
||||||
|
int s = read_file_int(path);
|
||||||
|
snprintf(path, sizeof path, "/sys/class/rfkill/%s/hard", de->d_name);
|
||||||
|
int h = read_file_int(path);
|
||||||
|
if (s < 0 || h < 0) continue; /* 读出失败:当作没找到这一类 */
|
||||||
|
found = 1;
|
||||||
|
if (s) s_any = 1;
|
||||||
|
if (h) h_any = 1;
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
if (!found) return 0;
|
||||||
|
*soft = s_any; *hard = h_any;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写 rfkill soft(block=1 关 / 0 开)。返回 0=至少写成功一个;-1=无该类型设备或全写失败。
|
||||||
|
* 同类多个节点全写(只写第一个会漏掉真正绑定驱动的那个)。netmgrd 的 rfkill_set_block
|
||||||
|
* 只写首个匹配节点(见 meow_netmgrd.c:992-1016)——已作为 finding 报给塔,不在本单改。
|
||||||
|
* M47:netmgrd 的 bt on/off 就是「hciconfig + rfkill soft」,直连路径必须写同一份状态,
|
||||||
|
* 否则「从本页关过蓝牙」之后从别的入口 `hciconfig up` 会被内核 rfkill 拦下(开关失真)。 */
|
||||||
|
static int rfkill_set_soft(const char *type, int block)
|
||||||
|
{
|
||||||
|
DIR *d = opendir("/sys/class/rfkill");
|
||||||
|
if (!d) return -1;
|
||||||
|
int rc = -1;
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(d))) {
|
||||||
|
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
|
||||||
|
char path[128], ty[32];
|
||||||
|
snprintf(path, sizeof path, "/sys/class/rfkill/%s/type", de->d_name);
|
||||||
|
if (net_read_sys(path, ty, sizeof ty) != 0) continue;
|
||||||
|
if (strcmp(ty, type) != 0) continue;
|
||||||
|
snprintf(path, sizeof path, "/sys/class/rfkill/%s/soft", de->d_name);
|
||||||
|
int fd = open(path, O_WRONLY);
|
||||||
|
if (fd < 0) continue;
|
||||||
|
char v[2] = { block ? '1' : '0', 0 };
|
||||||
|
if (write(fd, v, 1) == 1) rc = 0; /* 任一成功即视为已下发(不因后一个失败翻回 -1) */
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi 射频真实态:-1 未知 / 0 关 / 1 开。
|
||||||
|
* 1) 有 wlan rfkill 设备(netmgrd 的 wifi on/off 就是写它):hard/soft 任一 block = 关;
|
||||||
|
* 未 block 时接口存在才算开(无 wlan0 = 驱动没载,无法判定 → 未知)。
|
||||||
|
* 2) 无 rfkill 设备:退 wlan0 管理态 IFF_UP(与状态栏 wlan_radio_up 同源)。
|
||||||
|
* 3) 都读不到 = 未知(不当「已关」)。 */
|
||||||
|
int net_wifi_power_probe(void)
|
||||||
|
{
|
||||||
|
int soft = 0, hard = 0;
|
||||||
|
int rk = rfkill_state("wlan", &soft, &hard);
|
||||||
|
if (rk == 1) {
|
||||||
|
if (hard || soft) return 0;
|
||||||
|
return access("/sys/class/net/wlan0", F_OK) == 0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
if (rk < 0) return -1;
|
||||||
|
if (access("/sys/class/net/wlan0", F_OK) != 0) return -1;
|
||||||
|
char b[16];
|
||||||
|
if (net_read_sys("/sys/class/net/wlan0/flags", b, sizeof b) != 0) return -1;
|
||||||
|
return (int)(strtoul(b, NULL, 16) & 0x1UL) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BT 控制器真实态:-1 未知 / 0 关 / 1 开。
|
||||||
|
* 权威 = hciconfig hci0 的 UP/DOWN 状态行(与 netmgrd bt_status 的回退解析同源)。
|
||||||
|
* 命令超时/无输出/既无 UP 也无 DOWN(如 hci0 不存在)= 未知。 */
|
||||||
|
int net_bt_power_probe(void)
|
||||||
|
{
|
||||||
|
char *o = popen_read_tmo(1800,
|
||||||
|
"PATH=/bin:/sbin:/usr/bin:/usr/sbin hciconfig -a hci0 2>/dev/null");
|
||||||
|
if (!o) return -1;
|
||||||
|
int r = -1;
|
||||||
|
char *line = o;
|
||||||
|
while (line && *line) {
|
||||||
|
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
|
||||||
|
char *p = line;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
if (!strncmp(p, "DOWN", 4)) { r = 0; break; }
|
||||||
|
if (!strncmp(p, "UP", 2)) { r = 1; break; }
|
||||||
|
line = eol ? eol + 1 : NULL;
|
||||||
|
}
|
||||||
|
free(o);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 十六进制字符值('0'-'9'/'a'-'f'/'A'-'F');非法返回 -1。避免依赖 ctype.h(musl/内存受限)。
|
/* 十六进制字符值('0'-'9'/'a'-'f'/'A'-'F');非法返回 -1。避免依赖 ctype.h(musl/内存受限)。
|
||||||
* M2.40:wpa_cli 对非 ASCII SSID 输出 "\xNN" 转义文字(`\`+x+两位hex 的纯文字,非真字节),
|
* M2.40:wpa_cli 对非 ASCII SSID 输出 "\xNN" 转义文字(`\`+x+两位hex 的纯文字,非真字节),
|
||||||
* 需先还原成真实 UTF-8 字节才能进界面文本。 */
|
* 需先还原成真实 UTF-8 字节才能进界面文本。 */
|
||||||
@@ -733,7 +856,11 @@ int s_net_get(const srow_t *r, char *buf, int len)
|
|||||||
|
|
||||||
|
|
||||||
/* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。
|
/* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。
|
||||||
* 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。 */
|
* 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。
|
||||||
|
* M47:蓝牙补上 rfkill soft(与 netmgrd 的 bt on/off 同一语义)——此前本函数只发 hciconfig,
|
||||||
|
* 于是「在蓝牙页关过(rfkill soft=1)→ 再从通知中心/设置主页打开」时 `hciconfig up` 被内核
|
||||||
|
* rfkill 拦下、settings 却已写 1,开关显示 ON 实际 OFF(M45 盘点的开关失真)。开:先解 rfkill
|
||||||
|
* 再 up(顺序颠倒会先失败一次);关:先 down 再 block(与 netmgrd 一致)。 */
|
||||||
void s_net_toggle_wifi(int on)
|
void s_net_toggle_wifi(int on)
|
||||||
{
|
{
|
||||||
if (on) system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
|
if (on) system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
|
||||||
@@ -742,24 +869,33 @@ void s_net_toggle_wifi(int on)
|
|||||||
|
|
||||||
void s_net_toggle_bt(int on)
|
void s_net_toggle_bt(int on)
|
||||||
{
|
{
|
||||||
if (on) system("hciconfig hci0 up 2>/dev/null");
|
if (on) {
|
||||||
else system("hciconfig hci0 down 2>/dev/null");
|
rfkill_set_soft("bluetooth", 0);
|
||||||
|
system("hciconfig hci0 up 2>/dev/null");
|
||||||
|
} else {
|
||||||
|
system("hciconfig hci0 down 2>/dev/null");
|
||||||
|
rfkill_set_soft("bluetooth", 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 进入设置页时把 network.wifi_enabled/bluetooth_enabled 同步为真实硬件态,
|
/* 进入设置页时把 network.wifi_enabled/bluetooth_enabled 同步为真实硬件态,
|
||||||
* 让开关行反映 reality(仅同步内存 + 刷新缓存;不落盘 —— s_commit 的 save 才真正写)。
|
* 让开关行反映 reality(仅同步内存 + 刷新缓存;不落盘 —— s_commit 的 save 才真正写)。
|
||||||
* settings 未 init/取值失败则不动(保默认)。 */
|
* settings 未 init/取值失败则不动(保默认)。
|
||||||
|
* M47(M45 盘点):WiFi 判据不再是 wlan_connected()(=operstate up,即「已关联」)——网卡 up
|
||||||
|
* 但未关联时 operstate=dormant,旧写法会把 key 改成 0、WiFi 页开关显示 OFF 而射频明明开着;
|
||||||
|
* 改用 net_wifi_power_probe()(rfkill/网卡 up)。BT 改用三态探测:读不到(-1)就不写,
|
||||||
|
* 不拿 0 猜「已关」。 */
|
||||||
void s_net_sync_from_sys(void)
|
void s_net_sync_from_sys(void)
|
||||||
{
|
{
|
||||||
int cur = 0;
|
int cur = 0;
|
||||||
if (meow_settings_get_bool("network.wifi_enabled", &cur) == 0) {
|
if (meow_settings_get_bool("network.wifi_enabled", &cur) == 0) {
|
||||||
int real = wlan_connected() ? 1 : 0;
|
int real = net_wifi_power_probe();
|
||||||
if (real != cur) meow_settings_set_bool("network.wifi_enabled", real);
|
if (real >= 0 && real != cur) meow_settings_set_bool("network.wifi_enabled", real);
|
||||||
}
|
}
|
||||||
if (meow_settings_get_bool("network.bluetooth_enabled", &cur) == 0) {
|
if (meow_settings_get_bool("network.bluetooth_enabled", &cur) == 0) {
|
||||||
int real = hci_powered() ? 1 : 0;
|
int real = net_bt_power_probe();
|
||||||
if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
|
if (real >= 0 && real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
|
||||||
}
|
}
|
||||||
/* 只同步开关(快)+ 异步 kick;状态/列表由 run_settings_page 主循环 pump,绝不阻塞入口。 */
|
/* 只同步开关(快)+ 异步 kick;状态/列表由 run_settings_page 主循环 pump,绝不阻塞入口。 */
|
||||||
s_net_refresh_kick();
|
s_net_refresh_kick();
|
||||||
|
|||||||
@@ -15,6 +15,19 @@
|
|||||||
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
|
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
|
||||||
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
|
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
|
||||||
|
|
||||||
|
/* M47:net.c 的直连三态探测(-1 未知 / 0 关 / 1 开)。声明在此而非头文件——头文件不在本单
|
||||||
|
* 改动范围;两处定义与用法同属 launcher 网络线。 */
|
||||||
|
int net_bt_power_probe(void);
|
||||||
|
int net_wifi_power_probe(void);
|
||||||
|
|
||||||
|
/* 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 防热循环。 */
|
/* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */
|
||||||
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
|
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
|
||||||
static time_t g_nm_ok_ts = 0;
|
static time_t g_nm_ok_ts = 0;
|
||||||
@@ -50,7 +63,7 @@ static int nm_r_all(int fd, uint8_t *b, size_t n)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
|
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)
|
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype, int tmo_ms)
|
||||||
{
|
{
|
||||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||||
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
|
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
|
||||||
@@ -60,7 +73,7 @@ static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t
|
|||||||
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
|
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
|
||||||
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
|
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
|
||||||
struct pollfd pf = { fd, POLLIN, 0 };
|
struct pollfd pf = { fd, POLLIN, 0 };
|
||||||
int pr = poll(&pf, 1, 1500);
|
int pr = poll(&pf, 1, tmo_ms);
|
||||||
if (pr <= 0) return -1;
|
if (pr <= 0) return -1;
|
||||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||||
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
|
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
|
||||||
@@ -81,15 +94,21 @@ static int nm_hello(int fd)
|
|||||||
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
|
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
|
||||||
h.version = 1;
|
h.version = 1;
|
||||||
uint8_t rbuf[256]; uint16_t rtype = 0;
|
uint8_t rbuf[256]; uint16_t rtype = 0;
|
||||||
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype) < 0 || rtype != MSG_ACK)
|
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype, NM_TMO_DFL) < 0 || rtype != MSG_ACK)
|
||||||
return -1;
|
return -1;
|
||||||
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
|
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
|
||||||
return (ak.code == MEOW_OK) ? 0 : -1;
|
return (ak.code == MEOW_OK) ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。 */
|
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。
|
||||||
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
|
* tmo_ms:读超时(connect/connecth/scan 需覆盖 netmgrd 同步等待,见 NM_TMO_*)。
|
||||||
char *text, int tlen)
|
* read_timeout(可空):非空时,「帧收发失败/类型不符」这一支只回写 *read_timeout=1 且**不**
|
||||||
|
* 把可达性缓存打成 down——调用方据此区分「读超时」与「真不可达」。M47 的 BT 状态查询用它:
|
||||||
|
* netmgrd 单线程忙时(bt status 自己会跑 hciconfig)状态查询可能超过 1.5s,若据此标 down,
|
||||||
|
* 紧随的 bt_rows 会闪出「服务未运行(直连模式)」并把后续探测切到 1.8s 的 hciconfig
|
||||||
|
* (评审 r1 观察;global 超时常量不动,WiFi/DNS 路径行为不变)。 */
|
||||||
|
static int nm_cmd_t(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
|
||||||
|
char *text, int tlen, int tmo_ms, int *read_timeout)
|
||||||
{
|
{
|
||||||
m_net_cmd_t q;
|
m_net_cmd_t q;
|
||||||
memset(&q, 0, sizeof q);
|
memset(&q, 0, sizeof q);
|
||||||
@@ -100,8 +119,11 @@ static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const c
|
|||||||
if (nm_hello(fd) < 0) { close(fd); 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;
|
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
|
||||||
memset(rbuf, 0, sizeof rbuf);
|
memset(rbuf, 0, sizeof rbuf);
|
||||||
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype) < 0 || rtype != res_type) {
|
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;
|
close(fd);
|
||||||
|
if (read_timeout) *read_timeout = 1;
|
||||||
|
else nm_cache(0);
|
||||||
|
return NETMGR_UNREACH;
|
||||||
}
|
}
|
||||||
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
|
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
|
||||||
close(fd);
|
close(fd);
|
||||||
@@ -112,11 +134,20 @@ static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const c
|
|||||||
return (int)res.code;
|
return (int)res.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return nm_cmd_t(ctl_type, res_type, cmd, arg, text, tlen, tmo_ms, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen)
|
||||||
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, 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)
|
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); }
|
{ return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen, NM_TMO_DFL); }
|
||||||
|
|
||||||
int netmgr_ok(void)
|
int netmgr_ok(void)
|
||||||
{
|
{
|
||||||
@@ -361,7 +392,7 @@ int nm_wifi_saved(meow_net_t *out, int max)
|
|||||||
int nm_wifi_scan(meow_net_t *out, int max)
|
int nm_wifi_scan(meow_net_t *out, int max)
|
||||||
{
|
{
|
||||||
char text[MEOW_IPC_MAX - 32];
|
char text[MEOW_IPC_MAX - 32];
|
||||||
int rc = netmgr_wifi("scan", NULL, text, sizeof text); /* netmgrd 内含触发+有界等待+读结果 */
|
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 == NETMGR_UNREACH) return net_wifi_scan(out, max);
|
||||||
if (rc != MEOW_OK) return 0;
|
if (rc != MEOW_OK) return 0;
|
||||||
return nm_parse_scan(text, out, max);
|
return nm_parse_scan(text, out, max);
|
||||||
@@ -374,49 +405,133 @@ void nm_wifi_scan_trigger(void)
|
|||||||
if (!netmgr_ok()) net_wifi_scan_trigger();
|
if (!netmgr_ok()) net_wifi_scan_trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 直连回退:wpa_cli add/set/enable/select 新网络(复刻 netmgrd wifi_connect 逻辑)—— */
|
/* —— 直连回退:与 netmgrd wifi_connect/connecth 对齐(bug1/bug2/bug4)。
|
||||||
static int nm_direct_connect(const char *ssid, const char *psk, char *err, int elen)
|
* 匹配既有(\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)
|
||||||
{
|
{
|
||||||
const char *wpa = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
|
char cmd[384];
|
||||||
char cmd[256];
|
snprintf(cmd, sizeof cmd, "%s %s 2>/dev/null", NM_WPA_PREFIX, args);
|
||||||
snprintf(cmd, sizeof cmd, "%s add_network 2>/dev/null", wpa);
|
char *o = popen_read_tmo(tmo, cmd);
|
||||||
char *o = popen_read_tmo(1200, cmd);
|
if (!o) { out[0] = 0; return -1; }
|
||||||
int id = o ? atoi(o) : -1;
|
int l = (int)strlen(o);
|
||||||
if (o) free(o);
|
if (l >= cap) l = cap - 1;
|
||||||
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
|
memcpy(out, o, (size_t)l); out[l] = 0;
|
||||||
{
|
free(o);
|
||||||
char *r;
|
return l;
|
||||||
snprintf(cmd, sizeof cmd, "%s set_network %d ssid '\"%s\"' 2>/dev/null", wpa, id, ssid);
|
}
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
|
||||||
if (psk && psk[0]) {
|
static int nm_direct_save_config(void)
|
||||||
snprintf(cmd, sizeof cmd, "%s set_network %d psk '\"%s\"' 2>/dev/null", wpa, id, psk);
|
{
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
char out[128];
|
||||||
} else {
|
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
|
||||||
snprintf(cmd, sizeof cmd, "%s set_network %d key_mgmt NONE 2>/dev/null", wpa, id);
|
nm_direct_wpa(out, sizeof out, "set update_config 1", 1500);
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
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");
|
||||||
}
|
}
|
||||||
snprintf(cmd, sizeof cmd, "%s enable_network %d 2>/dev/null", wpa, id);
|
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
|
||||||
snprintf(cmd, sizeof cmd, "%s select_network %d 2>/dev/null", wpa, id);
|
|
||||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
|
||||||
}
|
}
|
||||||
if (err) snprintf(err, elen, "connect: ssid=%s network_id=%d", ssid, id);
|
int created = 0;
|
||||||
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s id=%d", ssid, id);
|
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;
|
return MEOW_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
|
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; }
|
if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; }
|
||||||
char arg[132];
|
char arg[132];
|
||||||
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* connect 带空 psk → netmgrd 复用既有网络 */
|
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* 带空 psk → netmgrd 复用既有网络 */
|
||||||
char text[192];
|
char text[192];
|
||||||
int rc = netmgr_wifi("connect", arg, text, sizeof text);
|
int rc = netmgr_wifi_tmo(hidden ? "connecth" : "connect", arg, text, sizeof text,
|
||||||
if (rc == NETMGR_UNREACH) return nm_direct_connect(ssid, psk, err, elen);
|
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);
|
if (err && elen > 0) snprintf(err, elen, "%s", text);
|
||||||
return rc;
|
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)
|
int nm_wifi_disconnect(char *err, int elen)
|
||||||
{
|
{
|
||||||
char text[128];
|
char text[128];
|
||||||
@@ -456,6 +571,36 @@ int nm_bt_powered(void)
|
|||||||
return atoi(v);
|
return atoi(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== M47:设置页开关的「真实态」三态查询(-1 未知 / 0 关 / 1 开)=====
|
||||||
|
* 与 nm_bt_powered()/nm_wifi_connected() 的区别:那两个把「问不到」塌缩成 0(看起来像
|
||||||
|
* 「已关」),正是开关显示不可信的根因之一。M47 起设置页开关只用下面两个函数。 */
|
||||||
|
int nm_bt_power_state(void)
|
||||||
|
{
|
||||||
|
char text[192];
|
||||||
|
int read_timeout = 0;
|
||||||
|
/* 状态查询走专用入口:读超时 = 「状态未知」,且不把 netmgrd 标成 down(见 nm_cmd_t 注释)。 */
|
||||||
|
int rc = nm_cmd_t(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, "status", NULL,
|
||||||
|
text, sizeof text, NM_TMO_DFL, &read_timeout);
|
||||||
|
if (read_timeout) return -1;
|
||||||
|
if (rc == NETMGR_UNREACH) return net_bt_power_probe(); /* 直连回退;失败=未知 */
|
||||||
|
if (rc != MEOW_OK) return -1; /* daemon 报错:不知道就是不知道 */
|
||||||
|
char v[8];
|
||||||
|
nm_val(text, "available=", v, sizeof v, 0);
|
||||||
|
if (v[0] && atoi(v) == 0) return 0; /* 无控制器 → 必为关 */
|
||||||
|
nm_val(text, "powered=", v, sizeof v, 0);
|
||||||
|
if (!v[0]) return -1; /* 无 powered 字段 → 未知 */
|
||||||
|
return atoi(v) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WiFi 射频真实态(三态)。netmgrd 的 `wifi status` 只报 wpa_state/连接态,不报射频开关
|
||||||
|
* (见 meow_netmgrd.c wifi_status,字段仅 state/connected/ssid/ip/rssi),无法据此判断
|
||||||
|
* 「wifi 开关」;而 daemon 的 wifi on/off 恰好就是 rfkill wlan block/unblock,所以射频态
|
||||||
|
* 取本地 rfkill(回退 wlan0 IFF_UP)——与 daemon 改的是同一份内核状态,且探测失败=未知。 */
|
||||||
|
int nm_wifi_power_state(void)
|
||||||
|
{
|
||||||
|
return net_wifi_power_probe();
|
||||||
|
}
|
||||||
|
|
||||||
int nm_bt_onoff(int on)
|
int nm_bt_onoff(int on)
|
||||||
{
|
{
|
||||||
char text[128];
|
char text[128];
|
||||||
|
|||||||
@@ -1168,16 +1168,15 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
|
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof 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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 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);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
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 touch_cooldown_until = now_ms() + 300;
|
||||||
@@ -1304,12 +1303,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
}
|
}
|
||||||
if (wak) {
|
if (wak) {
|
||||||
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
|
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);
|
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
||||||
@@ -1331,23 +1329,20 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
|
/* 按键:系统语义(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);
|
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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 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);
|
(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;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
if (wake_suppress) {
|
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 {
|
} else {
|
||||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
if (ev.vol_dn_edge) { 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_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||||
if (play && !m_prev_play) {
|
if (play && !m_prev_play) {
|
||||||
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
||||||
last_act = now;
|
last_act = now;
|
||||||
@@ -1366,12 +1361,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
show_power_menu();
|
show_power_menu();
|
||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
|
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);
|
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1; need_paint = 1;
|
after_modal = 1; need_paint = 1;
|
||||||
goto ncp_keys_seeded;
|
goto ncp_keys_seeded;
|
||||||
@@ -1390,14 +1384,14 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (vol_ramp_dir != 0) {
|
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);
|
int going = vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right();
|
paint_sb_right();
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
|
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:
|
ncp_keys_seeded:
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -1562,15 +1556,13 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
|||||||
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
|
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
|
||||||
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
|
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
|
||||||
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
|
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
|
||||||
|
/* 菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL);
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
|
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
|
||||||
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
|
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
|
||||||
|
|||||||
@@ -198,6 +198,54 @@ static void np_center_text(int y, const char *s, int size, uint32_t color, int m
|
|||||||
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
|
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 控件):
|
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
|
||||||
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
|
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
|
||||||
@@ -346,28 +394,49 @@ static void np_draw_home_bar(void)
|
|||||||
NP_HOME_BAR_H / 2, th->on_surface_variant);
|
NP_HOME_BAR_H / 2, th->on_surface_variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 歌词页整页绘制:背景统一黑色半透明叠加层(用户 2026-09-04,弃封面取色/封面图半透明底,
|
/* 网易云式歌词页封面背景(用户 2026-09-05):有封面 → 等比居中放大铺满内容区(cover-fit,
|
||||||
* 保证高亮封面下歌词对比度)→ lyric_view(自绘 scrim + 歌词/空态)。 */
|
* 超出部分由画布裁剪,镜像 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)
|
static void np_draw_lyric_page(int redraw_sb)
|
||||||
{
|
{
|
||||||
const meow_ui_theme_t *th = s_ui_theme();
|
const meow_ui_theme_t *th = s_ui_theme();
|
||||||
int W = fb_xres, H = fb_yres;
|
int W = fb_xres, H = fb_yres;
|
||||||
if (!g_np_init) np_widgets_create();
|
if (!g_np_init) np_widgets_create();
|
||||||
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
||||||
/* 批5:封面取色背景(主→次渐变)或主题面;并注入歌词页 set_colors。
|
/* 批5:np_cover_theme_update 取色(仅为播放页文字/按钮主题;歌词页背景不走取色,恒 ncm 白/灰)。
|
||||||
* 批6:有封面时以封面作整页背景(衬在 lyric_view 半透明 scrim 之下),取色只做附加;
|
* 批6:有封面时以封面作整页背景(网易云式 cover-fit 铺满,见 np_draw_lyric_cover_bg),
|
||||||
* 无封面回退渐变/主题面。
|
* 无封面回退纯黑底;压暗由下方黑色半透明叠加层(alpha176)统一完成,lyric_view 关内置 scrim。
|
||||||
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
|
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
|
||||||
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
|
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
|
||||||
np_cover_load(meow_media_current());
|
np_cover_load(meow_media_current());
|
||||||
np_cover_theme_update();
|
np_cover_theme_update();
|
||||||
np_apply_lyric_colors();
|
/* 网易云式封面背景(用户 2026-09-05):先铺封面底(cover-fit 原图铺满),无封面回退纯黑底;
|
||||||
/* 用户 2026-09-04:歌词页背景统一黑色半透明叠加层(弃封面取色/封面图半透明底)——
|
* 随后统一叠黑色半透明叠加层(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);
|
meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176);
|
||||||
np_lyric_refresh(meow_media_current());
|
np_lyric_refresh(meow_media_current());
|
||||||
if (g_np_lyric)
|
if (g_np_lyric)
|
||||||
meow_widget_draw(meow_lyric_view_widget(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 指示(底部上滑可回桌面) */
|
np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */
|
||||||
if (redraw_sb) draw_statusbar();
|
if (redraw_sb) draw_statusbar();
|
||||||
}
|
}
|
||||||
@@ -486,6 +555,31 @@ static int64_t np_progress_seek(int tx, int ty)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 播放页按键动作应用(音量 + 播放切换)。语义与主界面/子页一致:
|
||||||
|
* - 音量键 = 系统音量(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)
|
void run_now_playing(struct app *apps, int napps)
|
||||||
{
|
{
|
||||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
@@ -495,15 +589,17 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
|
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
|
||||||
kp_open(&kp_power, DEV_POWER);
|
kp_open(&kp_power, DEV_POWER);
|
||||||
struct kd_state kd_play;
|
struct kd_state kd_play; /* 音量键防抖/基线上收 key_router(进程唯一) */
|
||||||
memset(&kd_play, 0, sizeof kd_play);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
/* 读播放键按住态(用于边沿) */
|
/* 读播放键按住态(用于边沿) */
|
||||||
int m_prev_play = 0;
|
int m_prev_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))) m_prev_play = 1;
|
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
|
}
|
||||||
int np_prev_power = 0; /* 电源键边沿跟踪 */
|
int np_prev_power = 0; /* 电源键边沿跟踪 */
|
||||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 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) */
|
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;
|
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||||||
@@ -521,6 +617,7 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
||||||
* owner==active 闸门——漏 push 则 active()!=0x51,
|
* owner==active 闸门——漏 push 则 active()!=0x51,
|
||||||
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
||||||
|
key_router_page_enter(NP_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||||
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
|
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
|
||||||
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
|
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
|
||||||
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
|
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
|
||||||
@@ -631,17 +728,30 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 播放键边沿 → 播放/暂停切换 */
|
/* 播放键/音量键边沿 → 媒体播放/暂停切换 + 系统音量(与主界面/子页一致)。
|
||||||
|
* 修复:run_now_playing 原先只接播放/电源键,未接音量键 → 播放页/歌词页(同一循环,
|
||||||
|
* 与页无关)音量键无反应。 */
|
||||||
|
key_evt_t kev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||||
kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_keys, NULL, NULL);
|
||||||
int play = 0;
|
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 (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);
|
|
||||||
np_last_input = now_ms();
|
|
||||||
}
|
}
|
||||||
|
(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;
|
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 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
|
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
|
||||||
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
|
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
|
||||||
@@ -714,6 +824,7 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
usleep(10000);
|
usleep(10000);
|
||||||
}
|
}
|
||||||
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
|
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
|
||||||
|
key_router_page_exit(NP_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||||
if (tfd >= 0) close(tfd);
|
if (tfd >= 0) close(tfd);
|
||||||
kp_close(&kp_keys);
|
kp_close(&kp_keys);
|
||||||
kp_close(&kp_power);
|
kp_close(&kp_power);
|
||||||
|
|||||||
@@ -91,4 +91,26 @@ static inline int overlay_lock_wake_now(int pwr_edge, int touch_ready)
|
|||||||
return (pwr_edge || touch_ready) ? 1 : 0;
|
return (pwr_edge || touch_ready) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* M40 F4/P4:锁屏页「电源按下=息屏退出」必须在**按下边沿**生效,且进场后 ≥300ms 才允许。
|
||||||
|
* 旧实现用电平(kp_is_down)且无防抖:打开电源菜单的那次按压在 PIN 页出现时仍按着,
|
||||||
|
* PIN 页会被同一次按住立刻熄灭(用户症状「手动锁屏不熄屏而是进 PIN,此时点电源才熄屏」)。
|
||||||
|
* 300ms 也避开 launcher 进入锁屏页时的按键基线窗口。 */
|
||||||
|
#define OVERLAY_LOCK_POWER_GRACE_MS 300
|
||||||
|
|
||||||
|
static inline int overlay_lock_power_off_now(int pwr_down_edge, unsigned long long since_entry_ms)
|
||||||
|
{
|
||||||
|
return (pwr_down_edge && since_entry_ms >= OVERLAY_LOCK_POWER_GRACE_MS) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* M40 F3/P3:叠加层/电源菜单返回后的排空窗内,`/tmp/meow_app_activity` 是 App 被 SIGCONT
|
||||||
|
* 后重放的冻结期输入(不是用户新触摸),必须消费掉但不得触发「亮屏+锁屏」。
|
||||||
|
* 返回 1=仍在排空窗(忽略 activity 标记)。 */
|
||||||
|
static inline int overlay_activity_drain_active(int have_return_mark,
|
||||||
|
unsigned long long since_return_ms,
|
||||||
|
unsigned long long now_ms)
|
||||||
|
{
|
||||||
|
if (!have_return_mark) return 0;
|
||||||
|
return (now_ms - since_return_ms < OVERLAY_DRAIN_WINDOW_MS) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
#endif /* MEOW_OVERLAY_GATE_H */
|
#endif /* MEOW_OVERLAY_GATE_H */
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
/* pkg_install.c — M42: launcherd 侧包安装入口 = fork+exec `nyai install <包路径>`。
|
||||||
|
*
|
||||||
|
* 背景:M35 把 nyai 改成「包身份以已签名 app.json 清单为准 + SD 暂存 + 必清理」后,
|
||||||
|
* launcherd 的 meow_pkg_install() 仍是旧内联实现(文件名推包名、硬要 .mpk 后缀、
|
||||||
|
* 解压前 rm -rf 目标目录、失败不回滚备份、不拒 . / .. 、系统包大小写敏感比较),
|
||||||
|
* 会在 launcherd 入口 100% 复现原事故。本模块删掉那套内联解包,改委托 nyai:
|
||||||
|
* 解包→读清单→footer 一致性→验签(唯一权威 meow_verify)→防降级→rename 部署→清理
|
||||||
|
* 全部只有 nyai 一份实现,launcherd 只负责:
|
||||||
|
* ① 入口路径预检(纯逻辑,宿主可测);
|
||||||
|
* ② 启动子进程并把 stdout/stderr 分别收进有界缓冲(失败给 UI 可读文案);
|
||||||
|
* ③ 两段式 start/poll,安装期间调用方(UI 主循环)可按帧驱动、不假死。
|
||||||
|
*
|
||||||
|
* stdout/stderr 必须分开收:nyai 的进度走 stdout(管道 → 全缓冲,进程退出时才刷),
|
||||||
|
* 错误走 stderr(无缓冲,立即出)。合成一路时「最后一行」会被刷出的旧进度覆盖,
|
||||||
|
* 真机首测即踩到(降到 code 8 的包报错被「签名校验通过」顶掉)。错误文案只看 stderr。
|
||||||
|
*
|
||||||
|
* nyai 二进制:env MEOW_NYAI_BIN 覆写(宿主测试/非常规部署),缺省 /usr/bin/nyai
|
||||||
|
* (固件 fw_content.conf 安装点,与 MEOW_VERIFY_BIN 同口径,不依赖 PATH)。
|
||||||
|
*/
|
||||||
|
#include "meow_launcher_internal.h"
|
||||||
|
|
||||||
|
#define NYAI_BIN_DEFAULT "/usr/bin/nyai"
|
||||||
|
#define OUT_KEEP 1024 /* 每条流保留尾部字节数(够放 nyai 的最后几条信息) */
|
||||||
|
|
||||||
|
static pid_t g_pid = -1; /* >0 = 安装子进程在跑 */
|
||||||
|
static int g_fd_out = -1; /* stdout 读端 */
|
||||||
|
static int g_fd_err = -1; /* stderr 读端 */
|
||||||
|
static char g_buf_out[OUT_KEEP + 1];
|
||||||
|
static int g_len_out;
|
||||||
|
static char g_buf_err[OUT_KEEP + 1];
|
||||||
|
static int g_len_err;
|
||||||
|
static char g_err[256]; /* 最近一次失败的可读文案 */
|
||||||
|
|
||||||
|
/* ---------------- 纯逻辑(宿主单测:test_pkg_install.c) ---------------- */
|
||||||
|
|
||||||
|
/* 取字符串里最后一条非空行(去尾部空白;超长取尾部)。0=有内容 -1=空。 */
|
||||||
|
static int last_line_of(const char *s, char *out, int n)
|
||||||
|
{
|
||||||
|
if (!s || !*s || !out || n <= 0) return -1;
|
||||||
|
size_t L = strlen(s);
|
||||||
|
while (L > 0 && (s[L-1] == '\n' || s[L-1] == '\r' || s[L-1] == ' ' || s[L-1] == '\t')) L--;
|
||||||
|
if (L == 0) { out[0] = 0; return -1; }
|
||||||
|
size_t start = L;
|
||||||
|
while (start > 0 && s[start-1] != '\n') start--;
|
||||||
|
size_t len = L - start;
|
||||||
|
if (len >= (size_t)n) { start += len - (size_t)(n - 1); len = (size_t)(n - 1); }
|
||||||
|
memcpy(out, s + start, len);
|
||||||
|
out[len] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 入口路径预检:0=可以交给 nyai;<0=拒装(err 填可读文案)。纯逻辑,宿主单测。
|
||||||
|
* 只判「有没有这个文件、能不能读、有没有控制字符」——包身份一律由 nyai 从包内
|
||||||
|
* app.json 取,**不要求 .mpk 后缀、不看文件名**(M35 定案)。 */
|
||||||
|
int meow_pkg_install_check_path(const char *path, char *err, int n)
|
||||||
|
{
|
||||||
|
char dummy[8];
|
||||||
|
if (!err || n <= 0) { err = dummy; n = (int)sizeof dummy; }
|
||||||
|
err[0] = 0;
|
||||||
|
if (!path || !*path) { snprintf(err, n, "包路径为空"); return -1; }
|
||||||
|
if (strlen(path) >= 480) { snprintf(err, n, "包路径过长"); return -1; }
|
||||||
|
for (const char *p = path; *p; p++) {
|
||||||
|
unsigned char c = (unsigned char)*p;
|
||||||
|
if (c < 0x20 || c == 0x7f) { snprintf(err, n, "包路径含控制字符"); return -1; }
|
||||||
|
}
|
||||||
|
struct stat st;
|
||||||
|
if (stat(path, &st) != 0) { snprintf(err, n, "找不到包文件 %s", path); return -1; }
|
||||||
|
if (!S_ISREG(st.st_mode)) { snprintf(err, n, "不是普通文件 %s", path); return -1; }
|
||||||
|
if (access(path, R_OK) != 0) { snprintf(err, n, "包文件不可读 %s", path); return -1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出码 + 输出尾行 → 一条可读文案。rc 可为 nyai 原始退出码(>0 / >=128)或本模块
|
||||||
|
* 的 MEOW_PKG_E_* 负码。纯逻辑,宿主单测(文案不许为空、不许越界)。 */
|
||||||
|
void meow_pkg_install_error_text(int rc, const char *detail, char *out, int n)
|
||||||
|
{
|
||||||
|
if (!out || n <= 0) return;
|
||||||
|
out[0] = 0;
|
||||||
|
if (rc == 0) return;
|
||||||
|
char line[192];
|
||||||
|
if (last_line_of(detail, line, sizeof line) != 0) line[0] = 0;
|
||||||
|
|
||||||
|
if (rc == MEOW_PKG_E_BUSY) {
|
||||||
|
snprintf(out, n, "已有安装在运行,请稍后再试");
|
||||||
|
} else if (rc == MEOW_PKG_E_PATH) {
|
||||||
|
snprintf(out, n, "%s", line[0] ? line : "安装参数无效");
|
||||||
|
} else if (rc == MEOW_PKG_E_NYAI) {
|
||||||
|
snprintf(out, n, "包管理器 nyai 不可用%s%s", line[0] ? ": " : "", line);
|
||||||
|
} else if (rc == MEOW_PKG_E_TIMEOUT) {
|
||||||
|
snprintf(out, n, "安装超时(nyai 未在限时内结束)%s%s", line[0] ? ": " : "", line);
|
||||||
|
} else if (rc >= 128) {
|
||||||
|
snprintf(out, n, "安装失败(nyai 被信号 %d 终止)", rc - 128);
|
||||||
|
} else if (line[0]) {
|
||||||
|
snprintf(out, n, "%s", line);
|
||||||
|
} else if (rc > 0) {
|
||||||
|
snprintf(out, n, "安装失败(nyai 退出码 %d)", rc);
|
||||||
|
} else {
|
||||||
|
snprintf(out, n, "安装失败(错误码 %d)", rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 输出缓冲 ---------------- */
|
||||||
|
|
||||||
|
static void buf_put(char *buf, int *len, const char *s, int n)
|
||||||
|
{
|
||||||
|
if (n <= 0) return;
|
||||||
|
if (n >= OUT_KEEP) {
|
||||||
|
memcpy(buf, s + n - OUT_KEEP, OUT_KEEP - 1);
|
||||||
|
buf[OUT_KEEP - 1] = 0;
|
||||||
|
*len = OUT_KEEP - 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (*len + n > OUT_KEEP - 1) {
|
||||||
|
int drop = *len + n - (OUT_KEEP - 1);
|
||||||
|
memmove(buf, buf + drop, (size_t)(*len - drop));
|
||||||
|
*len -= drop;
|
||||||
|
}
|
||||||
|
memcpy(buf + *len, s, (size_t)n);
|
||||||
|
*len += n;
|
||||||
|
buf[*len] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读干一条流当前可用字节;*eof=1 表示写端已关。非阻塞读,调用方按帧驱动。 */
|
||||||
|
static void drain_fd(int *fd, char *buf, int *len, int *eof)
|
||||||
|
{
|
||||||
|
if (*fd < 0) { if (eof) *eof = 1; return; }
|
||||||
|
for (;;) {
|
||||||
|
char b[256];
|
||||||
|
ssize_t r = read(*fd, b, sizeof b);
|
||||||
|
if (r > 0) { buf_put(buf, len, b, (int)r); continue; }
|
||||||
|
if (r == 0) { if (eof) *eof = 1; break; }
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
|
||||||
|
if (eof) *eof = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子进程退出后把残留数据读干(此时无写端,读空即 EOF),然后关 fd。 */
|
||||||
|
static void drain_finish(int *fd, char *buf, int *len)
|
||||||
|
{
|
||||||
|
if (*fd < 0) return;
|
||||||
|
for (;;) {
|
||||||
|
char b[256];
|
||||||
|
ssize_t r = read(*fd, b, sizeof b);
|
||||||
|
if (r > 0) { buf_put(buf, len, b, (int)r); continue; }
|
||||||
|
if (r < 0 && errno == EINTR) continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
close(*fd);
|
||||||
|
*fd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 启动 / 轮询 / 同步封装 ---------------- */
|
||||||
|
|
||||||
|
const char *meow_pkg_install_error(void)
|
||||||
|
{
|
||||||
|
return g_err;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_pkg_install_busy(void)
|
||||||
|
{
|
||||||
|
return g_pid > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 启动一次安装(不等待)。0=已启动;<0=MEOW_PKG_E_*(g_err 已填)。 */
|
||||||
|
int meow_pkg_install_start(const char *path)
|
||||||
|
{
|
||||||
|
char perr[256];
|
||||||
|
if (g_pid > 0) {
|
||||||
|
meow_pkg_install_error_text(MEOW_PKG_E_BUSY, NULL, g_err, sizeof g_err);
|
||||||
|
return MEOW_PKG_E_BUSY;
|
||||||
|
}
|
||||||
|
if (meow_pkg_install_check_path(path, perr, sizeof perr) != 0) {
|
||||||
|
snprintf(g_err, sizeof g_err, "%s", perr);
|
||||||
|
logmsg("pkg_install: rejected path: %s", perr);
|
||||||
|
return MEOW_PKG_E_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *bin = getenv("MEOW_NYAI_BIN");
|
||||||
|
if (!bin || !*bin) bin = NYAI_BIN_DEFAULT;
|
||||||
|
if (access(bin, X_OK) != 0) {
|
||||||
|
snprintf(g_err, sizeof g_err, "包管理器 nyai 不可用: 无法执行 %s", bin);
|
||||||
|
logmsg("pkg_install: nyai unavailable at %s", bin);
|
||||||
|
return MEOW_PKG_E_NYAI;
|
||||||
|
}
|
||||||
|
|
||||||
|
int po[2], pe[2];
|
||||||
|
if (pipe(po) != 0) {
|
||||||
|
snprintf(g_err, sizeof g_err, "安装无法启动: pipe 失败 (%s)", strerror(errno));
|
||||||
|
return MEOW_PKG_E_NYAI;
|
||||||
|
}
|
||||||
|
if (pipe(pe) != 0) {
|
||||||
|
close(po[0]); close(po[1]);
|
||||||
|
snprintf(g_err, sizeof g_err, "安装无法启动: pipe 失败 (%s)", strerror(errno));
|
||||||
|
return MEOW_PKG_E_NYAI;
|
||||||
|
}
|
||||||
|
/* 读端不进子进程,且轮询按帧读不阻塞主循环。 */
|
||||||
|
fcntl(po[0], F_SETFD, FD_CLOEXEC);
|
||||||
|
fcntl(po[0], F_SETFL, O_NONBLOCK);
|
||||||
|
fcntl(pe[0], F_SETFD, FD_CLOEXEC);
|
||||||
|
fcntl(pe[0], F_SETFL, O_NONBLOCK);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
close(po[0]); close(po[1]); close(pe[0]); close(pe[1]);
|
||||||
|
snprintf(g_err, sizeof g_err, "安装无法启动: fork 失败 (%s)", strerror(errno));
|
||||||
|
logmsg("pkg_install: fork failed");
|
||||||
|
return MEOW_PKG_E_NYAI;
|
||||||
|
}
|
||||||
|
if (pid == 0) {
|
||||||
|
/* 子进程:stdout/stderr 分两条管道,再 exec nyai(argv 直传,不经 shell)。 */
|
||||||
|
dup2(po[1], 1);
|
||||||
|
dup2(pe[1], 2);
|
||||||
|
if (po[1] > 2) close(po[1]);
|
||||||
|
if (pe[1] > 2) close(pe[1]);
|
||||||
|
execl(bin, "nyai", "install", path, (char *)0);
|
||||||
|
{
|
||||||
|
char m[160];
|
||||||
|
int l = snprintf(m, sizeof m, "nyai: 无法执行 %s\n", bin);
|
||||||
|
if (l > 0) { ssize_t w = write(2, m, (size_t)l); (void)w; }
|
||||||
|
}
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
close(po[1]);
|
||||||
|
close(pe[1]);
|
||||||
|
g_pid = pid;
|
||||||
|
g_fd_out = po[0];
|
||||||
|
g_fd_err = pe[0];
|
||||||
|
g_buf_out[0] = g_buf_err[0] = 0;
|
||||||
|
g_len_out = g_len_err = 0;
|
||||||
|
g_err[0] = 0;
|
||||||
|
logmsg("pkg_install: exec %s install '%s' (pid %d)", bin, path, (int)pid);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 轮询:1=运行中;0=已结束(*rc = 退出码,信号为 128+sig);-1=没有安装任务。 */
|
||||||
|
int meow_pkg_install_poll(int *rc)
|
||||||
|
{
|
||||||
|
if (g_pid <= 0) return -1;
|
||||||
|
int eo = 0, ee = 0;
|
||||||
|
drain_fd(&g_fd_out, g_buf_out, &g_len_out, &eo);
|
||||||
|
drain_fd(&g_fd_err, g_buf_err, &g_len_err, &ee);
|
||||||
|
if (eo) { close(g_fd_out); g_fd_out = -1; }
|
||||||
|
if (ee) { close(g_fd_err); g_fd_err = -1; }
|
||||||
|
|
||||||
|
int st = 0;
|
||||||
|
pid_t w = waitpid(g_pid, &st, WNOHANG);
|
||||||
|
if (w < 0 && errno == EINTR) return 1;
|
||||||
|
if (w == 0) return 1;
|
||||||
|
|
||||||
|
drain_finish(&g_fd_out, g_buf_out, &g_len_out);
|
||||||
|
drain_finish(&g_fd_err, g_buf_err, &g_len_err);
|
||||||
|
|
||||||
|
int code;
|
||||||
|
if (w < 0) {
|
||||||
|
code = -1;
|
||||||
|
} else if (WIFEXITED(st)) {
|
||||||
|
code = WEXITSTATUS(st);
|
||||||
|
} else if (WIFSIGNALED(st)) {
|
||||||
|
code = 128 + WTERMSIG(st);
|
||||||
|
} else {
|
||||||
|
code = -1;
|
||||||
|
}
|
||||||
|
g_pid = -1;
|
||||||
|
if (rc) *rc = code;
|
||||||
|
|
||||||
|
if (code != 0) {
|
||||||
|
/* 错误文案只看 stderr(stdout 是缓冲进度,退出时才刷,会把真错误顶掉);
|
||||||
|
* stderr 空才退回 stdout 尾行。 */
|
||||||
|
meow_pkg_install_error_text(code, g_buf_err[0] ? g_buf_err : g_buf_out,
|
||||||
|
g_err, sizeof g_err);
|
||||||
|
logmsg("pkg_install: failed rc=%d (%s)", code, g_err);
|
||||||
|
} else {
|
||||||
|
char line[192];
|
||||||
|
if (last_line_of(g_buf_out, line, sizeof line) == 0)
|
||||||
|
logmsg("pkg_install: ok (%s)", line);
|
||||||
|
else
|
||||||
|
logmsg("pkg_install: ok");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 同步安装(CLI 入口用):start + 轮询到结束。0=成功;<0=MEOW_PKG_E_*。
|
||||||
|
* 超时默认 180s(MEOW_NYAI_TIMEOUT_MS 覆写,仅测试/调试),超时先 SIGTERM 再 SIGKILL。
|
||||||
|
* 轮询切片用固定 20ms usleep 计数,不依赖时钟 ABI(M40:mipsel time32/time64 混用教训)。 */
|
||||||
|
int meow_pkg_install(const char *path)
|
||||||
|
{
|
||||||
|
int r = meow_pkg_install_start(path);
|
||||||
|
if (r != 0) return r;
|
||||||
|
|
||||||
|
int timeout_ms = MEOW_PKG_INSTALL_TIMEOUT_MS;
|
||||||
|
const char *tv = getenv("MEOW_NYAI_TIMEOUT_MS");
|
||||||
|
if (tv && *tv) { int v = atoi(tv); if (v > 0) timeout_ms = v; }
|
||||||
|
int ticks = timeout_ms / 20;
|
||||||
|
if (ticks < 1) ticks = 1;
|
||||||
|
|
||||||
|
for (int i = 0; ; i++) {
|
||||||
|
int rc = 0;
|
||||||
|
int p = meow_pkg_install_poll(&rc);
|
||||||
|
if (p == 0) return (rc == 0) ? 0 : MEOW_PKG_E_FAIL;
|
||||||
|
if (p < 0) {
|
||||||
|
if (!g_err[0]) snprintf(g_err, sizeof g_err, "安装失败(无安装任务)");
|
||||||
|
return MEOW_PKG_E_FAIL;
|
||||||
|
}
|
||||||
|
if (i >= ticks) break;
|
||||||
|
usleep(20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超时:SIGTERM 给 nyai 走自身清理(stg_cleanup 的 signal 钩子),2s 后 SIGKILL 兜底。 */
|
||||||
|
int finished = 0;
|
||||||
|
if (g_pid > 0) kill(g_pid, SIGTERM);
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
int rc = 0;
|
||||||
|
if (meow_pkg_install_poll(&rc) == 0) { finished = 1; break; }
|
||||||
|
usleep(20000);
|
||||||
|
}
|
||||||
|
if (!finished && g_pid > 0) {
|
||||||
|
kill(g_pid, SIGKILL);
|
||||||
|
for (int i = 0; i < 100 && g_pid > 0; i++) {
|
||||||
|
int rc = 0;
|
||||||
|
meow_pkg_install_poll(&rc);
|
||||||
|
if (g_pid > 0) usleep(20000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(g_err, sizeof g_err, "安装超时(超过 %d 秒未完成,已终止)", timeout_ms / 1000);
|
||||||
|
logmsg("pkg_install: timeout after %d ms, child killed", timeout_ms);
|
||||||
|
return MEOW_PKG_E_TIMEOUT;
|
||||||
|
}
|
||||||
@@ -355,22 +355,59 @@ int meow_power_action(uint32_t cmd)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* M40 F2/P2:关闭菜单时恢复进入前的背景帧并 present。
|
||||||
|
* dim_screen() 就地压暗 back_buf;若不恢复,app_launcher 关闭会话时的 pm_capture_app_frame()
|
||||||
|
* 会把「压暗 App 帧 + 菜单卡」当成 App 帧拍回并上屏 → 菜单关闭后屏幕仍是菜单画面,
|
||||||
|
* 用户看到「再次单击没反应」(P2 主因)。所有退出路径(取消/锁定/关机/强关)统一恢复。 */
|
||||||
|
static void pm_restore_bg(unsigned char *snap, size_t len)
|
||||||
|
{
|
||||||
|
if (!snap || !len || !back_buf_ok || !fb) return;
|
||||||
|
memcpy(fb, snap, len);
|
||||||
|
meow_dirty_full();
|
||||||
|
meow_ui_flush();
|
||||||
|
}
|
||||||
|
|
||||||
/* 电源菜单(M2.6):2×2 网格,dirty 绘制消频闪;输入修复:
|
/* 电源菜单(M2.6):2×2 网格,dirty 绘制消频闪;输入修复:
|
||||||
* - 触摸按下捕获坐标、抬起命中按钮即确认(同 run_menu MT 防抖);
|
* - 触摸按下捕获坐标、抬起命中按钮即确认(同 run_menu MT 防抖);
|
||||||
* - 音量上/下导航;播放=确认;电源新按(松开长按后)=确认当前钮(非取消);
|
* - 音量上/下导航;播放=确认;电源新按(松开长按后)=确认当前钮(非取消);
|
||||||
* - 取消=选「取消」钮;8s 持续按住=强制关机(绕过)。
|
* - 取消=选「取消」钮;按住 power.forceoff_ms(缺省 8s,从按下起算)=强制关机(绕过)。
|
||||||
* 返回 1=采取动作,0=取消。 */
|
* 返回 1=采取动作,0=取消。 */
|
||||||
int show_power_menu(void)
|
int show_power_menu(void)
|
||||||
{
|
{
|
||||||
int fbfd = open(FB_DEV, O_RDWR);
|
int fbfd = open(FB_DEV, O_RDWR);
|
||||||
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } /* 亮起面板 */
|
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } /* 亮起面板 */
|
||||||
pm_capture_app_frame(); /* A-0:菜单背景 = app 当前帧(fb0 buffer0),再压暗绘制其上 */
|
meow_screen_on = 1; /* 菜单打开即视为亮屏 */
|
||||||
|
|
||||||
|
/* M40 F2:先开输入设备并采样第一轮基线。旧顺序把这一步放在 pm_capture/dim/建卡之后
|
||||||
|
* (真机实测菜单请求→keypoll ready 相隔 75ms),一次完整的按下+抬起若落在这段阻塞绘制里
|
||||||
|
* 就永远看不到边沿(EVIOCGKEY 是电平采样,不回溯事件队列)。重活挪到采样之后。 */
|
||||||
|
meow_touch mt;
|
||||||
|
mt_reset(&mt);
|
||||||
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
|
struct keypoll kp_power, kp_keys;
|
||||||
|
kp_open(&kp_power, DEV_POWER);
|
||||||
|
kp_open(&kp_keys, DEV_KEYS);
|
||||||
|
int prev_vold = 0, prev_volu = 0, prev_pwr = 0;
|
||||||
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
|
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
|
||||||
|
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
|
||||||
|
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
|
||||||
|
|
||||||
|
/* 背景帧:先把 app 当前帧读回 back_buf,快照后再压暗绘制 */
|
||||||
|
pm_capture_app_frame(); /* A-0:菜单背景 = app 当前帧(fb0 buffer0) */
|
||||||
|
unsigned char *pm_bg_snap = NULL;
|
||||||
|
size_t pm_bg_len = 0;
|
||||||
|
if (back_buf_ok && fb && fb_line > 0 && fb_yres > 0) {
|
||||||
|
pm_bg_len = (size_t)fb_line * (size_t)fb_yres;
|
||||||
|
pm_bg_snap = malloc(pm_bg_len);
|
||||||
|
if (pm_bg_snap) memcpy(pm_bg_snap, fb, pm_bg_len);
|
||||||
|
else { pm_bg_len = 0; logmsg("pm: bg snapshot malloc fail (len=%zu)", pm_bg_len); }
|
||||||
|
}
|
||||||
dim_screen();
|
dim_screen();
|
||||||
/* M2.8 Bug 3-residual:若电源菜单是在「息屏」态打开(按住 3s),此处 FBIOBLANK(0)
|
/* M2.8 Bug 3-residual:若电源菜单是在「息屏」态打开(按住 3s),此处 FBIOBLANK(0)
|
||||||
* 刚上电,LCDC 扫描地址可能非 buffer0;强制 re-pan 让压暗的应用列表 + 菜单落在被显示的
|
* 刚上电,LCDC 扫描地址可能非 buffer0;强制 re-pan 让压暗的应用列表 + 菜单落在被显示的
|
||||||
* 缓冲,否则菜单画在非显示缓冲上不可见。 */
|
* 缓冲,否则菜单画在非显示缓冲上不可见。 */
|
||||||
fb_pan_buf0_force();
|
fb_pan_buf0_force();
|
||||||
meow_screen_on = 1; /* 菜单打开即视为亮屏 */
|
|
||||||
|
|
||||||
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
|
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
|
||||||
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
|
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
|
||||||
@@ -383,29 +420,25 @@ int show_power_menu(void)
|
|||||||
pm_draw_card(); /* 卡片 + 标题 POWER MENU + 底部提示(SDK 主题色,i18n 文案) */
|
pm_draw_card(); /* 卡片 + 标题 POWER MENU + 底部提示(SDK 主题色,i18n 文案) */
|
||||||
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
|
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
|
||||||
|
|
||||||
/* 触摸状态(按下→抬起):统一 meow_touch 接口(goodix 时序免疫,原点由首个有效 ABS 锁) */
|
|
||||||
meow_touch mt;
|
|
||||||
mt_reset(&mt);
|
|
||||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
|
||||||
|
|
||||||
struct keypoll kp_power, kp_keys;
|
|
||||||
kp_open(&kp_power, DEV_POWER);
|
|
||||||
kp_open(&kp_keys, DEV_KEYS);
|
|
||||||
int prev_vold = 0, prev_volu = 0, prev_pwr = 0;
|
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
|
||||||
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
|
|
||||||
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
|
|
||||||
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
|
|
||||||
|
|
||||||
int act = -1;
|
int act = -1;
|
||||||
|
int pm_rc = 0;
|
||||||
unsigned long long pm_t0 = now_ms();
|
unsigned long long pm_t0 = now_ms();
|
||||||
|
/* 强关阈值:power.forceoff_ms 是**从按下起算**的总时长;菜单在 longpress 阈值处打开,
|
||||||
|
* 因此菜单内剩余等待 = forceoff - longpress(缺省 8000-3000=5000ms,与原硬编码值一致,
|
||||||
|
* 但不再忽略 settings)。配置异常(剩余<1s)时退回完整 forceoff。 */
|
||||||
|
unsigned long long pm_forceoff_after_ms = 5000;
|
||||||
|
if (power_forceoff_ms > power_longpress_menu_ms &&
|
||||||
|
power_forceoff_ms - power_longpress_menu_ms >= 1000)
|
||||||
|
pm_forceoff_after_ms = (unsigned long long)(power_forceoff_ms - power_longpress_menu_ms);
|
||||||
|
else if (power_forceoff_ms > 0)
|
||||||
|
pm_forceoff_after_ms = (unsigned long long)power_forceoff_ms;
|
||||||
unsigned long long pm_last_interact = pm_t0; /* M2.33:菜单空闲自动关闭基准(任何互动刷新) */
|
unsigned long long pm_last_interact = pm_t0; /* M2.33:菜单空闲自动关闭基准(任何互动刷新) */
|
||||||
int pm_init = 0, last_sel = -1;
|
int pm_init = 0, last_sel = -1;
|
||||||
/* M2.10 Bug:电源菜单 8s 强关(按住电源≥8s=绕过强关)会覆盖用户的菜单操作 —— 用户按住
|
/* M2.10 Bug:电源菜单强关(按住电源到 forceoff)会覆盖用户的菜单操作 —— 用户按住
|
||||||
* 电源打开菜单后(尤其仍按着电源)点选「回到HibyOS/重启」等按钮,8s 一到先触发强关 → 关机
|
* 电源打开菜单后(尤其仍按着电源)点选「回到HibyOS/重启」等按钮,强关一到先触发 → 关机
|
||||||
* 而非执行所选动作。修法:一旦用户与菜单互动(音量导航/触摸/播放/电源确认),置 pm_interacted=1
|
* 而非执行所选动作。修法:一旦用户与菜单互动(音量导航/触摸/播放/电源确认),置 pm_interacted=1
|
||||||
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
|
* 取消强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
|
||||||
* 8s=强关」。*/
|
* 到 forceoff=强关」。*/
|
||||||
int pm_interacted = 0;
|
int pm_interacted = 0;
|
||||||
int pm_press_idx = -1; /* M7.0b:当前按下钮(按压态走 SDK 状态机位) */
|
int pm_press_idx = -1; /* M7.0b:当前按下钮(按压态走 SDK 状态机位) */
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -488,17 +521,25 @@ int show_power_menu(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
|
/* 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
|
||||||
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
|
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。
|
||||||
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
|
* M40:阈值接 power.forceoff_ms(从按下起算的剩余时长),不再硬编码 5000/日志谎称 8s。 */
|
||||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); meow_ui_activity_pop(); return 1; }
|
if (pwr && !pm_interacted && now_ms() - pm_t0 >= pm_forceoff_after_ms) {
|
||||||
logmsg("pm: POWER HOLD 8s -> force poweroff");
|
if (sim_no_exec) {
|
||||||
|
logmsg("pm: [sim-no-exec] POWER HOLD %dms (menu-open + %llums) -> would force poweroff",
|
||||||
|
power_forceoff_ms, pm_forceoff_after_ms);
|
||||||
|
pm_rc = 1;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
logmsg("pm: POWER HOLD %dms (menu-open + %llums) -> force poweroff",
|
||||||
|
power_forceoff_ms, pm_forceoff_after_ms);
|
||||||
if (meow_power_action(POWER_SHUTDOWN) != MEOW_OK) {
|
if (meow_power_action(POWER_SHUTDOWN) != MEOW_OK) {
|
||||||
logmsg("pm: power service unavailable, abort force-off (menu closed, no action)");
|
logmsg("pm: power service unavailable, abort force-off (menu closed, no action)");
|
||||||
meow_ui_activity_pop(); return 0;
|
pm_rc = 0;
|
||||||
|
goto out;
|
||||||
}
|
}
|
||||||
meow_ui_activity_pop();
|
pm_rc = 1;
|
||||||
return 1;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* M2.33 菜单空闲超时自动关闭:菜单受 MeowOS screen_off_timeout 管理(系统界面不豁免)。
|
/* M2.33 菜单空闲超时自动关闭:菜单受 MeowOS screen_off_timeout 管理(系统界面不豁免)。
|
||||||
@@ -516,57 +557,64 @@ int show_power_menu(void)
|
|||||||
if (act >= 0) break;
|
if (act >= 0) break;
|
||||||
usleep(POLL_MS * 1000);
|
usleep(POLL_MS * 1000);
|
||||||
}
|
}
|
||||||
if (tfd >= 0) close(tfd);
|
|
||||||
meow_ui_activity_pop(); /* M7.0b:退出电源菜单,回主 activity(控件 owner 仍钉哨兵,复用) */
|
|
||||||
|
|
||||||
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与电源动作逐项核对:
|
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与电源动作逐项核对:
|
||||||
* act==PM_NUM-1(取消) -> 只关菜单,绝不关机(return 0,永不到电源动作);
|
* act==PM_NUM-1(取消) -> 只关菜单,绝不关机(rc=0,永不到电源动作);
|
||||||
* act==4(锁定) -> 手动锁(meow_lock_screen_engage,未配 PIN 则 no-op),关菜单;
|
* act==4(锁定) -> 手动锁(meow_lock_screen_engage,未配 PIN 则 no-op),关菜单;
|
||||||
* act==0(关机)/act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才经服务 IPC 执行电源动作。
|
* act==0(关机)/act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才经服务 IPC 执行电源动作。
|
||||||
* 只有 act 为 0/1/2/3 才可能发电源动作;power 键本身绝不触发关机。 */
|
* 只有 act 为 0/1/2/3 才可能发电源动作;power 键本身绝不触发关机。 */
|
||||||
logmsg("pm: act=%d -> %s", act,
|
logmsg("pm: act=%d -> %s", act,
|
||||||
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式"
|
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式"
|
||||||
: act == 4 ? "锁定" : "取消");
|
: act == 4 ? "锁定" : "取消");
|
||||||
if (act == PM_NUM - 1) { logmsg("pm: cancel (close menu only)"); return 0; }
|
if (act == PM_NUM - 1) { logmsg("pm: cancel (close menu only)"); pm_rc = 0; goto out; }
|
||||||
if (act == 4) {
|
if (act == 4) {
|
||||||
logmsg("pm: LOCK SCREEN");
|
logmsg("pm: LOCK SCREEN");
|
||||||
meow_lock_screen_engage();
|
meow_lock_screen_engage();
|
||||||
return 0; /* 关菜单;run_menu 继续(锁屏页已阻塞到解锁或电源息屏) */
|
pm_rc = 0; /* 关菜单;run_menu 继续(锁屏页已阻塞到解锁或电源息屏) */
|
||||||
|
goto out;
|
||||||
}
|
}
|
||||||
/* 动作统一经电源管理服务 IPC(meow_svc_power);launcher 不再直连 poweroff/reboot(2)/写 mtd5/
|
/* 动作统一经电源管理服务 IPC(meow_svc_power);launcher 不再直连 poweroff/reboot(2)/写 mtd5/
|
||||||
* 写 boot 默认——关机/重启/升级/回HiBy 的动作原语都在服务内实现(§3.2 单一动作链路)。 */
|
* 写 boot 默认——关机/重启/升级/回HiBy 的动作原语都在服务内实现(§3.2 单一动作链路)。 */
|
||||||
/* 公共服务:执行动作;服务不可达 → 按「取消」返回(launcher 存活,绝不误报已动作卡死)。
|
/* 公共服务:执行动作;服务不可达 → 按「取消」返回(launcher 存活,绝不误报已动作卡死)。
|
||||||
* meow_power_action 内部已先 meow_notif_before_poweroff()(转储未上卡通知)。 */
|
* meow_power_action 内部已先 meow_notif_before_poweroff()(转储未上卡通知)。 */
|
||||||
|
pm_rc = 1;
|
||||||
if (act == 0) {
|
if (act == 0) {
|
||||||
logmsg("pm: POWER OFF");
|
logmsg("pm: POWER OFF");
|
||||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would poweroff"); return 1; }
|
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would poweroff"); goto out; }
|
||||||
if (meow_power_action(POWER_SHUTDOWN) != MEOW_OK) {
|
if (meow_power_action(POWER_SHUTDOWN) != MEOW_OK) {
|
||||||
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
||||||
return 0;
|
pm_rc = 0;
|
||||||
}
|
}
|
||||||
} else if (act == 1) {
|
} else if (act == 1) {
|
||||||
logmsg("pm: REBOOT");
|
logmsg("pm: REBOOT");
|
||||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would reboot"); return 1; }
|
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would reboot"); goto out; }
|
||||||
if (meow_power_action(POWER_REBOOT) != MEOW_OK) {
|
if (meow_power_action(POWER_REBOOT) != MEOW_OK) {
|
||||||
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
||||||
return 0;
|
pm_rc = 0;
|
||||||
}
|
}
|
||||||
} else if (act == 2) {
|
} else if (act == 2) {
|
||||||
logmsg("pm: RETURN TO HIBYOS");
|
logmsg("pm: RETURN TO HIBYOS");
|
||||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would reboot to hiby"); return 1; }
|
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would reboot to hiby"); goto out; }
|
||||||
if (meow_power_action(POWER_BACK_HIBY) != MEOW_OK) {
|
if (meow_power_action(POWER_BACK_HIBY) != MEOW_OK) {
|
||||||
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
||||||
return 0;
|
pm_rc = 0;
|
||||||
}
|
}
|
||||||
} else if (act == 3) {
|
} else if (act == 3) {
|
||||||
logmsg("pm: ENTER UPGRADE");
|
logmsg("pm: ENTER UPGRADE");
|
||||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would enter upgrade"); return 1; }
|
if (sim_no_exec) { logmsg("pm: [sim-no-exec] would enter upgrade"); goto out; }
|
||||||
if (meow_power_action(POWER_UPGRADE) != MEOW_OK) {
|
if (meow_power_action(POWER_UPGRADE) != MEOW_OK) {
|
||||||
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
||||||
return 0;
|
pm_rc = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 1;
|
out:
|
||||||
|
if (tfd >= 0) close(tfd);
|
||||||
|
meow_ui_activity_pop(); /* M7.0b:退出电源菜单,回主 activity(控件 owner 仍钉哨兵,复用) */
|
||||||
|
/* M40 F2:恢复进入前的背景帧(否则 app_launcher 会把菜单帧当 App 帧上屏 = P2)。
|
||||||
|
* 放在动作执行之后:锁屏动作已 meow_panel_off(),此时恢复只写 fb0(面板黑),
|
||||||
|
* 下次唤醒 / App 整帧重画时得到的是正确底层帧而不是 PIN/菜单帧。 */
|
||||||
|
pm_restore_bg(pm_bg_snap, pm_bg_len);
|
||||||
|
if (pm_bg_snap) free(pm_bg_snap);
|
||||||
|
return pm_rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1315,16 +1315,15 @@ void run_settings_page(void)
|
|||||||
|
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof 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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 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);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
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 touch_cooldown_until = now_ms() + 300;
|
||||||
@@ -1358,6 +1357,7 @@ void run_settings_page(void)
|
|||||||
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
|
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
|
||||||
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
|
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
|
||||||
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
|
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
|
||||||
|
key_router_page_enter(SET_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||||
set_paint_page(sel);
|
set_paint_page(sel);
|
||||||
meow_dirty_full(); meow_ui_flush();
|
meow_dirty_full(); meow_ui_flush();
|
||||||
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
|
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
|
||||||
@@ -1396,13 +1396,12 @@ void run_settings_page(void)
|
|||||||
meow_auto_slept = 0;
|
meow_auto_slept = 0;
|
||||||
wake_suppress = 1;
|
wake_suppress = 1;
|
||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
|
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸。
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
* 音量防抖/基线上收 key_router:唤醒时重播音量基线;播放/电源仍本页自理。 */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
|
||||||
@@ -1423,28 +1422,25 @@ void run_settings_page(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 按键 ---- */
|
/* ---- 按键 ---- */
|
||||||
|
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 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);
|
(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;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
if (wake_suppress) {
|
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 {
|
} else {
|
||||||
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
|
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
|
||||||
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
|
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
|
||||||
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
|
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
|
||||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
if (ev.vol_dn_edge) { 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_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||||
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
|
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
|
||||||
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
|
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
|
||||||
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
|
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
|
||||||
@@ -1480,14 +1476,11 @@ void run_settings_page(void)
|
|||||||
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
|
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
|
||||||
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
|
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
|
||||||
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
|
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold);
|
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
|
||||||
memset(&kd_volu, 0, sizeof kd_volu);
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
memset(&kd_play, 0, sizeof kd_play);
|
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -1514,14 +1507,14 @@ void run_settings_page(void)
|
|||||||
}
|
}
|
||||||
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
|
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
|
||||||
if (vol_ramp_dir != 0) {
|
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);
|
int going = vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right();
|
paint_sb_right();
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
|
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:
|
settings_keys_seeded:
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -1658,14 +1651,10 @@ void run_settings_page(void)
|
|||||||
scroll_y0 = 0;
|
scroll_y0 = 0;
|
||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
key_router_rebase(); /* 音量基线重播 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold);
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
|
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
|
||||||
@@ -1703,14 +1692,10 @@ void run_settings_page(void)
|
|||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
/* 重置设置页按键防抖基线,防残边 */
|
/* 重置设置页按键防抖基线,防残边 */
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
key_router_rebase(); /* 音量基线重播 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold);
|
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
|
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
|
||||||
@@ -1768,6 +1753,7 @@ void run_settings_page(void)
|
|||||||
done:
|
done:
|
||||||
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
|
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
|
||||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||||
|
key_router_page_exit(SET_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||||
if (tfd >= 0) close(tfd);
|
if (tfd >= 0) close(tfd);
|
||||||
kp_close(&kp_power); kp_close(&kp_keys);
|
kp_close(&kp_power); kp_close(&kp_keys);
|
||||||
logmsg("settings page closed");
|
logmsg("settings page closed");
|
||||||
|
|||||||
@@ -247,12 +247,11 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
meow_auto_slept = 0;
|
meow_auto_slept = 0;
|
||||||
wake_suppress = 1;
|
wake_suppress = 1;
|
||||||
last_act = now;
|
last_act = now;
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
|
||||||
@@ -302,14 +301,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||||
pwr_suppress_click = 1;
|
pwr_suppress_click = 1;
|
||||||
|
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -456,14 +453,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
scroll_base = 0; scroll_y0 = 0;
|
scroll_base = 0; scroll_y0 = 0;
|
||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
|
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -546,7 +541,8 @@ static sub_page_t *g_sp; /* 当前运行子页(rows/act/detai
|
|||||||
static sub_row_t g_rows[SUB_ROWS_MAX]; /* 行缓冲:rows() 一次性填满,listview bind 只读 */
|
static sub_row_t g_rows[SUB_ROWS_MAX]; /* 行缓冲:rows() 一次性填满,listview bind 只读 */
|
||||||
static int g_nrows = 0; /* 当前行数(listview vsource count 实时读) */
|
static int g_nrows = 0; /* 当前行数(listview vsource count 实时读) */
|
||||||
static int g_top_sw_en = 0; /* 本页是否显示顶开关 */
|
static int g_top_sw_en = 0; /* 本页是否显示顶开关 */
|
||||||
static int g_top_on = 0; /* 顶开关当前值 */
|
static int g_top_on = 0; /* 顶开关当前值(0/1;未知时停 0 并置 g_top_sw_unknown) */
|
||||||
|
static int g_top_sw_unknown = 0; /* M47 第三态:真实态未知(页回调 top_sw 回 -1) */
|
||||||
|
|
||||||
static meow_widget *g_tb = NULL; /* topappbar */
|
static meow_widget *g_tb = NULL; /* topappbar */
|
||||||
static meow_widget *g_sw = NULL; /* 顶开关(仅在有顶开关的页创建/使用) */
|
static meow_widget *g_sw = NULL; /* 顶开关(仅在有顶开关的页创建/使用) */
|
||||||
@@ -601,7 +597,10 @@ static void sp_lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx
|
|||||||
/* ---- 顶栏标题栏右侧动作按钮矩形(批③:sub_page_t.top_action)---- */
|
/* ---- 顶栏标题栏右侧动作按钮矩形(批③:sub_page_t.top_action)---- */
|
||||||
static void sp_top_action_rect(int *x, int *y, int *w, int *h)
|
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 (x) *x = 0;
|
||||||
|
if (y) *y = 0;
|
||||||
|
if (w) *w = 0;
|
||||||
|
if (h) *h = 0;
|
||||||
if (!g_sp || !g_sp->top_action) return;
|
if (!g_sp || !g_sp->top_action) return;
|
||||||
int tw = ui_width(g_sp->top_action, 20);
|
int tw = ui_width(g_sp->top_action, 20);
|
||||||
if (tw <= 0) return;
|
if (tw <= 0) return;
|
||||||
@@ -670,22 +669,29 @@ static void sp_widgets_create(void)
|
|||||||
g_sp_init = 1;
|
g_sp_init = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 顶开关控件置态(M47:未知 -1 时 SDK 开关只有两态,停在「关」+ 灰色「?」角标区分)。 */
|
||||||
|
static void sp_top_sw_apply(void)
|
||||||
|
{
|
||||||
|
if (g_sw) meow_switch_set(g_sw, g_top_on == 1 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
/* 逐页配置:topappbar 标题 + 顶开关(按需创建/置态)+ listview 行数。 */
|
/* 逐页配置:topappbar 标题 + 顶开关(按需创建/置态)+ listview 行数。 */
|
||||||
static void sp_configure_page(const sub_page_t *p, int top_sw_en, int top_on)
|
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);
|
if (p->title && g_tb) meow_topappbar_set_title(g_tb, p->title);
|
||||||
g_top_sw_en = top_sw_en;
|
g_top_sw_en = top_sw_en;
|
||||||
g_top_on = top_on;
|
/* M47:top_sw 回调的 *on 是三态(-1 未知 / 0 / 1),见 bt_page/wifi_page。 */
|
||||||
|
g_top_sw_unknown = (top_on < 0) ? 1 : 0;
|
||||||
|
g_top_on = (top_on == 1) ? 1 : 0;
|
||||||
if (top_sw_en) {
|
if (top_sw_en) {
|
||||||
if (!g_sw) {
|
if (!g_sw) {
|
||||||
meow_ui_activity_push(SUB_SENTINEL_ACT); /* owner 钉哨兵(命中闸门) */
|
meow_ui_activity_push(SUB_SENTINEL_ACT); /* owner 钉哨兵(命中闸门) */
|
||||||
int swx = fb_xres - SUB_PAD_X - 56; /* meow_switch 固定 56×32 */
|
int swx = fb_xres - SUB_PAD_X - 56; /* meow_switch 固定 56×32 */
|
||||||
int swy = SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2;
|
int swy = SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2;
|
||||||
g_sw = meow_switch_create(swx, swy, top_on);
|
g_sw = meow_switch_create(swx, swy, g_top_on);
|
||||||
meow_ui_activity_pop();
|
meow_ui_activity_pop();
|
||||||
} else if (g_sw) {
|
|
||||||
meow_switch_set(g_sw, top_on);
|
|
||||||
}
|
}
|
||||||
|
sp_top_sw_apply();
|
||||||
}
|
}
|
||||||
/* 批③:行完全自绘 on/off —— 页提供 row_draw 用自绘;否则清 NULL 回默认 renderer。 */
|
/* 批③:行完全自绘 on/off —— 页提供 row_draw 用自绘;否则清 NULL 回默认 renderer。 */
|
||||||
if (g_lv) meow_listview_set_item_draw(g_lv, p->row_draw ? sp_lv_draw : NULL, NULL);
|
if (g_lv) meow_listview_set_item_draw(g_lv, p->row_draw ? sp_lv_draw : NULL, NULL);
|
||||||
@@ -699,7 +705,17 @@ static void sp_paint_page(void)
|
|||||||
meow_ui_fill_rect(0, 0, W, H, mt->surface); /* 全屏底 surface */
|
meow_ui_fill_rect(0, 0, W, H, mt->surface); /* 全屏底 surface */
|
||||||
draw_statusbar();
|
draw_statusbar();
|
||||||
if (g_tb) meow_widget_draw(g_tb); /* topappbar(含底部分隔线) */
|
if (g_tb) meow_widget_draw(g_tb); /* topappbar(含底部分隔线) */
|
||||||
if (g_sw && g_top_sw_en) meow_widget_draw(g_sw); /* 顶开关(叠顶栏右缘) */
|
if (g_sw && g_top_sw_en) {
|
||||||
|
meow_widget_draw(g_sw); /* 顶开关(叠顶栏右缘) */
|
||||||
|
if (g_top_sw_unknown) {
|
||||||
|
/* M47 第三态:真实态未知。SDK 开关只有开/关两态,故开关停「关」并在左侧画灰色
|
||||||
|
* 「?」——明确表达「不是关着,是不知道」,而不是拿「关」冒充。 */
|
||||||
|
int qh = meow_ui_text_height(20);
|
||||||
|
int qw = ui_width("?", 20);
|
||||||
|
ui_draw(fb_xres - SUB_PAD_X - 56 - qw - 6,
|
||||||
|
SUB_TB_Y + (SUB_TB_H - qh) / 2, "?", mt->outline, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
sp_paint_top_action(); /* 批③:顶栏右侧动作按钮 */
|
sp_paint_top_action(); /* 批③:顶栏右侧动作按钮 */
|
||||||
int lvx, lvy, lvw, lvh;
|
int lvx, lvy, lvw, lvh;
|
||||||
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
|
sp_lv_rect(&lvx, &lvy, &lvw, &lvh);
|
||||||
@@ -712,13 +728,40 @@ static void sp_paint_page(void)
|
|||||||
s_ui_theme()->outline, 18);
|
s_ui_theme()->outline, 18);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部开关切换:写设置 + 生效 + 触发重扫 + 重填行。 */
|
/* 顶开关回读真实态(M47:UI 只认权威回调,不做「点了就翻面」的乐观更新)。 */
|
||||||
|
static void sp_top_sw_resync(void)
|
||||||
|
{
|
||||||
|
if (!g_sp || !g_sp->top_sw) return;
|
||||||
|
int on = 0;
|
||||||
|
if (!g_sp->top_sw(g_sp->ctx, &on)) { g_top_sw_en = 0; return; }
|
||||||
|
g_top_sw_en = 1;
|
||||||
|
g_top_sw_unknown = (on < 0) ? 1 : 0;
|
||||||
|
g_top_on = (on == 1) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 「待确认」态:开关保持上一次已确认外观,内容区顶出示同步条——与稳定态肉眼可区分。
|
||||||
|
* 有界等待期间主循环阻塞在 top_sw_set 内,所以必须先画一帧再下发。 */
|
||||||
|
static void sp_paint_switching(int want)
|
||||||
|
{
|
||||||
|
const meow_theme *mt = meow_theme_get();
|
||||||
|
char line[64];
|
||||||
|
snprintf(line, sizeof line, "正在%s…(等待设备确认)", want ? "开启" : "关闭");
|
||||||
|
int h = 32, y = SUB_CONT_TOP;
|
||||||
|
meow_ui_fill_rect(0, y, fb_xres, h, mt->surfaceContainerHigh);
|
||||||
|
meow_ui_text_center(fb_xres / 2, y + (h - meow_ui_text_height(18)) / 2, line, 18, mt->primary);
|
||||||
|
meow_dirty_full();
|
||||||
|
meow_ui_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部开关切换:待确认态 → 页内下发+有界等待+失败弹窗 → 回读真实态 → 重填行。 */
|
||||||
static void sp_top_sw_toggle(void)
|
static void sp_top_sw_toggle(void)
|
||||||
{
|
{
|
||||||
if (!g_sp || !g_sp->top_sw_set) return;
|
if (!g_sp || !g_sp->top_sw_set) return;
|
||||||
g_sp->top_sw_set(g_sp->ctx, !g_top_on);
|
int want = (g_top_on == 1) ? 0 : 1; /* 未知(-1) 视同「关」:点一下表示「请打开」 */
|
||||||
g_top_on = !g_top_on;
|
sp_paint_switching(want);
|
||||||
if (g_sw) meow_switch_set(g_sw, g_top_on);
|
g_sp->top_sw_set(g_sp->ctx, want); /* 页内:下发 + 有界轮询真实态 + 失败回滚/弹窗 */
|
||||||
|
sp_top_sw_resync(); /* 定稿只认真实态 */
|
||||||
|
sp_top_sw_apply();
|
||||||
if (g_sp->rows) {
|
if (g_sp->rows) {
|
||||||
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
|
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_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
|
||||||
@@ -743,16 +786,15 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
{
|
{
|
||||||
struct keypoll kp_power, kp_keys;
|
struct keypoll kp_power, kp_keys;
|
||||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
struct kd_state kd_play, kd_pwr;
|
||||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof 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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 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);
|
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
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 touch_cooldown_until = now_ms() + 300;
|
||||||
@@ -780,6 +822,7 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
sp_configure_page(p, top_sw_en, top_on);
|
sp_configure_page(p, top_sw_en, top_on);
|
||||||
|
|
||||||
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
|
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
|
||||||
|
key_router_page_enter(SUB_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||||
sp_paint_page();
|
sp_paint_page();
|
||||||
meow_dirty_full(); meow_ui_flush();
|
meow_dirty_full(); meow_ui_flush();
|
||||||
|
|
||||||
@@ -814,12 +857,11 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
meow_auto_slept = 0;
|
meow_auto_slept = 0;
|
||||||
wake_suppress = 1;
|
wake_suppress = 1;
|
||||||
last_act = now;
|
last_act = now;
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
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);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
|
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
|
||||||
@@ -832,23 +874,20 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
|
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
|
||||||
|
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
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++) {
|
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_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 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);
|
(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;
|
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||||
if (wake_suppress) {
|
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 {
|
} else {
|
||||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
if (ev.vol_dn_edge) { 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_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 (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
|
||||||
if (pwr && !m_prev_pwr) {
|
if (pwr && !m_prev_pwr) {
|
||||||
pwr_hold_at = now; pwr_menu_state = 0;
|
pwr_hold_at = now; pwr_menu_state = 0;
|
||||||
@@ -867,14 +906,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
last_act = now_ms();
|
last_act = now_ms();
|
||||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||||
pwr_suppress_click = 1;
|
pwr_suppress_click = 1;
|
||||||
|
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -899,13 +936,13 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (vol_ramp_dir != 0) {
|
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;
|
||||||
(void)vol_ramp_tick(held);
|
(void)vol_ramp_tick(held);
|
||||||
g_sys.vol = settings_volume_get();
|
g_sys.vol = settings_volume_get();
|
||||||
paint_sb_right();
|
paint_sb_right();
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
}
|
}
|
||||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
m_prev_play = play; m_prev_pwr = pwr;
|
||||||
sub_keys_seeded:
|
sub_keys_seeded:
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -985,9 +1022,31 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
}
|
}
|
||||||
if (g_top_sw_en && g_sw && meow_widget_hit(g_sw, tx, ty)) {
|
if (g_top_sw_en && g_sw && meow_widget_hit(g_sw, tx, ty)) {
|
||||||
sp_top_sw_toggle();
|
sp_top_sw_toggle();
|
||||||
last_act = now;
|
last_act = now_ms(); /* 切换内可能阻塞数秒:取新鲜时刻,
|
||||||
|
* 否则空闲息屏会按陈旧时刻提前触发 */
|
||||||
|
/* 评审 r1 P2-4:M47 起顶开关路径也会开 s_dd_modal(切换失败/
|
||||||
|
* 无法确认)。s_dd_modal 是全屏压暗层,关闭后必须像行命中分支
|
||||||
|
* 一样排空触摸、复位触摸/按键基线并置 after_modal —— 否则
|
||||||
|
* 返回脏区不含状态栏(0..27),会残留压暗到下一次时钟重绘,
|
||||||
|
* 电源/播放键也可能带进弹窗里的陈旧边沿。 */
|
||||||
|
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_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;
|
need_paint = 1;
|
||||||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/* 列表行命中(SDK 命中:含滚动换算 + 越界/禁用行不响应) */
|
/* 列表行命中(SDK 命中:含滚动换算 + 越界/禁用行不响应) */
|
||||||
@@ -1021,14 +1080,12 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
drag_last_y = 0;
|
drag_last_y = 0;
|
||||||
tdx = tdy = tlx = tly = -1;
|
tdx = tdy = tlx = tly = -1;
|
||||||
touch_cooldown_until = now_ms() + 300;
|
touch_cooldown_until = now_ms() + 300;
|
||||||
|
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||||
|
key_router_rebase();
|
||||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
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);
|
memset(&kd_play, 0, sizeof kd_play);
|
||||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
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);
|
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||||
after_modal = 1;
|
after_modal = 1;
|
||||||
need_paint = 1;
|
need_paint = 1;
|
||||||
@@ -1045,6 +1102,14 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
/* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */
|
/* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */
|
||||||
if (g_sp && g_sp->live && now - last_live >= 2000) {
|
if (g_sp && g_sp->live && now - last_live >= 2000) {
|
||||||
last_live = now;
|
last_live = now;
|
||||||
|
/* M47:顶开关也按 live 周期回读真实态——外部(其它页面/守护进程)改了开关,
|
||||||
|
* 本页驻留期间也要跟着变,而不是停在进入页面时那一刻。 */
|
||||||
|
int sw_before = g_top_on, unk_before = g_top_sw_unknown;
|
||||||
|
if (g_top_sw_en) sp_top_sw_resync();
|
||||||
|
if (g_top_on != sw_before || g_top_sw_unknown != unk_before) {
|
||||||
|
sp_top_sw_apply();
|
||||||
|
need_paint = 1;
|
||||||
|
}
|
||||||
int new_n = g_sp->rows ? g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX) : 0;
|
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 > SUB_ROWS_MAX) new_n = SUB_ROWS_MAX;
|
||||||
if (new_n != g_nrows) {
|
if (new_n != g_nrows) {
|
||||||
@@ -1087,6 +1152,7 @@ int meow_subpage_run(sub_page_t *p)
|
|||||||
subdone:
|
subdone:
|
||||||
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
|
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
|
||||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||||
|
key_router_page_exit(SUB_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||||
if (tfd >= 0) close(tfd);
|
if (tfd >= 0) close(tfd);
|
||||||
kp_close(&kp_power); kp_close(&kp_keys);
|
kp_close(&kp_power); kp_close(&kp_keys);
|
||||||
logmsg("subpage '%s' closed", p->title ? p->title : "");
|
logmsg("subpage '%s' closed", p->title ? p->title : "");
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
/* test_bgmgr.c — 批③ 后台管理页 逻辑层宿主单测(meow_bgmgr)。
|
/* test_bgmgr.c — 批③ 后台管理页 逻辑层宿主单测(meow_bgmgr)。
|
||||||
*
|
*
|
||||||
* 验证:
|
* 验证:
|
||||||
* 1) bgmgr_collect:enumerate 后台表 → app_meta 补 name/icon/framework/is_fg → proc_sample 补
|
* 1) bgmgr_collect:enumerate 后台表 → app_meta 补 name/icon/protected_app/is_fg → proc_sample 补
|
||||||
* pcpu/rss_kb;行 killable 预判正确;name 回退 app_id。
|
* pcpu/rss_kb;行 killable 预判正确;name 回退 app_id。
|
||||||
* 2) bgmgr_can_kill 门控矩阵:framework=1 → 0(**框架 App 不 kill**);is_fg=1 → 0(当前前台不 kill);
|
* 2) bgmgr_can_kill 门控矩阵:protected_app=1 → 0(**受保护 App 不 kill**);is_fg=1 → 0(当前前台不 kill);
|
||||||
* pid<=0 → 0(非后台/无进程);普通后台(非框架/非前台/pid>0)→ 1。
|
* pid<=0 → 0(非后台/无进程);普通后台(非受保护/非前台/pid>0)→ 1。
|
||||||
* 3) bgmgr_kill_all 一键清:框架 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。
|
* 3) bgmgr_kill_all 一键清:受保护 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。
|
||||||
|
* 3b) 回归:app.json framework=1(SDK 框架 App 标志,现有 App 全写 1)**不影响**可杀性。
|
||||||
* 4) bgmgr_status_str / bgmgr_mem_str 文案。
|
* 4) bgmgr_status_str / bgmgr_mem_str 文案。
|
||||||
* 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编:
|
* 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编:
|
||||||
* gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr
|
* gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr
|
||||||
@@ -17,7 +18,9 @@
|
|||||||
#include "meow_bgmgr.h"
|
#include "meow_bgmgr.h"
|
||||||
|
|
||||||
static int g_fails = 0;
|
static int g_fails = 0;
|
||||||
|
static int g_checks = 0;
|
||||||
#define CHECK(cond, msg) do { \
|
#define CHECK(cond, msg) do { \
|
||||||
|
g_checks++; \
|
||||||
if (cond) printf("PASS: %s\n", msg); \
|
if (cond) printf("PASS: %s\n", msg); \
|
||||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||||
|
|
||||||
@@ -33,7 +36,7 @@ static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
|||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
if (max < 4) return 0;
|
if (max < 4) return 0;
|
||||||
/* 4 个后台行:hello=普通、reader=常驻、sysguard=framework、fgapp=当前前台 */
|
/* 4 个后台行:hello=普通、reader=常驻、sysguard=受保护、fgapp=当前前台 */
|
||||||
memset(&rows[0], 0, sizeof rows[0]);
|
memset(&rows[0], 0, sizeof rows[0]);
|
||||||
snprintf(rows[0].app_id, sizeof rows[0].app_id, "%s", g_meta_app[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;
|
rows[0].pid = 1001; rows[0].keepalive = 0; rows[0].on_card = 0; rows[0].bg_since_ms = 1000;
|
||||||
@@ -52,10 +55,10 @@ static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
|||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* meta:hello/reader/sysguard/fgapp 各给名字;sysguard 标 framework;pid==1004 标前台。 */
|
/* meta:hello/reader/sysguard/fgapp 各给名字;sysguard 标受保护;pid==1004 标前台。 */
|
||||||
static int fake_app_meta(void *ctx, const char *app_id, pid_t pid,
|
static int fake_app_meta(void *ctx, const char *app_id, pid_t pid,
|
||||||
char *name, int nl, char *icon, int il,
|
char *name, int nl, char *icon, int il,
|
||||||
char *framework, int *is_fg)
|
char *protected_app, int *is_fg)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放");
|
if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放");
|
||||||
@@ -64,8 +67,8 @@ static int fake_app_meta(void *ctx, const char *app_id, pid_t pid,
|
|||||||
else if (!strcmp(app_id, "os.meow.fgapp")) snprintf(name, nl, "前台应用");
|
else if (!strcmp(app_id, "os.meow.fgapp")) snprintf(name, nl, "前台应用");
|
||||||
else return -1; /* 未注册 → name 回退 app_id */
|
else return -1; /* 未注册 → name 回退 app_id */
|
||||||
snprintf(icon, il, "/tmp/icon_%s.png", app_id);
|
snprintf(icon, il, "/tmp/icon_%s.png", app_id);
|
||||||
*framework = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0;
|
*protected_app = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0;
|
||||||
*is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */
|
*is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +115,9 @@ int main(void)
|
|||||||
printf("=== 1) collect ===\n");
|
printf("=== 1) collect ===\n");
|
||||||
CHECK(n == 4, "collect returns 4 rows");
|
CHECK(n == 4, "collect returns 4 rows");
|
||||||
CHECK(!strcmp(rows[0].name, "播放") && rows[0].pid == 1001, "row0 hello name/pid");
|
CHECK(!strcmp(rows[0].name, "播放") && rows[0].pid == 1001, "row0 hello name/pid");
|
||||||
CHECK(rows[0].framework == 0, "row0 hello not framework");
|
CHECK(rows[0].protected_app == 0, "row0 hello not protected");
|
||||||
CHECK(!strcmp(rows[1].name, "阅读") && rows[1].keepalive == 1, "row1 reader keepalive");
|
CHECK(!strcmp(rows[1].name, "阅读") && rows[1].keepalive == 1, "row1 reader keepalive");
|
||||||
CHECK(rows[2].framework == '1', "row2 sysguard framework");
|
CHECK(rows[2].protected_app == '1', "row2 sysguard protected");
|
||||||
CHECK(rows[3].is_fg == 1, "row3 fgapp is_fg");
|
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].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(rows[0].killable == 1 && rows[2].killable == 0, "killable prefilled by can_kill");
|
||||||
@@ -132,7 +135,7 @@ int main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
printf("=== 2) can_kill gating ===\n");
|
printf("=== 2) can_kill gating ===\n");
|
||||||
CHECK(bgmgr_can_kill(&rows[2]) == 0, "framework app NOT killable");
|
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[3]) == 0, "fg app NOT killable (is_fg)");
|
||||||
CHECK(bgmgr_can_kill(&rows[0]) == 1, "normal bg app killable");
|
CHECK(bgmgr_can_kill(&rows[0]) == 1, "normal bg app killable");
|
||||||
CHECK(bgmgr_can_kill(&rows[1]) == 1, "keepalive bg app killable");
|
CHECK(bgmgr_can_kill(&rows[1]) == 1, "keepalive bg app killable");
|
||||||
@@ -155,11 +158,21 @@ int main(void)
|
|||||||
if (!strcmp(g_killed_app[i], "os.meow.fgapp")) killed_fg = 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;
|
if (!strcmp(g_killed_app[i], "os.meow.hello")) killed_bg = 1;
|
||||||
}
|
}
|
||||||
CHECK(killed_fw == 0, "kill_all did NOT kill framework app");
|
CHECK(killed_fw == 0, "kill_all did NOT kill protected app");
|
||||||
CHECK(killed_fg == 0, "kill_all did NOT kill fg app");
|
CHECK(killed_fg == 0, "kill_all did NOT kill fg app");
|
||||||
CHECK(killed_bg == 1, "kill_all killed a real bg 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");
|
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");
|
printf("=== 4) status/mem str ===\n");
|
||||||
CHECK(!strcmp(bgmgr_status_str(&rows[1]), "常驻"), "status_str keepalive=常驻");
|
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[0]), "后台"), "status_str normal=后台");
|
||||||
@@ -167,7 +180,7 @@ int main(void)
|
|||||||
CHECK(!strcmp(bgmgr_mem_str(512), "512KB"), "mem_str(<1MB)=KB");
|
CHECK(!strcmp(bgmgr_mem_str(512), "512KB"), "mem_str(<1MB)=KB");
|
||||||
CHECK(!strcmp(bgmgr_mem_str(2048), "2.0MB"), "mem_str(>=1MB)=MB");
|
CHECK(!strcmp(bgmgr_mem_str(2048), "2.0MB"), "mem_str(>=1MB)=MB");
|
||||||
|
|
||||||
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", 20);
|
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
|
||||||
else printf("\n%d FAILED\n", g_fails);
|
else printf("\n%d FAILED\n", g_fails);
|
||||||
return g_fails ? 1 : 0;
|
return g_fails ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
/* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。
|
/* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。
|
||||||
*
|
*
|
||||||
* 通过 #include bg_page.c 把其 FILE-STATIC 的 bgm_rows/bgm_btn_rect/bgm_row_tap/bgm_top_action
|
* 通过 #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,
|
* 平移进本 TU;桩掉 launcher 后台表(meow_bg_count/at/terminate)+ 时钟 + meow_str/logmsg,
|
||||||
* 直接验证:
|
* 直接验证:
|
||||||
* P1 bgm_rows 行骨架:后台 App 行(act=i)+ 空态行(act=-1);name/framework 经 app_meta 正确;
|
* P1 bgm_rows 行骨架:后台 App 行(act=i)+ 空态行(act=-1);name 经 app_meta 正确;
|
||||||
|
* **行副文本指向 ctx 持久缓冲且扛得住栈抖动**(曾指向 bgm_rows 栈帧 → 真机副标题乱码);
|
||||||
* P2 bgm_btn_rect 按钮几何:行右 46×34 圆角钮(rx+rw-12-46 定位);
|
* P2 bgm_btn_rect 按钮几何:行右 46×34 圆角钮(rx+rw-12-46 定位);
|
||||||
* P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;框架 App
|
* P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;
|
||||||
* 按钮点击 → 不 terminate(!framework 门控);空态行不响应;
|
* **app.json framework=1(SDK 框架 App)照样可杀**(2026-11-04 真机回归);
|
||||||
* P4 bgm_top_action 顶栏清除:kill_all **不杀框架 App**(sysguard 不 terminate),杀普通后台;
|
* 受保护/前台行按钮点击 → 不 terminate(门控,留诊断日志);空态行不响应;
|
||||||
* P5 门控矩阵:bgmgr_can_kill(framework/fg/pid<=0/普通)。
|
* 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 剔除。
|
* 不调用行自绘 bgm_row_draw(纯逻辑),故无需桩 SDK 绘制;行自绘段经 -ffunction-sections/--gc-sections 剔除。
|
||||||
*
|
*
|
||||||
* 编译(在 patch_v2/meow_launcherd 下):
|
* 编译(在 patch_v2/meow_launcherd 下):
|
||||||
@@ -44,6 +46,16 @@ int meow_bg_terminate(const char *app_id)
|
|||||||
}
|
}
|
||||||
unsigned long long now_ms(void) { 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 */
|
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 用) ---------- */
|
/* ---------- 测试态:launcher 应用表(app_meta 用) ---------- */
|
||||||
static struct app g_apps[4];
|
static struct app g_apps[4];
|
||||||
@@ -71,7 +83,9 @@ static void bg_reset(int n, const char *id0, pid_t pid0, const char *id1, pid_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int g_fails = 0;
|
static int g_fails = 0;
|
||||||
|
static int g_checks = 0;
|
||||||
#define CHECK(cond, msg) do { \
|
#define CHECK(cond, msg) do { \
|
||||||
|
g_checks++; \
|
||||||
if (cond) printf("PASS: %s\n", msg); \
|
if (cond) printf("PASS: %s\n", msg); \
|
||||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||||
|
|
||||||
@@ -86,21 +100,31 @@ int main(void)
|
|||||||
{
|
{
|
||||||
static bgm_ctx_t b;
|
static bgm_ctx_t b;
|
||||||
memset(&b, 0, sizeof b);
|
memset(&b, 0, sizeof b);
|
||||||
b.apps = g_apps; b.napps = 2;
|
b.apps = g_apps; b.napps = 3;
|
||||||
set_app(0, "os.meow.hello", "播放", 0);
|
set_app(0, "os.meow.hello", "播放", 0);
|
||||||
set_app(1, "os.meow.sysguard", "系统守护", '1');
|
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];
|
static sub_row_t rows[SUB_ROWS_MAX];
|
||||||
|
|
||||||
printf("=== P1) bgm_rows 行骨架 ===\n");
|
printf("=== P1) bgm_rows 行骨架 + 副文本持久化 ===\n");
|
||||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
|
||||||
int rn = bgm_rows(&b, rows, SUB_ROWS_MAX);
|
int rn = bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||||
CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)");
|
CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)");
|
||||||
CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0");
|
CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0");
|
||||||
CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)");
|
CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)");
|
||||||
CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1");
|
CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1");
|
||||||
CHECK(!strcmp(rows[1].label, "系统守护"), "row1 label=系统守护");
|
CHECK(!strcmp(rows[1].label, "流媒体客户端"), "row1 label=流媒体客户端");
|
||||||
CHECK(!strcmp(rows[1].sub, "") || rows[1].sub[0] != '\0', "row1 sub 非空(CPU/内存/状态)");
|
CHECK(rows[1].sub != NULL && !strncmp(rows[1].sub, "CPU ", 4), "row1 sub=CPU/内存/状态");
|
||||||
CHECK(b.n == 2, "bgm_ctx.n=2");
|
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");
|
printf("=== P2) bgm_btn_rect 按钮几何(行右 46×34) ===\n");
|
||||||
{
|
{
|
||||||
@@ -111,7 +135,7 @@ int main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
printf("=== P3) bgm_row_tap 行内杀按钮命中(仅按钮区 kill,行区不误杀) ===\n");
|
printf("=== P3) bgm_row_tap 行内杀按钮命中(仅按钮区 kill,行区不误杀) ===\n");
|
||||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
|
||||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||||
/* 按钮区命中:行(0,0,480,56),按钮 x 422-468, y 11-45 */
|
/* 按钮区命中:行(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(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 1, "按钮点击→返回1(重绘)");
|
||||||
@@ -122,9 +146,19 @@ int main(void)
|
|||||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 410, 28, 0, 0, 480, 56) == 0, "按钮左侧缘(410)→不杀");
|
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], 475, 28, 0, 0, 480, 56) == 0, "按钮右侧(475)→不杀");
|
||||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀");
|
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀");
|
||||||
/* 框架 App:按钮命中但不 terminate(!framework 门控) */
|
/* 回归(2026-11-04 真机「杀后台按钮按下去没反应」):dev.player.ncm 的 app.json 写 framework:1
|
||||||
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "框架App按钮→返回1(不可杀提示)");
|
* (= 用 SDK 框架,现有 App 全写 1),**必须照杀**——旧实现把它当受保护位,按钮置灰点不动。 */
|
||||||
CHECK(term_count_app("os.meow.sysguard") == 0, "框架App按钮点击→不 terminate");
|
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");
|
printf("=== P3b) 空态行不响应 ===\n");
|
||||||
bg_reset(0, NULL, 0, NULL, 0);
|
bg_reset(0, NULL, 0, NULL, 0);
|
||||||
@@ -133,12 +167,19 @@ int main(void)
|
|||||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 0, "空态行点击→返回0(不杀)");
|
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 0, "空态行点击→返回0(不杀)");
|
||||||
g_term_calls = 0;
|
g_term_calls = 0;
|
||||||
|
|
||||||
printf("=== P4) 顶栏清除(一键清除后台)不杀框架/前台 ===\n");
|
printf("=== P4) 顶栏清除(一键清除后台)不杀受保护/前台 ===\n");
|
||||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
||||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||||
|
b.rows[1].protected_app = '1';
|
||||||
CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)");
|
CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)");
|
||||||
CHECK(term_count_app("os.meow.hello") == 1, "清除→terminate 普通后台 hello");
|
CHECK(term_count_app("os.meow.hello") == 1, "清除→terminate 普通后台 hello");
|
||||||
CHECK(term_count_app("os.meow.sysguard") == 0, "清除→不杀框架 sysguard");
|
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 标前台再清 */
|
/* 前台 App(is_fg)不杀:给 hello 标前台再清 */
|
||||||
bg_reset(1, "os.meow.hello", 1001, NULL, 0);
|
bg_reset(1, "os.meow.hello", 1001, NULL, 0);
|
||||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||||
@@ -153,12 +194,12 @@ int main(void)
|
|||||||
{
|
{
|
||||||
bgmgr_row_t r; memset(&r, 0, sizeof r);
|
bgmgr_row_t r; memset(&r, 0, sizeof r);
|
||||||
r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀");
|
r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀");
|
||||||
r.framework = '1'; CHECK(bgmgr_can_kill(&r) == 0, "framework 不可杀");
|
r.protected_app = '1'; CHECK(bgmgr_can_kill(&r) == 0, "protected_app 不可杀");
|
||||||
r.framework = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀");
|
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 不可杀");
|
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", 32);
|
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
|
||||||
else printf("\n%d FAILED\n", g_fails);
|
else printf("\n%d FAILED\n", g_fails);
|
||||||
return g_fails ? 1 : 0;
|
return g_fails ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
* should_keep=0(停播后再退出=真退);
|
* should_keep=0(停播后再退出=真退);
|
||||||
* 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽;
|
* 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽;
|
||||||
* 4) 卡拔例外:on_card=1 + card_present 假 0 → tick SIGTERM 回收(防 SIGBUS);
|
* 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 回收(含恰等边界不回收);
|
* 4b) 批①+宽限:未声明(keepalive=0)媒体不活跃超 MEOW_BG_GRACE_MS 回收(含恰等边界不回收);
|
||||||
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
|
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
|
||||||
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
|
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
|
||||||
@@ -138,11 +140,17 @@ static int cond_svc_gone(void *ctx)
|
|||||||
return meow_svc_find("dev.test.svc", "daemon") == NULL;
|
return meow_svc_find("dev.test.svc", "daemon") == NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。 */
|
/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。
|
||||||
#define TEST_SVC_PATH "/tmp/meow_bgplay_test_svc.sh"
|
* 路径取 $TMPDIR(Termux/Android 主机没有 /tmp),缺省回退 /tmp。 */
|
||||||
|
static char g_svc_path[256];
|
||||||
|
#define TEST_SVC_PATH g_svc_path
|
||||||
static void write_test_svc(void)
|
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");
|
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);
|
fputs("#!/bin/sh\nexec sleep 300\n", f);
|
||||||
fclose(f);
|
fclose(f);
|
||||||
chmod(TEST_SVC_PATH, 0755);
|
chmod(TEST_SVC_PATH, 0755);
|
||||||
@@ -271,6 +279,31 @@ int main(void)
|
|||||||
"卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)");
|
"卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)");
|
||||||
g_card = 1;
|
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 / 重复 / 参数 ---- */
|
/* ---- 5) 表满:LRU 回收最久未激活的未声明槽 / 全常驻满→NOMEM / 重复 / 参数 ---- */
|
||||||
{
|
{
|
||||||
pid_t k[MEOW_BG_MAX];
|
pid_t k[MEOW_BG_MAX];
|
||||||
|
|||||||
@@ -8,16 +8,25 @@
|
|||||||
* P2 bt_act 命中 → 正确下发 netmgrd 命令(已连接→disconnect、已配对未连→connect、输出目标
|
* P2 bt_act 命中 → 正确下发 netmgrd 命令(已连接→disconnect、已配对未连→connect、输出目标
|
||||||
* 切换、编解码器循环);
|
* 切换、编解码器循环);
|
||||||
* P3 附近设备仅列未配对(已配对者归「已配对设备」)。
|
* P3 附近设备仅列未配对(已配对者归「已配对设备」)。
|
||||||
|
* P4 M47 开关三态:真实态未知(-1) → top_sw 回 -1 + 行内「蓝牙状态未知」(不当已关);
|
||||||
|
* P5 M47 点击收敛:下发成功且真实态达成 → 定稿、不弹窗;下发失败/超时/未知 → 回滚 + 弹窗;
|
||||||
|
* P6 M47 收敛矩阵(纯函数 meow_toggle_*,真实态×下发结果×探测 全组合)。
|
||||||
*
|
*
|
||||||
* 编译(在 patch_v2/meow_launcherd 下):
|
* 编译(在 patch_v2/meow_launcherd 下):
|
||||||
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined test_bt_page.c -o test_bt_page
|
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined test_bt_page.c -o test_bt_page
|
||||||
* 运行:./test_bt_page
|
* 运行:./test_bt_page(或 `bash build_launcher.sh test`)
|
||||||
*/
|
*/
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/* M47:把收敛等待改成宿主可接受的尺度(生产 4000ms/250ms,见 bt_page.c)。 */
|
||||||
|
#define BT_TOGGLE_SETTLE_MS 80
|
||||||
|
#define BT_TOGGLE_POLL_MS 10
|
||||||
|
|
||||||
#include "meow_launcher_internal.h"
|
#include "meow_launcher_internal.h"
|
||||||
#include "bt_page.c" /* bt_rows/bt_act/bt_top_sw/bt_detail 平移到本 TU */
|
#include "bt_page.c" /* bt_rows/bt_act/bt_top_sw/bt_detail 平移到本 TU */
|
||||||
@@ -29,9 +38,12 @@ static bt_ep_t g_eps[8]; static int g_eps_n;
|
|||||||
static char g_last_cmd[128]; /* 记录 act 下发命令,如 "disconnect:<mac>" */
|
static char g_last_cmd[128]; /* 记录 act 下发命令,如 "disconnect:<mac>" */
|
||||||
static int g_output_bt; /* audio.output_bt 桩值 */
|
static int g_output_bt; /* audio.output_bt 桩值 */
|
||||||
static char g_codec[16] = "auto"; /* bt.codec 桩值 */
|
static char g_codec[16] = "auto"; /* bt.codec 桩值 */
|
||||||
|
static int g_bt_state = 1; /* M47:真实态三态桩(-1 未知 / 0 / 1) */
|
||||||
|
static int g_onoff_rc = MEOW_OK; /* M47:nm_bt_onoff 下发结果桩 */
|
||||||
|
static int g_bt_en_setting = 1; /* M47:network.bluetooth_enabled 桩值(意图/回滚观察点) */
|
||||||
|
|
||||||
int nm_bt_powered(void) { return 1; }
|
int nm_bt_power_state(void) { return g_bt_state; }
|
||||||
int nm_bt_onoff(int on) { (void)on; return 0; }
|
int nm_bt_onoff(int on) { (void)on; return g_onoff_rc; }
|
||||||
int nm_bt_connected(meow_net_t *out, int max) {
|
int nm_bt_connected(meow_net_t *out, int max) {
|
||||||
if (max > g_conn_n) max = g_conn_n;
|
if (max > g_conn_n) max = g_conn_n;
|
||||||
memcpy(out, g_conn, (size_t)max * sizeof *out); return max;
|
memcpy(out, g_conn, (size_t)max * sizeof *out); return max;
|
||||||
@@ -65,14 +77,14 @@ void net_bt_mode_str(char *buf, int len) { snprintf(buf, (size_t)len, "主模式
|
|||||||
int meow_settings_get_bool(const char *k, int *v)
|
int meow_settings_get_bool(const char *k, int *v)
|
||||||
{
|
{
|
||||||
if (v) *v = 0;
|
if (v) *v = 0;
|
||||||
if (!strcmp(k, "network.bluetooth_enabled")) { if (v) *v = 1; return 0; }
|
if (!strcmp(k, "network.bluetooth_enabled")) { if (v) *v = g_bt_en_setting; return 0; }
|
||||||
if (!strcmp(k, "audio.output_bt")) { if (v) *v = g_output_bt; return 0; }
|
if (!strcmp(k, "audio.output_bt")) { if (v) *v = g_output_bt; return 0; }
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int meow_settings_set_bool(const char *k, int v)
|
int meow_settings_set_bool(const char *k, int v)
|
||||||
{
|
{
|
||||||
if (!strcmp(k, "audio.output_bt")) { g_output_bt = v; return 0; }
|
if (!strcmp(k, "audio.output_bt")) { g_output_bt = v; return 0; }
|
||||||
if (!strcmp(k, "network.bluetooth_enabled")) return 0;
|
if (!strcmp(k, "network.bluetooth_enabled")) { g_bt_en_setting = v; return 0; }
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int meow_settings_get_str(const char *k, char *b, int n)
|
int meow_settings_get_str(const char *k, char *b, int n)
|
||||||
@@ -91,9 +103,26 @@ void paint_sb_right(void) {}
|
|||||||
void logmsg(const char *fmt, ...) { (void)fmt; }
|
void logmsg(const char *fmt, ...) { (void)fmt; }
|
||||||
void bt_role_refresh(int force) { (void)force; }
|
void bt_role_refresh(int force) { (void)force; }
|
||||||
meow_sys_state g_sys;
|
meow_sys_state g_sys;
|
||||||
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur) { (void)title; (void)item; (void)n; (void)cur; return -1; }
|
static int g_modal_n = 0; /* M47:可见错误提示(弹窗)计数 */
|
||||||
|
static char g_modal_line[128] = "";
|
||||||
|
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur)
|
||||||
|
{
|
||||||
|
(void)title; (void)cur;
|
||||||
|
g_modal_n++;
|
||||||
|
g_modal_line[0] = 0;
|
||||||
|
if (item && n > 0 && item[0]) snprintf(g_modal_line, sizeof g_modal_line, "%s", item[0]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
int meow_subpage_run(sub_page_t *p) { (void)p; return 0; }
|
int meow_subpage_run(sub_page_t *p) { (void)p; return 0; }
|
||||||
|
|
||||||
|
/* M47:真实时钟(收敛等待用),毫秒单调。 */
|
||||||
|
unsigned long long now_ms(void)
|
||||||
|
{
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (unsigned long long)ts.tv_sec * 1000ULL + (unsigned long long)(ts.tv_nsec / 1000000);
|
||||||
|
}
|
||||||
|
|
||||||
static int g_fail = 0;
|
static int g_fail = 0;
|
||||||
#define CHECK(cond, ...) do { if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
|
#define CHECK(cond, ...) do { if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
|
||||||
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
|
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
|
||||||
@@ -111,6 +140,13 @@ static int find_act(const sub_row_t *r, int n, int from, const char *pref)
|
|||||||
if (r[i].kind == SUB_ACT && r[i].label && !strncmp(r[i].label, pref, strlen(pref))) return i;
|
if (r[i].kind == SUB_ACT && r[i].label && !strncmp(r[i].label, pref, strlen(pref))) return i;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
/* 按 label 精确找任意行(M47 未知态占位行用) */
|
||||||
|
static int row_label(const sub_row_t *r, int n, const char *lbl)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
if (r[i].label && !strcmp(r[i].label, lbl)) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
static void fill_state(void)
|
static void fill_state(void)
|
||||||
{
|
{
|
||||||
@@ -230,6 +266,95 @@ int main(void)
|
|||||||
bt_act(&c, ci, rows[ci].act);
|
bt_act(&c, ci, rows[ci].act);
|
||||||
CHECK(strcmp(g_codec, "SBC") == 0, "编解码器 auto → SBC (实际=%s)", g_codec);
|
CHECK(strcmp(g_codec, "SBC") == 0, "编解码器 auto → SBC (实际=%s)", g_codec);
|
||||||
|
|
||||||
|
printf("== P4 M47 开关三态(未知 ≠ 已关)==\n");
|
||||||
|
int sw = 0;
|
||||||
|
g_bt_state = -1; /* 真实态未知 */
|
||||||
|
memset(&c, 0, sizeof c); c.full = 1; fill_state();
|
||||||
|
CHECK(bt_top_sw(&c, &sw) == 1 && sw == -1, "真实态未知 → top_sw *on=-1 (实际=%d)", sw);
|
||||||
|
n = bt_rows(&c, rows, SUB_ROWS_MAX);
|
||||||
|
CHECK(row_label(rows, n, "蓝牙状态未知") >= 0, "未知态 → 行内含「蓝牙状态未知」");
|
||||||
|
g_bt_state = 0; /* 真实态为关 */
|
||||||
|
memset(&c, 0, sizeof c); c.full = 1;
|
||||||
|
sw = -2;
|
||||||
|
bt_top_sw(&c, &sw);
|
||||||
|
CHECK(sw == 0, "真实态关 → top_sw *on=0 (实际=%d)", sw);
|
||||||
|
n = bt_rows(&c, rows, SUB_ROWS_MAX);
|
||||||
|
CHECK(row_label(rows, n, "蓝牙状态未知") < 0, "真实态已知 → 无未知占位行");
|
||||||
|
|
||||||
|
printf("== P5 M47 点击收敛:定稿 / 回滚 + 可见错误 ==\n");
|
||||||
|
/* 5a 下发成功且真实态已达成 → 立即定稿,无弹窗,settings/状态栏=真实态 */
|
||||||
|
g_bt_state = 1; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 0;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
unsigned long long t0 = now_ms();
|
||||||
|
bt_top_sw_set(&c, 1);
|
||||||
|
unsigned long long dt = now_ms() - t0;
|
||||||
|
CHECK(g_modal_n == 0, "已达成 → 无错误弹窗 (弹窗数=%d)", g_modal_n);
|
||||||
|
CHECK(g_bt_en_setting == 1, "已达成 → settings 定稿为 1");
|
||||||
|
CHECK(g_sys.bt_on == 1, "已达成 → 状态栏 g_sys.bt_on=1");
|
||||||
|
CHECK(dt < BT_TOGGLE_SETTLE_MS, "已达成 → 不必等满超时 (%llums)", dt);
|
||||||
|
|
||||||
|
/* 5b 下发失败、真实态仍为开(要求关)→ 回滚到开 + 可见弹窗 */
|
||||||
|
g_bt_state = 1; g_onoff_rc = MEOW_ERR_NODEV; g_modal_n = 0; g_bt_en_setting = 1;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
bt_top_sw_set(&c, 0);
|
||||||
|
CHECK(g_modal_n == 1, "下发失败 → 有可见错误弹窗");
|
||||||
|
CHECK(g_bt_en_setting == 1, "下发失败 → settings 回滚为真实态 1 (实际=%d)", g_bt_en_setting);
|
||||||
|
CHECK(g_sys.bt_on == 1, "下发失败 → 状态栏仍为真实态 1");
|
||||||
|
|
||||||
|
/* 5c 下发成功但真实态不收敛(要求开、真实一直关)→ 等满上限后回滚 + 弹窗 */
|
||||||
|
g_bt_state = 0; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 1;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
t0 = now_ms();
|
||||||
|
bt_top_sw_set(&c, 1);
|
||||||
|
dt = now_ms() - t0;
|
||||||
|
CHECK(g_modal_n == 1, "超时未收敛 → 有可见错误弹窗(%s)", g_modal_line);
|
||||||
|
CHECK(g_bt_en_setting == 0, "超时未收敛 → settings 回滚为真实态 0");
|
||||||
|
CHECK(g_sys.bt_on == 0, "超时未收敛 → 状态栏=真实态 0");
|
||||||
|
CHECK(dt >= BT_TOGGLE_SETTLE_MS, "确实等满了上限 (%llums)", dt);
|
||||||
|
|
||||||
|
/* 5d 真实态未知 → 等满上限后仍是未知 + 弹窗(绝不把未知显示成成功);
|
||||||
|
* settings 保留原意图不动(评审 r1 P2-1:写 0 会把「不知道」污染成「确定已关」,
|
||||||
|
* notif_center/settings_page/app_launcher 都把这个键当显示态/自动拉起依据读)。 */
|
||||||
|
g_bt_state = -1; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 1;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
bt_top_sw_set(&c, 1);
|
||||||
|
CHECK(g_modal_n == 1 && strstr(g_modal_line, "无法确认") != NULL,
|
||||||
|
"未知 → 弹窗明说无法确认 (%s)", g_modal_line);
|
||||||
|
CHECK(c.bt_on == -1, "未知 → 定稿态仍为 -1 (实际=%d)", c.bt_on);
|
||||||
|
CHECK(g_bt_en_setting == 1, "未知 → settings 保留原意图 1,不被改写 (实际=%d)", g_bt_en_setting);
|
||||||
|
|
||||||
|
printf("== P6 M47 收敛矩阵(真实态 × 下发结果 × 探测 × 超时)==\n");
|
||||||
|
{
|
||||||
|
static const struct {
|
||||||
|
int last, want, ok, probe, to; /* 输入 */
|
||||||
|
int disp, pend, rb, err; /* 期望输出 */
|
||||||
|
const char *what;
|
||||||
|
} m[] = {
|
||||||
|
{ 1, 1, 1, 1, 0, 1, 0, 0, 0, "真开·下发成功·探测开" },
|
||||||
|
{ 1, 0, 1, 0, 0, 0, 0, 0, 0, "真开·下发成功·探测关" },
|
||||||
|
{ 1, 0, 1, 1, 1, 1, 0, 1, 1, "真开·下发成功·未收敛(超时)" },
|
||||||
|
{ 1, 0, 0, 1, 0, 1, 0, 1, 1, "真开·下发失败·探测仍开" },
|
||||||
|
{ 1, 0, 0, -1, 0, -1, 0, 1, 1, "真开·下发失败·探测未知 → 未知(不顶替)" },
|
||||||
|
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, "真开·下发失败但探测已关(不误报)" },
|
||||||
|
{ 0, 1, 1, 1, 0, 1, 0, 0, 0, "真关·下发成功·探测开" },
|
||||||
|
{ 0, 1, 1, 0, 1, 0, 0, 1, 1, "真关·下发成功·未收敛(超时)" },
|
||||||
|
{ 0, 1, 0, 0, 0, 0, 0, 1, 1, "真关·下发失败·仍关" },
|
||||||
|
{ 1, 0, 1, -1, 0, -1, 1, 1, 0, "真开·下发成功·探测未知(继续等)" },
|
||||||
|
{ 1, 0, 1, -1, 1, -1, 0, 1, 1, "真开·下发成功·探测未知(超时)→未知,不用 last_real 顶替" },
|
||||||
|
{-1, 1, 1, -1, 1, -1, 0, 1, 1, "未知·下发成功·探测仍未知(超时)" },
|
||||||
|
{-1, 1, 1, -1, 0, -1, 1, 1, 0, "未知·下发成功·探测仍未知(继续等)" },
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
|
||||||
|
int d = meow_toggle_display(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
|
||||||
|
int p = meow_toggle_pending(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
|
||||||
|
int r = meow_toggle_rollback(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
|
||||||
|
int e = meow_toggle_error(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
|
||||||
|
CHECK(d == m[i].disp && p == m[i].pend && r == m[i].rb && e == m[i].err,
|
||||||
|
"%s → disp=%d pend=%d rb=%d err=%d (期望 %d/%d/%d/%d)",
|
||||||
|
m[i].what, d, p, r, e, m[i].disp, m[i].pend, m[i].rb, m[i].err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||||
return g_fail;
|
return g_fail;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/* test_key_router_vol.c — 宿主单测:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1)。
|
||||||
|
*
|
||||||
|
* 测「key_router 自身的音量编排」——它只产「音量边沿 + 原始 held + ramp 状态」,
|
||||||
|
* 由调用页决定 settings_volume_adj(±step) / 光标导航 / ramp 推进。本测用内存桩:
|
||||||
|
* - kp_poll/kp_open/kp_is_down → 注入 raw 键位矩阵到 key_router 的静态 kp_keys.cur[];
|
||||||
|
* - kd_tick → 用 keys.c 的真实现(40ms 防抖,V4 依赖);keys.c 内 kp_open/kp_poll/
|
||||||
|
* kp_is_down/meow_volpage_armed 用宏重命名避开真实 ioctl 与桩冲突;
|
||||||
|
* - settings_volume_adj/settings_volume_step/vol_ramp_dir/vol_ramp_tick → 内存桩;
|
||||||
|
* - now_ms → 假时钟(手动推进,40ms 防抖可瞬时测);
|
||||||
|
* - meow_ui_activity_* → 内存桩(V5 嵌套:真语义=纯静态数组,桩复刻)。
|
||||||
|
*
|
||||||
|
* 断言:
|
||||||
|
* V1 音量减边沿 → settings_volume_adj(-step) 恰一次 + last_input 复位;
|
||||||
|
* V2 同会话重复(同 raw 仍按住)→ 不再触发 settings_volume_adj;
|
||||||
|
* V3 长按 held → vol_ramp_tick 被驱动(vol_ramp_dir!=0,held=正确方向 raw);松开 → dir=0;
|
||||||
|
* V4 40ms 防抖:raw 在 40ms 内抖动 → 不产边沿;稳定按下 ≥40ms → 才产边沿;
|
||||||
|
* V5 嵌套返回:page_enter(0x53)→page_exit(0x53)→page_enter(0x55),基线重播后 held 键不产假边沿;
|
||||||
|
* V6 meow_volpage_armed() → 音量边沿被抑制(不下发,不调 settings_volume_adj)。
|
||||||
|
*
|
||||||
|
* 编译(在 patch_v2/meow_launcherd 下):
|
||||||
|
* gcc -Wall -Wextra -I../sdk -I. -ffunction-sections -fdata-sections \
|
||||||
|
* -Wl,--gc-sections test_key_router_vol.c -o test_key_router_vol
|
||||||
|
* ./test_key_router_vol
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* ---- 假时钟 ---- */
|
||||||
|
static unsigned long long g_now = 100000;
|
||||||
|
unsigned long long now_ms(void) { return g_now; }
|
||||||
|
static void advance_ms(unsigned long long d) { g_now += d; }
|
||||||
|
|
||||||
|
/* ---- 音量机外部符号桩(key_router 依赖)---- */
|
||||||
|
int settings_volume_step = 5;
|
||||||
|
static int g_adj_count = 0, g_adj_delta = 0;
|
||||||
|
int vol_ramp_dir = 0; /* 桩全局:模拟 audio.c 的 ramp 状态 */
|
||||||
|
static int g_tick_count = 0, g_tick_held = -1;
|
||||||
|
|
||||||
|
void settings_volume_adj(int delta)
|
||||||
|
{
|
||||||
|
g_adj_count++; g_adj_delta = delta;
|
||||||
|
if (delta > 0) vol_ramp_dir = 1; else if (delta < 0) vol_ramp_dir = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vol_ramp_tick(int key_held)
|
||||||
|
{
|
||||||
|
g_tick_count++; g_tick_held = key_held;
|
||||||
|
if (!key_held) vol_ramp_dir = 0;
|
||||||
|
return vol_ramp_dir != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g_volpage = 0; /* 桩:meow_volpage_armed */
|
||||||
|
|
||||||
|
/* ---- keys.c 内 kp_open/kp_poll/kp_is_down/meow_volpage_armed 用宏重命名,避开真实 ioctl ----
|
||||||
|
* (宏在 #include keys.c 前生效,仅重命名 keys.c 体内的调用/定义) ---- */
|
||||||
|
#define kp_open kp_open_rt_
|
||||||
|
#define kp_poll kp_poll_rt_
|
||||||
|
#define kp_is_down kp_is_down_rt_
|
||||||
|
#define meow_volpage_armed meow_volpage_armed_rt_
|
||||||
|
#include "keys.c" /* 真 kd_tick(40ms 防抖素材) */
|
||||||
|
#undef kp_open
|
||||||
|
#undef kp_poll
|
||||||
|
#undef kp_is_down
|
||||||
|
#undef meow_volpage_armed
|
||||||
|
|
||||||
|
/* host 桩声明(key_router.c 需要;恢复到正常名,与 keys.c 被更名后的定义不冲突) */
|
||||||
|
struct keypoll;
|
||||||
|
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 kp_is_down(const struct keypoll *k, int code);
|
||||||
|
int meow_volpage_armed(void);
|
||||||
|
|
||||||
|
#include "key_router.c" /* 同 TU #include:拿到静态 kp_keys/kp_power/kd_vold/kd_volu + 全部函数 */
|
||||||
|
#include "key_router.h"
|
||||||
|
|
||||||
|
/* ---- meow_ui_activity_* 桩(复刻 sdk/meow_ui_activity.c:纯静态数组,真语义)---- */
|
||||||
|
static int g_act[32]; static int g_act_n = 0;
|
||||||
|
void meow_ui_activity_push(int id) { if (g_act_n < 32) g_act[g_act_n++] = id; }
|
||||||
|
void meow_ui_activity_pop(void) { if (g_act_n > 0) g_act_n--; }
|
||||||
|
int meow_ui_activity_active(void) { return g_act_n > 0 ? g_act[g_act_n - 1] : 0; }
|
||||||
|
|
||||||
|
/* ---- 注入 raw 到 key_router 静态 kp_keys ---- */
|
||||||
|
static void set_vol(int vold, int volu)
|
||||||
|
{
|
||||||
|
for (int w = 0; w < KEYBUF_WORDS; w++) kp_keys.cur[w] = 0;
|
||||||
|
if (vold) kp_keys.cur[KEY_VOLUMEDOWN / 32] |= (1UL << (KEY_VOLUMEDOWN % 32));
|
||||||
|
if (volu) kp_keys.cur[KEY_VOLUMEUP / 32] |= (1UL << (KEY_VOLUMEUP % 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- host 桩:kp_*(prev=cur 模拟真实 poll 的键位更新)+ meow_volpage_armed ---- */
|
||||||
|
int kp_open(struct keypoll *kp, const char *dev)
|
||||||
|
{ (void)dev; memset(kp, 0, sizeof *kp); kp->ok = 1; return 0; }
|
||||||
|
int kp_poll(struct keypoll *kp, int (*cb)(int, int, void *), void *ctx)
|
||||||
|
{
|
||||||
|
(void)cb; (void)ctx;
|
||||||
|
for (int w = 0; w < KEYBUF_WORDS; w++) kp->prev[w] = kp->cur[w];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int kp_is_down(const struct keypoll *k, int code)
|
||||||
|
{ return (k->cur[code / 32] >> (code % 32)) & 1; }
|
||||||
|
int meow_volpage_armed(void) { return g_volpage; }
|
||||||
|
|
||||||
|
/* 其它全局桩(keys.c 内被更名函数引用;本测 GC 后不保留,仅满足声明一致性) */
|
||||||
|
uint32_t menu_bg = 0;
|
||||||
|
int screen_off_timeout = 0, screen_off_mode = 0, power_longpress_menu_ms = 0;
|
||||||
|
int power_forceoff_ms = 0, refresh_rate_cfg = 60, refresh_always_max = 0;
|
||||||
|
|
||||||
|
void logmsg(const char *fmt, ...) { (void)fmt; }
|
||||||
|
|
||||||
|
static int g_fails = 0;
|
||||||
|
#define CHECK(cond, name) do { \
|
||||||
|
if (cond) printf(" PASS %s\n", name); \
|
||||||
|
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fails++; } } while (0)
|
||||||
|
|
||||||
|
static void reset_router(void)
|
||||||
|
{
|
||||||
|
set_vol(0, 0);
|
||||||
|
key_router_rebase();
|
||||||
|
g_adj_count = 0; g_adj_delta = 0; g_tick_count = 0; g_tick_held = -1;
|
||||||
|
vol_ramp_dir = 0; g_volpage = 0;
|
||||||
|
g_act_n = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按下并稳定:register(pump) → hold ≥40ms → pump(stable=1, 边沿)。 */
|
||||||
|
static key_evt_t press_settle(int vold, int volu)
|
||||||
|
{
|
||||||
|
set_vol(vold, volu);
|
||||||
|
key_router_pump(); /* 登记 raw 跳变(防抖窗口起点) */
|
||||||
|
advance_ms(50); /* ≥KEY_DEBOUNCE_MS(40) */
|
||||||
|
return key_router_pump(); /* stable→1,边沿 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 抬起并稳定:register(pump) → → pump(stable=0)。 */
|
||||||
|
static key_evt_t release_settle(void)
|
||||||
|
{
|
||||||
|
set_vol(0, 0);
|
||||||
|
key_router_pump(); /* 登记 release */
|
||||||
|
advance_ms(50);
|
||||||
|
return key_router_pump(); /* stable→0 */
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
unsigned long long last_input = 0;
|
||||||
|
|
||||||
|
/* ---- V1 音量减边沿 → settings_volume_adj(-step) 恰一次 + last_input 复位 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump(); /* 全抬起基线 */
|
||||||
|
last_input = 0;
|
||||||
|
key_evt_t ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V1 vol-down edge fires");
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_adj_delta == -settings_volume_step,
|
||||||
|
"V1 vol-down -> settings_volume_adj(-step) exactly once");
|
||||||
|
CHECK(last_input == g_now, "V1 last_input reset on edge");
|
||||||
|
|
||||||
|
/* ---- V2 同会话重复(同 raw 仍按住)→ 不再触发 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V2a press edge fires");
|
||||||
|
ev = key_router_pump(); /* 同 raw 仍按住,无新边沿 */
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V2 held: no re-trigger edge");
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_adj_count == 0, "V2 held: no double settings_volume_adj");
|
||||||
|
|
||||||
|
/* ---- V3 长按 held → vol_ramp_tick 被驱动(held=正确方向 raw);松开 → dir=0 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump(); last_input = 0;
|
||||||
|
ev = press_settle(1, 0); /* 按下(vol down)并转稳定 */
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_tick_count == 1 && g_tick_held == ev.vol_held_dn && vol_ramp_dir != 0,
|
||||||
|
"V3 hold vol-down -> ramp driven with held=raw down");
|
||||||
|
advance_ms(50);
|
||||||
|
ev = key_router_pump(); /* 继续长按(raw=1 仍按住) */
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_tick_count == 2 && g_tick_held == 1, "V3 long-press continues ramp");
|
||||||
|
ev = release_settle(); /* 松开:raw held 立即为 0 */
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(vol_ramp_dir == 0, "V3 release -> ramp dir 0 (stops)");
|
||||||
|
|
||||||
|
/* ---- V4 40ms 防抖:raw 在 40ms 内抖动不产边沿;稳定 ≥40ms 才产 ---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
set_vol(1, 0); advance_ms(20); key_router_pump(); /* 按下未满 40ms */
|
||||||
|
set_vol(0, 0); advance_ms(20); key_router_pump(); /* 40ms 内又抬起 */
|
||||||
|
ev = key_router_pump();
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V4 <40ms jitter -> no edge");
|
||||||
|
ev = press_settle(1, 0); /* 稳定按下 ≥40ms */
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V4 stable >=40ms press -> edge");
|
||||||
|
ev = release_settle(); /* 抬起稳定 */
|
||||||
|
CHECK(ev.vol_dn_edge == 0 && ev.vol_up_edge == 0, "V4 released -> no residual edge");
|
||||||
|
ev = press_settle(0, 1); /* 稳定按音量加 */
|
||||||
|
CHECK(ev.vol_up_edge == 1, "V4 vol-up stable press -> up edge");
|
||||||
|
|
||||||
|
/* ---- V5 嵌套返回:page_enter/exit 重播基线,held 键不产假边沿 ---- */
|
||||||
|
reset_router();
|
||||||
|
meow_ui_activity_push(0x55); key_router_page_enter(0x55); /* 主菜单 */
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V5a press edge in menu");
|
||||||
|
/* 嵌套进设置页:压栈 + 登记 */
|
||||||
|
meow_ui_activity_push(0x53); key_router_page_enter(0x53);
|
||||||
|
CHECK(meow_ui_activity_active() == 0x53, "V5b settings active sentinel");
|
||||||
|
ev = release_settle(); /* 设置页内把键抬起 */
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V5c settings-up: no fresh edge (consumer)");
|
||||||
|
/* 设置页返回(模拟 run_settings_page 结束):弹栈 + 重播基线 → m_prev=当前 raw(0) */
|
||||||
|
key_router_page_exit(0x53); meow_ui_activity_pop();
|
||||||
|
key_router_page_enter(0x55);
|
||||||
|
CHECK(meow_ui_activity_active() == 0x55, "V5d menu active again");
|
||||||
|
/* 主菜单基线已重播;再次按下应为新边沿,而非残留/双发 */
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 1, "V5e fresh press after nest return -> single edge");
|
||||||
|
|
||||||
|
/* ---- V6 meow_volpage_armed() → 音量边沿被抑制(不下发)---- */
|
||||||
|
reset_router();
|
||||||
|
set_vol(0, 0); advance_ms(10); key_router_pump();
|
||||||
|
g_volpage = 1;
|
||||||
|
ev = press_settle(1, 0);
|
||||||
|
CHECK(ev.vol_dn_edge == 0, "V6 volpage armed -> edge suppressed");
|
||||||
|
key_router_default_volume(&ev, &last_input);
|
||||||
|
CHECK(g_adj_count == 0, "V6 volpage armed -> settings_volume_adj NOT called");
|
||||||
|
g_volpage = 0;
|
||||||
|
|
||||||
|
printf("\nRESULT: %s (%d fails)\n", g_fails ? "FAIL" : "ALL PASS", g_fails);
|
||||||
|
return g_fails ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -40,8 +40,47 @@ int net_bt_paired(meow_net_t *out, int max) { (void)out; (void)max; return 0; }
|
|||||||
void s_net_toggle_wifi(int on) { (void)on; }
|
void s_net_toggle_wifi(int on) { (void)on; }
|
||||||
void s_net_toggle_bt(int on) { (void)on; }
|
void s_net_toggle_bt(int on) { (void)on; }
|
||||||
void net_wifi_scan_trigger(void) {}
|
void net_wifi_scan_trigger(void) {}
|
||||||
|
int net_bt_connected(meow_net_t *out, int max) { (void)out; (void)max; return 0; }
|
||||||
|
int net_bt_scan(meow_net_t *out, int max) { (void)out; (void)max; return 0; }
|
||||||
|
void net_bt_scan_trigger(void) {}
|
||||||
|
|
||||||
|
/* UI 桩:wifi_connecting_banner 走 SDK 绘制函数;宿主无 fb,空实现即可(不验证像素)。 */
|
||||||
|
const meow_ui_theme_t *s_ui_theme(void) { static meow_ui_theme_t th; return &th; }
|
||||||
|
int meow_ui_width(void) { return 480; }
|
||||||
|
void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t color)
|
||||||
|
{ (void)x; (void)y; (void)w; (void)h; (void)color; }
|
||||||
|
void meow_ui_text_center(int cx, int y, const char *utf8, int size, uint32_t color)
|
||||||
|
{ (void)cx; (void)y; (void)utf8; (void)size; (void)color; }
|
||||||
|
int meow_ui_text_height(int size) { return size; }
|
||||||
|
void meow_dirty_full(void) {}
|
||||||
|
void meow_ui_flush(void) {}
|
||||||
char *popen_read_tmo(int tmo_ms, const char *cmd)
|
char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||||||
{ (void)tmo_ms; (void)cmd; return strdup("0\n"); } /* 模拟 wpa_cli add_network 返回 id=0 */
|
{
|
||||||
|
(void)tmo_ms;
|
||||||
|
/* 直连回退(不可达分支)会跑 wpa_cli 剧本:list_networks/add_network/status/save_config。
|
||||||
|
* 桩按子命令给近似真机的应答——旧桩统一返回 "0\n",会让 nm_direct_connect_ex 等满 10s
|
||||||
|
* 再报 MEOW_ERR_NODEV(该断言因此长期失败;M47 顺手把桩修成可收敛)。 */
|
||||||
|
if (strstr(cmd, " status")) return strdup("wpa_state=COMPLETED\nip_address=192.168.1.5\nssid=Home2G\n");
|
||||||
|
if (strstr(cmd, "list_networks")) return strdup("network id / ssid / bssid / flags\n0\tHome2G\t00:11:22:33:44:01\t[WPA-PSK-CCMP]\n");
|
||||||
|
return strdup("OK\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- M47:net.c 三态直连探测桩(daemon 不可达时 nm_*_power_state 落到这里) ---------- */
|
||||||
|
int nm_bt_power_state(void); /* netmgr_ipc.c(头文件不在本单改动范围) */
|
||||||
|
static int g_bt_probe = -1, g_wifi_probe = -1;
|
||||||
|
int net_bt_power_probe(void) { return g_bt_probe; }
|
||||||
|
int net_wifi_power_probe(void) { return g_wifi_probe; }
|
||||||
|
|
||||||
|
/* M47:开关收敛判定纯函数的实现放在 bt_page.c,矩阵测试在 test_bt_page.c;本测试只验
|
||||||
|
* nm_*_power_state 的解析/回退与 wifi_rows 的未知态渲染,故这里给可链接的最小桩。 */
|
||||||
|
int meow_toggle_display(int intent, int last, int ok, int probe, int to)
|
||||||
|
{ (void)intent; (void)ok; (void)to; return (probe >= 0) ? probe : last; }
|
||||||
|
int meow_toggle_pending(int intent, int last, int ok, int probe, int to)
|
||||||
|
{ (void)intent; (void)last; (void)ok; (void)probe; (void)to; return 0; }
|
||||||
|
int meow_toggle_rollback(int intent, int last, int ok, int probe, int to)
|
||||||
|
{ (void)intent; (void)last; (void)ok; (void)probe; (void)to; return 0; }
|
||||||
|
int meow_toggle_error(int intent, int last, int ok, int probe, int to)
|
||||||
|
{ (void)intent; (void)last; (void)ok; (void)probe; (void)to; return 0; }
|
||||||
|
|
||||||
/* ---------- launcher/UI/IME 桩(wifi_page.c 链接所需;测试不触碰这些路径) ---------- */
|
/* ---------- launcher/UI/IME 桩(wifi_page.c 链接所需;测试不触碰这些路径) ---------- */
|
||||||
static unsigned long long g_test_ms = 0;
|
static unsigned long long g_test_ms = 0;
|
||||||
@@ -175,7 +214,9 @@ static int row_has_label(const sub_row_t *rows, int n, const char *needle)
|
|||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
char sock[110];
|
char sock[110];
|
||||||
snprintf(sock, sizeof sock, "/tmp/meow_net_test_%d.sock", (int)getpid());
|
/* 宿主 /tmp 可能不可写(Android 上 /tmp 属 shell):优先 TMPDIR。 */
|
||||||
|
snprintf(sock, sizeof sock, "%s/meow_net_test_%d.sock",
|
||||||
|
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp", (int)getpid());
|
||||||
|
|
||||||
fake_net_t f;
|
fake_net_t f;
|
||||||
memset(&f, 0, sizeof f);
|
memset(&f, 0, sizeof f);
|
||||||
@@ -236,7 +277,26 @@ int main(void)
|
|||||||
assert(strstr(err, "add_network") && "connect 失败带回原因");
|
assert(strstr(err, "add_network") && "connect 失败带回原因");
|
||||||
printf("[D] connect 失败: rc=%d err=%s PASS\n", rc, err);
|
printf("[D] connect 失败: rc=%d err=%s PASS\n", rc, err);
|
||||||
|
|
||||||
/* ---- ③ 降级占位:MEOW_NET_SOCK 指向不存在路径 ----
|
/* ---- ③ M47:开关真实态三态查询(daemon 权威解析) ---- */
|
||||||
|
{
|
||||||
|
f.bt_status_text = "bt available=1 powered=1 address=00:1A:7D:DA:71:13 paired=2 connected=0";
|
||||||
|
assert(nm_bt_power_state() == 1 && "bt status powered=1 → 1(开)");
|
||||||
|
f.bt_status_text = "bt available=1 powered=0 address=00:1A:7D:DA:71:13 paired=2 connected=0";
|
||||||
|
assert(nm_bt_power_state() == 0 && "bt status powered=0 → 0(关)");
|
||||||
|
f.bt_status_text = "bt available=0 powered=0 address=- paired=-1 connected=-1";
|
||||||
|
assert(nm_bt_power_state() == 0 && "无控制器(available=0) → 0");
|
||||||
|
f.bt_status_text = "bt available=1 address=00:1A:7D:DA:71:13 paired=-1 connected=-1";
|
||||||
|
assert(nm_bt_power_state() == -1 && "缺 powered 字段 → -1(未知,不猜 0)");
|
||||||
|
g_wifi_probe = -1;
|
||||||
|
assert(nm_wifi_power_state() == -1 && "wifi 探测失败 → -1(不再默认已开)");
|
||||||
|
g_wifi_probe = 1;
|
||||||
|
assert(nm_wifi_power_state() == 1 && "wifi 探测开 → 1");
|
||||||
|
g_wifi_probe = 0;
|
||||||
|
assert(nm_wifi_power_state() == 0 && "wifi 探测关 → 0");
|
||||||
|
printf("[F] nm_bt_power_state/nm_wifi_power_state 三态解析 PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- ④ 降级占位:MEOW_NET_SOCK 指向不存在路径 ----
|
||||||
* 先发一次 netmgr_wifi(内部强制重连,失败 → 置可达缓存=0),再断言 netmgr_ok()。 */
|
* 先发一次 netmgr_wifi(内部强制重连,失败 → 置可达缓存=0),再断言 netmgr_ok()。 */
|
||||||
setenv("MEOW_NET_SOCK", "/tmp/meow_net_nonexistent_zz.sock", 1);
|
setenv("MEOW_NET_SOCK", "/tmp/meow_net_nonexistent_zz.sock", 1);
|
||||||
assert(netmgr_wifi("status", NULL, err, sizeof err) == NETMGR_UNREACH && "netmgr_wifi=NETMGR_UNREACH");
|
assert(netmgr_wifi("status", NULL, err, sizeof err) == NETMGR_UNREACH && "netmgr_wifi=NETMGR_UNREACH");
|
||||||
@@ -254,6 +314,21 @@ int main(void)
|
|||||||
printf("[E] 降级: netmgr_ok=0, UNREACH, 回退不崩, 占位渲染 PASS\n");
|
printf("[E] 降级: netmgr_ok=0, UNREACH, 回退不崩, 占位渲染 PASS\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- ⑤ M47:daemon 不可达时真实态回退直连探测;探测失败=未知(不当已关/已开) ---- */
|
||||||
|
g_bt_probe = -1;
|
||||||
|
assert(nm_bt_power_state() == -1 && "BT daemon 不可达 + 直连探测失败 → -1");
|
||||||
|
g_bt_probe = 0;
|
||||||
|
assert(nm_bt_power_state() == 0 && "BT daemon 不可达 + 直连探测关 → 0");
|
||||||
|
g_wifi_probe = -1;
|
||||||
|
assert(nm_wifi_power_state() == -1 && "WiFi 探测失败 → -1");
|
||||||
|
{
|
||||||
|
wifi_ctx_t c; memset(&c, 0, sizeof c);
|
||||||
|
sub_row_t rows[SUB_ROWS_MAX];
|
||||||
|
int rn = wifi_rows(&c, rows, SUB_ROWS_MAX);
|
||||||
|
assert(row_has_label(rows, rn, "WiFi 开关状态未知") && "未知态 → 行内第三态占位");
|
||||||
|
printf("[G] 不可达回退三态: BT/WiFi 失败=未知, 行内第三态 PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
printf("ALL PASS\n");
|
printf("ALL PASS\n");
|
||||||
unlink(sock);
|
unlink(sock);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/* test_np_keys.c — 宿主单测:Now Playing 播放页/歌词页物理键分发(Task C)。
|
||||||
|
*
|
||||||
|
* 背景(真机现象):桌面播放页与歌词页上,音量键 + 播放键「完全无反应」。
|
||||||
|
* 根因(now_playing.c):run_now_playing() 原先只接播放键(164)与电源键,未接音量键
|
||||||
|
* (114/115),导致播放页/歌词页(同一循环,与页无关)调不了音量。
|
||||||
|
*
|
||||||
|
* 本测复现 run_now_playing() 的按键动作应用(np_apply_key_actions):音量键走
|
||||||
|
* settings_volume_adj(±step)、播放键走 meow_media_playctl(按会话当前态 1=播放 2=暂停)。
|
||||||
|
* 断言:
|
||||||
|
* K1 音量减边沿 → settings_volume_adj(-step);
|
||||||
|
* K2 音量加边沿 → settings_volume_adj(+step);
|
||||||
|
* K3 播放键边沿 + 会话正在播放 → meow_media_playctl(2,0)(暂停);
|
||||||
|
* K4 播放键边沿 + 会话已暂停 → meow_media_playctl(1,0)(播放);
|
||||||
|
* K5 播放键边沿 + 无会话 → 不下发(空态不误报);
|
||||||
|
* K6 无任何边沿 → 两个回调都不触发;
|
||||||
|
* K7 音量/播放边沿都会更新 last_input(自动息屏计时复位)。
|
||||||
|
*
|
||||||
|
* 编译(在 patch_v2/meow_launcherd 下):
|
||||||
|
* gcc -Wall -Wextra -I../sdk -I. -ffunction-sections -fdata-sections \
|
||||||
|
* -Wl,--gc-sections -DMEOW_NP_KEYS_TEST \
|
||||||
|
* test_np_keys.c -o test_np_keys
|
||||||
|
* ./test_np_keys
|
||||||
|
* 说明:now_playing.c 用 #include 同 TU 平移(惯例同 test_vol_ramp_jack);-ffunction-sections
|
||||||
|
* + --gc-sections 只保留 np_apply_key_actions 及其依赖,其余(渲染/控件/绘制)被回收,
|
||||||
|
* 故仅需桩 settings_volume_adj/settings_volume_step/meow_media_playctl/now_ms/logmsg。
|
||||||
|
*/
|
||||||
|
#include "now_playing.c" /* 同 TU #include:拿到 static np_apply_key_actions */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int g_fails = 0;
|
||||||
|
#define CHECK(cond, name) do { \
|
||||||
|
if (cond) printf(" PASS %s\n", name); \
|
||||||
|
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fails++; } } while (0)
|
||||||
|
|
||||||
|
/* ---- 桩:np_apply_key_actions 依赖的 launcher 外部符号 ---- */
|
||||||
|
int settings_volume_step = 5;
|
||||||
|
static int g_adj_count = 0, g_adj_delta = 0;
|
||||||
|
void settings_volume_adj(int delta) { g_adj_count++; g_adj_delta = delta; }
|
||||||
|
|
||||||
|
static int g_playctl_count = 0;
|
||||||
|
static uint32_t g_playctl_cmd = 0;
|
||||||
|
static int64_t g_playctl_seek = 0;
|
||||||
|
int meow_media_playctl(uint32_t cmd, int64_t seek_ms)
|
||||||
|
{ g_playctl_count++; g_playctl_cmd = cmd; g_playctl_seek = seek_ms; return 0; }
|
||||||
|
|
||||||
|
static unsigned long long g_now = 100000;
|
||||||
|
unsigned long long now_ms(void) { return g_now; }
|
||||||
|
|
||||||
|
void logmsg(const char *fmt, ...) { (void)fmt; }
|
||||||
|
|
||||||
|
/* 会话(meow_media_session_t 来自 meow_media.h,含 state 字段) */
|
||||||
|
static meow_media_session_t g_s;
|
||||||
|
|
||||||
|
static void reset_callbacks(void)
|
||||||
|
{
|
||||||
|
g_adj_count = 0; g_adj_delta = 0;
|
||||||
|
g_playctl_count = 0; g_playctl_cmd = 0; g_playctl_seek = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
memset(&g_s, 0, sizeof g_s);
|
||||||
|
unsigned long long last_input = 0;
|
||||||
|
|
||||||
|
/* K1 音量减边沿 → settings_volume_adj(-step) */
|
||||||
|
reset_callbacks(); last_input = 0; g_now = 100000;
|
||||||
|
np_apply_key_actions(1, 0, 0, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_adj_delta == -settings_volume_step,
|
||||||
|
"K1 vol-down edge -> settings_volume_adj(-step)");
|
||||||
|
CHECK(last_input == g_now, "K7a vol-down updates last_input");
|
||||||
|
|
||||||
|
/* K2 音量加边沿 → settings_volume_adj(+step) */
|
||||||
|
reset_callbacks(); last_input = 0; g_now = 100050;
|
||||||
|
np_apply_key_actions(0, 1, 0, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_adj_delta == +settings_volume_step,
|
||||||
|
"K2 vol-up edge -> settings_volume_adj(+step)");
|
||||||
|
CHECK(last_input == g_now, "K7b vol-up updates last_input");
|
||||||
|
|
||||||
|
/* K3 播放键边沿 + 正在播放 → MSG_PLAYCTL(2,0) 暂停 */
|
||||||
|
g_s.state = MEOW_MEDIA_PLAYING;
|
||||||
|
reset_callbacks();
|
||||||
|
np_apply_key_actions(0, 0, 1, &g_s, &last_input);
|
||||||
|
CHECK(g_playctl_count == 1 && g_playctl_cmd == 2 && g_playctl_seek == 0,
|
||||||
|
"K3 play edge (playing) -> playctl pause(2)");
|
||||||
|
|
||||||
|
/* K4 播放键边沿 + 已暂停 → MSG_PLAYCTL(1,0) 播放 */
|
||||||
|
g_s.state = MEOW_MEDIA_PAUSED;
|
||||||
|
reset_callbacks();
|
||||||
|
np_apply_key_actions(0, 0, 1, &g_s, &last_input);
|
||||||
|
CHECK(g_playctl_count == 1 && g_playctl_cmd == 1 && g_playctl_seek == 0,
|
||||||
|
"K4 play edge (paused) -> playctl play(1)");
|
||||||
|
|
||||||
|
/* K5 播放键边沿 + 无会话 → 不下发(并仍复位 last_input) */
|
||||||
|
reset_callbacks(); last_input = 0; g_now = 100100;
|
||||||
|
np_apply_key_actions(0, 0, 1, NULL, &last_input);
|
||||||
|
CHECK(g_playctl_count == 0, "K5 play edge (no session) -> no playctl");
|
||||||
|
CHECK(last_input == g_now, "K7c play edge updates last_input (no session)");
|
||||||
|
|
||||||
|
/* K6 无任何边沿 → 两个回调都不触发 */
|
||||||
|
reset_callbacks(); last_input = 0;
|
||||||
|
np_apply_key_actions(0, 0, 0, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 0 && g_playctl_count == 0, "K6 no edge -> no callbacks");
|
||||||
|
|
||||||
|
/* K7 音量递减时播放键不同时触发(音量边沿优先,播放单独判) */
|
||||||
|
g_s.state = MEOW_MEDIA_PLAYING;
|
||||||
|
reset_callbacks();
|
||||||
|
np_apply_key_actions(1, 0, 1, &g_s, &last_input);
|
||||||
|
CHECK(g_adj_count == 1 && g_playctl_count == 1,
|
||||||
|
"K8 vol-down + play_edge both fire (independent)");
|
||||||
|
|
||||||
|
printf("\nRESULT: %s (%d fails)\n", g_fails ? "FAIL" : "ALL PASS", g_fails);
|
||||||
|
return g_fails ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
* S4 锁屏页自睡(294 遗留) → overlay_lock_idle_off_sec():阈值 = min(系统 screen_off_timeout, 30s)
|
* S4 锁屏页自睡(294 遗留) → overlay_lock_idle_off_sec():阈值 = min(系统 screen_off_timeout, 30s)
|
||||||
* (0/超30→30,尊重更短);overlay_lock_should_sleep()/overlay_lock_wake_now()
|
* (0/超30→30,尊重更短);overlay_lock_should_sleep()/overlay_lock_wake_now()
|
||||||
* 判定超时灭屏(锁态保持)与唤醒回锁屏。
|
* 判定超时灭屏(锁态保持)与唤醒回锁屏。
|
||||||
|
* M40 F3/F4 → overlay_activity_drain_active():叠加/菜单返回后的排空窗内忽略
|
||||||
|
* activity 标记(防冻结期输入重放成「亮屏+锁屏」);
|
||||||
|
* overlay_lock_power_off_now():锁屏电源必须按下边沿 + 进场≥300ms。
|
||||||
*
|
*
|
||||||
* 编译运行(在 patch_v2/meow_launcherd 下,无其它依赖):
|
* 编译运行(在 patch_v2/meow_launcherd 下,无其它依赖):
|
||||||
* gcc -Wall -Wextra -o test_overlay_gate test_overlay_gate.c && ./test_overlay_gate
|
* gcc -Wall -Wextra -o test_overlay_gate test_overlay_gate.c && ./test_overlay_gate
|
||||||
@@ -80,6 +83,19 @@ int main(void)
|
|||||||
CHECK(overlay_lock_wake_now(0, 1) == 1, "S4 touch -> wake to lock");
|
CHECK(overlay_lock_wake_now(0, 1) == 1, "S4 touch -> wake to lock");
|
||||||
CHECK(overlay_lock_wake_now(0, 0) == 0, "S4 no wake input -> stay asleep");
|
CHECK(overlay_lock_wake_now(0, 0) == 0, "S4 no wake input -> stay asleep");
|
||||||
|
|
||||||
|
/* ---- M40 F4/P4:锁屏页电源「按下边沿 + 进场 ≥300ms」才熄屏 ----
|
||||||
|
* 电平判定会让「打开菜单的那次长按」在 PIN 页出现时立刻把页面熄灭(用户症状)。 */
|
||||||
|
CHECK(OVERLAY_LOCK_POWER_GRACE_MS == 300, "F4 lock power grace = 300ms");
|
||||||
|
CHECK(overlay_lock_power_off_now(1, 299) == 0, "F4 press edge inside 300ms grace -> no off");
|
||||||
|
CHECK(overlay_lock_power_off_now(1, 300) == 1, "F4 press edge at 300ms -> off");
|
||||||
|
CHECK(overlay_lock_power_off_now(0, 5000) == 0, "F4 held level (no edge) -> never re-fires");
|
||||||
|
CHECK(overlay_lock_power_off_now(1, 5000) == 1, "F4 new press edge after grace -> off");
|
||||||
|
|
||||||
|
/* ---- M40 F3/P3:叠加层/菜单返回后的 activity 标记排空窗 ---- */
|
||||||
|
CHECK(overlay_activity_drain_active(0, 0, 999999) == 0, "F3 no overlay return mark -> activity honored");
|
||||||
|
CHECK(overlay_activity_drain_active(1, 1000, 1299) == 1, "F3 marker inside 300ms drain -> ignored");
|
||||||
|
CHECK(overlay_activity_drain_active(1, 1000, 1300) == 0, "F3 marker after drain -> honored");
|
||||||
|
|
||||||
/* ---- S3 App 死亡触发主屏重绘:崩溃必须走统一退出路径 ---- */
|
/* ---- S3 App 死亡触发主屏重绘:崩溃必须走统一退出路径 ---- */
|
||||||
CHECK(lock_overlay_app_died_exit(1) == 1, "S3 app died -> unified exit path (redraw home)");
|
CHECK(lock_overlay_app_died_exit(1) == 1, "S3 app died -> unified exit path (redraw home)");
|
||||||
CHECK(lock_overlay_app_died_exit(0) == 0, "S3 app alive -> normal restore");
|
CHECK(lock_overlay_app_died_exit(0) == 0, "S3 app alive -> normal restore");
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
/* test_pkg_install.c — M42 宿主单测:安装入口路径预检 + 错误文案 + exec 合同(假 nyai)。
|
||||||
|
*
|
||||||
|
* 编译运行(在 patch_v2/meow_launcherd 下,需 -I../sdk 因内部头含 SDK 声明):
|
||||||
|
* gcc -Wall -Wextra -Os -I../sdk test_pkg_install.c pkg_install.c -o test_pkg_install && ./test_pkg_install
|
||||||
|
* 全 PASS -> exit 0。
|
||||||
|
*
|
||||||
|
* 覆盖:
|
||||||
|
* A. 路径预检:任意文件名/后缀可装(M35 定案:身份只来自包内 app.json)、目录/缺失/不可读/
|
||||||
|
* 控制字符/超长一律拒。
|
||||||
|
* B. 错误文案:nyai 退出码 + 输出尾行 → 单条可读文案(不空、不越界)。
|
||||||
|
* C. exec 合同:argv 直传(不经 shell,带空格/引号的路径原样到达)、退出码映射、
|
||||||
|
* start/poll 非阻塞(start 立即返回)、并发拒装、超时kill。
|
||||||
|
* 假 nyai 用 MEOW_NYAI_BIN 注入(与 MEOW_VERIFY_BIN 同口径),不碰真机/真包。
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include "meow_launcher_internal.h"
|
||||||
|
|
||||||
|
/* pkg_install.c 依赖的 launcher 日志:宿主打桩(宿主无 util.c)。 */
|
||||||
|
void logmsg(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
(void)fmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g_pass = 0, g_fail = 0;
|
||||||
|
static void check(const char *name, int ok)
|
||||||
|
{
|
||||||
|
if (ok) { g_pass++; printf("PASS %s\n", name); }
|
||||||
|
else { g_fail++; printf("FAIL %s\n", name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static long long now_ms_mono(void)
|
||||||
|
{
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char g_dir[512];
|
||||||
|
|
||||||
|
/* 在临时目录写一个文件,返回其路径(buf)。 */
|
||||||
|
static const char *mkfile(const char *name, const char *content, char *buf, int n)
|
||||||
|
{
|
||||||
|
snprintf(buf, n, "%s/%s", g_dir, name);
|
||||||
|
FILE *f = fopen(buf, "w");
|
||||||
|
if (!f) return NULL;
|
||||||
|
if (content) fputs(content, f);
|
||||||
|
fclose(f);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写一个可执行假 nyai。 */
|
||||||
|
static const char *mkscript(const char *name, const char *body, char *buf, int n)
|
||||||
|
{
|
||||||
|
if (!mkfile(name, body, buf, n)) return NULL;
|
||||||
|
chmod(buf, 0755);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- A. 路径预检 ---------------- */
|
||||||
|
static void test_path_check(void)
|
||||||
|
{
|
||||||
|
char err[256], f[512];
|
||||||
|
|
||||||
|
check("A1 null -> reject", meow_pkg_install_check_path(NULL, err, sizeof err) != 0);
|
||||||
|
check("A1 null -> has text", err[0] != 0);
|
||||||
|
err[0] = 0;
|
||||||
|
check("A2 empty -> reject", meow_pkg_install_check_path("", err, sizeof err) != 0 && err[0]);
|
||||||
|
err[0] = 0;
|
||||||
|
check("A3 missing -> reject", meow_pkg_install_check_path("/nonexistent/x.mpk", err, sizeof err) != 0);
|
||||||
|
check("A3 missing -> 找不到", strstr(err, "找不到") != NULL);
|
||||||
|
err[0] = 0;
|
||||||
|
check("A4 directory -> reject", meow_pkg_install_check_path(g_dir, err, sizeof err) != 0);
|
||||||
|
check("A4 directory -> 不是普通文件", strstr(err, "不是普通文件") != NULL);
|
||||||
|
err[0] = 0;
|
||||||
|
check("A5 dot -> reject", meow_pkg_install_check_path(".", err, sizeof err) != 0);
|
||||||
|
check("A6 dotdot -> reject", meow_pkg_install_check_path("..", err, sizeof err) != 0);
|
||||||
|
err[0] = 0;
|
||||||
|
check("A7 control char -> reject",
|
||||||
|
meow_pkg_install_check_path("/tmp/a\x01" "b", err, sizeof err) != 0 &&
|
||||||
|
strstr(err, "控制字符") != NULL);
|
||||||
|
{
|
||||||
|
char longp[700];
|
||||||
|
memset(longp, 'a', sizeof longp - 1);
|
||||||
|
longp[0] = '/'; longp[sizeof longp - 1] = 0;
|
||||||
|
err[0] = 0;
|
||||||
|
check("A8 too long -> reject",
|
||||||
|
meow_pkg_install_check_path(longp, err, sizeof err) != 0 &&
|
||||||
|
strstr(err, "过长") != NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关键回归:任意文件名/无 .mpk 后缀都可装(m35 事故:改名后装不上)。 */
|
||||||
|
if (mkfile("weird name-v0.1.7.bin", "x", f, sizeof f))
|
||||||
|
check("A9 any name/no .mpk -> accept",
|
||||||
|
meow_pkg_install_check_path(f, err, sizeof err) == 0);
|
||||||
|
else check("A9 any name/no .mpk -> accept", 0);
|
||||||
|
if (mkfile("OS.MEOW.READER", "x", f, sizeof f))
|
||||||
|
check("A10 uppercase name -> accept (nyai 拒,入口不按名字判)",
|
||||||
|
meow_pkg_install_check_path(f, err, sizeof err) == 0);
|
||||||
|
else check("A10 uppercase name -> accept (nyai 拒,入口不按名字判)", 0);
|
||||||
|
|
||||||
|
if (mkfile("noread.bin", "x", f, sizeof f)) {
|
||||||
|
chmod(f, 0000);
|
||||||
|
err[0] = 0;
|
||||||
|
check("A11 unreadable -> reject",
|
||||||
|
meow_pkg_install_check_path(f, err, sizeof err) != 0 &&
|
||||||
|
strstr(err, "不可读") != NULL);
|
||||||
|
chmod(f, 0644);
|
||||||
|
} else check("A11 unreadable -> reject", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- B. 错误文案 ---------------- */
|
||||||
|
static void test_error_text(void)
|
||||||
|
{
|
||||||
|
char out[256];
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(0, "whatever", out, sizeof out);
|
||||||
|
check("B1 rc=0 -> empty", out[0] == 0);
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(1,
|
||||||
|
"Reading package lists... Done\r\nnyai: install: 不允许降级: 新版本 1 < 已装 2\n\n",
|
||||||
|
out, sizeof out);
|
||||||
|
check("B2 last non-empty line", strcmp(out, "nyai: install: 不允许降级: 新版本 1 < 已装 2") == 0);
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(7, "", out, sizeof out);
|
||||||
|
check("B3 no output -> exit code text", strstr(out, "退出码 7") != NULL);
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(137, "x", out, sizeof out);
|
||||||
|
check("B4 signal -> 128+sig text", strstr(out, "信号 9") != NULL);
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(MEOW_PKG_E_TIMEOUT, NULL, out, sizeof out);
|
||||||
|
check("B5 timeout text", strstr(out, "超时") != NULL);
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(MEOW_PKG_E_NYAI, "无法执行 /usr/bin/nyai", out, sizeof out);
|
||||||
|
check("B6 nyai unavailable text", strstr(out, "不可用") && strstr(out, "/usr/bin/nyai"));
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(MEOW_PKG_E_BUSY, NULL, out, sizeof out);
|
||||||
|
check("B7 busy text", strstr(out, "已有安装") != NULL);
|
||||||
|
|
||||||
|
meow_pkg_install_error_text(MEOW_PKG_E_PATH, "找不到包文件 /x", out, sizeof out);
|
||||||
|
check("B8 path text", strcmp(out, "找不到包文件 /x") == 0);
|
||||||
|
|
||||||
|
{
|
||||||
|
char tiny[24];
|
||||||
|
memset(tiny, 0x5a, sizeof tiny);
|
||||||
|
meow_pkg_install_error_text(1,
|
||||||
|
"nyai: 这是一个非常非常长的错误信息,用来验证截断不会越界写坏栈\n", tiny, 16);
|
||||||
|
int guard = 1;
|
||||||
|
for (int i = 16; i < (int)sizeof tiny; i++) if (tiny[i] != 0x5a) guard = 0;
|
||||||
|
check("B9 truncation bounded", strlen(tiny) < 16 && guard);
|
||||||
|
}
|
||||||
|
meow_pkg_install_error_text(1, " \n\t\n", out, sizeof out);
|
||||||
|
check("B10 whitespace-only detail -> generic", out[0] != 0 && strstr(out, "退出码 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- C. exec 合同(假 nyai) ---------------- */
|
||||||
|
static void test_exec(void)
|
||||||
|
{
|
||||||
|
char script[512], pkg[512], log[512], err[256];
|
||||||
|
|
||||||
|
/* C1: 失败透传 —— stderr 尾行进错误文案,退出码映射 MEOW_PKG_E_FAIL */
|
||||||
|
if (mkscript("nyai_fail.sh",
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"echo \"Reading package lists... Done\"\n"
|
||||||
|
"echo \"nyai: 假失败:测试包损坏\" >&2\n"
|
||||||
|
"exit 7\n", script, sizeof script) &&
|
||||||
|
mkfile("pkg-fail.bin", "x", pkg, sizeof pkg)) {
|
||||||
|
setenv("MEOW_NYAI_BIN", script, 1);
|
||||||
|
int r = meow_pkg_install(pkg);
|
||||||
|
check("C1 exit!=0 -> MEOW_PKG_E_FAIL", r == MEOW_PKG_E_FAIL);
|
||||||
|
check("C1 stderr tail surfaced", strstr(meow_pkg_install_error(), "假失败") != NULL);
|
||||||
|
check("C1 not busy after finish", meow_pkg_install_busy() == 0);
|
||||||
|
} else check("C1 exit!=0 -> MEOW_PKG_E_FAIL", 0);
|
||||||
|
|
||||||
|
/* C1b: 真机回归 —— nyai 的 stdout 是管道全缓冲(退出时才刷),错误走 stderr 先到。
|
||||||
|
* 合成一路时「最后一行」会把真错误顶成旧进度;错误文案必须取 stderr。 */
|
||||||
|
if (mkscript("nyai_buforder.sh",
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"echo \"nyai: install: 不允许降级: 新版本 code 8 < 已装 code 9\" >&2\n"
|
||||||
|
"echo \"Reading package lists... Done\"\n"
|
||||||
|
"echo \"nyai: 签名校验通过 (package=os.meow.reader)\"\n"
|
||||||
|
"exit 1\n", script, sizeof script) &&
|
||||||
|
mkfile("pkg-buforder.bin", "x", pkg, sizeof pkg)) {
|
||||||
|
setenv("MEOW_NYAI_BIN", script, 1);
|
||||||
|
int r = meow_pkg_install(pkg);
|
||||||
|
check("C1b fail rc", r == MEOW_PKG_E_FAIL);
|
||||||
|
check("C1b stderr beats buffered stdout",
|
||||||
|
strstr(meow_pkg_install_error(), "不允许降级") != NULL &&
|
||||||
|
strstr(meow_pkg_install_error(), "签名校验通过") == NULL);
|
||||||
|
} else check("C1b stderr beats buffered stdout", 0);
|
||||||
|
|
||||||
|
/* C2: argv 直传(不经 shell):带空格/单引号的路径原样到 $2;$1 必须是 install */
|
||||||
|
snprintf(log, sizeof log, "%s/argv.log", g_dir);
|
||||||
|
if (mkscript("nyai_argv.sh",
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"printf '%s|%s\\n' \"$1\" \"$2\" > \"$M42_ARGV_LOG\"\n"
|
||||||
|
"exit 0\n", script, sizeof script) &&
|
||||||
|
mkfile("name with space 'q'.bin", "x", pkg, sizeof pkg)) {
|
||||||
|
setenv("MEOW_NYAI_BIN", script, 1);
|
||||||
|
setenv("M42_ARGV_LOG", log, 1);
|
||||||
|
int r = meow_pkg_install(pkg);
|
||||||
|
check("C2 success rc=0", r == 0);
|
||||||
|
FILE *f = fopen(log, "r");
|
||||||
|
char line[600] = "";
|
||||||
|
if (f) { if (!fgets(line, sizeof line, f)) line[0] = 0; fclose(f); }
|
||||||
|
char want[700];
|
||||||
|
snprintf(want, sizeof want, "install|%s\n", pkg);
|
||||||
|
check("C2 argv passed verbatim (no shell)", strcmp(line, want) == 0);
|
||||||
|
unsetenv("M42_ARGV_LOG");
|
||||||
|
} else check("C2 success rc=0", 0);
|
||||||
|
|
||||||
|
/* C3: start 立即返回 + 并发拒装(UI 不假死的关键合同) */
|
||||||
|
if (mkscript("nyai_slow.sh", "#!/bin/sh\nsleep 0.4\nexit 0\n", script, sizeof script) &&
|
||||||
|
mkfile("pkg-slow.bin", "x", pkg, sizeof pkg)) {
|
||||||
|
setenv("MEOW_NYAI_BIN", script, 1);
|
||||||
|
long long t0 = now_ms_mono();
|
||||||
|
int r = meow_pkg_install_start(pkg);
|
||||||
|
long long dt = now_ms_mono() - t0;
|
||||||
|
check("C3 start returns fast", r == 0 && dt < 150);
|
||||||
|
check("C3 busy while running", meow_pkg_install_busy() == 1);
|
||||||
|
int r2 = meow_pkg_install_start(pkg);
|
||||||
|
check("C3 second start -> BUSY", r2 == MEOW_PKG_E_BUSY);
|
||||||
|
check("C3 busy error text", strstr(meow_pkg_install_error(), "已有安装") != NULL);
|
||||||
|
int rc = -1, p = 1, spins = 0;
|
||||||
|
while ((p = meow_pkg_install_poll(&rc)) == 1 && spins++ < 500) usleep(10000);
|
||||||
|
check("C3 poll finish rc=0", p == 0 && rc == 0);
|
||||||
|
check("C3 no longer busy", meow_pkg_install_busy() == 0);
|
||||||
|
} else check("C3 start returns fast", 0);
|
||||||
|
|
||||||
|
/* C4: 超时 —— 子进程被终止,返回 TIMEOUT 且不拖住调用方 */
|
||||||
|
if (mkscript("nyai_hang.sh", "#!/bin/sh\nexec sleep 5\n", script, sizeof script) &&
|
||||||
|
mkfile("pkg-hang.bin", "x", pkg, sizeof pkg)) {
|
||||||
|
setenv("MEOW_NYAI_BIN", script, 1);
|
||||||
|
setenv("MEOW_NYAI_TIMEOUT_MS", "100", 1);
|
||||||
|
long long t0 = now_ms_mono();
|
||||||
|
int r = meow_pkg_install(pkg);
|
||||||
|
long long dt = now_ms_mono() - t0;
|
||||||
|
check("C4 timeout -> MEOW_PKG_E_TIMEOUT", r == MEOW_PKG_E_TIMEOUT);
|
||||||
|
check("C4 timeout bounded (<3s)", dt < 3000);
|
||||||
|
check("C4 timeout text", strstr(meow_pkg_install_error(), "超时") != NULL);
|
||||||
|
unsetenv("MEOW_NYAI_TIMEOUT_MS");
|
||||||
|
} else check("C4 timeout -> MEOW_PKG_E_TIMEOUT", 0);
|
||||||
|
|
||||||
|
/* C5: nyai 不存在 → 明确报错,不回落旧内联实现、不静默 */
|
||||||
|
setenv("MEOW_NYAI_BIN", "/nonexistent/nyai", 1);
|
||||||
|
if (mkfile("pkg-ok.bin", "x", pkg, sizeof pkg)) {
|
||||||
|
err[0] = 0;
|
||||||
|
int r = meow_pkg_install(pkg);
|
||||||
|
check("C5 missing nyai -> MEOW_PKG_E_NYAI", r == MEOW_PKG_E_NYAI);
|
||||||
|
check("C5 missing nyai -> readable", strstr(meow_pkg_install_error(), "不可用") != NULL);
|
||||||
|
} else check("C5 missing nyai -> MEOW_PKG_E_NYAI", 0);
|
||||||
|
|
||||||
|
/* C6: nyai 存在但不可执行 → 同 E_NYAI */
|
||||||
|
if (mkfile("nyai_noexec.sh", "#!/bin/sh\nexit 0\n", script, sizeof script) &&
|
||||||
|
mkfile("pkg-ok2.bin", "x", pkg, sizeof pkg)) {
|
||||||
|
chmod(script, 0644);
|
||||||
|
setenv("MEOW_NYAI_BIN", script, 1);
|
||||||
|
check("C6 non-executable nyai -> MEOW_PKG_E_NYAI",
|
||||||
|
meow_pkg_install(pkg) == MEOW_PKG_E_NYAI);
|
||||||
|
chmod(script, 0755);
|
||||||
|
} else check("C6 non-executable nyai -> MEOW_PKG_E_NYAI", 0);
|
||||||
|
|
||||||
|
/* C7: 入口路径预检先于 exec(坏路径不启动子进程) */
|
||||||
|
unsetenv("MEOW_NYAI_BIN");
|
||||||
|
check("C7 bad path -> MEOW_PKG_E_PATH",
|
||||||
|
meow_pkg_install("/nonexistent/x.mpk") == MEOW_PKG_E_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
const char *tmp = getenv("TMPDIR");
|
||||||
|
if (!tmp || !*tmp) tmp = "/tmp";
|
||||||
|
snprintf(g_dir, sizeof g_dir, "%s/m42pkg-XXXXXX", tmp);
|
||||||
|
if (!mkdtemp(g_dir)) {
|
||||||
|
/* 退路:当前目录(宿主 CI 也能跑) */
|
||||||
|
snprintf(g_dir, sizeof g_dir, "./m42pkg-XXXXXX");
|
||||||
|
if (!mkdtemp(g_dir)) { printf("FAIL cannot create temp dir\n"); return 1; }
|
||||||
|
}
|
||||||
|
printf("tmp=%s\n", g_dir);
|
||||||
|
|
||||||
|
test_path_check();
|
||||||
|
test_error_text();
|
||||||
|
test_exec();
|
||||||
|
|
||||||
|
/* 清理(先恢复文件权限,避免删不掉) */
|
||||||
|
{
|
||||||
|
char cmd[700];
|
||||||
|
snprintf(cmd, sizeof cmd, "chmod -R u+rwx '%s' 2>/dev/null; rm -rf '%s'", g_dir, g_dir);
|
||||||
|
if (system(cmd) != 0) printf("warn: cleanup %s failed\n", g_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n%d PASS, %d FAIL\n", g_pass, g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -186,23 +186,29 @@ static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
|
|||||||
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
|
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
|
||||||
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
|
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
|
||||||
|
|
||||||
|
/* MD3 容器化(P0):行=surface 上 surfaceContainer 圆角卡;段头=surface 上的 primary 段标签(无卡
|
||||||
|
* 无分隔线);与 settings_page.c set_lv_draw 同构造。 */
|
||||||
|
int cx = x + SET_CARD_X;
|
||||||
|
int cw = w - 2 * SET_CARD_X;
|
||||||
|
int cy = y + 8 / 2;
|
||||||
|
int ch = SET_ITEM_H - 8;
|
||||||
if (r->ctrl == SC_HEADER) {
|
if (r->ctrl == SC_HEADER) {
|
||||||
meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh);
|
|
||||||
int ty = y + (SET_ITEM_H - thh) / 2;
|
int ty = y + (SET_ITEM_H - thh) / 2;
|
||||||
if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary);
|
if (ty >= 0) meow_ui_text(cx + 16, ty, r->label, mt->font_body, mt->primary);
|
||||||
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer);
|
meow_gfx_fill_round(cx, cy, cw, ch, 12, mt->surfaceContainer);
|
||||||
else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26);
|
if (sel) meow_gfx_fill_round(cx, cy, cw, ch, 12, mt->primaryContainer);
|
||||||
|
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, ch, 12, mt->primary, 26);
|
||||||
int ly = y + (SET_ITEM_H - thh) / 2;
|
int ly = y + (SET_ITEM_H - thh) / 2;
|
||||||
|
int rgt = cx + cw - 16;
|
||||||
switch (r->ctrl) {
|
switch (r->ctrl) {
|
||||||
case SC_SWITCH: {
|
case SC_SWITCH: {
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
meow_widget *sw = set_sw_take();
|
meow_widget *sw = set_sw_take();
|
||||||
if (sw) {
|
if (sw) {
|
||||||
int on = 0; meow_settings_get_switch(r->key, &on);
|
int on = 0; meow_settings_get_switch(r->key, &on);
|
||||||
meow_widget_set_rect(sw, x + w - 16 - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
|
meow_widget_set_rect(sw, rgt - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
|
||||||
meow_switch_set(sw, on);
|
meow_switch_set(sw, on);
|
||||||
meow_widget_draw(sw);
|
meow_widget_draw(sw);
|
||||||
}
|
}
|
||||||
@@ -211,26 +217,25 @@ static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
|
|||||||
case SC_SLIDER: {
|
case SC_SLIDER: {
|
||||||
/* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由
|
/* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由
|
||||||
* 真机 s_draw_slider 呈现(settings_page.c set_lv_draw 已复用)。 */
|
* 真机 s_draw_slider 呈现(settings_page.c set_lv_draw 已复用)。 */
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
char vb[32]; s_fmt_value(r, vb, sizeof vb);
|
char vb[32]; s_fmt_value(r, vb, sizeof vb);
|
||||||
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
|
meow_ui_text(rgt - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SC_LIST: {
|
case SC_LIST: {
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
meow_ui_text(x + w - 16 - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol);
|
meow_ui_text(rgt - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: { /* SC_ENUM / SC_PRESET */
|
default: { /* SC_ENUM / SC_PRESET */
|
||||||
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol);
|
meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||||
char vb[40]; s_display_value(r, vb, sizeof vb);
|
char vb[40]; s_display_value(r, vb, sizeof vb);
|
||||||
int vx = x + w - 16 - meow_ui_text_width(vb, mt->font_body);
|
int vx = rgt - meow_ui_text_width(vb, mt->font_body);
|
||||||
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
|
meow_ui_text(vx, ly, vb, mt->font_body, subcol);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
|
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_gfx_fill_round_alpha(cx, cy, cw, ch, 12, mt->onSurface, 90);
|
||||||
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void mk_widgets(void)
|
static void mk_widgets(void)
|
||||||
@@ -247,7 +252,7 @@ static void draw_all(void)
|
|||||||
{
|
{
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
|
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
|
||||||
meow_ui_fill_rect(0, SET_LIST_Y, XRES, LV_H, th->surfaceVariant);
|
meow_ui_fill_rect(0, SET_LIST_Y, XRES, LV_H, th->surface); /* P0:内容底 surface(卡在其上抬升) */
|
||||||
if (lv) { g_set_sw_use = 0; meow_widget_draw(lv); }
|
if (lv) { g_set_sw_use = 0; meow_widget_draw(lv); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,16 +310,17 @@ int main(void)
|
|||||||
meow_ui_activity_push(SET_SENTINEL);
|
meow_ui_activity_push(SET_SENTINEL);
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
draw_all();
|
draw_all();
|
||||||
/* 段头行整体填 surfaceContainerHigh(覆盖列表底 surfaceVariant)→ 对列表底背景即非背景;
|
/* MD3 容器化(P0):段头=surface 上的 primary 段标签(无满幅 band/卡,宿主文本为 no-op →
|
||||||
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「段头底 vs 列表底」判渲染而非文字像素。 */
|
* 段头区应回 surface);普通行=surfaceContainer 圆角卡(region_nonbg 应见大量非 surface 像素)。
|
||||||
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
|
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「卡/段头 vs 列表底」判渲染而非文字像素。 */
|
||||||
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
|
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
|
||||||
CHECK(hdrcnt > 0, "section header band renders (hdr=%d)", hdrcnt);
|
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
|
||||||
CHECK(rowcnt > 40, "row band renders (title/value/switch) cnt=%d", rowcnt);
|
CHECK(hdrcnt == 0, "section header no-band(label-on-surface) nb=%d", hdrcnt);
|
||||||
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景 */
|
CHECK(rowcnt > 40, "row card renders (title/value/switch) cnt=%d", rowcnt);
|
||||||
|
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景(卡内 switch) */
|
||||||
{
|
{
|
||||||
int swy = SET_LIST_Y + 2 * SET_ITEM_H + SET_ITEM_H / 2;
|
int swy = SET_LIST_Y + 2 * SET_ITEM_H + SET_ITEM_H / 2;
|
||||||
int swc = region_nonbg(XRES - 16 - 56, swy - 4, 56, 8, th->surfaceVariant);
|
int swc = region_nonbg(XRES - 16 - 56, swy - 4, 56, 8, th->surface);
|
||||||
CHECK(swc > 0, "switch row right tail (meow_switch) renders swc=%d", swc);
|
CHECK(swc > 0, "switch row right tail (meow_switch) renders swc=%d", swc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ static void mk_widgets(void)
|
|||||||
meow_listview_set_vsource(lv, sp_lv_count, sp_lv_bind, NULL);
|
meow_listview_set_vsource(lv, sp_lv_count, sp_lv_bind, NULL);
|
||||||
meow_listview_set_item_renderer(lv, 2, 0, 1);
|
meow_listview_set_item_renderer(lv, 2, 0, 1);
|
||||||
meow_listview_set_item_h(lv, SUB_ITEM_H);
|
meow_listview_set_item_h(lv, SUB_ITEM_H);
|
||||||
|
meow_listview_set_container(lv, 1); /* MD3 容器模式(M7.1a P0):圆角卡+空隙+去分隔线 */
|
||||||
sw = meow_switch_create(XRES - 16 - 56, SUB_TB_Y + (SUB_TB_H - 32) / 2, 1);
|
sw = meow_switch_create(XRES - 16 - 56, SUB_TB_Y + (SUB_TB_H - 32) / 2, 1);
|
||||||
meow_ui_activity_pop();
|
meow_ui_activity_pop();
|
||||||
}
|
}
|
||||||
@@ -153,12 +154,14 @@ int main(void)
|
|||||||
meow_ui_activity_push(SUB_SENTINEL);
|
meow_ui_activity_push(SUB_SENTINEL);
|
||||||
const meow_theme *th = meow_theme_get();
|
const meow_theme *th = meow_theme_get();
|
||||||
draw_all();
|
draw_all();
|
||||||
|
/* MD3 容器模式:段头=surface 上的 primary 段标签(无满幅 band/卡,宿主文本为 no-op →
|
||||||
|
* 段头区应回 surface);普通行=surfaceContainer 圆角卡(region_nonbg 应见大量非 surface 像素)。 */
|
||||||
int hdrcnt = region_nonbg(2, SUB_CONT_TOP + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
int hdrcnt = region_nonbg(2, SUB_CONT_TOP + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
||||||
int rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
int rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
||||||
char nm[80];
|
char nm[80];
|
||||||
snprintf(nm, sizeof nm, "section header renders (band=%d)", hdrcnt);
|
snprintf(nm, sizeof nm, "section header no-band(label-on-surface) nb=%d", hdrcnt);
|
||||||
CHECK(hdrcnt > 0, "%s", nm);
|
CHECK(hdrcnt == 0, "%s", nm);
|
||||||
snprintf(nm, sizeof nm, "row renders (title hdr/switch/arrow) cnt=%d", rowcnt);
|
snprintf(nm, sizeof nm, "row renders (card) cnt=%d", rowcnt);
|
||||||
CHECK(rowcnt > 40, "%s", nm);
|
CHECK(rowcnt > 40, "%s", nm);
|
||||||
|
|
||||||
/* ============ P2 listview 命中(行号 + right 操作区) ============ */
|
/* ============ P2 listview 命中(行号 + right 操作区) ============ */
|
||||||
@@ -190,6 +193,7 @@ int main(void)
|
|||||||
meow_listview_set_vsource(scroll_lv, big_count, big_bind, NULL);
|
meow_listview_set_vsource(scroll_lv, big_count, big_bind, NULL);
|
||||||
meow_listview_set_item_renderer(scroll_lv, 2, 0, 1);
|
meow_listview_set_item_renderer(scroll_lv, 2, 0, 1);
|
||||||
meow_listview_set_item_h(scroll_lv, SUB_ITEM_H);
|
meow_listview_set_item_h(scroll_lv, SUB_ITEM_H);
|
||||||
|
meow_listview_set_container(scroll_lv, 1); /* 与子页一致:MD3 容器模式 */
|
||||||
meow_scroll *s = meow_scroll_wrap(scroll_lv, 1); /* 1=W_LISTVIEW */
|
meow_scroll *s = meow_scroll_wrap(scroll_lv, 1); /* 1=W_LISTVIEW */
|
||||||
CHECK(s != NULL, "meow_scroll_wrap(listview)");
|
CHECK(s != NULL, "meow_scroll_wrap(listview)");
|
||||||
int maxs = meow_listview_max_scroll(scroll_lv);
|
int maxs = meow_listview_max_scroll(scroll_lv);
|
||||||
|
|||||||
@@ -907,6 +907,52 @@ int meow_ipc_conn_alive(int fd)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- E:系统级音量翻页——app_id→持久 IPC fd 最近活跃映射(util.c 单写者 = ipc_dispatch)----
|
||||||
|
* App 的「持久连接」(如 reader_app/app/reader_ipc.c 后台线程)连上后发 MSG_HELLO,本映射在该
|
||||||
|
* HELLO 被 ipc_dispatch 处理时记录 app_id→m->fd;连接关闭(meow_ipc_process 剔除)时清对应项。
|
||||||
|
* 只记录「declares volume_page」的 App(meow_volpage_app_allows),保持映射极小且无歧义。
|
||||||
|
* meow_ipc_app_fd() 供 app_launcher.c 前台会话取持久 fd 武装翻页 sink。 */
|
||||||
|
#define VOLPAGE_FD_MAX 8
|
||||||
|
static struct { char app_id[MEOW_APP_ID_MAX]; int fd; } g_volpage_fd[VOLPAGE_FD_MAX];
|
||||||
|
static int g_volpage_fd_n;
|
||||||
|
|
||||||
|
static void volpage_fd_set(const char *app_id, int fd)
|
||||||
|
{
|
||||||
|
if (!app_id || !app_id[0] || fd < 0) return;
|
||||||
|
for (int i = 0; i < g_volpage_fd_n; i++) {
|
||||||
|
if (!strcmp(g_volpage_fd[i].app_id, app_id)) { g_volpage_fd[i].fd = fd; return; }
|
||||||
|
}
|
||||||
|
if (g_volpage_fd_n < VOLPAGE_FD_MAX) {
|
||||||
|
snprintf(g_volpage_fd[g_volpage_fd_n].app_id, sizeof g_volpage_fd[0].app_id, "%s", app_id);
|
||||||
|
g_volpage_fd[g_volpage_fd_n].fd = fd;
|
||||||
|
g_volpage_fd_n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void volpage_fd_clear_fd(int fd)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < g_volpage_fd_n; i++) {
|
||||||
|
if (g_volpage_fd[i].fd == fd) {
|
||||||
|
for (int j = i; j < g_volpage_fd_n - 1; j++) g_volpage_fd[j] = g_volpage_fd[j + 1];
|
||||||
|
g_volpage_fd_n--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_ipc_app_fd(const char *app_id)
|
||||||
|
{
|
||||||
|
if (!app_id) return -1;
|
||||||
|
for (int i = 0; i < g_volpage_fd_n; i++) {
|
||||||
|
if (!strcmp(g_volpage_fd[i].app_id, app_id)) {
|
||||||
|
if (meow_ipc_conn_alive(g_volpage_fd[i].fd)) return g_volpage_fd[i].fd;
|
||||||
|
volpage_fd_clear_fd(g_volpage_fd[i].fd); /* fd 已死:清残留 */
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* §10.5:待处理的 App 前台「展开通知中心」请求(ipc_dispatch 单写者置位;launch_app waitpid
|
/* §10.5:待处理的 App 前台「展开通知中心」请求(ipc_dispatch 单写者置位;launch_app waitpid
|
||||||
* 循环 meow_notif_open_take 读取并清除)。仅当 g_app_session_active(App 前台会话)期间才置位,
|
* 循环 meow_notif_open_take 读取并清除)。仅当 g_app_session_active(App 前台会话)期间才置位,
|
||||||
* 否则回 ERR(无 App 前台,不展开)。 */
|
* 否则回 ERR(无 App 前台,不展开)。 */
|
||||||
@@ -1099,6 +1145,7 @@ static void *ipc_thread(void *arg)
|
|||||||
if (ipc_conn_read(cc) < 0) {
|
if (ipc_conn_read(cc) < 0) {
|
||||||
logmsg("ipc: conn %d closed", cc->fd);
|
logmsg("ipc: conn %d closed", cc->fd);
|
||||||
close(cc->fd);
|
close(cc->fd);
|
||||||
|
volpage_fd_clear_fd(cc->fd); /* E:清已断连接的翻页 fd 映射 */
|
||||||
for (int j = i; j < nfds - 1; j++) {
|
for (int j = i; j < nfds - 1; j++) {
|
||||||
fds[j] = fds[j + 1];
|
fds[j] = fds[j + 1];
|
||||||
ipc_conns[j - 1] = ipc_conns[j];
|
ipc_conns[j - 1] = ipc_conns[j];
|
||||||
@@ -1209,6 +1256,8 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
|
|||||||
memcpy(&h, m->payload, m->plen < (uint16_t)sizeof h ? m->plen : (uint16_t)sizeof h);
|
memcpy(&h, m->payload, m->plen < (uint16_t)sizeof h ? m->plen : (uint16_t)sizeof h);
|
||||||
h.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
h.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||||
meow_acct_hello(h.app_id, h.version, h.flags);
|
meow_acct_hello(h.app_id, h.version, h.flags);
|
||||||
|
/* E:记录声明 volume_page 的 App 的持久连接 fd(HELLO=App 侧持久 IPC 连接;仅读卡 app.json 一次)。 */
|
||||||
|
if (meow_volpage_app_allows(h.app_id)) volpage_fd_set(h.app_id, m->fd);
|
||||||
m_ack_t a;
|
m_ack_t a;
|
||||||
a.code = MEOW_OK;
|
a.code = MEOW_OK;
|
||||||
a.status = (uint32_t)g_naccts;
|
a.status = (uint32_t)g_naccts;
|
||||||
@@ -1417,15 +1466,23 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
|
|||||||
memcpy(&k, m->payload, m->plen < (uint16_t)sizeof k ? m->plen : (uint16_t)sizeof k);
|
memcpy(&k, m->payload, m->plen < (uint16_t)sizeof k ? m->plen : (uint16_t)sizeof k);
|
||||||
k.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
k.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||||
m_ack_t a;
|
m_ack_t a;
|
||||||
if (g_app_session_active) {
|
a.status = 0;
|
||||||
|
/* 先打后台保活表:后台 App 的终止由主循环单写者就地执行(meow_bg_terminate 置
|
||||||
|
* term_at_ms + SIGTERM,meow_bg_tick 续 SIGKILL→reap→on_bg_dead 上报 amsd)——
|
||||||
|
* 与后台管理页「杀」按钮**同一条通道**,故 `am kill` 与页面按钮现行为对称。
|
||||||
|
* 此前只置 pending 交前台会话消费:用户不在 App 里时(桌面/后台页/面板)没有前台会话
|
||||||
|
* → 只能回 -2(MEOW_ERR_NOENT),amsd 判「拒绝」不杀(ACK 及时)或超时后盲发
|
||||||
|
* SIGTERM(ACK 迟到)——2026-11-04 真机 ACK 0xFFFFFFFE 即此,行为随主循环负载漂移。 */
|
||||||
|
if (meow_bg_terminate(k.app_id) == 0) {
|
||||||
|
a.code = MEOW_OK;
|
||||||
|
} else if (g_app_session_active) {
|
||||||
meow_ams_pending_kill_set(k.app_id);
|
meow_ams_pending_kill_set(k.app_id);
|
||||||
a.code = MEOW_OK;
|
a.code = MEOW_OK;
|
||||||
} else {
|
} else {
|
||||||
a.code = (uint32_t)MEOW_ERR_NOENT; /* 无前台 App 可终止 */
|
a.code = (uint32_t)MEOW_ERR_NOENT; /* 既非后台保活 App,也无前台 App 会话 */
|
||||||
}
|
}
|
||||||
a.status = 0;
|
|
||||||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||||||
logmsg("ipc: AMS_KILL app=%s -> ACK code=%u", k.app_id, a.code);
|
logmsg("ipc: AMS_KILL app=%s -> ACK code=%u(%d)", k.app_id, a.code, (int)a.code);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
/* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。
|
/* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。
|
||||||
|
|||||||
@@ -11,6 +11,11 @@
|
|||||||
* 【M6.0 迁移】全部查询/操作走 netmgr IPC(netmgr_ipc.c 的 nm_* 封装),netmgrd 是 WiFi 权威;
|
* 【M6.0 迁移】全部查询/操作走 netmgr IPC(netmgr_ipc.c 的 nm_* 封装),netmgrd 是 WiFi 权威;
|
||||||
* netmgrd 不可达(NETMGR_UNREACH)→ 自动回退 net.c 直连 wpa_cli(双轨保功能),页面显示降级占位
|
* netmgrd 不可达(NETMGR_UNREACH)→ 自动回退 net.c 直连 wpa_cli(双轨保功能),页面显示降级占位
|
||||||
* 「网络管理服务未运行(直连模式)」。
|
* 「网络管理服务未运行(直连模式)」。
|
||||||
|
*
|
||||||
|
* 【M47 开关可信化】顶栏开关显示态只来自真实态三态查询 nm_wifi_power_state()(rfkill wlan
|
||||||
|
* soft/hard,回退 wlan0 IFF_UP;探测失败=未知),**读不到不再默认「已开」**;settings 的
|
||||||
|
* network.wifi_enabled 只承载用户意图。点击先出示「正在开启/关闭…(等待确认)」,下发 → 有界
|
||||||
|
* 轮询真实态 → 真实态定稿;失败/超时回滚并弹窗(不许乐观显示成功)。
|
||||||
*/
|
*/
|
||||||
#include "meow_launcher_internal.h"
|
#include "meow_launcher_internal.h"
|
||||||
|
|
||||||
@@ -19,6 +24,7 @@
|
|||||||
#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */
|
#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */
|
||||||
#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */
|
#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */
|
||||||
#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */
|
#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */
|
||||||
|
#define WF_ACT_MANUAL 2000 /* 「手动添加隐藏网络」动作行(SUB_ACT,connecth) */
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
meow_net_t saved[S_NET_MAX];
|
meow_net_t saved[S_NET_MAX];
|
||||||
@@ -26,32 +32,96 @@ typedef struct {
|
|||||||
int saved_n, scan_n;
|
int saved_n, scan_n;
|
||||||
char ssid[64];
|
char ssid[64];
|
||||||
int connected; /* nm_wifi_connected()(IPC>直连回退 wlan_connected) */
|
int connected; /* nm_wifi_connected()(IPC>直连回退 wlan_connected) */
|
||||||
int wifi_on; /* network.wifi_enabled */
|
int sw_state; /* 真实射频态三态:-1 未知 / 0 关 / 1 开(M47) */
|
||||||
|
unsigned long long sw_ts; /* 上面这次查询的时刻(缓存去重) */
|
||||||
long last_scan_ms; /* 扫描触发节流 */
|
long last_scan_ms; /* 扫描触发节流 */
|
||||||
} wifi_ctx_t;
|
} wifi_ctx_t;
|
||||||
|
|
||||||
|
/* M47:开关收敛参数(宿主测试可预先定义改小)。WiFi 走 wifi_up.sh(起 wpa_supplicant +
|
||||||
|
* 等关联/DHCP),比 BT 慢,所以上限 5s——且从**点击时刻**起算(含下发耗时,评审 r1 P2-3)。
|
||||||
|
* 快路径(rfkill 写入在 netmgrd 内是毫秒级)5s 内约 20 次探测;慢路径(netmgrd 卡住 →
|
||||||
|
* on/off 读超时 1.5s → 直连 wifi_up.sh 自身后台化)下探测只剩 1~2 次,此时只能确认
|
||||||
|
* 「探测超时」,不能宣称「射频无响应」——弹窗文案按此区分。
|
||||||
|
* 250ms 只是快路径的轮询间隔,不 20ms 级忙轮询。 */
|
||||||
|
#ifndef WF_TOGGLE_SETTLE_MS
|
||||||
|
#define WF_TOGGLE_SETTLE_MS 5000
|
||||||
|
#endif
|
||||||
|
#ifndef WF_TOGGLE_POLL_MS
|
||||||
|
#define WF_TOGGLE_POLL_MS 250
|
||||||
|
#endif
|
||||||
|
#define WF_STATE_TTL_MS 800
|
||||||
|
|
||||||
/* 顶栏开关当前值 */
|
/* 真实态查询(带缓存)。定义见 netmgr_ipc.c:netmgrd 不报射频态,故取 rfkill/wlan0。 */
|
||||||
|
extern int nm_wifi_power_state(void);
|
||||||
|
/* 收敛判定纯函数,实现见 bt_page.c(两个页面共用一套;矩阵测试在 test_bt_page.c) */
|
||||||
|
extern int meow_toggle_display(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
|
||||||
|
extern int meow_toggle_pending(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
|
||||||
|
extern int meow_toggle_rollback(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
|
||||||
|
extern int meow_toggle_error(int intent, int last_real, int dispatch_ok, int probe, int timed_out);
|
||||||
|
|
||||||
|
static int wifi_state_now(wifi_ctx_t *c, int force)
|
||||||
|
{
|
||||||
|
unsigned long long t = now_ms();
|
||||||
|
if (!force && c->sw_ts && t - c->sw_ts < WF_STATE_TTL_MS) return c->sw_state;
|
||||||
|
c->sw_state = nm_wifi_power_state();
|
||||||
|
c->sw_ts = t;
|
||||||
|
return c->sw_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 顶栏开关当前值:只报真实态(-1=未知 → subpage 画「?」)。绝不再「读不到就默认 1」。 */
|
||||||
static int wifi_top_sw(void *ctx, int *on)
|
static int wifi_top_sw(void *ctx, int *on)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
wifi_ctx_t *c = ctx;
|
||||||
int v = 1;
|
*on = wifi_state_now(c, 0);
|
||||||
if (meow_settings_get_bool("network.wifi_enabled", &v) != 0) v = 1;
|
|
||||||
*on = v;
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 顶栏开关切换:写设置 + 开/关网卡(netmgr on/off,回退直连)+ 触发重扫 + 同步状态栏 */
|
/* 顶栏开关切换:落盘意图 → 开/关网卡(netmgr on/off,回退直连)→ 有界等待真实态收敛 →
|
||||||
|
* 真实态定稿/回滚 + 可见错误提示。 */
|
||||||
static void wifi_top_sw_set(void *ctx, int on)
|
static void wifi_top_sw_set(void *ctx, int on)
|
||||||
{
|
{
|
||||||
wifi_ctx_t *c = ctx;
|
wifi_ctx_t *c = ctx;
|
||||||
meow_settings_set_bool("network.wifi_enabled", on);
|
unsigned long long t_click = now_ms(); /* 上限从点击起算(含下发耗时,见参数注释) */
|
||||||
nm_wifi_toggle(on);
|
int want = on ? 1 : 0;
|
||||||
g_sys.wifi_on = on;
|
int last_real = wifi_state_now(c, 0);
|
||||||
paint_sb_right(); /* 状态栏 WiFi 图标随开关即时刷新 */
|
meow_settings_set_bool("network.wifi_enabled", want); /* 意图持久化 */
|
||||||
meow_settings_save();
|
meow_settings_save();
|
||||||
if (on) { nm_wifi_scan_trigger(); c->last_scan_ms = now_ms(); }
|
int dispatch_ok = (nm_wifi_toggle(want) == MEOW_OK);
|
||||||
|
unsigned long long deadline = t_click + WF_TOGGLE_SETTLE_MS;
|
||||||
|
int probe = wifi_state_now(c, 1), timed_out = 0;
|
||||||
|
while (meow_toggle_pending(want, last_real, dispatch_ok, probe, timed_out)) {
|
||||||
|
if (now_ms() >= deadline) { timed_out = 1; break; }
|
||||||
|
usleep(WF_TOGGLE_POLL_MS * 1000);
|
||||||
|
probe = wifi_state_now(c, 1);
|
||||||
|
}
|
||||||
|
int disp = meow_toggle_display(want, last_real, dispatch_ok, probe, timed_out);
|
||||||
|
int rb = meow_toggle_rollback(want, last_real, dispatch_ok, probe, timed_out);
|
||||||
|
int err = meow_toggle_error(want, last_real, dispatch_ok, probe, timed_out);
|
||||||
|
unsigned long long elapsed = now_ms() - t_click;
|
||||||
|
c->sw_state = disp; c->sw_ts = now_ms();
|
||||||
|
/* 只在拿到真实态时改写 settings;disp == -1(探测未知)保留原意图不动——该键被
|
||||||
|
* notif_center/settings_page 当显示态读,写 0 会把「不知道」污染成「确定已关」(P2-1)。 */
|
||||||
|
if (disp >= 0) {
|
||||||
|
meow_settings_set_bool("network.wifi_enabled", disp == 1);
|
||||||
|
meow_settings_save();
|
||||||
|
}
|
||||||
|
if (disp >= 0) g_sys.wifi_on = disp;
|
||||||
|
paint_sb_right(); /* 状态栏 WiFi 图标随「已确认」状态刷新 */
|
||||||
|
if (disp == 1) { nm_wifi_scan_trigger(); c->last_scan_ms = now_ms(); }
|
||||||
|
logmsg("wifi_page: toggle want=%d ok=%d probe=%d to=%d disp=%d rollback=%d err=%d elapsed=%llums",
|
||||||
|
want, dispatch_ok, probe, timed_out, disp, rb, err, elapsed);
|
||||||
|
if (err) {
|
||||||
|
char line[128];
|
||||||
|
const char *txt[1]; txt[0] = line;
|
||||||
|
if (disp < 0)
|
||||||
|
snprintf(line, sizeof line, "无法确认当前状态(上次已知:%s)",
|
||||||
|
last_real < 0 ? "未知" : (last_real ? "开启" : "关闭"));
|
||||||
|
else
|
||||||
|
snprintf(line, sizeof line, "切换未生效:当前仍为%s", disp ? "开启" : "关闭");
|
||||||
|
s_dd_modal("WiFi", txt, 1, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -71,6 +141,7 @@ static void wifi_scan(void *ctx)
|
|||||||
static int wifi_rows(void *ctx, sub_row_t *rbuf, int max)
|
static int wifi_rows(void *ctx, sub_row_t *rbuf, int max)
|
||||||
{
|
{
|
||||||
wifi_ctx_t *c = ctx;
|
wifi_ctx_t *c = ctx;
|
||||||
|
c->sw_state = wifi_state_now(c, 0); /* 真实射频态(三态);未知时下面显式说明 */
|
||||||
c->connected = nm_wifi_connected();
|
c->connected = nm_wifi_connected();
|
||||||
if (c->connected) {
|
if (c->connected) {
|
||||||
nm_wifi_ssid(c->ssid, sizeof c->ssid);
|
nm_wifi_ssid(c->ssid, sizeof c->ssid);
|
||||||
@@ -89,6 +160,12 @@ static int wifi_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
|
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
|
||||||
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
|
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
|
||||||
}
|
}
|
||||||
|
/* 第三态显式呈现(M47):射频态未知 ≠ 已关;顶栏开关同时画「?」,这里补文字说明。 */
|
||||||
|
if (c->sw_state < 0 && n < max) {
|
||||||
|
rbuf[n].kind = SUB_ROW; rbuf[n].label = "WiFi 开关状态未知";
|
||||||
|
rbuf[n].sub = "无法读取射频状态(rfkill/网卡无响应)";
|
||||||
|
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_HDR; rbuf[n].label = "当前连接"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||||
if (n < max) {
|
if (n < max) {
|
||||||
@@ -122,13 +199,22 @@ static int wifi_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i;
|
rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
|
/* bug2:附近网络末尾的「手动添加隐藏网络」入口(SUB_ACT → wifi_act)。 */
|
||||||
|
if (n < max) {
|
||||||
|
rbuf[n].kind = SUB_ACT;
|
||||||
|
rbuf[n].label = "手动添加隐藏网络";
|
||||||
|
rbuf[n].sub = "";
|
||||||
|
rbuf[n].on = 0;
|
||||||
|
rbuf[n].act = WF_ACT_MANUAL;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* SDK IME 输密码(PW 模式=ASCII 软键盘输入,现有输入法复用;回车提交/ESC或点背景取消)。
|
/* SDK IME 通用文本输入(MEOW_IME_ASCII:QWERTY,顶条「中」可切拼音输中文 SSID)。
|
||||||
* 返回 1=已输入;0=取消。 */
|
* 返回 1=已输入;0=取消。 */
|
||||||
static int wifi_ask_psk(char *psk, int maxlen)
|
static int wifi_ask_text(char *out, int maxlen)
|
||||||
{
|
{
|
||||||
meow_ime_ctx ic;
|
meow_ime_ctx ic;
|
||||||
memset(&ic, 0, sizeof ic);
|
memset(&ic, 0, sizeof ic);
|
||||||
@@ -136,36 +222,72 @@ static int wifi_ask_psk(char *psk, int maxlen)
|
|||||||
ic.maxlen = maxlen;
|
ic.maxlen = maxlen;
|
||||||
ic.buf[0] = 0;
|
ic.buf[0] = 0;
|
||||||
int res = meow_ui_text_input(&ic); /* 阻塞:提交后 ctx.buf 回填最终文本 */
|
int res = meow_ui_text_input(&ic); /* 阻塞:提交后 ctx.buf 回填最终文本 */
|
||||||
if (res && ic.buf[0]) { snprintf(psk, maxlen, "%s", ic.buf); return 1; }
|
if (res && ic.buf[0]) { snprintf(out, maxlen, "%s", ic.buf); return 1; }
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* bug1:connect 阻塞前画非模态「正在连接…」横幅。子页单线程,阻塞期间画面停在此横幅。 */
|
||||||
|
static void wifi_connecting_banner(const char *ssid)
|
||||||
|
{
|
||||||
|
const meow_ui_theme_t *th = s_ui_theme();
|
||||||
|
int W = meow_ui_width();
|
||||||
|
int y = 108, h = 44;
|
||||||
|
char line[160];
|
||||||
|
snprintf(line, sizeof line, "正在连接 %s ...", ssid && ssid[0] ? ssid : "网络");
|
||||||
|
meow_ui_fill_rect(0, y, W, h, th->surface_container);
|
||||||
|
meow_ui_text_center(W / 2, y + h / 2 - meow_ui_text_height(20) / 2, line, 20, th->on_surface);
|
||||||
|
meow_dirty_full();
|
||||||
|
meow_ui_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* bug2:手动添加隐藏网络(附近网络占位行点击 / 「手动添加隐藏网络」动作行共用)。 */
|
||||||
|
static void wifi_manual_add(wifi_ctx_t *c)
|
||||||
|
{
|
||||||
|
char ssid[96] = "";
|
||||||
|
if (!wifi_ask_text(ssid, sizeof ssid)) return;
|
||||||
|
const char *items[2];
|
||||||
|
items[0] = "加密网络(WPA/WPA2/WPA3)";
|
||||||
|
items[1] = "开放网络";
|
||||||
|
int pick = s_dd_modal("网络加密方式", items, 2, -1);
|
||||||
|
if (pick < 0) return;
|
||||||
|
int open = (pick == 1);
|
||||||
|
char psk[96] = "";
|
||||||
|
if (!open && !wifi_ask_text(psk, sizeof psk)) return;
|
||||||
|
char err[160]; err[0] = 0;
|
||||||
|
wifi_connecting_banner(ssid);
|
||||||
|
int rc = nm_wifi_connect_ex(ssid, open ? NULL : psk, 1, err, sizeof err);
|
||||||
|
char line[192]; const char *txt[1]; txt[0] = line;
|
||||||
|
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
|
||||||
|
logmsg("wifi_page: manual connect ssid=%s hidden=1 open=%d rc=%d err=%s", ssid, open, rc, err);
|
||||||
|
s_dd_modal(ssid, txt, 1, -1);
|
||||||
|
nm_wifi_scan_trigger();
|
||||||
|
c->last_scan_ms = now_ms();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 附近网络点选 →【WiFi 新网络录入】连接流程。返回 1=重绘。 */
|
/* 附近网络点选 →【WiFi 新网络录入】连接流程。返回 1=重绘。 */
|
||||||
static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx)
|
static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx)
|
||||||
{
|
{
|
||||||
if (scan_idx < 0 || scan_idx >= c->scan_n) return 0;
|
if (scan_idx < 0 || scan_idx >= c->scan_n) return 0;
|
||||||
meow_net_t *s = &c->scan[scan_idx];
|
meow_net_t *s = &c->scan[scan_idx];
|
||||||
if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) {
|
if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) {
|
||||||
const char *txt[1]; txt[0] = "隐藏网络无法直接连接";
|
wifi_manual_add(c); /* bug2:占位隐藏网络 → 手动录入(SSID 待输) */
|
||||||
s_dd_modal("附近网络", txt, 1, -1);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open"));
|
int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open"));
|
||||||
char psk[96] = "";
|
char psk[96] = "";
|
||||||
if (!open) {
|
if (!open) {
|
||||||
if (!wifi_ask_psk(psk, sizeof psk)) return 1; /* 用户取消输密码 */
|
if (!wifi_ask_text(psk, sizeof psk)) return 1; /* 用户取消输密码 */
|
||||||
}
|
}
|
||||||
char err[160]; err[0] = 0;
|
char err[160]; err[0] = 0;
|
||||||
|
wifi_connecting_banner(s->name); /* bug1:阻塞前横幅,避免「点完无反馈」 */
|
||||||
int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err);
|
int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err);
|
||||||
char line[192];
|
char line[192];
|
||||||
const char *txt[1]; txt[0] = line;
|
const char *txt[1]; txt[0] = line;
|
||||||
if (rc == MEOW_OK) {
|
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
|
||||||
snprintf(line, sizeof line, "连接成功:%s%s", s->name,
|
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
|
||||||
err[0] ? "" : "(已启用)");
|
|
||||||
} else {
|
|
||||||
snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误");
|
|
||||||
}
|
|
||||||
logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err);
|
logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err);
|
||||||
/* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */
|
/* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */
|
||||||
s_dd_modal(s->name, txt, 1, -1);
|
s_dd_modal(s->name, txt, 1, -1);
|
||||||
@@ -208,20 +330,23 @@ static int wifi_detail(void *ctx, int idx, int act)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 动作行(SUB_ACT)点击:已保存网络 → netmgr connect(ssid;复用既有网络) */
|
/* 动作行(SUB_ACT)点击:已保存网络 → netmgr connect(ssid;复用既有网络)。
|
||||||
|
* 另承接「手动添加隐藏网络」入口(bug2)。 */
|
||||||
static int wifi_act(void *ctx, int idx, int act)
|
static int wifi_act(void *ctx, int idx, int act)
|
||||||
{
|
{
|
||||||
(void)idx;
|
(void)idx;
|
||||||
wifi_ctx_t *c = ctx;
|
wifi_ctx_t *c = ctx;
|
||||||
|
if (act == WF_ACT_MANUAL) { wifi_manual_add(c); return 1; }
|
||||||
if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 SUB_ROW(detail),不走这里 */
|
if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 SUB_ROW(detail),不走这里 */
|
||||||
int sidx = act - WF_ACT_SAVED;
|
int sidx = act - WF_ACT_SAVED;
|
||||||
if (sidx < 0 || sidx >= c->saved_n) return 0;
|
if (sidx < 0 || sidx >= c->saved_n) return 0;
|
||||||
if (c->saved[sidx].id < 0) return 0;
|
if (c->saved[sidx].id < 0) return 0;
|
||||||
char err[160]; err[0] = 0;
|
char err[160]; err[0] = 0;
|
||||||
|
wifi_connecting_banner(c->saved[sidx].name); /* bug1:阻塞前横幅 */
|
||||||
int rc = nm_wifi_connect(c->saved[sidx].name, NULL, err, sizeof err);
|
int rc = nm_wifi_connect(c->saved[sidx].name, NULL, err, sizeof err);
|
||||||
char line[192]; const char *txt[1]; txt[0] = line;
|
char line[192]; const char *txt[1]; txt[0] = line;
|
||||||
if (rc == MEOW_OK) snprintf(line, sizeof line, "已连接:%s", c->saved[sidx].name);
|
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
|
||||||
else snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误");
|
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
|
||||||
logmsg("wifi_page: saved connect ssid=%s rc=%d err=%s", c->saved[sidx].name, rc, err);
|
logmsg("wifi_page: saved connect ssid=%s rc=%d err=%s", c->saved[sidx].name, rc, err);
|
||||||
s_dd_modal(c->saved[sidx].name, txt, 1, -1);
|
s_dd_modal(c->saved[sidx].name, txt, 1, -1);
|
||||||
nm_wifi_scan_trigger();
|
nm_wifi_scan_trigger();
|
||||||
|
|||||||
@@ -9,25 +9,44 @@
|
|||||||
# 用法:
|
# 用法:
|
||||||
# ./build_netmgrd.sh # 交叉编译 → patch_v2/meow_netmgrd/meow_netmgrd(mipsel 静态)
|
# ./build_netmgrd.sh # 交叉编译 → patch_v2/meow_netmgrd/meow_netmgrd(mipsel 静态)
|
||||||
# ./build_netmgrd.sh host # 宿主编译 → patch_v2/meow_netmgrd/meow_netmgrd_host
|
# ./build_netmgrd.sh host # 宿主编译 → patch_v2/meow_netmgrd/meow_netmgrd_host
|
||||||
|
# OUT=/path/to/out ./build_netmgrd.sh # 产物另存(M49 起:仓库里不提交二进制,验证产物放仓库外)
|
||||||
set -e
|
set -e
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
ROOT="$HOME/Project/R3ProII"
|
# 仓库根按本脚本位置推导(<root>/patch_v2/meow_netmgrd/),MEOW_ROOT 可覆盖。
|
||||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
# 旧版硬编码 $HOME/Project/R3ProII —— 换 checkout 路径/用 worktree 时直接编不动。
|
||||||
|
ROOT="${MEOW_ROOT:-$(cd ../.. && pwd)}"
|
||||||
|
# 交叉编译器:ZIGCC env 优先,其次 <root>/.tools/,最后 $HOME/.tools/(.tools 常只在主 checkout 里)
|
||||||
|
CC="${ZIGCC:-$ROOT/.tools/zig-mipsel-cc}"
|
||||||
|
[ -x "$CC" ] || CC="$HOME/.tools/zig-mipsel-cc"
|
||||||
I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/MSG_NET_*/m_net_*)
|
I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/MSG_NET_*/m_net_*)
|
||||||
|
OUT="${OUT:-.}" # 产物目录:默认脚本目录;设了则在交叉编译后把产物移过去
|
||||||
|
[ -d "$OUT" ] || mkdir -p "$OUT"
|
||||||
|
|
||||||
if [ "$1" = "host" ]; then
|
if [ "$1" = "host" ]; then
|
||||||
HOST_CC="${CC_HOST:-cc}"
|
HOST_CC="${CC_HOST:-cc}"
|
||||||
# -Wno-format-truncation:宿主 glibc 对固定缓冲 snprintf 的「安全截断」误报(musl 交叉不报)。
|
# -Wno-format-truncation:宿主 glibc 对固定缓冲 snprintf 的「安全截断」误报(musl 交叉不报)。
|
||||||
"$HOST_CC" -Os -Wall -Wextra -U_FORTIFY_SOURCE -Wno-format-truncation -I"$I_HDR" \
|
"$HOST_CC" -Os -Wall -Wextra -U_FORTIFY_SOURCE -Wno-format-truncation -I"$I_HDR" \
|
||||||
meow_netmgrd.c -o meow_netmgrd_host
|
meow_netmgrd.c -o "$OUT/meow_netmgrd_host"
|
||||||
echo "== host build =="
|
echo "== host build =="
|
||||||
ls -la meow_netmgrd_host
|
ls -la "$OUT/meow_netmgrd_host"
|
||||||
file meow_netmgrd_host
|
file "$OUT/meow_netmgrd_host"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meow_netmgrd.c -o meow_netmgrd
|
# meow_time32.c:独立 o32 time32 取时/睡眠(netmgrd 不链 meow_ui.a);修 DNS TTL 基准
|
||||||
|
# 与 nanosleep 退避。守护/CLI 同一二进制,一次构建两处安装。(M43 的 time32 接入,必须保留:
|
||||||
|
# 少了它 mono_now()/轮询退避会踩 mipsel o32 的 time64/time32 结构错位。)
|
||||||
|
# 注意:`-o meow_netmgrd` 保持字面量(不带目录前缀)——sdk/test_time32_abi.sh 的构建门禁正是
|
||||||
|
# 按 `-o <产物名>` 定位编译行来断言 time32 接入;写成 `-o "$OUT/..."` 会让门禁报「未找到」。
|
||||||
|
# 编译在临时目录里做(产物名仍是字面量),编完再落到 OUT:这样设了 OUT 也**不会**覆盖/删除
|
||||||
|
# 仓库里那份被跟踪的 patch_v2/meow_netmgrd/meow_netmgrd(M49 曾因 mv 把它删掉过一次)。
|
||||||
|
HERE="$(pwd)"
|
||||||
|
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/netmgrd-build.XXXXXX")"
|
||||||
|
( cd "$BUILD_DIR" && "$CC" -Os -Wall -Wextra -static -I"$I_HDR" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
"$HERE/meow_netmgrd.c" "$HERE/../sdk/meow_time32.c" -o meow_netmgrd )
|
||||||
|
mv -f "$BUILD_DIR/meow_netmgrd" "$OUT/meow_netmgrd"
|
||||||
|
rmdir "$BUILD_DIR" 2>/dev/null || true
|
||||||
echo "== meow_netmgrd (mipsel static) =="
|
echo "== meow_netmgrd (mipsel static) =="
|
||||||
ls -la meow_netmgrd
|
ls -la "$OUT/meow_netmgrd"
|
||||||
md5sum meow_netmgrd
|
md5sum "$OUT/meow_netmgrd"
|
||||||
file meow_netmgrd
|
file "$OUT/meow_netmgrd"
|
||||||
Binary file not shown.
+1055
-280
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,13 @@
|
|||||||
/* test_bt_ctl.c — meow_netmgrd 蓝牙 P1 权威收口 纯逻辑宿主自测(本程序非生产代码)。
|
/* test_bt_ctl.c — meow_netmgrd 蓝牙控制面 纯逻辑宿主自测(本程序非生产代码)。
|
||||||
*
|
*
|
||||||
* 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问
|
* 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问
|
||||||
* FILE-STATIC 函数/全局:bt_cmd_is_write / bt_write_authorize / bt_ctl_execute(connect/disconnect/
|
* FILE-STATIC 函数/全局:三个纯解析器(bt_scan_line_parse / bt_pcm_paths_parse+bt_pcm_info_parse /
|
||||||
* pair/unpair/connected/a2dp/devices/status)。工具经桩脚本(g_bluetoothctl/g_hcitool/g_bluealsa_cli/
|
* bt_acl_parse)、run_cmd_ex 四态、bt_cmd_is_write / bt_write_authorize、bt_ctl_execute
|
||||||
* g_bt_scan_cache 指向 $T/桩)覆盖,不依赖真实蓝牙栈。
|
* (status/paired/connected/a2dp/devices/on/off/connect/disconnect/pair/unpair/scan)。
|
||||||
|
* 后端工具经有状态桩脚本(g_bluetoothctl/g_hcitool/g_bluealsa_cli/g_hciconfig/g_bt_scan_cache)
|
||||||
|
* 覆盖,不依赖真实蓝牙栈。
|
||||||
|
*
|
||||||
|
* 字节样本来源:2026-11-11 真机 `od -c /tmp/meow_bt_scan.txt`(0.8.49,BluetoothCtl 5.54 交互输出)。
|
||||||
*
|
*
|
||||||
* 用法:cc -Wall -Wextra -I../meow_launcherd test_bt_ctl.c -o test_bt_ctl && ./test_bt_ctl
|
* 用法:cc -Wall -Wextra -I../meow_launcherd test_bt_ctl.c -o test_bt_ctl && ./test_bt_ctl
|
||||||
*/
|
*/
|
||||||
@@ -27,46 +31,396 @@ static void wf(const char *p, const char *s)
|
|||||||
if (f) { fputs(s, f); fclose(f); }
|
if (f) { fputs(s, f); fclose(f); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 真机原始字节样本(含 ANSI ESC / \001\002 标记 / \r\033[K / 提示符) ---- */
|
||||||
|
static const char *S_NEW_PUDDING =
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# \r\033[K"
|
||||||
|
"[\001\033[0;92m\002NEW\001\033[0m\002] Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n";
|
||||||
|
static const char *S_CHG_POWERED =
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# \r\033[K"
|
||||||
|
"[\001\033[0;93m\002CHG\001\033[0m\002] Controller 4C:BC:98:B0:98:DB Powered: no\n";
|
||||||
|
static const char *S_DEL_LINE = "[DEL] Device 00:11:22:33:44:55\n";
|
||||||
|
static const char *S_PROMPT_DEVICES = "\033[0;94m[bluetooth]\033[0m# devices\n";
|
||||||
|
static const char *S_PLAIN_BLOCK = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n";
|
||||||
|
static const char *S_TRUNC_MAC =
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# \r\033[K[NEW] Device 33:DE:2E:21:A";
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
static const char *T = "/tmp/nmg_bt";
|
/* 宿主临时目录:Termux/Android 上 /tmp 不可写,优先 $TMPDIR(mktemp 同规则) */
|
||||||
char p[192];
|
const char *base = getenv("TMPDIR");
|
||||||
mkdir(T, 0755);
|
if (!base || !base[0]) base = "/tmp";
|
||||||
|
static char Tbuf[256];
|
||||||
|
snprintf(Tbuf, sizeof Tbuf, "%s/nmg_bt", base);
|
||||||
|
const char *T = Tbuf;
|
||||||
|
char p[256];
|
||||||
|
char cmd[512];
|
||||||
|
/* 可重复运行(r1 评审 P2):上一轮遗留的 conn_ / paired_ 状态文件会把「无连接」断言推翻。
|
||||||
|
* 每次开跑先整棵删掉重来,退出时也清(见文件末尾)。 */
|
||||||
|
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", Tbuf); (void)!system(rm); }
|
||||||
|
mkdir(Tbuf, 0755);
|
||||||
/* 假 /sys/class/bluetooth/hci0(available/powered/address) */
|
/* 假 /sys/class/bluetooth/hci0(available/powered/address) */
|
||||||
{ snprintf(p, sizeof p, "%s/bt", T); mkdir(p, 0755);
|
{ snprintf(p, sizeof p, "%s/bt", T); mkdir(p, 0755);
|
||||||
snprintf(p, sizeof p, "%s/bt/hci0", T); mkdir(p, 0755);
|
snprintf(p, sizeof p, "%s/bt/hci0", T); mkdir(p, 0755);
|
||||||
snprintf(p, sizeof p, "%s/bt/hci0/address", T); wf(p, "00:1A:7D:DA:71:13\n");
|
snprintf(p, sizeof p, "%s/bt/hci0/address", T); wf(p, "4c:bc:98:b0:98:db\n");
|
||||||
snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "on\n"); }
|
snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "off\n"); }
|
||||||
|
/* 假 rfkill:真机有两个 bluetooth 节点(rfkill0/rfkill3),任一仍 block 就等于没开 */
|
||||||
|
{ snprintf(p, sizeof p, "%s/rfkill", T); mkdir(p, 0755);
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
char d[300];
|
||||||
|
snprintf(d, sizeof d, "%s/rfkill/rfkill%d", T, i ? 3 : 0); mkdir(d, 0755);
|
||||||
|
snprintf(p, sizeof p, "%s/type", d); wf(p, "bluetooth\n");
|
||||||
|
snprintf(p, sizeof p, "%s/soft", d); wf(p, "1\n");
|
||||||
|
} }
|
||||||
|
/* 有状态桩的状态目录 */
|
||||||
|
snprintf(p, sizeof p, "%s/state", T); mkdir(p, 0755);
|
||||||
|
|
||||||
/* 桩:bluetoothctl(paired/devices/connect/remove)、hcitool con、bluealsa-cli a2dp、扫描缓存 */
|
/* 桩:bluetoothctl(paired-devices/devices/info/connect/disconnect/pair/remove,靠状态文件变迁)
|
||||||
snprintf(p, sizeof p, "%s/bluetoothctl", T); wf(p,
|
* 注意:生产代码给后端命令前置 PATH=/bin:/sbin:/usr/bin:/usr/sbin,Termux 上 cat/basename/grep
|
||||||
"#!/bin/sh\ncase \"$1\" in\n"
|
* 都不在这几个目录里 —— 桩脚本只能用 shell 内建(read/echo/[ /参数展开),否则测试会假失败。 */
|
||||||
" paired|paired-devices|devices) echo 'Device 33:DE:2E:21:AD:30 Pudding'; echo 'Device 11:22:33:44:55:66 Speaker';;\n"
|
snprintf(p, sizeof p, "%s/bluetoothctl", T);
|
||||||
" connect|disconnect|pair|remove) echo 'Controller 00:1A:7D:DA:71:13'; echo 'Device 33:DE:2E:21:AD:30 Connected: yes';;\n"
|
{
|
||||||
"esac\nexit 0\n"); chmod(p, 0755);
|
char sh[3200];
|
||||||
snprintf(p, sizeof p, "%s/hcitool", T); wf(p,
|
snprintf(sh, sizeof sh,
|
||||||
"#!/bin/sh\necho '\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER'\n"
|
"#!/bin/sh\n"
|
||||||
"echo '\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE'\nexit 0\n"); chmod(p, 0755);
|
"S='%s/state'\n"
|
||||||
snprintf(p, sizeof p, "%s/bluealsa-cli", T); wf(p,
|
"catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\n"
|
||||||
"#!/bin/sh\nif [ \"${1:-}\" = \"a2dp\" ]; then\n"
|
"case \"$1\" in\n"
|
||||||
"echo '/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:'\n"
|
" paired-devices) catf \"$S/paired_list.txt\"; exit 0;;\n"
|
||||||
"echo '\tCodec: SBC'\necho '\tSampling: 44100 Hz'\necho '\tChannels: 2'\n"
|
" devices) catf \"$S/dev_list.txt\"; exit 0;;\n"
|
||||||
"echo '\tBits per Sample: 16'\necho '\tState: RUNNING'\necho '\tAcquired: yes'\nfi\nexit 0\n"); chmod(p, 0755);
|
" info)\n"
|
||||||
snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p,
|
" mac=\"$2\"\n"
|
||||||
"[NEW] Device 33:DE:2E:21:AD:30 Pudding\n[NEW] Device AA:BB:CC:DD:EE:FF Earbuds\n");
|
" if [ ! -f \"$S/paired_$mac\" ]; then echo \"Device $mac not available\"; exit 1; fi\n"
|
||||||
|
" pv=no; IFS= read -r pv < \"$S/paired_$mac\"\n"
|
||||||
|
" if [ \"$pv\" = gone ]; then echo \"Device $mac not available\"; exit 1; fi\n"
|
||||||
|
" cv=no\n"
|
||||||
|
" if [ -f \"$S/conn_$mac\" ]; then IFS= read -r cv < \"$S/conn_$mac\"; fi\n"
|
||||||
|
" echo \"Device $mac\"\n"
|
||||||
|
" echo \" Name: Pudding\"\n"
|
||||||
|
" echo \" Paired: $pv\"\n"
|
||||||
|
" echo \" Connected: $cv\"\n"
|
||||||
|
" exit 0;;\n"
|
||||||
|
" connect)\n"
|
||||||
|
" case \"$2\" in CC:*) sleep 5;; esac\n"
|
||||||
|
" case \"$2\" in AA:*) echo 'Failed to connect: org.bluez.Error.Failed br-connection-refused'; exit 1;; esac\n"
|
||||||
|
" echo yes > \"$S/conn_$2\"; echo yes > \"$S/paired_$2\"\n"
|
||||||
|
" echo 'Attempting to connect'; echo 'Connection successful'; exit 0;;\n"
|
||||||
|
" disconnect)\n"
|
||||||
|
" case \"$2\" in AA:*) echo 'Failed to disconnect: not connected'; exit 1;; esac\n"
|
||||||
|
" case \"$2\" in BB:*) echo 'Successful disconnected'; exit 0;; esac\n"
|
||||||
|
" echo no > \"$S/conn_$2\"; echo 'Successful disconnected'; exit 0;;\n"
|
||||||
|
" pair)\n"
|
||||||
|
" case \"$2\" in CC:*) sleep 5;; esac\n"
|
||||||
|
" case \"$2\" in AA:*) echo 'Failed to pair: org.bluez.Error.AuthenticationFailed'; exit 1;; esac\n"
|
||||||
|
" echo yes > \"$S/paired_$2\"; echo 'Pairing successful'; exit 0;;\n"
|
||||||
|
" remove)\n"
|
||||||
|
" echo gone > \"$S/paired_$2\"; echo no > \"$S/conn_$2\"; echo 'Device has been removed'; exit 0;;\n"
|
||||||
|
"esac\n"
|
||||||
|
"exit 0\n", T);
|
||||||
|
wf(p, sh); chmod(p, 0755);
|
||||||
|
}
|
||||||
|
/* 桩:hcitool con —— 按 conn_<mac>=yes 标记生成 ACL 行 */
|
||||||
|
snprintf(p, sizeof p, "%s/hcitool", T);
|
||||||
|
{
|
||||||
|
char sh[900];
|
||||||
|
snprintf(sh, sizeof sh,
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"S='%s/state'\n"
|
||||||
|
"echo 'Connections:'\n"
|
||||||
|
"for f in \"$S\"/conn_*; do\n"
|
||||||
|
" [ -f \"$f\" ] || continue\n"
|
||||||
|
" v=no; IFS= read -r v < \"$f\"\n"
|
||||||
|
" [ \"$v\" = yes ] || continue\n"
|
||||||
|
" m=\"${f##*/}\"; m=\"${m#conn_}\"\n"
|
||||||
|
" echo \"\t< ACL $m handle 12 state 1 lm MASTER\"\n"
|
||||||
|
"done\n"
|
||||||
|
"exit 0\n", T);
|
||||||
|
wf(p, sh); chmod(p, 0755);
|
||||||
|
}
|
||||||
|
/* 桩:hciconfig —— `hci0 up/down` 改 sysfs power,`hci0` 打印状态 */
|
||||||
|
snprintf(p, sizeof p, "%s/hciconfig", T);
|
||||||
|
{
|
||||||
|
char sh[900];
|
||||||
|
snprintf(sh, sizeof sh,
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"S='%s'\n"
|
||||||
|
"if [ -f \"$S/state/hang_hciconfig\" ]; then sleep 5; fi\n"
|
||||||
|
"blocked() { for t in \"$S\"/rfkill/*; do\n"
|
||||||
|
" [ -f \"$t/type\" ] || continue\n"
|
||||||
|
" v=\"\"; IFS= read -r v < \"$t/type\"; [ \"$v\" = bluetooth ] || continue\n"
|
||||||
|
" b=0; IFS= read -r b < \"$t/soft\"; [ \"$b\" = 1 ] && return 0\n"
|
||||||
|
" done; return 1; }\n"
|
||||||
|
"case \"$2\" in\n"
|
||||||
|
" up) if blocked; then echo 'hci0: Operation not possible due to RF-kill' >&2; exit 1; fi\n"
|
||||||
|
" echo on > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' UP RUNNING';;\n"
|
||||||
|
" down) echo off > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' DOWN';;\n"
|
||||||
|
" *) echo 'hci0: Type: Primary Bus: UART'\n"
|
||||||
|
" v=off; if [ -f \"$S/bt/hci0/power\" ]; then IFS= read -r v < \"$S/bt/hci0/power\"; fi\n"
|
||||||
|
" if [ \"$v\" = on ]; then echo ' UP RUNNING'; else echo ' DOWN'; fi;;\n"
|
||||||
|
"esac\n"
|
||||||
|
"exit 0\n", T);
|
||||||
|
wf(p, sh); chmod(p, 0755);
|
||||||
|
}
|
||||||
|
/* 桩:bluealsa-cli —— list-pcms 端点由 $S/pcms.txt 控制,info 按路径返回属性 */
|
||||||
|
snprintf(p, sizeof p, "%s/bluealsa-cli", T);
|
||||||
|
{
|
||||||
|
char sh[3200];
|
||||||
|
snprintf(sh, sizeof sh,
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"S='%s/state'\n"
|
||||||
|
"catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\n"
|
||||||
|
"case \"$1\" in\n"
|
||||||
|
" list-pcms) catf \"$S/pcms.txt\"; exit 0;;\n"
|
||||||
|
" info)\n"
|
||||||
|
" if [ -f \"$S/hang_info\" ]; then sleep 5; fi\n"
|
||||||
|
" path=\"$2\"; found=0\n"
|
||||||
|
" if [ -f \"$S/pcms.txt\" ]; then\n"
|
||||||
|
" while IFS= read -r l || [ -n \"$l\" ]; do\n"
|
||||||
|
" if [ \"$l\" = \"$path\" ]; then found=1; fi\n"
|
||||||
|
" done < \"$S/pcms.txt\"\n"
|
||||||
|
" fi\n"
|
||||||
|
" case \"$path\" in *dev_33_DE_2E_21_AD_30*) ;; *) found=0;; esac\n"
|
||||||
|
" if [ \"$found\" != 1 ]; then\n"
|
||||||
|
" echo \"bluealsa-cli: E: CMD \\\"info\\\": Couldn't get BlueALSA PCM: Object path not found: '$path'\" >&2\n"
|
||||||
|
" exit 1\n"
|
||||||
|
" fi\n"
|
||||||
|
" case \"$path\" in *dev_33_DE_2E_21_AD_30*) ;; *) exit 1;; esac\n"
|
||||||
|
" if [ -f \"$S/running\" ]; then run=true; else run=false; fi\n"
|
||||||
|
" echo \"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\"\n" /* ← 真机原文(M50 2026-11-11 18:26) */
|
||||||
|
" echo 'Sequence: 0'\n"
|
||||||
|
" echo 'Transport: A2DP-source'\n"
|
||||||
|
" echo 'Mode: sink'\n"
|
||||||
|
" echo \"Running: $run\"\n"
|
||||||
|
" echo 'Format: S16_LE'\n"
|
||||||
|
" echo 'Channels: 2'\n"
|
||||||
|
" echo 'Sampling: 48000 Hz'\n"
|
||||||
|
" echo 'Available codecs: SBC AAC'\n"
|
||||||
|
" echo 'Selected codec: AAC'\n"
|
||||||
|
" echo 'Delay: 15.0 ms'\n"
|
||||||
|
" echo 'SoftVolume: false'\n"
|
||||||
|
" echo 'Volume: L: 127 R: 127'\n"
|
||||||
|
" echo 'Muted: L: false R: false'\n"
|
||||||
|
" exit 0;;\n"
|
||||||
|
"esac\n"
|
||||||
|
"exit 0\n", T);
|
||||||
|
wf(p, sh); chmod(p, 0755);
|
||||||
|
}
|
||||||
|
/* 初始状态:两台已配对(真机 MOONDROP / 17OP15),无 ACL */
|
||||||
|
snprintf(p, sizeof p, "%s/state/paired_list.txt", T);
|
||||||
|
wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n");
|
||||||
|
snprintf(p, sizeof p, "%s/state/dev_list.txt", T);
|
||||||
|
wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n"
|
||||||
|
"Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n");
|
||||||
|
snprintf(p, sizeof p, "%s/state/paired_33:DE:2E:21:AD:30", T); wf(p, "yes\n");
|
||||||
|
snprintf(p, sizeof p, "%s/state/paired_E0:42:6D:79:C5:98", T); wf(p, "yes\n");
|
||||||
|
|
||||||
g_bluetoothctl = ""; { char x[160]; snprintf(x, sizeof x, "%s/bluetoothctl", T); static char s[160];
|
/* 指向桩(g_* 是 const char*,用静态缓冲承载) */
|
||||||
snprintf(s, sizeof s, "%s", x); g_bluetoothctl = s; }
|
static char sb_bt[256], sb_hci[256], sb_ba[256], sb_hc[256], sb_cache[256];
|
||||||
g_hcitool = ""; { char x[160]; snprintf(x, sizeof x, "%s/hcitool", T); static char s[160];
|
snprintf(sb_bt, sizeof sb_bt, "%s/bluetoothctl", T);
|
||||||
snprintf(s, sizeof s, "%s", x); g_hcitool = s; }
|
snprintf(sb_hci, sizeof sb_hci, "%s/hcitool", T);
|
||||||
g_bluealsa_cli = ""; { char x[160]; snprintf(x, sizeof x, "%s/bluealsa-cli", T); static char s[160];
|
snprintf(sb_ba, sizeof sb_ba, "%s/bluealsa-cli", T);
|
||||||
snprintf(s, sizeof s, "%s", x); g_bluealsa_cli = s; }
|
snprintf(sb_hc, sizeof sb_hc, "%s/hciconfig", T);
|
||||||
g_bt_scan_cache = ""; { char x[160]; snprintf(x, sizeof x, "%s/bt_scan.txt", T); static char s[160];
|
snprintf(sb_cache, sizeof sb_cache, "%s/bt_scan.txt", T);
|
||||||
snprintf(s, sizeof s, "%s", x); g_bt_scan_cache = s; }
|
g_bluetoothctl = sb_bt;
|
||||||
|
g_hcitool = sb_hci;
|
||||||
|
g_bluealsa_cli = sb_ba;
|
||||||
|
g_hciconfig = sb_hc;
|
||||||
|
g_bt_scan_cache = sb_cache;
|
||||||
g_bt_dir[0] = 0; snprintf(g_bt_dir, sizeof g_bt_dir, "%s/bt", T);
|
g_bt_dir[0] = 0; snprintf(g_bt_dir, sizeof g_bt_dir, "%s/bt", T);
|
||||||
|
g_rfkill_dir[0] = 0; snprintf(g_rfkill_dir, sizeof g_rfkill_dir, "%s/rfkill", T);
|
||||||
|
|
||||||
ctl_result_t r;
|
ctl_result_t r;
|
||||||
|
|
||||||
|
/* ================= 1. run_cmd_ex 四态(M49 新增) ================= */
|
||||||
|
printf("== run_cmd_ex 四态:成功有输出/成功无输出/非 0 退出/超时/命令不存在/stderr 抓取 ==\n");
|
||||||
|
{
|
||||||
|
runcmd_res_t rr;
|
||||||
|
char b[128];
|
||||||
|
int n = run_cmd_ex(b, sizeof b, 1500, "echo hi", &rr);
|
||||||
|
CHECK(n == 3);
|
||||||
|
CHECK(rr.status == RUNCMD_OK);
|
||||||
|
CHECK(rr.exit_code == 0);
|
||||||
|
n = run_cmd_ex(b, sizeof b, 1500, "true", &rr);
|
||||||
|
CHECK(n == 0);
|
||||||
|
CHECK(rr.status == RUNCMD_EMPTY); /* 成功但无输出 ≠ 失败 */
|
||||||
|
n = run_cmd_ex(b, sizeof b, 1500, "exit 3", &rr);
|
||||||
|
CHECK(n == 0);
|
||||||
|
CHECK(rr.status == RUNCMD_EXIT);
|
||||||
|
CHECK(rr.exit_code == 3);
|
||||||
|
n = run_cmd_ex(b, sizeof b, 1500, "nosuchcmd_m49_xyz", &rr);
|
||||||
|
CHECK(rr.status == RUNCMD_EXIT);
|
||||||
|
CHECK(rr.exit_code == 127); /* 命令不存在(sh 报 127) */
|
||||||
|
n = run_cmd_ex(b, sizeof b, 150, "sleep 5", &rr);
|
||||||
|
CHECK(rr.status == RUNCMD_TIMEOUT); /* 限时内不退出 */
|
||||||
|
CHECK(n >= 0);
|
||||||
|
n = run_cmd_ex(b, sizeof b, 1500, "echo boom >&2; exit 4", &rr);
|
||||||
|
CHECK(rr.status == RUNCMD_EXIT);
|
||||||
|
CHECK(rr.exit_code == 4);
|
||||||
|
CHECK(strstr(rr.err, "boom") != NULL); /* stderr 不再丢 /dev/null */
|
||||||
|
int isfail = 0;
|
||||||
|
const char *why = run_cmd_why(&rr, &isfail);
|
||||||
|
CHECK(isfail == 1);
|
||||||
|
CHECK(strstr(why, "exit 4") != NULL);
|
||||||
|
CHECK(strstr(why, "boom") != NULL);
|
||||||
|
CHECK(run_cmd(b, sizeof b, 1500, "echo ok") == 3); /* 旧外壳契约不变 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 2. 扫描缓存解析(真机字节) ================= */
|
||||||
|
printf("== bt_clean_ansi / bt_scan_line_parse(真机 od 字节样本) ==\n");
|
||||||
|
{
|
||||||
|
char clean[512], mac[24], name[64];
|
||||||
|
bt_clean_ansi(S_NEW_PUDDING, clean, sizeof clean);
|
||||||
|
CHECK(strstr(clean, "\033") == NULL);
|
||||||
|
CHECK(strstr(clean, "\001") == NULL && strstr(clean, "\002") == NULL);
|
||||||
|
CHECK(strstr(clean, "\r") == NULL);
|
||||||
|
CHECK(strcmp(clean, "[bluetooth]# [NEW] Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63") == 0);
|
||||||
|
|
||||||
|
CHECK(bt_scan_line_parse(S_NEW_PUDDING, mac, sizeof mac, name, sizeof name) == 1);
|
||||||
|
CHECK(strcmp(mac, "79:85:D6:7B:73:63") == 0);
|
||||||
|
CHECK(strcmp(name, "79-85-D6-7B-73-63") == 0);
|
||||||
|
|
||||||
|
CHECK(bt_scan_line_parse(S_PLAIN_BLOCK, mac, sizeof mac, name, sizeof name) == 1);
|
||||||
|
CHECK(strcmp(mac, "33:DE:2E:21:AD:30") == 0);
|
||||||
|
CHECK(strcmp(name, "MOONDROP Pudding") == 0);
|
||||||
|
|
||||||
|
/* [CHG]/[DEL] 不是「附近有这台」 */
|
||||||
|
CHECK(bt_scan_line_parse(S_CHG_POWERED, mac, sizeof mac, name, sizeof name) == 0);
|
||||||
|
CHECK(bt_scan_line_parse(S_DEL_LINE, mac, sizeof mac, name, sizeof name) == 0);
|
||||||
|
/* 提示符回显行 / 空行 / 截断到半截 MAC / 无关行 */
|
||||||
|
CHECK(bt_scan_line_parse(S_PROMPT_DEVICES, mac, sizeof mac, name, sizeof name) == 0);
|
||||||
|
CHECK(bt_scan_line_parse("", mac, sizeof mac, name, sizeof name) == 0);
|
||||||
|
CHECK(bt_scan_line_parse(S_TRUNC_MAC, mac, sizeof mac, name, sizeof name) == 0);
|
||||||
|
CHECK(bt_scan_line_parse("Waiting to connect to bluetoothd...\r\n", mac, sizeof mac, name, sizeof name) == 0);
|
||||||
|
/* MAC 后无名字(截断输出)也要给出设备 */
|
||||||
|
CHECK(bt_scan_line_parse("[NEW] Device AA:BB:CC:DD:EE:FF\n", mac, sizeof mac, name, sizeof name) == 1);
|
||||||
|
CHECK(strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0);
|
||||||
|
CHECK(name[0] == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 3. hcitool con 解析 ================= */
|
||||||
|
printf("== bt_acl_parse(hcitool con 原始输出/空/截断) ==\n");
|
||||||
|
{
|
||||||
|
char macs[8][24];
|
||||||
|
CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0);
|
||||||
|
CHECK(bt_acl_parse("", macs, 8) == 0);
|
||||||
|
int n = bt_acl_parse("Connections:\n"
|
||||||
|
"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER\n"
|
||||||
|
"\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE\n", macs, 8);
|
||||||
|
CHECK(n == 2);
|
||||||
|
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||||
|
CHECK(strcmp(macs[1], "11:22:33:44:55:66") == 0);
|
||||||
|
/* SCO 行不是 ACL;截断 MAC 丢掉 */
|
||||||
|
n = bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 12 state 1\n"
|
||||||
|
"\t< ACL 33:DE:2E:21:A\n", macs, 8);
|
||||||
|
CHECK(n == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 4. list-pcms / info 解析 ================= */
|
||||||
|
printf("== bt_pcm_paths_parse / bt_path_mac / bt_pcm_info_parse ==\n");
|
||||||
|
{
|
||||||
|
char paths[4][80];
|
||||||
|
int n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n", paths, 4);
|
||||||
|
CHECK(n == 1);
|
||||||
|
CHECK(strcmp(paths[0], "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc") == 0);
|
||||||
|
n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n"
|
||||||
|
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk\r\n"
|
||||||
|
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/sco\n", paths, 4);
|
||||||
|
CHECK(n == 2); /* SCO/HFP 端点不算 A2DP */
|
||||||
|
CHECK(strcmp(paths[1], "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk") == 0);
|
||||||
|
CHECK(bt_pcm_paths_parse("", paths, 4) == 0);
|
||||||
|
/* 真机 bluealsa v4.x 端点路径带末端段,且**结尾字面量不可依赖**:
|
||||||
|
* 塔 2026-11-11 首连实测是 `/a2dpsrc/sink`(不是 /source),M50 的旧 bt_dev_detect_mac()
|
||||||
|
* 按「最后一个 / 之后」取 token 就会拿到 "sink" 而不是 MAC。此处两种结尾都验。 */
|
||||||
|
n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n"
|
||||||
|
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk/source\n", paths, 4);
|
||||||
|
CHECK(n == 2);
|
||||||
|
CHECK(strcmp(paths[0], "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0);
|
||||||
|
char mac2[24];
|
||||||
|
CHECK(bt_path_mac(paths[0], mac2, sizeof mac2) == 1);
|
||||||
|
CHECK(strcmp(mac2, "33:DE:2E:21:AD:30") == 0); /* 不是 "sink" */
|
||||||
|
CHECK(bt_path_mac(paths[1], mac2, sizeof mac2) == 1);
|
||||||
|
CHECK(strcmp(mac2, "E0:42:6D:79:C5:98") == 0);
|
||||||
|
char mac3[24]; /* 无末端段的老形态也兼容 */
|
||||||
|
CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc", mac3, sizeof mac3) == 1);
|
||||||
|
CHECK(strcmp(mac3, "33:DE:2E:21:AD:30") == 0);
|
||||||
|
/* 截断输出(半截路径)也不许崩、不许当成端点 */
|
||||||
|
bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_D", paths, 4);
|
||||||
|
char mac[24];
|
||||||
|
CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc", mac, sizeof mac) == 1);
|
||||||
|
CHECK(strcmp(mac, "33:DE:2E:21:AD:30") == 0);
|
||||||
|
CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_3", mac, sizeof mac) == 0);
|
||||||
|
CHECK(bt_path_mac("/org/bluealsa/hci0/nothing", mac, sizeof mac) == 0);
|
||||||
|
|
||||||
|
/* 真机原文(M50 2026-11-11 18:26 实测,idle 与 running 各一份),逐字照抄 */
|
||||||
|
static const char *REAL_IDLE =
|
||||||
|
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
|
||||||
|
"Sequence: 0\n"
|
||||||
|
"Transport: A2DP-source\n"
|
||||||
|
"Mode: sink\n"
|
||||||
|
"Running: false\n"
|
||||||
|
"Format: S16_LE\n"
|
||||||
|
"Channels: 2\n"
|
||||||
|
"Sampling: 48000 Hz\n"
|
||||||
|
"Available codecs: SBC AAC\n"
|
||||||
|
"Selected codec: AAC\n"
|
||||||
|
"Delay: 15.0 ms\n"
|
||||||
|
"SoftVolume: false\n"
|
||||||
|
"Volume: L: 127 R: 127\n"
|
||||||
|
"Muted: L: false R: false\n";
|
||||||
|
static const char *REAL_RUNNING =
|
||||||
|
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
|
||||||
|
"Sequence: 3\n"
|
||||||
|
"Transport: A2DP-source\n"
|
||||||
|
"Mode: sink\n"
|
||||||
|
"Running: true\n"
|
||||||
|
"Format: S16_LE\n"
|
||||||
|
"Channels: 2\n"
|
||||||
|
"Sampling: 48000 Hz\n"
|
||||||
|
"Available codecs: SBC AAC\n"
|
||||||
|
"Selected codec: AAC\n"
|
||||||
|
"Delay: 17.2 ms\n"
|
||||||
|
"SoftVolume: false\n"
|
||||||
|
"Volume: L: 127 R: 127\n"
|
||||||
|
"Muted: L: false R: false\n";
|
||||||
|
bt_pcm_info_t info;
|
||||||
|
bt_pcm_info_parse(REAL_IDLE, &info);
|
||||||
|
CHECK(info.has == 1);
|
||||||
|
CHECK(strcmp(info.codec, "AAC") == 0); /* Selected codec,不是 Available 列表 */
|
||||||
|
CHECK(strcmp(info.rate, "48000 Hz") == 0);
|
||||||
|
CHECK(strcmp(info.channels, "2") == 0);
|
||||||
|
CHECK(strcmp(info.bits, "16") == 0); /* 从 Format: S16_LE 推出来 */
|
||||||
|
CHECK(info.running == 0); /* Running: false */
|
||||||
|
bt_pcm_info_parse(REAL_RUNNING, &info);
|
||||||
|
CHECK(info.running == 1); /* Running: true 才代表真在传 */
|
||||||
|
CHECK(strcmp(info.codec, "AAC") == 0);
|
||||||
|
CHECK(strcmp(info.bits, "16") == 0);
|
||||||
|
/* 兼容旧/其它形态(有人给的是 Codec:/Bits per Sample:/State: RUNNING) */
|
||||||
|
bt_pcm_info_parse(" Transport: A2DP-source\n"
|
||||||
|
" Codec: SBC\n"
|
||||||
|
" CodecConfiguration: 0x3f\n"
|
||||||
|
" Sampling: 44100 Hz\n"
|
||||||
|
" Channels: 2\n"
|
||||||
|
" Bits per Sample: 16\n"
|
||||||
|
" State: RUNNING\n", &info);
|
||||||
|
CHECK(strcmp(info.codec, "SBC") == 0); /* 不被 CodecConfiguration 抢走 */
|
||||||
|
CHECK(strcmp(info.rate, "44100 Hz") == 0);
|
||||||
|
CHECK(strcmp(info.bits, "16") == 0);
|
||||||
|
CHECK(info.running == 1);
|
||||||
|
/* 只有 Format 一行(属性打包)时的兜底 */
|
||||||
|
bt_pcm_info_parse(" Format: 48000 Hz, 2 channels, 24 bits\n", &info);
|
||||||
|
CHECK(info.has == 1);
|
||||||
|
CHECK(strcmp(info.rate, "48000") == 0);
|
||||||
|
CHECK(strcmp(info.channels, "2") == 0);
|
||||||
|
CHECK(strcmp(info.bits, "24") == 0);
|
||||||
|
CHECK(info.running == -1);
|
||||||
|
/* 空输出 / 错误输出(endpoint 不存在)→ has=0,不许编造 */
|
||||||
|
bt_pcm_info_parse("", &info);
|
||||||
|
CHECK(info.has == 0);
|
||||||
|
bt_pcm_info_parse("bluealsa-cli: E: CMD \"info\": Couldn't get BlueALSA PCM: "
|
||||||
|
"Object path not found: '/org/bluealsa/hci0/dev_00/a2dpsrc'\n", &info);
|
||||||
|
CHECK(info.has == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 5. 读写分类与授权 ================= */
|
||||||
printf("== bt_cmd_is_write 分类(读/写级) ==\n");
|
printf("== bt_cmd_is_write 分类(读/写级) ==\n");
|
||||||
CHECK(bt_cmd_is_write("status") == 0);
|
CHECK(bt_cmd_is_write("status") == 0);
|
||||||
CHECK(bt_cmd_is_write("paired") == 0);
|
CHECK(bt_cmd_is_write("paired") == 0);
|
||||||
@@ -81,46 +435,278 @@ int main(void)
|
|||||||
CHECK(bt_cmd_is_write("unpair") == 1);
|
CHECK(bt_cmd_is_write("unpair") == 1);
|
||||||
CHECK(bt_cmd_is_write("scan") == 1);
|
CHECK(bt_cmd_is_write("scan") == 1);
|
||||||
|
|
||||||
printf("== bt_ctl_execute 写级(connect/disconnect/pair/unpair,经桩 bluetoothctl) ==\n");
|
/* ================= 6. 读级(status/paired/connected/a2dp/devices) ================= */
|
||||||
|
printf("== 读级:status/paired/connected/a2dp/devices ==\n");
|
||||||
|
bt_ctl_execute("status", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "paired=2") != NULL); /* paired-devices 计数 */
|
||||||
|
CHECK(strstr(r.text, "connected=0") != NULL); /* 内核 ACL 权威:当前无连接 */
|
||||||
|
|
||||||
|
bt_ctl_execute("paired", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
|
||||||
|
CHECK(strstr(r.text, "Device E0:42:6D:79:C5:98 17OP15") != NULL);
|
||||||
|
|
||||||
|
/* 没有已配对设备 = 空列表(不是错误) */
|
||||||
|
snprintf(p, sizeof p, "%s/state/paired_list.txt", T); wf(p, "");
|
||||||
|
bt_ctl_execute("paired", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(r.text[0] == 0);
|
||||||
|
snprintf(p, sizeof p, "%s/state/paired_list.txt", T);
|
||||||
|
wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n");
|
||||||
|
|
||||||
|
/* a2dp:list-pcms → info,无端点时 "A2DP none" */
|
||||||
|
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
|
||||||
|
wf(p, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n");
|
||||||
|
bt_ctl_execute("a2dp", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "A2DP 33:DE:2E:21:AD:30") != NULL);
|
||||||
|
CHECK(strstr(r.text, "codec=AAC") != NULL); /* 真机 Selected codec */
|
||||||
|
CHECK(strstr(r.text, "rate=48000 Hz") != NULL);
|
||||||
|
CHECK(strstr(r.text, "channels=2") != NULL);
|
||||||
|
CHECK(strstr(r.text, "bits=16") != NULL);
|
||||||
|
CHECK(strstr(r.text, "acquired=0") != NULL); /* Running: false(空闲,端点还在) */
|
||||||
|
CHECK(strstr(r.text, "role=src") != NULL);
|
||||||
|
{ /* Running: true → acquired=1(权威布尔) */
|
||||||
|
snprintf(p, sizeof p, "%s/state/running", T); wf(p, "1\n");
|
||||||
|
bt_ctl_execute("a2dp", NULL, &r);
|
||||||
|
CHECK(strstr(r.text, "acquired=1") != NULL);
|
||||||
|
unlink(p);
|
||||||
|
}
|
||||||
|
/* 列表里有端点但 info 查不到(刚断开)→ 跳过,不编造 */
|
||||||
|
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
|
||||||
|
wf(p, "/org/bluealsa/hci0/dev_00_00_00_00_00_00/a2dpsrc\n");
|
||||||
|
bt_ctl_execute("a2dp", NULL, &r);
|
||||||
|
CHECK(strcmp(r.text, "A2DP none\n") == 0);
|
||||||
|
wf(p, "");
|
||||||
|
bt_ctl_execute("a2dp", NULL, &r);
|
||||||
|
CHECK(strcmp(r.text, "A2DP none\n") == 0);
|
||||||
|
|
||||||
|
printf("== A2DP 预算:端点全卡死时不许把守护拖住(r1 评审 P2-1) ==\n");
|
||||||
|
{
|
||||||
|
char pcms[512];
|
||||||
|
snprintf(pcms, sizeof pcms,
|
||||||
|
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n"
|
||||||
|
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/sink\n"
|
||||||
|
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/sink\n"
|
||||||
|
"/org/bluealsa/hci0/dev_12_34_56_78_9A_BC/a2dpsnk/source\n");
|
||||||
|
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, pcms);
|
||||||
|
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
|
||||||
|
long t0 = mono_ms();
|
||||||
|
bt_ctl_execute("a2dp", NULL, &r);
|
||||||
|
long dt = mono_ms() - t0;
|
||||||
|
printf(" budget-a2dp(4 端点全卡死) = %ld ms\n", dt);
|
||||||
|
CHECK(dt < 1200); /* 旧实现实测 6053ms */
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
unlink(p);
|
||||||
|
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
|
||||||
|
|
||||||
|
/* status JSON 的 a2dp 摘要:无连接→0 次 fork;有连接→TTL 缓存(第二次几乎免费) */
|
||||||
|
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "yes\n");
|
||||||
|
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
|
||||||
|
wf(p, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n");
|
||||||
|
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
|
||||||
|
static char js[8192];
|
||||||
|
g_bt_a2dp_cache_ms = 0;
|
||||||
|
long t1 = mono_ms();
|
||||||
|
build_status_json(js, sizeof js);
|
||||||
|
long d1 = mono_ms() - t1;
|
||||||
|
long t2 = mono_ms();
|
||||||
|
build_status_json(js, sizeof js);
|
||||||
|
long d2 = mono_ms() - t2;
|
||||||
|
printf(" budget-status-json 1st = %ld ms, 2nd(cache) = %ld ms\n", d1, d2);
|
||||||
|
CHECK(d1 < 700); /* 单次刷新受 BT_A2DP_STATUS_BUDGET_MS 约束 */
|
||||||
|
CHECK(d2 < 60); /* 缓存命中:不再 fork */
|
||||||
|
CHECK(strstr(js, "\"a2dp_codec\"") != NULL); /* 字段保留(MeowPanel btCard 依赖) */
|
||||||
|
unlink(p);
|
||||||
|
unlink(snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T) ? p : p);
|
||||||
|
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("== 扫描缓存 → bt devices(真机 ANSI 字节 + devices 尾块 + 去重) ==\n");
|
||||||
|
{
|
||||||
|
char cache[4096];
|
||||||
|
/* 结构与真机 /tmp/meow_bt_scan.txt 一致:开头等待行 + ESC[P + 提示符,实时 NEW 行,
|
||||||
|
* 末尾 devices 块,最后一行被截断(没有换行)。 */
|
||||||
|
snprintf(cache, sizeof cache,
|
||||||
|
"Waiting to connect to bluetoothd...\r\033[P\033[P\033[P"
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# \033[K\r\033[KAgent registered\n"
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# scan on\n"
|
||||||
|
"%s"
|
||||||
|
"%s"
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# scan off\n"
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# devices\n"
|
||||||
|
"Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n"
|
||||||
|
"Device E0:42:6D:79:C5:98 17OP15\n"
|
||||||
|
"Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n"
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# quit\n"
|
||||||
|
"\033[0;94m[bluetooth]\033[0m# \r\033[K"
|
||||||
|
"[\001\033[0;92m\002NEW\001\033[0m\002] Device AA:BB:CC:DD:EE:FF Earbuds",
|
||||||
|
S_NEW_PUDDING, S_CHG_POWERED);
|
||||||
|
snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p, cache);
|
||||||
|
bt_ctl_execute("devices", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
/* 实时 NEW 行(ANSI 开头)现在能解析出来 */
|
||||||
|
CHECK(strstr(r.text, "Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63") != NULL);
|
||||||
|
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
|
||||||
|
CHECK(strstr(r.text, "Device E0:42:6D:79:C5:98 17OP15") != NULL);
|
||||||
|
/* 截断的最后一行(无换行)也要收;[CHG] 行不许混进来 */
|
||||||
|
CHECK(strstr(r.text, "Device AA:BB:CC:DD:EE:FF Earbuds") != NULL);
|
||||||
|
CHECK(strstr(r.text, "Controller 4C:BC:98") == NULL);
|
||||||
|
CHECK(strstr(r.text, "Powered") == NULL);
|
||||||
|
/* 去重:79:.. 实时行与 devices 块各一次,输出只一行 */
|
||||||
|
{ int cnt = 0; const char *q = r.text;
|
||||||
|
while ((q = strstr(q, "79:85:D6:7B:73:63")) != NULL) { cnt++; q += 17; }
|
||||||
|
CHECK(cnt == 1); }
|
||||||
|
/* 缓存不存在 → 空文本 */
|
||||||
|
unlink(p);
|
||||||
|
bt_ctl_execute("devices", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(r.text[0] == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 7. 写级:on/off + connect/disconnect/pair/unpair ================= */
|
||||||
|
printf("== bt on/off:按 power 真态定稿 + rfkill 顺序(先 unblock 再 up / 先 down 再 block) ==\n");
|
||||||
|
/* 初始两个 bluetooth rfkill 节点 soft=1(真机形态)。旧实现在这里必败:
|
||||||
|
* 先 hciconfig up 被 rfkill 拦下,再 unblock 也不会重试 → 报「未生效」(假失败)。 */
|
||||||
|
bt_ctl_execute("on", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "已复查") != NULL);
|
||||||
|
CHECK(bt_powered_now(1500) == 1);
|
||||||
|
{ /* 两个节点都必须被解 block(只写首个会让另一个仍 block) */
|
||||||
|
int all_unblocked = 1;
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
char f[300], v[8] = "1";
|
||||||
|
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
|
||||||
|
FILE *fp = fopen(f, "r");
|
||||||
|
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
|
||||||
|
if (strcmp(v, "0") != 0) all_unblocked = 0;
|
||||||
|
}
|
||||||
|
CHECK(all_unblocked == 1);
|
||||||
|
}
|
||||||
|
bt_ctl_execute("off", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(bt_powered_now(1500) == 0);
|
||||||
|
{ /* 关也要写全部节点 */
|
||||||
|
int all_blocked = 1;
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
char f[300], v[8] = "";
|
||||||
|
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
|
||||||
|
FILE *fp = fopen(f, "r");
|
||||||
|
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
|
||||||
|
if (strcmp(v, "1") != 0) all_blocked = 0;
|
||||||
|
}
|
||||||
|
CHECK(all_blocked == 1);
|
||||||
|
}
|
||||||
|
/* 反向验证:桩确实会在 blocked 时拒绝 up(否则上面的断言是空转) */
|
||||||
|
{ char f[300]; snprintf(f, sizeof f, "%s/rfkill/rfkill0/soft", T); wf(f, "1\n");
|
||||||
|
snprintf(f, sizeof f, "%s/rfkill/rfkill3/soft", T); wf(f, "1\n");
|
||||||
|
char out[128]; snprintf(out, sizeof out, "%s/hciconfig hci0 up 2>&1", sb_hc);
|
||||||
|
runcmd_res_t rr; run_cmd_ex(out, sizeof out, 800, out, &rr);
|
||||||
|
CHECK(rr.status != RUNCMD_OK); }
|
||||||
|
{ char f[300]; snprintf(f, sizeof f, "%s/bt/hci0/power", T); wf(f, "off\n");
|
||||||
|
for (int i = 0; i < 2; i++) { snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0); wf(f, "0\n"); } }
|
||||||
|
|
||||||
|
printf("== 预算上界:命令卡死时写操作必须落在客户端 1500ms IPC 窗口内(r1 评审 P1) ==\n");
|
||||||
|
{ /* connect:bluetoothctl 卡住 5s(耳机没上电的真实形态)→ 总耗时必须 < 1450ms */
|
||||||
|
long t0 = mono_ms();
|
||||||
|
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
|
||||||
|
long dt = mono_ms() - t0;
|
||||||
|
printf(" budget-connect = %ld ms (code=%d)\n", dt, r.code);
|
||||||
|
CHECK(dt < 1450);
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV); /* 未连上就必须报未生效 */
|
||||||
|
CHECK(strstr(r.text, "未生效") != NULL);
|
||||||
|
}
|
||||||
|
{ long t0 = mono_ms();
|
||||||
|
bt_ctl_execute("pair", "CC:CC:CC:CC:CC:CC", &r);
|
||||||
|
long dt = mono_ms() - t0;
|
||||||
|
printf(" budget-pair = %ld ms (code=%d)\n", dt, r.code);
|
||||||
|
CHECK(dt < 1450);
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV);
|
||||||
|
}
|
||||||
|
{ /* bt on:hciconfig 卡住 5s → 同样必须 < 1450ms(旧实现实测 2424ms) */
|
||||||
|
char f[300]; snprintf(f, sizeof f, "%s/state/hang_hciconfig", T); wf(f, "1\n");
|
||||||
|
long t0 = mono_ms();
|
||||||
|
bt_ctl_execute("on", NULL, &r);
|
||||||
|
long dt = mono_ms() - t0;
|
||||||
|
printf(" budget-bt-on = %ld ms (code=%d)\n", dt, r.code);
|
||||||
|
CHECK(dt < 1450);
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV); /* 卡住未生效:如实报 */
|
||||||
|
unlink(f);
|
||||||
|
}
|
||||||
|
{ /* 熔断:连续 3 次超时后,第 4 次必须**立刻**拒绝、不再 fork(防 55 个挂死 bluetoothctl
|
||||||
|
* 吃光进程槽的事故)。冷却 30s,测试里手工复位继续后续用例。 */
|
||||||
|
CHECK(g_bt_tmo_streak >= BT_TMO_STREAK_MAX);
|
||||||
|
long t0 = mono_ms();
|
||||||
|
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
|
||||||
|
long dt = mono_ms() - t0;
|
||||||
|
printf(" circuit-breaker 4th = %ld ms, code=%d\n", dt, r.code);
|
||||||
|
CHECK(dt < 100); /* 没再起子进程 */
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV);
|
||||||
|
CHECK(strstr(r.text, "熔断中") != NULL);
|
||||||
|
g_bt_tmo_streak = 0; /* 测试专用复位(生产靠 30s 冷却+成功清零) */
|
||||||
|
g_bt_tmo_until = 0;
|
||||||
|
}
|
||||||
|
{ /* 恢复:正常 on,供后面的用例使用;同时验证「非超时返回会清零熔断计数」 */
|
||||||
|
bt_ctl_execute("on", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(g_bt_tmo_streak == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("== bt connect:成功路径以 hcitool con 复查 ==\n");
|
||||||
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
||||||
CHECK(r.code == MEOW_OK);
|
CHECK(r.code == MEOW_OK);
|
||||||
CHECK(strstr(r.text, "Connected") != NULL);
|
CHECK(strstr(r.text, "已连接") != NULL);
|
||||||
|
CHECK(strstr(r.text, "hcitool con") != NULL);
|
||||||
|
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 1);
|
||||||
|
|
||||||
|
printf("== bt disconnect:成功路径以 hcitool con 复查 ==\n");
|
||||||
bt_ctl_execute("disconnect", "33:DE:2E:21:AD:30", &r);
|
bt_ctl_execute("disconnect", "33:DE:2E:21:AD:30", &r);
|
||||||
CHECK(r.code == MEOW_OK);
|
CHECK(r.code == MEOW_OK);
|
||||||
bt_ctl_execute("pair", "33:DE:2E:21:AD:30", &r);
|
CHECK(strstr(r.text, "已断开") != NULL);
|
||||||
|
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 0);
|
||||||
|
|
||||||
|
printf("== bt connect 失败:bluetoothctl 报错 + 无 ACL → ERR_NODEV(旧实现恒报成功) ==\n");
|
||||||
|
bt_ctl_execute("connect", "AA:AA:AA:AA:AA:AA", &r);
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV);
|
||||||
|
CHECK(strstr(r.text, "未生效") != NULL);
|
||||||
|
CHECK(strstr(r.text, "br-connection-refused") != NULL); /* 失败原因来自 stderr/stdout 片段 */
|
||||||
|
|
||||||
|
printf("== bt disconnect 假成功:命令 exit 0 但 ACL 仍在 → 仍报 ERR_NODEV ==\n");
|
||||||
|
/* BB:* 的 disconnect 桩打印成功但不删 ACL(模拟「命令说成功、链路其实还在」)。 */
|
||||||
|
snprintf(p, sizeof p, "%s/state/conn_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
|
||||||
|
snprintf(p, sizeof p, "%s/state/paired_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
|
||||||
|
bt_ctl_execute("disconnect", "BB:BB:BB:BB:BB:BB", &r);
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV);
|
||||||
|
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
|
||||||
|
bt_ctl_execute("disconnect", "AA:AA:AA:AA:AA:AA", &r); /* 本来就没 ACL:真态=已断开 */
|
||||||
CHECK(r.code == MEOW_OK);
|
CHECK(r.code == MEOW_OK);
|
||||||
bt_ctl_execute("unpair", "33:DE:2E:21:AD:30", &r);
|
CHECK(strstr(r.text, "已断开") != NULL);
|
||||||
|
bt_ctl_execute("connect", "11:22:33:44:55:66", &r);
|
||||||
CHECK(r.code == MEOW_OK);
|
CHECK(r.code == MEOW_OK);
|
||||||
CHECK(strstr(r.text, "Connected") != NULL); /* remove 桩回 Connected: yes */
|
|
||||||
|
printf("== bt pair/unpair:以 bluetoothctl info Paired: 复查 ==\n");
|
||||||
|
bt_ctl_execute("pair", "11:22:33:44:55:66", &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "已配对") != NULL);
|
||||||
|
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired", 1500) == 1);
|
||||||
|
bt_ctl_execute("unpair", "11:22:33:44:55:66", &r); /* remove → info 查不到也算解除 */
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "已解除配对") != NULL);
|
||||||
|
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired", 1500) == -1);
|
||||||
|
bt_ctl_execute("pair", "AA:AA:AA:AA:AA:AA", &r);
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV);
|
||||||
|
CHECK(strstr(r.text, "未生效") != NULL);
|
||||||
bt_ctl_execute("connect", "", &r);
|
bt_ctl_execute("connect", "", &r);
|
||||||
CHECK(r.code == MEOW_ERR_INVAL); /* 缺 mac → INVAL */
|
CHECK(r.code == MEOW_ERR_INVAL); /* 缺 mac → INVAL */
|
||||||
|
|
||||||
|
printf("== bt scan on/off(写级;桩 bluetoothctl 现在被 scan 真正调用) ==\n");
|
||||||
bt_ctl_execute("scan", NULL, &r);
|
bt_ctl_execute("scan", NULL, &r);
|
||||||
CHECK(r.code == MEOW_OK); /* scan on(默认) */
|
CHECK(r.code == MEOW_OK);
|
||||||
CHECK(strstr(r.text, "scan on") != NULL);
|
CHECK(strstr(r.text, "scan on") != NULL);
|
||||||
bt_ctl_execute("scan", "off", &r);
|
bt_ctl_execute("scan", "off", &r);
|
||||||
CHECK(r.code == MEOW_OK);
|
CHECK(r.code == MEOW_OK);
|
||||||
|
|
||||||
printf("== bt_ctl_execute 读级(connected/a2dp/devices/status,权威) ==\n");
|
|
||||||
bt_ctl_execute("connected", NULL, &r);
|
|
||||||
CHECK(r.code == MEOW_OK);
|
|
||||||
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30") != NULL);
|
|
||||||
CHECK(strstr(r.text, "Pudding") != NULL);
|
|
||||||
bt_ctl_execute("a2dp", NULL, &r);
|
|
||||||
CHECK(r.code == MEOW_OK);
|
|
||||||
CHECK(strstr(r.text, "codec=SBC") != NULL);
|
|
||||||
CHECK(strstr(r.text, "rate=44100") != NULL);
|
|
||||||
CHECK(strstr(r.text, "acquired=1") != NULL);
|
|
||||||
CHECK(strstr(r.text, "role=src") != NULL);
|
|
||||||
snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p, /* 重写(bt scan on 曾覆盖) */
|
|
||||||
"[NEW] Device 33:DE:2E:21:AD:30 Pudding\n[NEW] Device AA:BB:CC:DD:EE:FF Earbuds\n");
|
|
||||||
bt_ctl_execute("devices", NULL, &r);
|
|
||||||
CHECK(r.code == MEOW_OK);
|
|
||||||
CHECK(strstr(r.text, "Earbuds") != NULL);
|
|
||||||
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30") != NULL);
|
|
||||||
bt_ctl_execute("status", NULL, &r);
|
|
||||||
CHECK(r.code == MEOW_OK);
|
|
||||||
CHECK(strstr(r.text, "connected=2") != NULL);
|
|
||||||
|
|
||||||
printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n");
|
printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n");
|
||||||
caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER;
|
caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER;
|
||||||
caller_t sys; memset(&sys, 0, sizeof sys); sys.kind = CALL_SYS;
|
caller_t sys; memset(&sys, 0, sizeof sys); sys.kind = CALL_SYS;
|
||||||
@@ -130,6 +716,8 @@ int main(void)
|
|||||||
CHECK(bt_write_authorize(&sys, 0, "connect") == MEOW_ERR_PERM); /* 系统+hiby → 拒 */
|
CHECK(bt_write_authorize(&sys, 0, "connect") == MEOW_ERR_PERM); /* 系统+hiby → 拒 */
|
||||||
CHECK(bt_write_authorize(&other, 1, "status") == MEOW_OK); /* 读级任意 → 放 */
|
CHECK(bt_write_authorize(&other, 1, "status") == MEOW_OK); /* 读级任意 → 放 */
|
||||||
|
|
||||||
|
(void)cmd;
|
||||||
|
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", T); (void)!system(rm); } /* 退出即清,保证可重复 */
|
||||||
if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; }
|
if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; }
|
||||||
printf("\nBT_CTL TEST: ALL PASS\n");
|
printf("\nBT_CTL TEST: ALL PASS\n");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ set -u
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
BIN="${1:-./meow_netmgrd_host}"
|
BIN="${1:-./meow_netmgrd_host}"
|
||||||
Q="${QEMU:-}"
|
Q="${QEMU:-}"
|
||||||
T=$(mktemp -d /tmp/netmgrdtest.XXXXXX)
|
T=$(mktemp -d "${TMPDIR:-/tmp}/netmgrdtest.XXXXXX")
|
||||||
trap 'kill $DPID 2>/dev/null; rm -rf "$T"' EXIT
|
trap 'kill $DPID 2>/dev/null; rm -rf "$T"' EXIT
|
||||||
PASS=0; FAIL=0
|
PASS=0; FAIL=0
|
||||||
ck() { # ck <名> <期望子串> <实际>
|
ck() { # ck <名> <期望子串> <实际>
|
||||||
@@ -97,16 +97,29 @@ exit 0
|
|||||||
EOF
|
EOF
|
||||||
cat > "$T/bluealsa-cli" <<'EOF'
|
cat > "$T/bluealsa-cli" <<'EOF'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
if [ "${1:-}" = "a2dp" ]; then
|
# v4.1.1 实际命令表:list-services/list-pcms/status/info/codec/volume/...(没有 a2dp 子命令)
|
||||||
echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:"
|
case "${1:-}" in
|
||||||
echo " Type: A2DP Source"
|
list-pcms)
|
||||||
echo " Codec: SBC"
|
# 真机路径带末端段,且结尾字面量不可依赖(首连实测是 /a2dpsrc/sink):
|
||||||
echo " Sampling: 44100 Hz"
|
# /org/bluealsa/hci0/dev_XX/a2dpsrc/sink
|
||||||
echo " Channels: 2"
|
echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink";;
|
||||||
echo " Bits per Sample: 16"
|
info)
|
||||||
echo " State: RUNNING"
|
# 真机 v4.1.1 原文(M50 2026-11-11 18:26 抓,PCM 被 audiod 撑住时);此处取「正在传音频」态
|
||||||
echo " Acquired: yes"
|
echo "Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30"
|
||||||
fi
|
echo "Sequence: 3"
|
||||||
|
echo "Transport: A2DP-source"
|
||||||
|
echo "Mode: sink"
|
||||||
|
echo "Running: true"
|
||||||
|
echo "Format: S16_LE"
|
||||||
|
echo "Channels: 2"
|
||||||
|
echo "Sampling: 48000 Hz"
|
||||||
|
echo "Available codecs: SBC AAC"
|
||||||
|
echo "Selected codec: AAC"
|
||||||
|
echo "Delay: 17.2 ms"
|
||||||
|
echo "SoftVolume: false"
|
||||||
|
echo "Volume: L: 127 R: 127"
|
||||||
|
echo "Muted: L: false R: false";;
|
||||||
|
esac
|
||||||
exit 0
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
cat > "$T/ip" <<'EOF'
|
cat > "$T/ip" <<'EOF'
|
||||||
@@ -179,22 +192,32 @@ OUT=$(env $ENVS $Q "$T/meow_net" bt connected 2>&1)
|
|||||||
ck T5e "Device 33:DE:2E:21:AD:30 Pudding" "$OUT"
|
ck T5e "Device 33:DE:2E:21:AD:30 Pudding" "$OUT"
|
||||||
ck T5e2 "Device 11:22:33:44:55:66 Speaker" "$OUT"
|
ck T5e2 "Device 11:22:33:44:55:66 Speaker" "$OUT"
|
||||||
|
|
||||||
# ---- T5f: bt a2dp(P1:bluealsa-cli a2dp list → codec/rate/acquired/role)----
|
# ---- T5f: bt a2dp(M49:bluealsa-cli list-pcms → info <path> → codec/rate/acquired/role)----
|
||||||
|
# v4.1.1 没有 `a2dp list` 子命令,桩只认 list-pcms/info;acquired 由 `Running: true` 推出。
|
||||||
OUT=$(env $ENVS $Q "$T/meow_net" bt a2dp 2>&1)
|
OUT=$(env $ENVS $Q "$T/meow_net" bt a2dp 2>&1)
|
||||||
ck T5f "codec=SBC" "$OUT"
|
ck T5f "A2DP 33:DE:2E:21:AD:30" "$OUT"
|
||||||
ck T5f2 "rate=44100" "$OUT"
|
ck T5f2 "codec=AAC" "$OUT" # 真机 Selected codec(不是 Available codecs 列表)
|
||||||
ck T5f3 "acquired=1" "$OUT"
|
ck T5f3 "rate=48000 Hz" "$OUT"
|
||||||
ck T5f4 "role=src" "$OUT"
|
ck T5f4 "channels=2" "$OUT"
|
||||||
|
ck T5f5 "bits=16" "$OUT" # 从 Format: S16_LE 推出
|
||||||
|
ck T5f6 "acquired=1" "$OUT"
|
||||||
|
ck T5f7 "role=src" "$OUT"
|
||||||
|
|
||||||
# ---- T5g: bt devices(P1:netmgrd 权威扫描缓存)----
|
# ---- T5g: bt devices(P1:netmgrd 权威扫描缓存;M49:实时 [NEW] 行带 ANSI ESC 也要能解析)----
|
||||||
cat > "$T/bt_scan.txt" <<'EOF'
|
# 真机 /tmp/meow_bt_scan.txt 的实时行以 ESC[0;94m[bluetooth]ESC[0m# \rESC[K[001 ESC[0;92m 002NEW 001 ESC[0m 002] 开头
|
||||||
[NEW] Device 33:DE:2E:21:AD:30 Pudding
|
{ printf '%s\n' 'Waiting to connect to bluetoothd...'
|
||||||
[NEW] Device AA:BB:CC:DD:EE:FF Earbuds
|
printf '\033[0;94m[bluetooth]\033[0m# \r\033[K[\001\033[0;92m\002NEW\001\033[0m\002] Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n'
|
||||||
[DEL] Device 00:11:22:33:44:55
|
printf '\033[0;94m[bluetooth]\033[0m# scan off\n'
|
||||||
EOF
|
printf '\033[0;94m[bluetooth]\033[0m# devices\n'
|
||||||
|
printf 'Device AA:BB:CC:DD:EE:FF Earbuds\n'
|
||||||
|
printf '[DEL] Device 00:11:22:33:44:55\n'
|
||||||
|
} > "$T/bt_scan.txt"
|
||||||
OUT=$(env $ENVS $Q "$T/meow_net" bt devices 2>&1)
|
OUT=$(env $ENVS $Q "$T/meow_net" bt devices 2>&1)
|
||||||
ck T5g "Device 33:DE:2E:21:AD:30 Pudding" "$OUT"
|
ck T5g "Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63" "$OUT" # 实时 ANSI 行(旧实现全丢)
|
||||||
ck T5g2 "Device AA:BB:CC:DD:EE:FF Earbuds" "$OUT"
|
ck T5g2 "Device AA:BB:CC:DD:EE:FF Earbuds" "$OUT" # 尾部 devices 块
|
||||||
|
if echo "$OUT" | grep -q "00:11:22:33:44:55"; then
|
||||||
|
FAIL=$((FAIL+1)); echo "FAIL T5g3 [DEL] 行被误当成附近设备: $OUT"
|
||||||
|
else PASS=$((PASS+1)); echo "PASS T5g3 [DEL] 不算附近设备"; fi
|
||||||
|
|
||||||
# ---- T5h: bt 写级(connect/pair 等)在宿主 CLI=CALL_OTHER → 拒(仅系统身份+MeowOS 放行,见 T6)----
|
# ---- T5h: bt 写级(connect/pair 等)在宿主 CLI=CALL_OTHER → 拒(仅系统身份+MeowOS 放行,见 T6)----
|
||||||
OUT=$(env $ENVS $Q "$T/meow_net" bt connect 33:DE:2E:21:AD:30 2>&1); RC=$?
|
OUT=$(env $ENVS $Q "$T/meow_net" bt connect 33:DE:2E:21:AD:30 2>&1); RC=$?
|
||||||
@@ -360,8 +383,13 @@ OUT=$(env $ENVS $Q "$T/meow_net" status 2>&1)
|
|||||||
ck T18d "rssi=-45" "$OUT"
|
ck T18d "rssi=-45" "$OUT"
|
||||||
|
|
||||||
# ---- T14: DNS 代理 wire 实测(守护 B:高端口 + 桩上游 + 全局 deny;测 deny/forward/cache)----
|
# ---- T14: DNS 代理 wire 实测(守护 B:高端口 + 桩上游 + 全局 deny;测 deny/forward/cache)----
|
||||||
|
# 需要「非回环的本机地址」当上游:daemon 明确把 127.0.0.1 排除在上游之外(resolv_backup_apply
|
||||||
|
# 防环路,meow_netmgrd.c:2588)。宿主拿不到这样的地址(例如 Android/Termux 的 hostname 不支持
|
||||||
|
# -I)时这套 wire 测试无法成立 —— 显式 SKIP,而不是留一堆假失败。
|
||||||
HOSTIP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
HOSTIP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
[ -n "$HOSTIP" ] || HOSTIP="127.0.0.1"
|
if [ -z "$HOSTIP" ] || [ "$HOSTIP" = "127.0.0.1" ]; then
|
||||||
|
echo "SKIP T14 DNS wire(本机无 hostname -I 或只有回环地址;daemon 拒绝把 127.0.0.1 当上游以防环路)"
|
||||||
|
else
|
||||||
cat > "$T/policy_dns" <<'EOF'
|
cat > "$T/policy_dns" <<'EOF'
|
||||||
global domain=-ads.example.com dns.filter_aaaa=1
|
global domain=-ads.example.com dns.filter_aaaa=1
|
||||||
EOF
|
EOF
|
||||||
@@ -422,6 +450,26 @@ ck T14e "deny=1" "$OUT"
|
|||||||
kill $D2PID 2>/dev/null; wait $D2PID 2>/dev/null
|
kill $D2PID 2>/dev/null; wait $D2PID 2>/dev/null
|
||||||
# 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认)
|
# 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认)
|
||||||
grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; }
|
grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; }
|
||||||
|
fi # T14 上游能力守卫
|
||||||
|
|
||||||
|
# ---- T19: WiFi connect 状态机/持久化/隐藏网络/中文 SSID(宿主桩直驱 wifi_connect_ex)----
|
||||||
|
# daemon IPC 写级需系统身份(SO_PEERCRED /proc/exe),宿主 CLI=CALL_OTHER 必拒;故用 #include
|
||||||
|
# meow_netmgrd.c 的独立宿主二进制直驱 static wifi_connect_ex,桩 wpa_cli 由二进制自建。
|
||||||
|
if [ -z "$Q" ]; then
|
||||||
|
WCON="$T/test_wifi_connect"
|
||||||
|
if cc -Os -Wall -Wextra -Wno-format-truncation -I../meow_launcherd \
|
||||||
|
test_wifi_connect.c -o "$WCON" >/dev/null 2>&1; then
|
||||||
|
if "$WCON" >"$T/wcon.out" 2>/dev/null && grep -q "FAIL=0" "$T/wcon.out"; then
|
||||||
|
PASS=$((PASS+1)); echo "PASS T19 wifi-connect host (12 asserts, FAIL=0)"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL+1)); echo "FAIL T19 wifi-connect host"; cat "$T/wcon.out"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL+1)); echo "FAIL T19 wifi-connect host build"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "SKIP T19 wifi-connect(qemu 下 fork/exec 宿主 /bin/sh 不便,宿主已覆盖)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "=============================="
|
echo "=============================="
|
||||||
echo "PASS=$PASS FAIL=$FAIL"
|
echo "PASS=$PASS FAIL=$FAIL"
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/* test_wifi_connect.c — WiFi 设置页 bug1/bug2/bug4 宿主桩测试(2026-09-15)。
|
||||||
|
*
|
||||||
|
* 通过 #include meow_netmgrd.c 直接驱动 static 的 wifi_connect_ex(),绕过 daemon 的
|
||||||
|
* SO_PEERCRED 系统身份鉴权(宿主 CLI=CALL_OTHER 写级必拒,无法经 IPC 测连接路径)。
|
||||||
|
* 桩 wpa_cli 由本程序写入临时目录;控制文件(status_mode/list/save_fail_once)决定
|
||||||
|
* status 序列 / list_networks 原文 / save_config 是否首败,调用日志写入 <dir>/log 供断言。
|
||||||
|
*
|
||||||
|
* 编译:cc -Wall -Wextra -I../meow_launcherd test_wifi_connect.c -o test_wifi_connect
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#define main meow_netmgrd_main
|
||||||
|
#include "meow_netmgrd.c"
|
||||||
|
#undef main
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
static char g_dir[128];
|
||||||
|
static int g_pass = 0, g_fail = 0;
|
||||||
|
|
||||||
|
static void ck(const char *name, int cond, const char *detail)
|
||||||
|
{
|
||||||
|
if (cond) { g_pass++; printf("PASS %s\n", name); }
|
||||||
|
else { g_fail++; printf("FAIL %s: %s\n", name, detail ? detail : ""); }
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void putf(const char *name, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char path[192], body[512];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(body, sizeof body, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
snprintf(path, sizeof path, "%s/%s", g_dir, name);
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (f) { fputs(body, f); fclose(f); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int log_has(const char *needle)
|
||||||
|
{
|
||||||
|
char path[192], buf[8192];
|
||||||
|
snprintf(path, sizeof path, "%s/log", g_dir);
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return 0;
|
||||||
|
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
||||||
|
fclose(f);
|
||||||
|
buf[n] = 0;
|
||||||
|
return strstr(buf, needle) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset_log(void)
|
||||||
|
{
|
||||||
|
char path[192];
|
||||||
|
snprintf(path, sizeof path, "%s/log", g_dir);
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (f) fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_stub(void)
|
||||||
|
{
|
||||||
|
char path[192];
|
||||||
|
snprintf(path, sizeof path, "%s/wpa_cli", g_dir);
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (!f) { fprintf(stderr, "stub open fail\n"); exit(2); }
|
||||||
|
fputs(
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"D=\"${MN_WIFI_DIR:-/tmp}\"\n"
|
||||||
|
"catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\n"
|
||||||
|
"echo \"$@\" >> \"$D/log\"\n"
|
||||||
|
"case \"$1\" in\n"
|
||||||
|
" status)\n"
|
||||||
|
" m=completed\n"
|
||||||
|
" if [ -f \"$D/status_mode\" ]; then IFS= read -r m < \"$D/status_mode\"; fi\n"
|
||||||
|
" if [ \"$m\" = \"associating\" ]; then\n"
|
||||||
|
" echo \"wpa_state=ASSOCIATING\"; echo \"ssid=MyWiFi\"\n"
|
||||||
|
" else\n"
|
||||||
|
" echo \"wpa_state=COMPLETED\"; echo \"ssid=MyWiFi\"\n"
|
||||||
|
" echo \"ip_address=10.78.193.50\"; echo \"signal=-45\"\n"
|
||||||
|
" fi\n"
|
||||||
|
" ;;\n"
|
||||||
|
" list_networks)\n"
|
||||||
|
" if [ -f \"$D/list\" ]; then catf \"$D/list\"\n"
|
||||||
|
" else printf 'network id / ssid / bssid / flags\\n0\\tMyWiFi\\tany\\t[CURRENT]\\n'\n"
|
||||||
|
" fi\n"
|
||||||
|
" ;;\n"
|
||||||
|
" add_network) echo 1 ;;\n"
|
||||||
|
" set_network) echo OK ;;\n"
|
||||||
|
" enable_network) echo OK ;;\n"
|
||||||
|
" select_network) echo OK ;;\n"
|
||||||
|
" disconnect) echo OK ;;\n"
|
||||||
|
" save_config)\n"
|
||||||
|
" v=\"\"\n"
|
||||||
|
" if [ -f \"$D/save_fail_once\" ]; then IFS= read -r v < \"$D/save_fail_once\"; fi\n"
|
||||||
|
" if [ \"$v\" = \"1\" ]; then echo \"\" > \"$D/save_fail_once\"; echo FAIL; else echo OK; fi\n"
|
||||||
|
" ;;\n"
|
||||||
|
" set) echo OK ;;\n"
|
||||||
|
" remove_network) echo OK ;;\n"
|
||||||
|
" *) echo OK ;;\n"
|
||||||
|
"esac\n"
|
||||||
|
"exit 0\n",
|
||||||
|
f);
|
||||||
|
fclose(f);
|
||||||
|
chmod(path, 0755);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
/* 宿主临时目录:Termux/Android 上 /tmp 不可写 → 跟随 $TMPDIR */
|
||||||
|
const char *tmp = getenv("TMPDIR");
|
||||||
|
if (!tmp || !tmp[0]) tmp = "/tmp";
|
||||||
|
snprintf(g_dir, sizeof g_dir, "%s/mnwifi.XXXXXX", tmp);
|
||||||
|
if (!mkdtemp(g_dir)) { fprintf(stderr, "mkdtemp fail\n"); return 2; }
|
||||||
|
write_stub();
|
||||||
|
setenv("MN_WIFI_DIR", g_dir, 1);
|
||||||
|
|
||||||
|
g_mode_meow = 1;
|
||||||
|
g_connect_wait_ms = 400;
|
||||||
|
g_connecth_wait_ms = 400;
|
||||||
|
{
|
||||||
|
static char p[192];
|
||||||
|
snprintf(p, sizeof p, "%s/wpa_cli", g_dir);
|
||||||
|
g_wpa_cli = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl_result_t r;
|
||||||
|
|
||||||
|
/* ① 成功:命中既有 id=0 + 带新 psk → 更新凭据,等待 COMPLETED,成功报 ip。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
reset_log();
|
||||||
|
wifi_connect_ex("MyWiFi,secret", &r, 0);
|
||||||
|
ck("connect-success code", r.code == MEOW_OK, r.text);
|
||||||
|
ck("connect-success text ip", strstr(r.text, "已连接") && strstr(r.text, "ip=10.78.193.50"), r.text);
|
||||||
|
ck("connect-success psk update", log_has("set_network 0 psk"), "");
|
||||||
|
ck("connect-success enable all", log_has("enable_network all"), "");
|
||||||
|
|
||||||
|
/* ② 错误密码/不在范围:status 恒 ASSOCIATING → 超时失败,新建 id 被回收。 */
|
||||||
|
putf("status_mode", "associating");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("OtherWiFi", &r, 0);
|
||||||
|
ck("connect-timeout code", r.code == MEOW_ERR_NODEV, r.text);
|
||||||
|
ck("connect-timeout text", strstr(r.text, "连接失败") != NULL, r.text);
|
||||||
|
ck("connect-timeout remove new", log_has("remove_network 1"), "");
|
||||||
|
|
||||||
|
/* ③ 中文 SSID \xNN 还原匹配:list_networks 原文转义,输入 UTF-8 名 → 命中 id=0 不新建。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
putf("list", "network id / ssid / bssid / flags\n0\t\\xe5\\xa4\\xa9\\xe5\\xa4\\xa9\tany\t[CURRENT]\n");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("天天", &r, 0);
|
||||||
|
ck("connect-escape match", r.code == MEOW_OK, r.text);
|
||||||
|
ck("connect-escape no add", !log_has("add_network"), "");
|
||||||
|
|
||||||
|
/* ④ connecth:隐藏网络命中既有 id → 追加 scan_ssid 1。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
putf("list", "network id / ssid / bssid / flags\n0\tMyWiFi\tany\t[CURRENT]\n");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("MyWiFi", &r, 1);
|
||||||
|
ck("connecth scan_ssid", r.code == MEOW_OK && log_has("set_network 0 scan_ssid 1"), r.text);
|
||||||
|
|
||||||
|
/* ⑤ save_config 首败 → set update_config 1 重试 → 持久化成功。 */
|
||||||
|
putf("status_mode", "completed");
|
||||||
|
putf("list", "network id / ssid / bssid / flags\n0\tMyWiFi\tany\t[CURRENT]\n");
|
||||||
|
putf("save_fail_once", "1");
|
||||||
|
reset_log();
|
||||||
|
ctl_reset(&r, MEOW_OK);
|
||||||
|
wifi_connect_ex("MyWiFi", &r, 0);
|
||||||
|
ck("save-config retry", r.code == MEOW_OK && !strstr(r.text, "记录未持久化"), r.text);
|
||||||
|
ck("save-config set update_config", log_has("set update_config 1"), "");
|
||||||
|
|
||||||
|
printf("==============================\nPASS=%d FAIL=%d\n", g_pass, g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -24,7 +24,10 @@ if [ "$1" = "host" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meow_powermgr.c -o meow_powermgr
|
# meow_time32.c:独立 o32 time32 取时/睡眠(powermgr 不链 meow_ui.a);采样时间戳与
|
||||||
|
# usleep 轮询恢复真实毫秒。
|
||||||
|
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_powermgr.c ../sdk/meow_time32.c -o meow_powermgr
|
||||||
echo "== meow_powermgr (mipsel static) =="
|
echo "== meow_powermgr (mipsel static) =="
|
||||||
ls -la meow_powermgr
|
ls -la meow_powermgr
|
||||||
md5sum meow_powermgr
|
md5sum meow_powermgr
|
||||||
|
|||||||
Binary file not shown.
@@ -22,7 +22,10 @@ if [ "$1" = "host" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meow_svc_perf.c -o meow_svc_perf
|
# meow_time32.c:独立 o32 time32 取时/睡眠(svc_perf 不链 meow_ui.a);采样间隔/卡死快照
|
||||||
|
# 冷却恢复真实毫秒。
|
||||||
|
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_svc_perf.c ../sdk/meow_time32.c -o meow_svc_perf
|
||||||
echo "== meow_svc_perf (mipsel static) =="
|
echo "== meow_svc_perf (mipsel static) =="
|
||||||
ls -la meow_svc_perf
|
ls -la meow_svc_perf
|
||||||
md5sum meow_svc_perf
|
md5sum meow_svc_perf
|
||||||
|
|||||||
Binary file not shown.
@@ -21,7 +21,10 @@ if [ "$1" = "host" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -Os -Wall -Wextra -static meow_svcd.c -o meow_svcd
|
# meow_time32.c:独立 o32 time32 取时/睡眠(svcd 不链 meow_ui.a);LIBC_OVERRIDES 在静态
|
||||||
|
# 链接期接管 clock_gettime/time/usleep/nanosleep,修 musl time64 头与 time32 实现的 ABI 错位。
|
||||||
|
"$CC" -Os -Wall -Wextra -static -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_svcd.c ../sdk/meow_time32.c -o meow_svcd
|
||||||
echo "== meow_svcd (mipsel static) =="
|
echo "== meow_svcd (mipsel static) =="
|
||||||
ls -la meow_svcd
|
ls -la meow_svcd
|
||||||
md5sum meow_svcd
|
md5sum meow_svcd
|
||||||
|
|||||||
Binary file not shown.
@@ -21,7 +21,10 @@ if [ "$1" = "host" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -Os -Wall -Wextra -static meow_sysui.c -o meow_sysui
|
# meow_time32.c:独立 o32 time32 取时/睡眠(sysui 不链 meow_ui.a);LIBC_OVERRIDES 在静态
|
||||||
|
# 链接期接管 clock_gettime/time/usleep/nanosleep,修 musl time64 头与 time32 实现的 ABI 错位。
|
||||||
|
"$CC" -Os -Wall -Wextra -static -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_sysui.c ../sdk/meow_time32.c -o meow_sysui
|
||||||
echo "== meow_sysui (mipsel static) =="
|
echo "== meow_sysui (mipsel static) =="
|
||||||
ls -la meow_sysui
|
ls -la meow_sysui
|
||||||
md5sum meow_sysui
|
md5sum meow_sysui
|
||||||
|
|||||||
Binary file not shown.
@@ -8,16 +8,20 @@ set -e
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
ROOT=../..
|
ROOT=../..
|
||||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||||
|
INC="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(音量收编 v2:MSG_AUDIO_VOLUME/MSG_ACK)
|
||||||
|
|
||||||
if [ "$1" = "host" ]; then
|
if [ "$1" = "host" ]; then
|
||||||
gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
gcc -Wall -Wextra -Os -I"$INC" -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
||||||
test_wd_fw.c -o test_wd_fw -lm
|
-Wno-unused-variable test_wd_fw.c ../sdk/meow_time32.c -o test_wd_fw -lm
|
||||||
./test_wd_fw
|
./test_wd_fw
|
||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== meow_watchdog (mipsel static) =="
|
echo "== meow_watchdog (mipsel static) =="
|
||||||
"$CC" -Os -Wall -Wextra -static meow_watchdog.c -o meow_watchdog
|
# meow_time32.c:独立 time32 取时/睡眠(watchdog 不链 meow_ui.a)。LIBC_OVERRIDES 让
|
||||||
|
# 未逐个改写的 libc 取时/睡眠入口也一并修正(静态链接,本对象强定义覆盖 libc 归档成员)。
|
||||||
|
"$CC" -Os -Wall -Wextra -static -I"$INC" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
meow_watchdog.c ../sdk/meow_time32.c -o meow_watchdog
|
||||||
ls -la meow_watchdog
|
ls -la meow_watchdog
|
||||||
md5sum meow_watchdog
|
md5sum meow_watchdog
|
||||||
file meow_watchdog
|
file meow_watchdog
|
||||||
Binary file not shown.
@@ -12,14 +12,18 @@
|
|||||||
* run — short press (<3s release) = toggle screen off/on (brightness 0 +
|
* run — short press (<3s release) = toggle screen off/on (brightness 0 +
|
||||||
* FBIOBLANK(4) / FBIOBLANK(0)+brightness restore, with a force re-pan to
|
* FBIOBLANK(4) / FBIOBLANK(0)+brightness restore, with a force re-pan to
|
||||||
* buffer0 on wake).
|
* buffer0 on wake).
|
||||||
* - M2.24 全面按键接管(KEY_CONTROL_DESIGN):应用运行期所有物理键由本进程统一消费,
|
* - M2.24/M40 全面按键接管(KEY_CONTROL_DESIGN):应用运行期所有物理键由本进程统一消费,
|
||||||
* 应用绝不碰物理键。
|
* 应用绝不碰物理键。
|
||||||
* 电源单击(<2s) -> 息屏/亮屏(沿用 M2.22);
|
* 电源单击(松开时 held < power.longpress_menu_ms,缺省 3000)-> 息屏/亮屏(沿用 M2.22);
|
||||||
* 电源长按 ≥2s -> 写 /tmp/meow_power_menu,由 launcher SIGSTOP app 后弹统一电源菜单
|
* 电源长按 ≥power.longpress_menu_ms -> 写 /tmp/meow_power_menu,由 launcher SIGSTOP app
|
||||||
* (launcher 在 waitpid 轮询中消费;菜单期该进程靠 /tmp/meow_power_menu_active
|
* 后弹统一电源菜单(launcher 在 waitpid 轮询中消费;菜单期该进程靠
|
||||||
* 让位,绝不与 launcher 抢屏/抢键);launcher 画菜单,本进程不自绘。
|
* /tmp/meow_power_menu_active 让位,绝不与 launcher 抢屏/抢键);
|
||||||
* 电源长按 ≥8s -> 强制关机兜底(仅当菜单未生效/launcher 未接管)。
|
* launcher 画菜单,本进程不自绘。
|
||||||
* 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。
|
* 电源长按 ≥power.forceoff_ms(缺省 8000)-> 强制关机兜底(仅当菜单未被 launcher 接管)。
|
||||||
|
* 音量±(115/114) -> 音量收编 v2(FUTURE_AUDIO 事项三):首选发 audiod 权威
|
||||||
|
* MSG_AUDIO_VOLUME(audiod 写 DAC numid=1/2 + DSD_VOLUME + 持久化
|
||||||
|
* settings.conf,同源同键单写者);audiod 未起(HibyOS/开机早期)
|
||||||
|
* 才回退本地 amixer 直写兜底 + 本地持久化。
|
||||||
* 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿
|
* 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿
|
||||||
* (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。
|
* (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。
|
||||||
* 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。
|
* 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。
|
||||||
@@ -28,6 +32,9 @@
|
|||||||
* (true|1|yes)。判定为 SDK 框架 App -> 音量/媒体键让位 SDK 输入层(meow_ui_input.c 已消化,
|
* (true|1|yes)。判定为 SDK 框架 App -> 音量/媒体键让位 SDK 输入层(meow_ui_input.c 已消化,
|
||||||
* 防同一次按键双步进);电源仍由本进程兜底(SDK 对电源是 no-op)。裸自绘 App(framework=0/
|
* 防同一次按键双步进);电源仍由本进程兜底(SDK 对电源是 no-op)。裸自绘 App(framework=0/
|
||||||
* 无 framework)或 app.json 不可读 -> 音量/媒体键仍由本进程兜底(不造成功能真空)。
|
* 无 framework)或 app.json 不可读 -> 音量/媒体键仍由本进程兜底(不造成功能真空)。
|
||||||
|
* - M20 亮屏亮度:settings.conf 的 display.brightness(clamp 到 brightness_min..100)为唯一
|
||||||
|
* 权威;settings 瞬时不可读用**真实观测快照**;两者都未知时**不写背光**(不再回退固定 60,
|
||||||
|
* 那会覆盖 launcher 刚设好的用户值 → 开机/亮屏跳 60)。面板已黑且无任何值来源才写安全低亮。
|
||||||
* Exits when the app process disappears (kill(pid,0) -> ESRCH) or on
|
* Exits when the app process disappears (kill(pid,0) -> ESRCH) or on
|
||||||
* SIGTERM from the parent.
|
* SIGTERM from the parent.
|
||||||
*
|
*
|
||||||
@@ -50,8 +57,14 @@
|
|||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <sys/time.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include "meow_ipc.h" /* MSG_AUDIO_VOLUME/MSG_ACK/帧格式(音量收编 v2:首选 audiod 权威) */
|
||||||
|
#include "../sdk/meow_time32.h" /* o32 time32 取时/睡眠(watchdog 不链 meow_ui.a) */
|
||||||
|
#include "wd_power.h" /* 电源阈值/边沿纯判定(宿主单测同源) */
|
||||||
|
|
||||||
/* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */
|
/* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */
|
||||||
#define FBIOGET_VSCREENINFO 0x4600
|
#define FBIOGET_VSCREENINFO 0x4600
|
||||||
@@ -85,11 +98,10 @@
|
|||||||
#define KEY_NEXTSONG 163
|
#define KEY_NEXTSONG 163
|
||||||
#define KEY_PREVIOUS 19
|
#define KEY_PREVIOUS 19
|
||||||
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
|
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
|
||||||
/* M2.24 全面按键接管:电源单击(<2s)=息屏/亮屏;长按≥2s=通知 launcher 弹统一电源菜单;
|
/* M2.24 全面按键接管:电源单击(<power.longpress_menu_ms)=息屏/亮屏;长按≥该值=通知
|
||||||
* ≥8s=强制关机兜底(仅当 launcher 未接管菜单时)。音量± = 写真实 DAC;播放/上下曲 = 抓消费。 */
|
* launcher 弹统一电源菜单;≥power.forceoff_ms=强制关机兜底(launcher 未接管菜单时)。
|
||||||
#define PWR_TOGGLE_MS 2000 /* 短按上限:<2s 松开=单击(镜像 launcher 单单击语义) */
|
* 阈值不硬编码:启动时读 settings.conf(缺省 3000/8000),与 launcher 主屏同一语义。
|
||||||
#define PWR_MENU_MS 2000 /* 长按≥2s -> 写 /tmp/meow_power_menu 请 launcher 弹菜单 */
|
* 音量± = 写真实 DAC;播放/上下曲 = 抓消费。 */
|
||||||
#define PWR_FORCE_MS 8000 /* 长按≥8s -> 强制关机(launcher 未接管时的兜底) */
|
|
||||||
#define PWR_MENU_MARKER "/tmp/meow_power_menu" /* watchdog->launcher 菜单请求 */
|
#define PWR_MENU_MARKER "/tmp/meow_power_menu" /* watchdog->launcher 菜单请求 */
|
||||||
#define PWR_MENU_ACTIVE "/tmp/meow_power_menu_active" /* launcher 画菜单中,watchdog 让位 */
|
#define PWR_MENU_ACTIVE "/tmp/meow_power_menu_active" /* launcher 画菜单中,watchdog 让位 */
|
||||||
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 音量持久化(与 launcher settings.conf 同源) */
|
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 音量持久化(与 launcher settings.conf 同源) */
|
||||||
@@ -140,11 +152,14 @@ static int read_state_file(void)
|
|||||||
* stderr when unset (e.g. no TF card at launch time). */
|
* stderr when unset (e.g. no TF card at launch time). */
|
||||||
static int wd_log_fd = -1;
|
static int wd_log_fd = -1;
|
||||||
|
|
||||||
|
/* 单调毫秒:**必须**走 meow_time32(o32 内核 time32 取时)。
|
||||||
|
* zig musl 的 time64 ABI 在 mipsel 上自相矛盾:直接 clock_gettime() 时结构按 16 字节写、
|
||||||
|
* libc 只写 8 字节 time32,tv_sec 高 32 位会读成 nsec → held 变天文数字,每次单击都被
|
||||||
|
* 判成长按并立刻弹菜单(真机日志 `power hold 12392415168036864000ms`,反汇编证实只有
|
||||||
|
* 4263/无 4403)。meow_time32.c 不依赖 meow_ui.a,静态链入即可修正。 */
|
||||||
static unsigned long long now_ms(void)
|
static unsigned long long now_ms(void)
|
||||||
{
|
{
|
||||||
struct timespec ts;
|
return (unsigned long long)meow_time32_monotonic_ms();
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
||||||
return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void wd_log_init(void)
|
static void wd_log_init(void)
|
||||||
@@ -185,16 +200,47 @@ static int g_fw_app = 0;
|
|||||||
|
|
||||||
/* ---- M2.22(A)+ M2.24 电源键接管:息屏/亮屏 + 长按菜单/强关 ---- */
|
/* ---- M2.22(A)+ M2.24 电源键接管:息屏/亮屏 + 长按菜单/强关 ---- */
|
||||||
static int wd_fbfd = -1; /* fb0 fd(main 里打开,供 FBIOBLANK/FBIOPAN 用) */
|
static int wd_fbfd = -1; /* fb0 fd(main 里打开,供 FBIOBLANK/FBIOPAN 用) */
|
||||||
static int wd_saved_bri = 60; /* 熄屏前亮度快照 */
|
|
||||||
|
/* M20(远古 bug 回归:开机/亮屏背光跳到固定 60):0 是「未知」哨兵,不是亮度值
|
||||||
|
* (0 = 面板全黑/息屏,任何情况下都不是有效用户亮度)。旧代码让 60 同时表达
|
||||||
|
* 「未知快照」和「兜底值」,于是一旦没有可信来源就把 60 写进背光,覆盖 launcher
|
||||||
|
* 刚按 display.brightness 设好的用户值(真机 0.8.46:用户亮度 1,亮屏仍写 60)。 */
|
||||||
|
#define WD_BRI_UNKNOWN 0
|
||||||
|
/* 唯一保留的固定亮度:settings 读不到 + 快照未知 + **当前亮度确实是 0**(面板黑)时才用。
|
||||||
|
* 这条退路上不存在任何可保留的有效用户值,不写就是永久黑屏;取低亮(≈10% of
|
||||||
|
* max_brightness 101)而不是 60 —— 宁可暗到用户手动调亮,也不要夜里突然刺眼。
|
||||||
|
* 注意 60 是固件 S11jpeg_display_shell 画完开机 logo 后写死的值(/etc/init.d,不在本仓库),
|
||||||
|
* 本进程任何分支都不再写 60。 */
|
||||||
|
#define WD_BRI_SAFE_LOW 10
|
||||||
|
/* M20 短期修复 A:settings 读取重试。settings.conf 由 launcher/audiod 以
|
||||||
|
* 「写 .tmp + rename」原子替换,开机早期 /usr/data(UBIFS) 也可能尚未就绪,
|
||||||
|
* fopen 存在瞬时失败窗口。3 次 × 10ms 只覆盖该窗口:最坏总阻塞 (3-1)*10 = 20ms,
|
||||||
|
* 且只在失败时发生(成功路径 0 开销)。亮屏轮询粒度本身是 REFRESH_MS=100ms,
|
||||||
|
* 20ms 在「按键→亮屏」路径上不可感知(见 M20 报文)。 */
|
||||||
|
#define WD_SETTINGS_TRIES 3
|
||||||
|
#define WD_SETTINGS_GAP_MS 10
|
||||||
|
|
||||||
|
static int wd_saved_bri = WD_BRI_UNKNOWN; /* 熄屏前亮度快照;未知=WD_BRI_UNKNOWN(不再用 60) */
|
||||||
|
/* 电源阈值(启动时读 settings.conf;缺省 3000/8000,与 launcher 主屏一致)。 */
|
||||||
|
static unsigned long long wd_pwr_longpress_ms = WD_PWR_LONGPRESS_DEFAULT_MS;
|
||||||
|
static unsigned long long wd_pwr_forceoff_ms = WD_PWR_FORCEOFF_DEFAULT_MS;
|
||||||
static int pwr_down = 0; /* 电源当前按住 */
|
static int pwr_down = 0; /* 电源当前按住 */
|
||||||
static unsigned long long pwr_down_ms = 0;
|
static unsigned long long pwr_down_ms = 0;
|
||||||
static int pwr_woke = 0; /* 按下时屏已黑 → 本次只作唤醒,松开不再 toggle */
|
static int pwr_woke = 0; /* 按下时屏已黑 → 本次只作唤醒,松开不再 toggle */
|
||||||
static int pwr_menu_notified = 0; /* 已写 /tmp/meow_power_menu 请 launcher 弹菜单 */
|
static int pwr_menu_notified = 0; /* 已写 /tmp/meow_power_menu 请 launcher 弹菜单 */
|
||||||
static int pwr_force_done = 0; /* 8s 强关已发(防重复) */
|
static int pwr_force_done = 0; /* 8s 强关已发(防重复) */
|
||||||
|
|
||||||
|
/* 背光 sysfs 路径(缺省 BACKLIGHT;env MEOW_BACKLIGHT 覆盖,供宿主测试隔离——与
|
||||||
|
* MEOW_SETTINGS_FILE 同款测试钩子,真机无 env 走默认,行为不变)。 */
|
||||||
|
static const char *wd_backlight_path(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("MEOW_BACKLIGHT");
|
||||||
|
return (e && e[0]) ? e : BACKLIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
static int read_brightness(void)
|
static int read_brightness(void)
|
||||||
{
|
{
|
||||||
int fd = open(BACKLIGHT, O_RDONLY);
|
int fd = open(wd_backlight_path(), O_RDONLY);
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
char buf[16];
|
char buf[16];
|
||||||
int n = read(fd, buf, sizeof buf - 1);
|
int n = read(fd, buf, sizeof buf - 1);
|
||||||
@@ -208,24 +254,40 @@ static void write_brightness(int val)
|
|||||||
{
|
{
|
||||||
char buf[8];
|
char buf[8];
|
||||||
int l = snprintf(buf, sizeof buf, "%d\n", val);
|
int l = snprintf(buf, sizeof buf, "%d\n", val);
|
||||||
int fd = open(BACKLIGHT, O_WRONLY);
|
int fd = open(wd_backlight_path(), O_WRONLY);
|
||||||
if (fd < 0) return;
|
if (fd < 0) return;
|
||||||
if (write(fd, buf, l) < 0) wd_log("backlight write %d: %s", val, strerror(errno));
|
if (write(fd, buf, l) < 0) wd_log("backlight write %d: %s", val, strerror(errno));
|
||||||
close(fd);
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* settings.conf 路径(缺省 /usr/data/meow/settings.conf;env MEOW_SETTINGS_FILE 覆盖,
|
||||||
|
* 供宿主测试隔离——与 audiod MEOW_AUDIO_SETTINGS 同款测试钩子,真机无 env 走默认)。 */
|
||||||
|
static const char *wd_settings_path(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("MEOW_SETTINGS_FILE");
|
||||||
|
return (e && e[0]) ? e : MEOW_SETTINGS_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
/* M6.23:亮屏恢复亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness),
|
/* M6.23:亮屏恢复亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness),
|
||||||
* 而非 wd_saved_bri 快照——该快照可能被污染:panel OFF 前 read_brightness() 可能读到 launcher
|
* 而非 wd_saved_bri 快照——该快照可能被污染:panel OFF 前 read_brightness() 可能读到 launcher
|
||||||
* 先写好的 0(launcher meow_panel_off 已 write_brightness(0)),`wd_saved_bri=(br>0)?br:60` 便落到
|
* 先写好的 0(launcher meow_panel_off 已 write_brightness(0)),旧代码 `(br>0)?br:60` 便落到
|
||||||
* 默认 60;panel ON 恢复 60 与 launcher 按用户设置恢复 7 竞态 → 亮屏亮度跳变(真机「跳高到 60 /
|
* 默认 60;panel ON 恢复 60 与 launcher 按用户设置恢复 7 竞态 → 亮屏亮度跳变(真机「跳高到 60 /
|
||||||
* 又降回 7」,2026-09-03)。
|
* 又降回 7」,2026-09-03)。旧公式里的 `:60` 已在 M20 移除。
|
||||||
* 与 launcher meow_settings_brightness_clamped() 同源(同文件、同 clamp:下限 display.brightness_min
|
* 与 launcher meow_settings_brightness_clamped() 同源(同文件、同 clamp:下限 display.brightness_min
|
||||||
* 默认 1=极暗但可见、上限 100=防闪屏);本进程自身 wd_vol_load 早已读该文件,不引入 launcher 全量
|
* 默认 1=极暗但可见、上限 100=防闪屏);本进程自身 wd_vol_load 早已读该文件,不引入 launcher 全量
|
||||||
* 依赖,保持 watchdog 独立性。返回 clamped 值;settings 文件不可读(S70 早启动 /usr/data 未就绪)
|
* 依赖,保持 watchdog 独立性。返回 clamped 值;settings 文件不可读(S70 早启动 /usr/data 未就绪)
|
||||||
* → 返回 -1,调用方回退 wd_saved_bri(保持旧行为,防开机竞态)。 */
|
* M20 起:fopen 失败做 WD_SETTINGS_TRIES 次、间隔 WD_SETTINGS_GAP_MS 的短重试,
|
||||||
|
* 覆盖 settings.conf 原子替换 / 开机早期 /usr/data 未就绪的瞬时窗口;仍失败才返回 -1。
|
||||||
|
* 只重试 open:rename 是原子的,已打开的 fd 指向旧 inode、内容完整,读内容无需重试。
|
||||||
|
* 返回 -1 时调用方**不再**回退「默认 60」,取值语义统一由 wd_bri_decide() 决定。 */
|
||||||
static int wd_settings_brightness(void)
|
static int wd_settings_brightness(void)
|
||||||
{
|
{
|
||||||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
FILE *f = NULL;
|
||||||
|
for (int i = 0; i < WD_SETTINGS_TRIES; i++) {
|
||||||
|
f = fopen(wd_settings_path(), "r");
|
||||||
|
if (f) break;
|
||||||
|
if (i + 1 < WD_SETTINGS_TRIES) meow_time32_sleep_us(WD_SETTINGS_GAP_MS * 1000);
|
||||||
|
}
|
||||||
if (!f) return -1;
|
if (!f) return -1;
|
||||||
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
|
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
|
||||||
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
||||||
@@ -247,6 +309,42 @@ static int wd_settings_brightness(void)
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 读电源阈值(power.longpress_menu_ms / power.forceoff_ms,缺省 3000/8000)。
|
||||||
|
* 与 wd_settings_brightness 同款重试窗口;文件不可读时**保留缺省**(绝不退回 2000——
|
||||||
|
* 旧硬编码会让 App 内 2s 出菜单、主屏 3s,且 PWR_TOGGLE==PWR_MENU 时「长按不出菜单」
|
||||||
|
* 的区间不存在)。解析后经 pure helper 清洗(非正数→缺省;forceoff≤longpress→抬 1s)。 */
|
||||||
|
static void wd_power_settings_load(void)
|
||||||
|
{
|
||||||
|
FILE *f = NULL;
|
||||||
|
for (int i = 0; i < WD_SETTINGS_TRIES; i++) {
|
||||||
|
f = fopen(wd_settings_path(), "r");
|
||||||
|
if (f) break;
|
||||||
|
if (i + 1 < WD_SETTINGS_TRIES) meow_time32_sleep_us(WD_SETTINGS_GAP_MS * 1000);
|
||||||
|
}
|
||||||
|
if (!f) {
|
||||||
|
wd_pwr_resolve_thresholds(0, 0, &wd_pwr_longpress_ms, &wd_pwr_forceoff_ms);
|
||||||
|
wd_log("power: settings unreadable -> defaults longpress=%llums forceoff=%llums",
|
||||||
|
wd_pwr_longpress_ms, wd_pwr_forceoff_ms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int lp = 0, fo = 0;
|
||||||
|
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++;
|
||||||
|
if (!strcmp(k, "power.longpress_menu_ms")) lp = atoi(v);
|
||||||
|
else if (!strcmp(k, "power.forceoff_ms")) fo = atoi(v);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
wd_pwr_resolve_thresholds(lp, fo, &wd_pwr_longpress_ms, &wd_pwr_forceoff_ms);
|
||||||
|
wd_log("power: settings longpress=%llums forceoff=%llums (raw %d/%d)",
|
||||||
|
wd_pwr_longpress_ms, wd_pwr_forceoff_ms, lp, fo);
|
||||||
|
}
|
||||||
|
|
||||||
/* 强制 re-pan 到 buffer0:FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位到非 buffer0/失效
|
/* 强制 re-pan 到 buffer0:FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位到非 buffer0/失效
|
||||||
* 位置(launcher M2.8 Bug-3-residual 根因),而应用始终画 buffer0,必须无条件把显示翻回
|
* 位置(launcher M2.8 Bug-3-residual 根因),而应用始终画 buffer0,必须无条件把显示翻回
|
||||||
* buffer0,否则唤醒后黑屏/旧帧。 */
|
* buffer0,否则唤醒后黑屏/旧帧。 */
|
||||||
@@ -261,26 +359,53 @@ static void wd_pan_buf0_force(void)
|
|||||||
ioctl(wd_fbfd, FBIOPAN_DISPLAY, v);
|
ioctl(wd_fbfd, FBIOPAN_DISPLAY, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 亮屏亮度决策(纯函数:宿主单测直接断言,见 test_wd_fw.c)。
|
||||||
|
* 返回 >0 = 要写入的背光值;返回 -1 = **不写背光**(保持当前值)。
|
||||||
|
* *src 输出取值来源,原样写进日志供真机排查:
|
||||||
|
* settings settings.conf 的 display.brightness(已 clamp)—— 唯一权威
|
||||||
|
* snapshot 本次运行期熄屏前读到的真实亮度(>0 才算数)—— settings 瞬时不可读时的过渡值
|
||||||
|
* untouched settings 与快照都未知、但面板已亮 → 不覆盖现场(M20 核心修复)
|
||||||
|
* safe-low settings/快照都未知且面板当前为 0(黑)→ 点亮,用低亮而不是 60
|
||||||
|
* 优先级:settings → 快照(真实近似值)→ 保持现状 → 安全低亮。
|
||||||
|
* 任何分支都不再写「固定 60」:60 只在固件 S11 画完开机 logo 后写一次(固件侧问题,非本进程)。 */
|
||||||
|
static int wd_bri_decide(int user, int saved, int cur, const char **src)
|
||||||
|
{
|
||||||
|
if (user > 0) { *src = "settings"; return user; } /* 用户设置优先 */
|
||||||
|
if (saved > 0) { *src = "snapshot"; return saved; } /* 真实观测到的近似值 */
|
||||||
|
if (cur > 0) { *src = "untouched"; return -1; } /* 已有可见亮度:绝不覆盖 */
|
||||||
|
*src = "safe-low"; return WD_BRI_SAFE_LOW; /* 面板黑 + 无任何值来源 */
|
||||||
|
}
|
||||||
|
|
||||||
static void wd_panel_off(void)
|
static void wd_panel_off(void)
|
||||||
{
|
{
|
||||||
int br = read_brightness();
|
int br = read_brightness();
|
||||||
wd_saved_bri = (br > 0) ? br : 60;
|
/* M20:快照只记「真实观测到的正亮度」。br<=0(读失败,或 launcher meow_panel_off 抢先
|
||||||
|
* 写成 0)时保留上一次已知真实值,绝不写死 60 —— 否则 60 会在下次亮屏被当成用户值回放
|
||||||
|
* (远古 bug 的燃料)。真的一次都没观测到正亮度时保持 WD_BRI_UNKNOWN。 */
|
||||||
|
if (br > 0) wd_saved_bri = br;
|
||||||
write_brightness(0);
|
write_brightness(0);
|
||||||
if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 4); /* 面板下电 + 挂起 goodix 触摸 */
|
if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 4); /* 面板下电 + 挂起 goodix 触摸 */
|
||||||
wd_log("power: panel OFF (was %d -> 0, blank 4)", br);
|
wd_log("power: panel OFF (was %d -> 0, blank 4, saved=%d)", br, wd_saved_bri);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void wd_panel_on(void)
|
static void wd_panel_on(void)
|
||||||
{
|
{
|
||||||
if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 0);
|
if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 0);
|
||||||
/* M6.23:恢复亮度以**用户设置**为准(wd_settings_brightness),settings 不可用时才回退
|
/* M6.23 + M20:用户设置为唯一权威;settings 瞬时不可读用真实快照;两者都未知时
|
||||||
* wd_saved_bri 快照(再退默认 60)——与 launcher meow_panel_on 同语义,杜绝「亮屏恢复 60
|
* **不写背光**(旧代码在这里写死 60,直接覆盖 launcher 刚按 display.brightness 设好的
|
||||||
* 与 launcher 恢复 7 竞态 → 跳变」。 */
|
* 用户值 → 开机/亮屏跳 60,真机 0.8.46 实证)。取值分支见 wd_bri_decide()。 */
|
||||||
int b = wd_settings_brightness();
|
int user = wd_settings_brightness(); /* >0=用户设置;-1=不可读 */
|
||||||
int restore = (b > 0) ? b : (wd_saved_bri > 0 ? wd_saved_bri : 60);
|
int cur = read_brightness(); /* 当前系统背光:判定「不写」是否安全 + 日志 */
|
||||||
write_brightness(restore);
|
const char *src = "?";
|
||||||
|
int restore = wd_bri_decide(user, wd_saved_bri, cur, &src);
|
||||||
|
if (restore > 0) write_brightness(restore);
|
||||||
wd_pan_buf0_force(); /* 唤醒后强制回 buffer0 */
|
wd_pan_buf0_force(); /* 唤醒后强制回 buffer0 */
|
||||||
wd_log("power: panel ON (blank 0 + brightness %d, user=%d saved=%d)", restore, b, wd_saved_bri);
|
if (restore > 0)
|
||||||
|
wd_log("power: panel ON (blank 0 + brightness %d src=%s user=%d saved=%d cur=%d)",
|
||||||
|
restore, src, user, wd_saved_bri, cur);
|
||||||
|
else
|
||||||
|
wd_log("power: panel ON (blank 0 + brightness UNTOUCHED cur=%d src=%s user=%d saved=%d)",
|
||||||
|
cur, src, user, wd_saved_bri);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* launcher 正在画电源菜单(/tmp/meow_power_menu_active 存在)→ watchdog 完全让位。 */
|
/* launcher 正在画电源菜单(/tmp/meow_power_menu_active 存在)→ watchdog 完全让位。 */
|
||||||
@@ -297,15 +422,17 @@ static void wd_do_poweroff(void)
|
|||||||
syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
|
syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
|
||||||
LINUX_REBOOT_CMD_POWER_OFF, NULL);
|
LINUX_REBOOT_CMD_POWER_OFF, NULL);
|
||||||
wd_log("sys_reboot returned: %s", strerror(errno)); /* 正常不会返回 */
|
wd_log("sys_reboot returned: %s", strerror(errno)); /* 正常不会返回 */
|
||||||
for (;;) usleep(1000000);
|
for (;;) meow_time32_sleep_us(1000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 电源键(KEY_POWER)边沿驱动:单击(短按松开 <3s)= 息屏/亮屏;按下时屏已黑 = 只唤醒。
|
/* 电源键(KEY_POWER)边沿驱动:单击(松开时 held < power.longpress_menu_ms,缺省 3s)= 息屏/亮屏;
|
||||||
* 长按(≥3s)由 wd_power_tick() 在轮询中检测 → 写 /tmp/meow_power_menu 请 launcher 弹菜单;
|
* 按下时屏已黑 = 只唤醒。长按(≥longpress)由 wd_power_tick() 在轮询中检测 → 写
|
||||||
* 松开边沿不再 toggle(菜单/长按语义归 launcher 菜单,防「长按后仍 toggle」)。 */
|
* /tmp/meow_power_menu 请 launcher 弹菜单;松开边沿不再 toggle(菜单/长按语义归 launcher
|
||||||
|
* 菜单,防「长按后仍 toggle」)。 */
|
||||||
static void handle_power(int val)
|
static void handle_power(int val)
|
||||||
{
|
{
|
||||||
if (wd_menu_active()) return; /* 电源菜单期:launcher 独占物理键语义 */
|
if (wd_menu_active()) return; /* 电源菜单期:launcher 独占物理键语义 */
|
||||||
|
if (val == 2) return; /* EV_KEY 重复值:本设备 event0 无 EV_REP,防御性忽略 */
|
||||||
if (val) {
|
if (val) {
|
||||||
/* 按下边沿 */
|
/* 按下边沿 */
|
||||||
pwr_down = 1;
|
pwr_down = 1;
|
||||||
@@ -315,28 +442,40 @@ static void handle_power(int val)
|
|||||||
int br = read_brightness();
|
int br = read_brightness();
|
||||||
pwr_woke = (br <= 0) ? 1 : 0; /* 屏已黑:本次电源=唤醒,松开不 toggle */
|
pwr_woke = (br <= 0) ? 1 : 0; /* 屏已黑:本次电源=唤醒,松开不 toggle */
|
||||||
if (pwr_woke) wd_panel_on(); /* 立即亮屏(launcher 自动息屏唤醒路径同步醒,双保险幂等) */
|
if (pwr_woke) wd_panel_on(); /* 立即亮屏(launcher 自动息屏唤醒路径同步醒,双保险幂等) */
|
||||||
wd_log("power down (bri=%d woke=%d)", br, pwr_woke);
|
wd_log("power down (bri=%d woke=%d longpress=%llums)", br, pwr_woke,
|
||||||
|
wd_pwr_longpress_ms);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* 松开边沿 */
|
/* 松开边沿 */
|
||||||
if (!pwr_down) return; /* 未跟踪到按下(启动前已按住/残留):忽略,防误 toggle */
|
if (!pwr_down) return; /* 未跟踪到按下(启动前已按住/残留):忽略,防误 toggle */
|
||||||
pwr_down = 0;
|
pwr_down = 0;
|
||||||
unsigned long long held = now_ms() - pwr_down_ms;
|
unsigned long long held = now_ms() - pwr_down_ms;
|
||||||
if (pwr_woke) { pwr_woke = 0; wd_log("power release after wake (%llums) -> no toggle", held); return; }
|
int br = read_brightness();
|
||||||
|
wd_pwr_release_act act = wd_pwr_release_action(pwr_woke, pwr_menu_notified, held,
|
||||||
|
wd_pwr_longpress_ms, br);
|
||||||
|
if (pwr_woke) {
|
||||||
|
pwr_woke = 0;
|
||||||
|
wd_log("power release after wake (%llums) -> no toggle", held);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (pwr_menu_notified) {
|
if (pwr_menu_notified) {
|
||||||
/* 本次已请求电源菜单(≥3s):菜单是否画出由 launcher 负责,这里绝不 toggle。 */
|
/* 本次已请求电源菜单(≥longpress):菜单是否画出由 launcher 负责,这里绝不 toggle。 */
|
||||||
wd_log("power release after menu req (%llums) -> menu owns toggle", held);
|
wd_log("power release after menu req (%llums) -> menu owns toggle", held);
|
||||||
pwr_menu_notified = 0;
|
pwr_menu_notified = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (held >= PWR_TOGGLE_MS) { wd_log("power hold %llums -> no-op (menu/force-off in tick)", held); return; }
|
if (act == WD_PWR_REL_NONE) {
|
||||||
int br = read_brightness();
|
/* held ≥ longpress 但 tick 还没发出菜单(轮询粒度/边界):仍按长按处理,不 toggle。 */
|
||||||
if (br > 0) wd_panel_off();
|
wd_log("power hold %llums -> no-op (menu/force-off in tick)", held);
|
||||||
else wd_panel_on(); /* 屏已黑仍短按:亮屏 */
|
return;
|
||||||
|
}
|
||||||
|
if (act == WD_PWR_REL_TOGGLE_OFF) wd_panel_off();
|
||||||
|
else wd_panel_on(); /* 屏已黑仍短按:亮屏 */
|
||||||
wd_log("power short-press %llums -> toggle panel (bri was %d)", held, br);
|
wd_log("power short-press %llums -> toggle panel (bri was %d)", held, br);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 电源长按检测(每轮询 tick 调):≥3s 写菜单请求;≥8s 强制关机兜底(仅当菜单未生效时)。 */
|
/* 电源长按检测(每轮询 tick 调):≥longpress 写菜单请求;≥forceoff 强制关机兜底
|
||||||
|
* (launcher 已接管菜单时顶部即让位,不会走到这里)。阈值来自 settings。 */
|
||||||
static void wd_power_tick(void)
|
static void wd_power_tick(void)
|
||||||
{
|
{
|
||||||
/* launcher 已接管电源菜单:本进程完全让位,清空电源状态,下一手势从零开始。 */
|
/* launcher 已接管电源菜单:本进程完全让位,清空电源状态,下一手势从零开始。 */
|
||||||
@@ -347,14 +486,15 @@ static void wd_power_tick(void)
|
|||||||
}
|
}
|
||||||
if (!pwr_down) return;
|
if (!pwr_down) return;
|
||||||
unsigned long long held = now_ms() - pwr_down_ms;
|
unsigned long long held = now_ms() - pwr_down_ms;
|
||||||
if (held >= PWR_MENU_MS && !pwr_menu_notified) {
|
wd_pwr_tick_act act = wd_pwr_tick_action(0, pwr_down, pwr_menu_notified, pwr_force_done,
|
||||||
|
held, wd_pwr_longpress_ms, wd_pwr_forceoff_ms);
|
||||||
|
if (act == WD_PWR_TICK_MENU) {
|
||||||
int fd = open(PWR_MENU_MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
int fd = open(PWR_MENU_MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
if (fd >= 0) { write(fd, "1", 1); close(fd); }
|
if (fd >= 0) { write(fd, "1", 1); close(fd); }
|
||||||
pwr_menu_notified = 1;
|
pwr_menu_notified = 1;
|
||||||
wd_log("power hold %llums -> %s (launcher draws menu)", held, PWR_MENU_MARKER);
|
wd_log("power hold %llums -> %s (launcher draws menu)", held, PWR_MENU_MARKER);
|
||||||
}
|
} else if (act == WD_PWR_TICK_FORCEOFF) {
|
||||||
if (held >= PWR_FORCE_MS && !pwr_force_done && !pwr_menu_notified) {
|
/* 长按 ≥forceoff 仍无菜单接管(launcher 未响应/未消费)→ 强制关机兜底。 */
|
||||||
/* 长按 8s 仍无菜单接管(launcher 未响应/未消费)→ 强制关机兜底。 */
|
|
||||||
pwr_force_done = 1;
|
pwr_force_done = 1;
|
||||||
wd_log("power hold %llums -> FORCE POWER OFF", held);
|
wd_log("power hold %llums -> FORCE POWER OFF", held);
|
||||||
wd_do_poweroff();
|
wd_do_poweroff();
|
||||||
@@ -372,7 +512,7 @@ static int wd_vol_step = 1; /* 跳步 */
|
|||||||
static void wd_vol_load(void)
|
static void wd_vol_load(void)
|
||||||
{
|
{
|
||||||
wd_vol = 60; wd_vol_max = 100; wd_vol_step = 1;
|
wd_vol = 60; wd_vol_max = 100; wd_vol_step = 1;
|
||||||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
FILE *f = fopen(wd_settings_path(), "r");
|
||||||
if (!f) { wd_log("vol: no settings, default %d", wd_vol); return; }
|
if (!f) { wd_log("vol: no settings, default %d", wd_vol); return; }
|
||||||
char buf[128];
|
char buf[128];
|
||||||
while (fgets(buf, sizeof buf, f)) {
|
while (fgets(buf, sizeof buf, f)) {
|
||||||
@@ -391,13 +531,17 @@ static void wd_vol_load(void)
|
|||||||
wd_log("vol: loaded volume=%d max=%d step=%d", wd_vol, wd_vol_max, wd_vol_step);
|
wd_log("vol: loaded volume=%d max=%d step=%d", wd_vol, wd_vol_max, wd_vol_step);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 写真实 DAC(CS43198, numid 1/2, 0-255),与 launcher meow_dac_vol_write 同路径。 */
|
/* 写真实 DAC(CS43198, numid 1/2, 0-255),与 launcher meow_dac_vol_write 同路径。
|
||||||
|
* ⚠️ 音量收编 v2 兜底函数:仅 audiod 未起(HibyOS/开机早期)时由 wd_vol_adj 回退调用;
|
||||||
|
* MeowOS 常态一律走 wd_audiod_set_volume(audiod 唯一权威,防多写者漂移)。
|
||||||
|
* 锥度为定案平方锥度(与 audiod dac_vol_set 同款,勿改语义:CS43198=衰减量,0 最响/255 静音)。 */
|
||||||
static void wd_vol_write(int pct)
|
static void wd_vol_write(int pct)
|
||||||
{
|
{
|
||||||
int t = 100 - pct, dac;
|
int t = 100 - pct, dac;
|
||||||
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
|
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
|
||||||
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
|
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
|
||||||
if (dac > 255) dac = 255; if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
|
if (dac > 255) dac = 255;
|
||||||
|
if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
|
||||||
char cmd[160];
|
char cmd[160];
|
||||||
snprintf(cmd, sizeof cmd,
|
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",
|
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
|
||||||
@@ -406,14 +550,97 @@ static void wd_vol_write(int pct)
|
|||||||
wd_log("DAC vol -> L/R=%d (pct %d)", dac, pct);
|
wd_log("DAC vol -> L/R=%d (pct %d)", dac, pct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 音量收编 v2(FUTURE_AUDIO 事项三):首选 audiod 权威 MSG_AUDIO_VOLUME ----
|
||||||
|
* watchdog 为 per-App 小进程,audiod sock 0666 无鉴权可直接连。一次性短连接(按键 rare,
|
||||||
|
* 无常驻连接/陈旧态负担):非阻塞 connect + poll 300ms,SO_SNDTIMEO/RCVTIMEO 1s,发 i32 pct
|
||||||
|
* 等 MSG_ACK。audiod 侧已异步化(末值去抖,ACK=已应用+已持久化),正常 <<1s 返回。
|
||||||
|
* 返回 0=audiod 已受理(DAC 写+持久化均由 audiod 完成,本进程不再写 settings 防双写);
|
||||||
|
* -1=audiod 缺席/失败(调用方回退 wd_vol_write 本地直写 + wd_vol_save 本地持久化)。 */
|
||||||
|
#define WD_AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock" /* 同 audiod AUD_SOCK_DEFAULT */
|
||||||
|
#define WD_AUD_CONN_TMO_MS 300
|
||||||
|
#define WD_AUD_IO_TMO_MS 1000
|
||||||
|
|
||||||
|
static const char *wd_aud_sock(void)
|
||||||
|
{
|
||||||
|
const char *e = getenv("MEOW_AUDIO_SOCK"); /* 与 SDK sock_path() 同款 env(宿主测覆盖) */
|
||||||
|
return (e && e[0]) ? e : WD_AUD_SOCK_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wd_wr_all(int fd, const void *p, size_t n)
|
||||||
|
{
|
||||||
|
const uint8_t *b = p; size_t o = 0;
|
||||||
|
while (o < n) { ssize_t r = write(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } o += (size_t)r; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static int wd_rd_all(int fd, void *p, size_t n)
|
||||||
|
{
|
||||||
|
uint8_t *b = p; size_t o = 0;
|
||||||
|
while (o < n) { ssize_t r = read(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } if (r == 0) return -1; o += (size_t)r; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int wd_audiod_set_volume(int pct)
|
||||||
|
{
|
||||||
|
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", wd_aud_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, WD_AUD_CONN_TMO_MS) > 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); /* 回阻塞模式;读写超时由 SO_*TIMEO 兜底 */
|
||||||
|
struct timeval tv;
|
||||||
|
tv.tv_sec = WD_AUD_IO_TMO_MS / 1000; tv.tv_usec = (WD_AUD_IO_TMO_MS % 1000) * 1000;
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||||
|
/* 发 MSG_AUDIO_VOLUME(i32 pct);req_id 任意(一次性连接单请求) */
|
||||||
|
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
|
||||||
|
uint32_t magic = MEOW_IPC_MAGIC, flen = sizeof(int32_t), freq = 1;
|
||||||
|
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
|
||||||
|
memcpy(frame + 0, &magic, 4);
|
||||||
|
memcpy(frame + 4, &ver, 2);
|
||||||
|
memcpy(frame + 6, &type, 2);
|
||||||
|
memcpy(frame + 8, &freq, 4);
|
||||||
|
memcpy(frame + 12, &flen, 4);
|
||||||
|
int32_t p32 = pct;
|
||||||
|
memcpy(frame + MEOW_IPC_HDR_LEN, &p32, sizeof p32);
|
||||||
|
if (wd_wr_all(fd, frame, sizeof frame) != 0) { close(fd); return -1; }
|
||||||
|
/* 等 MSG_ACK(有界:RCVTIMEO;audiod 异步执行点应用后回 ACK) */
|
||||||
|
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||||
|
if (wd_rd_all(fd, hdr, sizeof hdr) != 0) { close(fd); return -1; }
|
||||||
|
uint32_t rmagic, rlen;
|
||||||
|
uint16_t rver, rtype;
|
||||||
|
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 || rtype != MSG_ACK || rlen > 64) { close(fd); return -1; }
|
||||||
|
uint8_t pl[64];
|
||||||
|
if (rlen && wd_rd_all(fd, pl, rlen) != 0) { close(fd); return -1; }
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 ——
|
/* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 ——
|
||||||
* 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。
|
* 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。
|
||||||
* 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换),
|
* 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换),
|
||||||
* 与 SDK meow_ui_vol_set 同款语义。 */
|
* 与 SDK meow_ui_vol_set 同款语义。 */
|
||||||
static void wd_vol_save(void)
|
static void wd_vol_save(void)
|
||||||
{
|
{
|
||||||
system("mkdir -p /usr/data/meow 2>/dev/null");
|
const char *path = wd_settings_path();
|
||||||
const char *path = MEOW_SETTINGS_FILE;
|
if (strcmp(path, MEOW_SETTINGS_FILE) == 0) /* 仅默认路径补目录(env 覆盖=宿主测,目录自备) */
|
||||||
|
system("mkdir -p /usr/data/meow 2>/dev/null");
|
||||||
char tmp[300];
|
char tmp[300];
|
||||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||||
FILE *in = fopen(path, "r");
|
FILE *in = fopen(path, "r");
|
||||||
@@ -490,7 +717,9 @@ static int wd_jack_debounce(void)
|
|||||||
(now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS);
|
(now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。 */
|
/* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。
|
||||||
|
* 音量收编 v2:首选 audiod 权威(IPC;DAC 写+持久化 audiod 完成);audiod 缺席才本地
|
||||||
|
* amixer 直写 + 本地持久化兜底(wd_vol 本地缓存仅作步进基数,两路都维护)。 */
|
||||||
static void wd_vol_adj(int dir)
|
static void wd_vol_adj(int dir)
|
||||||
{
|
{
|
||||||
int target = wd_vol + dir * wd_vol_step;
|
int target = wd_vol + dir * wd_vol_step;
|
||||||
@@ -498,9 +727,13 @@ static void wd_vol_adj(int dir)
|
|||||||
if (target > wd_vol_max) target = wd_vol_max;
|
if (target > wd_vol_max) target = wd_vol_max;
|
||||||
if (target == wd_vol) { wd_log("volume %s at limit (%d)", dir > 0 ? "UP" : "DOWN", wd_vol); return; }
|
if (target == wd_vol) { wd_log("volume %s at limit (%d)", dir > 0 ? "UP" : "DOWN", wd_vol); return; }
|
||||||
wd_vol = target;
|
wd_vol = target;
|
||||||
wd_vol_write(wd_vol);
|
if (wd_audiod_set_volume(wd_vol) == 0) {
|
||||||
|
wd_log("volume %s -> %d (via audiod)", dir > 0 ? "UP" : "DOWN", wd_vol);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wd_vol_write(wd_vol); /* 兜底:仅 audiod 未起(HibyOS/开机早期) */
|
||||||
wd_vol_save();
|
wd_vol_save();
|
||||||
wd_log("volume %s -> %d", dir > 0 ? "UP" : "DOWN", wd_vol);
|
wd_log("volume %s -> %d (local amixer fallback, audiod absent)", dir > 0 ? "UP" : "DOWN", wd_vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handle_touch(int immersive, int app_pid,
|
static void handle_touch(int immersive, int app_pid,
|
||||||
@@ -631,6 +864,7 @@ int main(int argc, char **argv)
|
|||||||
setvbuf(stderr, NULL, _IONBF, 0);
|
setvbuf(stderr, NULL, _IONBF, 0);
|
||||||
wd_log_init();
|
wd_log_init();
|
||||||
wd_vol_load(); /* M2.24:音量初始值读自与 launcher 同源 settings.conf */
|
wd_vol_load(); /* M2.24:音量初始值读自与 launcher 同源 settings.conf */
|
||||||
|
wd_power_settings_load(); /* M40:电源长按/强关阈值(缺省 3000/8000,与 launcher 主屏同源) */
|
||||||
/* M2.86(226):SDK 框架 App 判定(app.json framework=1)-> 音量/媒体键让位 SDK。 */
|
/* M2.86(226):SDK 框架 App 判定(app.json framework=1)-> 音量/媒体键让位 SDK。 */
|
||||||
g_fw_app = wd_framework_from_pid(app_pid);
|
g_fw_app = wd_framework_from_pid(app_pid);
|
||||||
wd_log("app_pid=%d sim_tap=%d sim_swipe=%d framework=%d%s",
|
wd_log("app_pid=%d sim_tap=%d sim_swipe=%d framework=%d%s",
|
||||||
@@ -696,7 +930,7 @@ int main(int argc, char **argv)
|
|||||||
if (sim_tap && elapsed >= (long)sim_tap * 1000) {
|
if (sim_tap && elapsed >= (long)sim_tap * 1000) {
|
||||||
wd_log("SIM TAP");
|
wd_log("SIM TAP");
|
||||||
kill(app_pid, SIGTERM);
|
kill(app_pid, SIGTERM);
|
||||||
usleep(1000000);
|
meow_time32_sleep_us(1000000);
|
||||||
kill(app_pid, SIGKILL);
|
kill(app_pid, SIGKILL);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,11 +1,15 @@
|
|||||||
/* test_wd_fw.c — 宿主单测:meow_watchdog 的「SDK 框架 App 判定」逻辑(M2.86, issue 226)。
|
/* test_wd_fw.c — 宿主单测:meow_watchdog 的纯逻辑(M2.86 框架 App 判定 + M20 亮屏亮度 +
|
||||||
|
* M40 time32 取时/睡眠 + 电源阈值行为矩阵)。
|
||||||
*
|
*
|
||||||
* 编译:#include meow_watchdog.c(去 main 拿 wd_app_is_framework/wd_json_str/wd_framework_from_pid)。
|
* 编译:#include meow_watchdog.c(去 main 拿 wd_app_is_framework/json_str/from_pid、
|
||||||
|
* wd_bri_decide、wd_settings_brightness、wd_panel_off/on;wd_power.h 的纯判定)。
|
||||||
* gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
* gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
||||||
* test_wd_fw.c -o test_wd_fw
|
* test_wd_fw.c ../sdk/meow_time32.c -o test_wd_fw
|
||||||
* (meow_watchdog.c 含仅 main 使用的静态函数,宿主去 main 后须 -Wno-unused-function 静音;
|
* (meow_watchdog.c 含仅 main 使用的静态函数,宿主去 main 后须 -Wno-unused-function 静音;
|
||||||
* mips 目标构建不定义 WD_NO_MAIN,不受影响。)
|
* mips 目标构建不定义 WD_NO_MAIN,不受影响。)
|
||||||
* 运行:./test_wd_fw;全 PASS -> exit 0。
|
* 运行:./test_wd_fw;全 PASS -> exit 0。
|
||||||
|
* M20 用例用 env MEOW_BACKLIGHT / MEOW_SETTINGS_FILE 把背光与 settings 指到临时文件,
|
||||||
|
* 真的走 wd_panel_off/wd_panel_on 的读写路径(真机无这两个 env,走 /sys 与 /usr/data 默认)。
|
||||||
*/
|
*/
|
||||||
#define WD_NO_MAIN
|
#define WD_NO_MAIN
|
||||||
#include "meow_watchdog.c"
|
#include "meow_watchdog.c"
|
||||||
@@ -24,6 +28,14 @@ static void check(const char *name, int got, int want)
|
|||||||
else { g_fail++; printf("FAIL %s: got %d want %d\n", name, got, want); }
|
else { g_fail++; printf("FAIL %s: got %d want %d\n", name, got, want); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 临时目录根:优先 $TMPDIR(Termux/Android 上 /tmp 属于 shell 用户、本进程不可写),
|
||||||
|
* 否则 /tmp(Linux 构建容器)。让 `build_watchdog.sh host` 在两种环境都能直接跑。 */
|
||||||
|
static const char *tmp_root(void)
|
||||||
|
{
|
||||||
|
const char *t = getenv("TMPDIR");
|
||||||
|
return (t && t[0]) ? t : "/tmp";
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- M2.87:SIGUSR1 手势让位(handle_touch 左缘滑动分支)----
|
/* ---- M2.87:SIGUSR1 手势让位(handle_touch 左缘滑动分支)----
|
||||||
* 判定(meow_watchdog.c):`!g_fw_app && immersive` 才发 SIGUSR1(只给裸沉浸 App);
|
* 判定(meow_watchdog.c):`!g_fw_app && immersive` 才发 SIGUSR1(只给裸沉浸 App);
|
||||||
* SDK 框架 App(g_fw_app=1)不发(返回由 SDK on_swipe 处理),防 SIGUSR1 杀死 App。 */
|
* SDK 框架 App(g_fw_app=1)不发(返回由 SDK on_swipe 处理),防 SIGUSR1 杀死 App。 */
|
||||||
@@ -68,10 +80,233 @@ static int expect_fw(const char *base, const char *name, const char *appjson, in
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- M20:亮屏亮度决策(wd_bri_decide 纯函数)----
|
||||||
|
* 真机 bug(0.8.46,2026-11-05):settings display.brightness=1,亮屏时 watchdog 写入固定 60
|
||||||
|
* 覆盖它。新语义:settings 优先 → 真实快照 → 面板已亮则不写 → 面板黑才写安全低亮。 */
|
||||||
|
static void test_bri_decide(void)
|
||||||
|
{
|
||||||
|
const char *src = NULL;
|
||||||
|
int v;
|
||||||
|
|
||||||
|
v = wd_bri_decide(1, 60, 60, &src); /* 用户值 1 对上被污染的 60/当前 60:必须写 1 */
|
||||||
|
check("bri: settings wins over saved/cur", v == 1 && !strcmp(src, "settings"), 1);
|
||||||
|
v = wd_bri_decide(7, 0, 7, &src);
|
||||||
|
check("bri: settings 7", v == 7 && !strcmp(src, "settings"), 1);
|
||||||
|
v = wd_bri_decide(-1, 42, 3, &src); /* settings 不可读 → 用真实快照 */
|
||||||
|
check("bri: snapshot fallback", v == 42 && !strcmp(src, "snapshot"), 1);
|
||||||
|
v = wd_bri_decide(-1, 0, 3, &src); /* 都未知但面板已亮 → 不写(核心修复) */
|
||||||
|
check("bri: untouched keeps cur", v == -1 && !strcmp(src, "untouched"), 1);
|
||||||
|
v = wd_bri_decide(-1, 0, 0, &src); /* 都未知且面板黑 → 安全低亮,绝不 60 */
|
||||||
|
check("bri: safe-low not 60", v == WD_BRI_SAFE_LOW && v != 60 && v > 0 &&
|
||||||
|
!strcmp(src, "safe-low"), 1);
|
||||||
|
v = wd_bri_decide(-1, 0, -1, &src); /* 背光读失败按「黑」处理,仍要点亮 */
|
||||||
|
check("bri: read-fail -> safe-low", v == WD_BRI_SAFE_LOW && !strcmp(src, "safe-low"), 1);
|
||||||
|
v = wd_bri_decide(0, -1, 5, &src); /* 非正值一律按「未知」,不当作亮度 */
|
||||||
|
check("bri: non-positive treated unknown", v == -1 && !strcmp(src, "untouched"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- M20:settings 解析 + 重试 ----
|
||||||
|
* 用真机 /usr/data/meow/settings.conf 的关键行断言「路径/键名/解析/clamp 都是对的」,
|
||||||
|
* 证明真机写 60 不是本函数返回 -1(原因见报文:出货二进制根本没编进这段代码)。 */
|
||||||
|
static void test_settings_brightness(void)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
snprintf(path, sizeof path, "%s/wdsetXXXXXX", tmp_root());
|
||||||
|
int fd = mkstemp(path);
|
||||||
|
if (fd < 0) { g_fail++; printf("FAIL settings: mkstemp\n"); return; }
|
||||||
|
FILE *f = fdopen(fd, "w");
|
||||||
|
fputs("# meow-settings 1\n"
|
||||||
|
"display.brightness = 1\n"
|
||||||
|
"display.brightness_min = 1\n"
|
||||||
|
"audio.volume = 10\n", f);
|
||||||
|
fclose(f);
|
||||||
|
setenv("MEOW_SETTINGS_FILE", path, 1);
|
||||||
|
check("settings: device file -> 1", wd_settings_brightness(), 1);
|
||||||
|
|
||||||
|
f = fopen(path, "w"); /* clamp 抬到 brightness_min */
|
||||||
|
fputs("display.brightness = 0\ndisplay.brightness_min = 3\n", f);
|
||||||
|
fclose(f);
|
||||||
|
check("settings: clamp up to min", wd_settings_brightness(), 3);
|
||||||
|
|
||||||
|
f = fopen(path, "w"); /* clamp 压到 100 */
|
||||||
|
fputs("display.brightness = 200\ndisplay.brightness_min = 1\n", f);
|
||||||
|
fclose(f);
|
||||||
|
check("settings: clamp down to 100", wd_settings_brightness(), 100);
|
||||||
|
|
||||||
|
f = fopen(path, "w"); /* 键缺失:与 launcher 设置表同默认 101 -> clamp 100 */
|
||||||
|
fputs("audio.volume = 10\n", f);
|
||||||
|
fclose(f);
|
||||||
|
check("settings: missing key -> 100", wd_settings_brightness(), 100);
|
||||||
|
|
||||||
|
/* 不可读(原子替换/未挂载窗口):open 重试 3 次仍失败才 -1,总耗时 ≈ 20ms */
|
||||||
|
unlink(path);
|
||||||
|
unsigned long long t0 = now_ms();
|
||||||
|
int r = wd_settings_brightness();
|
||||||
|
unsigned long long dt = now_ms() - t0;
|
||||||
|
check("settings: missing -> -1", r, -1);
|
||||||
|
check("settings: open retried (>=15ms)", dt >= 15, 1);
|
||||||
|
unsetenv("MEOW_SETTINGS_FILE");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put_file(const char *path, const char *body)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "w");
|
||||||
|
if (!f) return;
|
||||||
|
fputs(body, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_file_int(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return -1;
|
||||||
|
int v = -1;
|
||||||
|
if (fscanf(f, "%d", &v) != 1) v = -1;
|
||||||
|
fclose(f);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- M20:panel OFF/ON 端到端(真的读写一个「背光文件」,不 mock 函数)----
|
||||||
|
* 复现真机场景:settings display.brightness=1 → 熄屏 → 亮屏。旧代码亮屏写 60,
|
||||||
|
* 新代码必须写 1;且 settings 不可读、快照未知时保持当前值而不是回退 60。 */
|
||||||
|
static void test_panel_brightness_e2e(void)
|
||||||
|
{
|
||||||
|
char bl[600], st[600];
|
||||||
|
snprintf(bl, sizeof bl, "%s/wdblXXXXXX", tmp_root());
|
||||||
|
snprintf(st, sizeof st, "%s/wdstXXXXXX", tmp_root());
|
||||||
|
int fd = mkstemp(bl);
|
||||||
|
if (fd < 0) { g_fail++; printf("FAIL e2e: mkstemp bl\n"); return; }
|
||||||
|
close(fd);
|
||||||
|
fd = mkstemp(st);
|
||||||
|
if (fd < 0) { g_fail++; printf("FAIL e2e: mkstemp st\n"); unlink(bl); return; }
|
||||||
|
close(fd);
|
||||||
|
setenv("MEOW_BACKLIGHT", bl, 1);
|
||||||
|
setenv("MEOW_SETTINGS_FILE", st, 1);
|
||||||
|
|
||||||
|
/* 1) 真机 bug 场景:settings=1、屏亮 1 → OFF 快照 1;ON 必须写 1,绝不 60 */
|
||||||
|
put_file(bl, "1\n");
|
||||||
|
put_file(st, "display.brightness = 1\ndisplay.brightness_min = 1\n");
|
||||||
|
wd_saved_bri = WD_BRI_UNKNOWN;
|
||||||
|
wd_panel_off();
|
||||||
|
check("e2e: panel OFF writes 0", get_file_int(bl), 0);
|
||||||
|
check("e2e: panel OFF snapshot = 1", wd_saved_bri, 1);
|
||||||
|
wd_panel_on();
|
||||||
|
check("e2e: panel ON restores settings 1 (not 60)", get_file_int(bl), 1);
|
||||||
|
|
||||||
|
/* 2) 用户改了亮度:settings 仍是唯一权威(旧快照 1 不得覆盖 5) */
|
||||||
|
put_file(st, "display.brightness = 5\ndisplay.brightness_min = 1\n");
|
||||||
|
put_file(bl, "0\n");
|
||||||
|
wd_panel_on();
|
||||||
|
check("e2e: panel ON follows new settings 5", get_file_int(bl), 5);
|
||||||
|
|
||||||
|
/* 3) settings 不可读 + 快照未知 + 屏亮 3 → 不写(保持 3,不再回退 60) */
|
||||||
|
unlink(st);
|
||||||
|
put_file(bl, "3\n");
|
||||||
|
wd_saved_bri = WD_BRI_UNKNOWN;
|
||||||
|
wd_panel_on();
|
||||||
|
check("e2e: panel ON leaves 3 untouched", get_file_int(bl), 3);
|
||||||
|
|
||||||
|
/* 4) settings 不可读 + 快照未知 + 屏黑 0 → 写安全低亮(>0 且 != 60) */
|
||||||
|
put_file(bl, "0\n");
|
||||||
|
wd_panel_on();
|
||||||
|
check("e2e: panel ON dark -> safe-low", get_file_int(bl), WD_BRI_SAFE_LOW);
|
||||||
|
|
||||||
|
/* 5) settings 不可读但本次运行期真观测到 7 → 快照优先于「不动」 */
|
||||||
|
put_file(bl, "7\n");
|
||||||
|
wd_saved_bri = WD_BRI_UNKNOWN;
|
||||||
|
wd_panel_off(); /* 观测到 7,写 0 */
|
||||||
|
check("e2e: panel OFF snapshot = 7", wd_saved_bri, 7);
|
||||||
|
wd_panel_on();
|
||||||
|
check("e2e: panel ON uses real snapshot 7", get_file_int(bl), 7);
|
||||||
|
|
||||||
|
unlink(bl);
|
||||||
|
unsetenv("MEOW_BACKLIGHT");
|
||||||
|
unsetenv("MEOW_SETTINGS_FILE");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- M40:time32 取时/睡眠(与 libc 正常取时一致;o32 syscall 路径由交叉二进制反汇编验证)---- */
|
||||||
|
static void test_time32(void)
|
||||||
|
{
|
||||||
|
struct meow_time32 t;
|
||||||
|
check("time32 gettime ok", meow_time32_clock_gettime(CLOCK_MONOTONIC, &t) == 0, 1);
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
check("time32 sec == libc sec", t.tv_sec == (int32_t)ts.tv_sec, 1);
|
||||||
|
long dn = (long)t.tv_nsec - (long)ts.tv_nsec;
|
||||||
|
if (dn < 0) dn = -dn;
|
||||||
|
check("time32 nsec == libc nsec (<1ms)", dn < 1000000, 1);
|
||||||
|
|
||||||
|
uint64_t a = meow_time32_monotonic_ms();
|
||||||
|
uint64_t b = meow_time32_monotonic_ms();
|
||||||
|
check("monotonic nonzero", a > 0, 1);
|
||||||
|
check("monotonic nondecreasing", b >= a, 1);
|
||||||
|
check("ms from parts 1135.008s", meow_time_ms_from_parts(1135, 8000000) == 1135008ULL, 1);
|
||||||
|
|
||||||
|
unsigned long long s0 = now_ms();
|
||||||
|
meow_time32_sleep_us(20000);
|
||||||
|
unsigned long long el = now_ms() - s0;
|
||||||
|
check("sleep 20ms actually sleeps (>=15ms)", el >= 15, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- M40:电源阈值/边沿行为矩阵(settings 取不同值)---- */
|
||||||
|
static void test_power_matrix(void)
|
||||||
|
{
|
||||||
|
unsigned long long lp = 0, fo = 0;
|
||||||
|
|
||||||
|
wd_pwr_resolve_thresholds(0, 0, &lp, &fo);
|
||||||
|
check("pwr default longpress=3000", lp == 3000, 1);
|
||||||
|
check("pwr default forceoff=8000", fo == 8000, 1);
|
||||||
|
|
||||||
|
wd_pwr_resolve_thresholds(1500, 5000, &lp, &fo);
|
||||||
|
check("pwr settings longpress=1500", lp == 1500, 1);
|
||||||
|
check("pwr settings forceoff=5000", fo == 5000, 1);
|
||||||
|
|
||||||
|
wd_pwr_resolve_thresholds(-1, 0, &lp, &fo);
|
||||||
|
check("pwr invalid -> default longpress", lp == 3000, 1);
|
||||||
|
check("pwr invalid -> default forceoff", fo == 8000, 1);
|
||||||
|
|
||||||
|
wd_pwr_resolve_thresholds(3000, 2000, &lp, &fo);
|
||||||
|
check("pwr forceoff<=longpress lifted to lp+1000", fo == 4000, 1);
|
||||||
|
|
||||||
|
/* 松开边沿:单击 / 长按但未到阈值 / 到阈值 / 唤醒 / 菜单已请求 */
|
||||||
|
check("release 100ms screen-on -> toggle off",
|
||||||
|
wd_pwr_release_action(0, 0, 100, 3000, 50) == WD_PWR_REL_TOGGLE_OFF, 1);
|
||||||
|
check("release 100ms screen-off -> toggle on",
|
||||||
|
wd_pwr_release_action(0, 0, 100, 3000, 0) == WD_PWR_REL_TOGGLE_ON, 1);
|
||||||
|
check("release 2500ms (<3000) still click -> toggle off",
|
||||||
|
wd_pwr_release_action(0, 0, 2500, 3000, 50) == WD_PWR_REL_TOGGLE_OFF, 1);
|
||||||
|
check("release 3000ms (>=longpress) -> no toggle",
|
||||||
|
wd_pwr_release_action(0, 0, 3000, 3000, 50) == WD_PWR_REL_NONE, 1);
|
||||||
|
check("release after wake -> no toggle",
|
||||||
|
wd_pwr_release_action(1, 0, 100, 3000, 0) == WD_PWR_REL_NONE, 1);
|
||||||
|
check("release after menu req -> no toggle",
|
||||||
|
wd_pwr_release_action(0, 1, 3500, 3000, 50) == WD_PWR_REL_NONE, 1);
|
||||||
|
|
||||||
|
/* 按住 tick:单击窗 / 长按出菜单 / 强关兜底 / 菜单已接管 / 已强关 */
|
||||||
|
check("tick 2999ms -> none",
|
||||||
|
wd_pwr_tick_action(0, 1, 0, 0, 2999, 3000, 8000) == WD_PWR_TICK_NONE, 1);
|
||||||
|
check("tick 3000ms -> menu",
|
||||||
|
wd_pwr_tick_action(0, 1, 0, 0, 3000, 3000, 8000) == WD_PWR_TICK_MENU, 1);
|
||||||
|
check("tick 8000ms menu requested, no takeover -> forceoff",
|
||||||
|
wd_pwr_tick_action(0, 1, 1, 0, 8000, 3000, 8000) == WD_PWR_TICK_FORCEOFF, 1);
|
||||||
|
check("tick 8000ms launcher menu active -> none",
|
||||||
|
wd_pwr_tick_action(1, 1, 1, 0, 8000, 3000, 8000) == WD_PWR_TICK_NONE, 1);
|
||||||
|
check("tick forceoff already done -> none",
|
||||||
|
wd_pwr_tick_action(0, 1, 1, 1, 9000, 3000, 8000) == WD_PWR_TICK_NONE, 1);
|
||||||
|
check("tick not down -> none",
|
||||||
|
wd_pwr_tick_action(0, 0, 0, 0, 9000, 3000, 8000) == WD_PWR_TICK_NONE, 1);
|
||||||
|
/* settings 取短值:1000/2000 */
|
||||||
|
check("tick custom 1000ms -> menu",
|
||||||
|
wd_pwr_tick_action(0, 1, 0, 0, 1000, 1000, 2000) == WD_PWR_TICK_MENU, 1);
|
||||||
|
check("tick custom 2000ms no takeover -> forceoff",
|
||||||
|
wd_pwr_tick_action(0, 1, 1, 0, 2000, 1000, 2000) == WD_PWR_TICK_FORCEOFF, 1);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
signal(SIGUSR1, on_sigusr1); /* 捕获 handle_touch 可能发出的 SIGUSR1(测命中) */
|
signal(SIGUSR1, on_sigusr1); /* 捕获 handle_touch 可能发出的 SIGUSR1(测命中) */
|
||||||
char dir[] = "/tmp/wdfwXXXXXX";
|
char dir[600];
|
||||||
|
snprintf(dir, sizeof dir, "%s/wdfwXXXXXX", tmp_root());
|
||||||
if (!mkdtemp(dir)) { perror("mkdtemp"); return 2; }
|
if (!mkdtemp(dir)) { perror("mkdtemp"); return 2; }
|
||||||
|
|
||||||
/* 框架 App:各值形态 */
|
/* 框架 App:各值形态 */
|
||||||
@@ -109,6 +344,15 @@ int main(void)
|
|||||||
check("sigusr1 bare-immersive -> 1", sim_swipe_sigusr1(0, 1), 1); /* 裸沉浸:发(命中) */
|
check("sigusr1 bare-immersive -> 1", sim_swipe_sigusr1(0, 1), 1); /* 裸沉浸:发(命中) */
|
||||||
check("sigusr1 bare-nonimmersive -> 0", sim_swipe_sigusr1(0, 0), 0); /* 裸非沉浸:不发 */
|
check("sigusr1 bare-nonimmersive -> 0", sim_swipe_sigusr1(0, 0), 0); /* 裸非沉浸:不发 */
|
||||||
|
|
||||||
|
/* ---- M20:亮屏亮度(决策 + settings 解析/重试 + panel OFF/ON 端到端)---- */
|
||||||
|
test_bri_decide();
|
||||||
|
test_settings_brightness();
|
||||||
|
test_panel_brightness_e2e();
|
||||||
|
|
||||||
|
/* ---- M40:time32 取时/睡眠 + 电源阈值行为矩阵 ---- */
|
||||||
|
test_time32();
|
||||||
|
test_power_matrix();
|
||||||
|
|
||||||
/* 清理 */
|
/* 清理 */
|
||||||
{
|
{
|
||||||
char path[560];
|
char path[560];
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* wd_power.h — watchdog 电源键判定(纯函数,宿主可单测,无设备/无文件系统依赖)。
|
||||||
|
*
|
||||||
|
* 语义与 launcher 主屏(main_menu.c)统一:
|
||||||
|
* 单击(松开时 held < power.longpress_menu_ms) → 息屏/亮屏切换;
|
||||||
|
* 长按 held ≥ power.longpress_menu_ms → 写菜单请求(watchdog 让位 launcher);
|
||||||
|
* 长按 held ≥ power.forceoff_ms 且菜单未被接管 → 强制关机兜底。
|
||||||
|
* 旧实现把 PWR_TOGGLE_MS 与 PWR_MENU_MS 都硬编码成 2000,导致「长按但不出菜单」的区间
|
||||||
|
* 不存在,且 App 内 2s / launcher 主屏 3s 不一致;现在两者都读 settings(缺省 3000/8000)。
|
||||||
|
*
|
||||||
|
* 阈值清洗:
|
||||||
|
* - 非正数 → 默认 3000 / 8000(settings 允许 0,但 0ms 阈值会让每次按下立刻出菜单,无意义);
|
||||||
|
* - forceoff ≤ longpress → 抬到 longpress+1000,避免「强关先于菜单」的自相矛盾配置。
|
||||||
|
*/
|
||||||
|
#ifndef WD_POWER_H
|
||||||
|
#define WD_POWER_H
|
||||||
|
|
||||||
|
#define WD_PWR_LONGPRESS_DEFAULT_MS 3000
|
||||||
|
#define WD_PWR_FORCEOFF_DEFAULT_MS 8000
|
||||||
|
|
||||||
|
static inline void wd_pwr_resolve_thresholds(int cfg_longpress, int cfg_forceoff,
|
||||||
|
unsigned long long *longpress_ms,
|
||||||
|
unsigned long long *forceoff_ms)
|
||||||
|
{
|
||||||
|
unsigned long long lp = (cfg_longpress > 0)
|
||||||
|
? (unsigned long long)cfg_longpress
|
||||||
|
: WD_PWR_LONGPRESS_DEFAULT_MS;
|
||||||
|
unsigned long long fo = (cfg_forceoff > 0)
|
||||||
|
? (unsigned long long)cfg_forceoff
|
||||||
|
: WD_PWR_FORCEOFF_DEFAULT_MS;
|
||||||
|
if (fo <= lp) fo = lp + 1000;
|
||||||
|
*longpress_ms = lp;
|
||||||
|
*forceoff_ms = fo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 松开边沿的动作。bright>0=屏亮 → 息屏;否则亮屏。
|
||||||
|
* woke(按下时屏已黑,本次只唤醒)/ menu_notified(已请求菜单)→ 不再 toggle。 */
|
||||||
|
typedef enum {
|
||||||
|
WD_PWR_REL_NONE = 0,
|
||||||
|
WD_PWR_REL_TOGGLE_OFF,
|
||||||
|
WD_PWR_REL_TOGGLE_ON
|
||||||
|
} wd_pwr_release_act;
|
||||||
|
|
||||||
|
static inline wd_pwr_release_act wd_pwr_release_action(int woke, int menu_notified,
|
||||||
|
unsigned long long held_ms,
|
||||||
|
unsigned long long longpress_ms,
|
||||||
|
int bright)
|
||||||
|
{
|
||||||
|
if (woke || menu_notified) return WD_PWR_REL_NONE;
|
||||||
|
if (held_ms >= longpress_ms) return WD_PWR_REL_NONE; /* 长按已由 tick 处理 */
|
||||||
|
return (bright > 0) ? WD_PWR_REL_TOGGLE_OFF : WD_PWR_REL_TOGGLE_ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 轮询 tick(按住中)的动作。menu_active=launcher 已接管 → 全让位。
|
||||||
|
* 强关不要求 menu_notified:launcher 未接管菜单时 8s 兜底仍须生效。 */
|
||||||
|
typedef enum {
|
||||||
|
WD_PWR_TICK_NONE = 0,
|
||||||
|
WD_PWR_TICK_MENU,
|
||||||
|
WD_PWR_TICK_FORCEOFF
|
||||||
|
} wd_pwr_tick_act;
|
||||||
|
|
||||||
|
static inline wd_pwr_tick_act wd_pwr_tick_action(int menu_active, int down,
|
||||||
|
int menu_notified, int force_done,
|
||||||
|
unsigned long long held_ms,
|
||||||
|
unsigned long long longpress_ms,
|
||||||
|
unsigned long long forceoff_ms)
|
||||||
|
{
|
||||||
|
if (menu_active || !down) return WD_PWR_TICK_NONE;
|
||||||
|
if (held_ms >= forceoff_ms && !force_done) return WD_PWR_TICK_FORCEOFF;
|
||||||
|
if (held_ms >= longpress_ms && !menu_notified) return WD_PWR_TICK_MENU;
|
||||||
|
return WD_PWR_TICK_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* WD_POWER_H */
|
||||||
@@ -25,6 +25,10 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel
|
|||||||
./build_meowpanel.sh host # 宿主编译 → meowpanel_host(本机 ld,仅供调试)
|
./build_meowpanel.sh host # 宿主编译 → meowpanel_host(本机 ld,仅供调试)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **后端源码已按模块拆分**(`src/*.c` + 共享内部头 `src/mp.h`):`mp_server`(HTTP/路由/main) + `mp_util`/`mp_json` + `mp_store`(设置/主题) + `mp_ipc`(IPC 帧/单一收发) + 各 api_* 域 `mp_perf`/`mp_power`/`mp_audio`/`mp_net`/`mp_ams`/`mp_svcd`/`mp_files`/`mp_fonts`/`mp_apps`/`mp_logs`/`mp_notif`/`mp_auth`/`mp_key`/`mp_screen`。
|
||||||
|
`mp.h` 承载全部共享类型/常量/外部全局 + 跨模块函数原型;每个 `.c` 以 `#include "mp.h"` 起步。
|
||||||
|
拆分保持后端接口/行为**完全不变**(宿主 `-Wall -Wextra` 0 warning;重编产物与拆分前**逐字节一致**——同一 mock 环境全端点 diff 无差异)。
|
||||||
|
原单体 `meowpanel.c` 保留在根目录作参考,**不在构建源列表**(构建以 `src/*.c` 为准)。
|
||||||
- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。
|
- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。
|
||||||
- 产出(本次构建实测):
|
- 产出(本次构建实测):
|
||||||
- `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
|
- `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
|
||||||
@@ -120,8 +124,19 @@ http://<host>.local:8080/
|
|||||||
| POST | `/api/audio/volume` | 写音量(**写级**,PIN 已护):`MSG_AUDIO_VOLUME`,**clamp 到系统 vol_max**(读 settings.conf `audio.volume_max`,缺省 100;设备当前 50);audiod 仅 clamp 0-100 无 vol_max 防护,面板是把守方(防炸耳红线) | ✅(clamp 后写;audiod 不在→503) |
|
| POST | `/api/audio/volume` | 写音量(**写级**,PIN 已护):`MSG_AUDIO_VOLUME`,**clamp 到系统 vol_max**(读 settings.conf `audio.volume_max`,缺省 100;设备当前 50);audiod 仅 clamp 0-100 无 vol_max 防护,面板是把守方(防炸耳红线) | ✅(clamp 后写;audiod 不在→503) |
|
||||||
| GET | `/api/power` | 功耗管理:meow_powermgr IPC `MSG_PM_QUERY→RESULT`(battery/estimate/breakdown,confidence 原样透传) | ✅(powermgr 不在→503 `POWER_UNAVAILABLE`) |
|
| GET | `/api/power` | 功耗管理:meow_powermgr IPC `MSG_PM_QUERY→RESULT`(battery/estimate/breakdown,confidence 原样透传) | ✅(powermgr 不在→503 `POWER_UNAVAILABLE`) |
|
||||||
| GET | `/api/ams` | App 运行表:`am list`(pkg/name/state/pid/launches/crashes/last_kind/last_code) | ✅(amsd 不在→`available:false`) |
|
| GET | `/api/ams` | App 运行表:`am list`(pkg/name/state/pid/launches/crashes/last_kind/last_code) | ✅(amsd 不在→`available:false`) |
|
||||||
|
| GET | `/api/ams/bg` | **后台活动列表**(M6.38,批④):`am list` 仅取 `state=="bg"`,逐进程 `/proc` 采样 `VmRSS`/`cpu_pct`(首采样/不可得→`null` 前端显 `—`,绝不编造);同源于 amsd FG/BG 元数据,杀/清走既有 `/api/ams/:pkg/kill` | ✅(只读,需 PIN 会话;amsd 不在→`available:false`) |
|
||||||
| GET | `/api/svcd` | 服务表:`meow_svcd status all`(name/state/pid/restart/backoff_ms/trigger/src/exit/exec) | ✅(svcd 不在→`available:false`) |
|
| GET | `/api/svcd` | 服务表:`meow_svcd status all`(name/state/pid/restart/backoff_ms/trigger/src/exit/exec) | ✅(svcd 不在→`available:false`) |
|
||||||
| GET | `/api/overview` | **首页聚合**(单线程后端只暴露聚合):后端顺序 fan-out perf/power/audio/net/ams/svcd 六源合成一份 JSON;任一源失败→该子块 `available:false`,绝不假数据 | ✅(**读级,需 PIN 会话**) |
|
| GET | `/api/overview` | **首页聚合**(单线程后端只暴露聚合):后端顺序 fan-out perf/power+events/audio/net+union dns/ams+fg/svcd 六源合成一份 JSON;任一源失败→该子块 `available:false`,绝不假数据 | ✅(**读级,需 PIN 会话**) |
|
||||||
|
| GET | `/api/net/ifaces` | 接口列表详情(kind/ip/gw/rx/tx/rssi/rfkill/dns;netmgrd 0xE2+0xE0 合成) | ✅(只读,需 PIN 会话) |
|
||||||
|
| GET | `/api/net/policy` | 管控策略只读(0xEA SHOW;`enforce=dns_only` 如实)+ **`policy_write_enabled`**(guard③ 门禁状态)| ✅(只读,需 PIN 会话;上游坏字节→`available:false`) |
|
||||||
|
| POST | `/api/net/policy` | 写管控策略(`set_net`/`set_domain`/`del`):**guard① 防自锁**(`SELF_LOCK` 422)+ **guard③ 默认关**(`POLICY_DISABLED` 403)+ 二次确认 | ✅(**写级**,PIN 会话;自锁→422,门禁→403,IPC 权限→403 FORBIDDEN) |
|
||||||
|
| GET | `/api/power/policy` | 电源策略快照(profile 四档/阈值/`enacted`)+ **`policy_write_enabled`**(guard③;注入后 jv_parse 自校验) | ✅(只读,需 PIN 会话;坏字节→503) |
|
||||||
|
| POST | `/api/power/policy` | 写电源策略(**只写 settings.conf `power.*` 键**):**guard③ 默认关**(`POLICY_DISABLED` 403)+ 二次确认 | ✅(**写级**,PIN 会话;非法值→422,权限→403) |
|
||||||
|
| GET | `/api/power/events` | 电源异常事件(充放电/低电/亮灭屏/背光跳变;overview 摘要 top2 的来源) | ✅(只读,需 PIN 会话;powermgr 不在→503) |
|
||||||
|
| GET | `/api/power/history` | 功耗采样历史(30s 环形 4h,不落盘) | ✅(只读,需 PIN 会话;powermgr 不在→503) |
|
||||||
|
| GET | `/api/ams/:pkg` | 单 App incident 历史(`am history`,崩溃根因 detail;坏 detail→降级 `available:false`) | ✅(只读,需 PIN 会话) |
|
||||||
|
| POST | `/api/ams/:pkg/start` | 启动 App(`am start <pkg>`;**写级**,PIN 会话) | ✅(包名非法→400,launcher 离线→503) |
|
||||||
|
| POST | `/api/ams/:pkg/kill` | 结束 App(`am kill <pkg>`;**写级**,PIN 会话,前台前端二次确认) | ✅(包名非法→400,not registered→404,busy→409) |
|
||||||
| GET | `/api/apps` | 扫描 `MeowAPPs/` 读 `app.json` | ✅ |
|
| GET | `/api/apps` | 扫描 `MeowAPPs/` 读 `app.json` | ✅ |
|
||||||
| GET | `/api/apps/:pkg` | 应用详情 | ✅ |
|
| GET | `/api/apps/:pkg` | 应用详情 | ✅ |
|
||||||
| POST | `/api/apps/install` | 经 `nyai install`(multipart 或 path) | ✅ |
|
| POST | `/api/apps/install` | 经 `nyai install`(multipart 或 path) | ✅ |
|
||||||
@@ -396,6 +411,33 @@ http://<host>.local:8080/
|
|||||||
同款统计);DNS 不可用则省略 `dns` 键仅保留网络状态,**绝不假数据**。首页 netCard 改读 `net.dns`,去掉单独 `/api/net/dns`
|
同款统计);DNS 不可用则省略 `dns` 键仅保留网络状态,**绝不假数据**。首页 netCard 改读 `net.dns`,去掉单独 `/api/net/dns`
|
||||||
(首页严格单请求)。
|
(首页严格单请求)。
|
||||||
|
|
||||||
|
### 5.16 批 P1 写端点(power/net/ams 写)+ 三条护栏(MEOWPANEL_ENHANCE_PLAN P1)
|
||||||
|
> 三个写端点全部落在 `POST` 分支 + `require_auth` 之后(无一漏到无鉴权路径);响应**如实回错误码**,绝不掩盖。
|
||||||
|
|
||||||
|
- **三条护栏**:
|
||||||
|
1. **guard① 防自锁**(仅 `/api/net/policy`):对系统关键网络/服务组件禁设「阻断」(`set_net`+`arg=block`、
|
||||||
|
或任何 `set_domain` 域名限制)。命中即 `422 {"code":"SELF_LOCK"}`。自锁实体(取 `svc:|app:|proc:` 后裸名),
|
||||||
|
`net_locked_entity()` 精确比对 `dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init`(netmgrd 加载时也剔除,面板侧抢先硬拒)。
|
||||||
|
「放行 / 删除规则」不阻断(恢复连通)仍放行。
|
||||||
|
2. **guard② PIN 会话鉴权**:复用全局 `require_auth`(§5.9);无会话 → `401 UNAUTHORIZED`。
|
||||||
|
3. **guard③ 策略写默认关**:`panel.policy_enabled`(新设置项,`settings.conf`「面板」组,默认 `0`)。
|
||||||
|
`0`(缺省)时 `POST /api/net/policy`、`POST /api/power/policy` 在解析 body **前**即拒 `403 POLICY_DISABLED`
|
||||||
|
(只读 GET 不受此限);置 `1`(设置页 / 电源·网络策略卡的「开启策略写」)放行。与 powermgr `enacted` **独立**。
|
||||||
|
|
||||||
|
- **`POST /api/net/policy`**:`{op:"set_net|set_domain|del", entity:"app:…|svc:…|proc:…", arg:"…"}`(`del` 免 `arg`)。
|
||||||
|
成功 `{"accepted":true,"message":…}`;权限→`403 FORBIDDEN`、非法→`422 UNSUPPORTED`、自锁→`422 SELF_LOCK`、门禁→`403 POLICY_DISABLED`、
|
||||||
|
IPC 不可达→`503 NET_UNAVAILABLE`。`enforce=dns_only` 如实显示「仅 DNS 层拒绝」,不谎称断连。
|
||||||
|
- **`GET /api/net/policy`**:`net_policy_parse` 输出 `{"available":true,"policy_write_enabled":<bool>,"enforce","log_level","rules":[…]}`;
|
||||||
|
坏字节(实体名/域名)降级 `{"available":false}`。前端据此渲染「默认关」横幅 + 「开启策略写」(`enablePolicyWrite`)。
|
||||||
|
- **`POST /api/power/policy`**:`{item:"profile|low-pct|crit-pct|bri-cap|scr-cap", value:"…"}`,**只写 settings.conf `power.*` 键**;
|
||||||
|
成功后透传 powermgr 应答。非法值→`422`(服务端 `ok:false` 按 `why` 映射)、权限→`403`、门禁→`403 POLICY_DISABLED`、
|
||||||
|
IPC 不可达→`503 POWER_UNAVAILABLE`。
|
||||||
|
- **`GET /api/power/policy`**:透传 powermgr 策略快照(含 `enacted`)后经 `power_policy_append_flag()` **追加顶层 `policy_write_enabled`**
|
||||||
|
(剥尾拼键 + jv_parse 自校验,绝不发非法 JSON);非对象答原样透传。
|
||||||
|
- **`POST /api/ams/:pkg/{start,kill}`**:`am start/kill <pkg>`;包名经 `ams_pkg_ok`(仅 ASCII 可见、禁 `/ : 空白`)。成功
|
||||||
|
`{"accepted":true,"op","pkg","message"}`;`not registered`→404、`busy`/`not running`→409、`launcher offline`→503、其余→500。
|
||||||
|
**kill 前台 App 前端二次确认**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md)
|
## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md)
|
||||||
@@ -486,17 +528,46 @@ http://<host>.local:8080/
|
|||||||
|
|
||||||
```
|
```
|
||||||
patch_v2/meowpanel/
|
patch_v2/meowpanel/
|
||||||
├── meowpanel.c # 后端源码(约 5590 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面+增量帧/触摸注入/通知 IPC 实时化/字体库 tar.gz 导入导出/自然排序/audiod 状态快照 + 批2 崩溃/日志中心 + 音频写 + overview DNS 聚合)
|
├── meowpanel.c # 拆分前单体源码(约 6372 行,保留作参考,**不在构建源列表**;构建以 src/*.c 为准)
|
||||||
├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm,-Wall -Wextra 0 warning)
|
├── src/ # 后端模块化源码(模块化拆分,接口/行为不变,-Wall -Wextra 0 warning)
|
||||||
|
│ ├── mp.h # 共享内部头:全部共享类型/常量/外部全局 + 跨模块函数原型(含鉴权/实时画面等)
|
||||||
|
│ ├── mp_server.c # HTTP 服务器/路由分发 + 静态托管 + main(单线程 + poll 串行)
|
||||||
|
│ ├── mp_util.c # sbuf/UTF-8 消毒/内存搜索/命令管道/路径与 token 工具
|
||||||
|
│ ├── mp_json.c # 最简 JSON 解析/序列化(jv_t 树)
|
||||||
|
│ ├── mp_store.c # 设置镜像(settings.conf)+ 主题 + /api/settings*,/api/theme* + g_sets 表
|
||||||
|
│ ├── mp_ipc.c # IPC 帧读写 + meow_svc_perf/power/netmgr 客户端(单点收发)+ socket 路径
|
||||||
|
│ ├── mp_perf.c # /api/perf,/api/system,action,pin-unlock(perf 聚合)
|
||||||
|
│ ├── mp_power.c # meow_powermgr IPC(/api/power*,policy,window,events)+ ipc_pm_send
|
||||||
|
│ ├── mp_audio.c # meow_audiod IPC(状态快照 + 写命令 route/volume)
|
||||||
|
│ ├── mp_net.c # meow_netmgrd IPC(状态/连接/DNS/ifaces/policy,期2/期3)
|
||||||
|
│ ├── mp_ams.c # App 运行表 + amd 历史 + start/kill 控制
|
||||||
|
│ ├── mp_svcd.c # svcd 服务表 + 首页聚合 /api/overview
|
||||||
|
│ ├── mp_files.c # 路径安全/白名单 + /api/files* + multipart 上传 + 卡根路径
|
||||||
|
│ ├── mp_fonts.c # 字体库 tar.gz 导入/导出 + 列表(busybox gzip|tar 管道)
|
||||||
|
│ ├── mp_apps.c # MeowAPPs 扫描 + nyai 安装/导出/移除
|
||||||
|
│ ├── mp_logs.c # /api/logs 列表 + 读 + 崩溃汇总
|
||||||
|
│ ├── mp_notif.c # 通知(优先 launcher IPC 活跃集,回退 TF 归档)
|
||||||
|
│ ├── mp_auth.c # 访问级 PIN 鉴权(panel.pin)+ 内联 SHA-256 + 内存会话
|
||||||
|
│ ├── mp_key.c # 按键注入(写 /dev/input/event* EV_KEY,物理键红线内)
|
||||||
|
│ └── mp_screen.c # 实时画面(fb0→JPEG,增量帧)+ 触摸注入 + delta 缓存
|
||||||
|
├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm)
|
||||||
├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG)
|
├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG)
|
||||||
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning;meowpanel.c + mp_jpeg.c)
|
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning;src/*.c + mp_jpeg.c + -I.)
|
||||||
├── meowpanel # 交叉产物(mipsel 静态,约 218 KB,md5 015f9cf1…)
|
├── meowpanel # 交叉产物(mipsel 静态,本次 md5 fead4852…)
|
||||||
├── meowpanel_host # 宿主编译(本机 ld,调试用)
|
├── meowpanel_host # 宿主编译(本机 ld,调试用)
|
||||||
├── services.d/
|
├── services.d/
|
||||||
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
|
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
|
||||||
├── web/ # 前端(已存在;部署时拷到后端同目录)
|
└── web/ # 前端(模块化拆分;部署时拷到后端同目录)
|
||||||
│ ├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航
|
├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航;脚本按序加载
|
||||||
│ ├── meowpanel.css # 含登录页样式
|
├── meowpanel.css # 含登录页样式 + 「屏幕·遥控」合并页样式(screen-remote/remote-btn)
|
||||||
│ └── meowpanel.js # API 客户端带 token/401 登录、设置页鉴权卡片、音频页(5s轮询)/字体页
|
├── mp_shared.js # 共享框架:全局状态 + API 客户端(token/401) + 公共工具 + 主题/弹窗/登录/滑条(window.MP)
|
||||||
└── README.md # 本文件
|
├── mp_pages.js # 各页渲染/绑定:home/perf/power/audio/net/files/apps/fonts/settings/system/logs/notif/proc + 合并的屏幕·遥控
|
||||||
|
└── meowpanel.js # 前端入口:ROUTES/路由/启动(读取 window.MP 装配)
|
||||||
```
|
```
|
||||||
|
**前端拆分说明**:`meowpanel.js`(单体 IIFE)按「公共 util+api(client) / 各页渲染 / 应用入口」拆为
|
||||||
|
`mp_shared.js` + `mp_pages.js` + `meowpanel.js` 三文件(经典脚本按序加载,函数体保持原样,仅顶部从
|
||||||
|
`window.MP` 解构共享框架,**行为不变**)。
|
||||||
|
「屏幕」+「控制」两页**合并为一页**(route `screen`,导航「屏幕·遥控」):内容区左侧=屏幕实时画面
|
||||||
|
(复用 /api/screen.jpeg + 触摸注入 + fps 滑条 + 增量省流开关),右侧=MD3 矢量图标遥控板(音量± / 亮度± /
|
||||||
|
指定亮度滑条 / 播放·暂停 / 下一曲 / 上一曲 / 电源单击 / 电源长按),**全矢量内联 SVG,无 emoji**。
|
||||||
|
`返回`/`主页` 属物理键红线(后端 /api/key 禁注入 158/172),未提供注入按钮(遥控板附说明)。
|
||||||
@@ -25,7 +25,10 @@ if [ "$1" = "host" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"$CC" -Os -Wall -Wextra -static -I. -I"$I_HDR" src/*.c mp_jpeg.c -o meowpanel
|
# meow_time32.c:独立 o32 time32 取时/睡眠(meowpanel 不链 meow_ui.a);修 time(NULL)
|
||||||
|
# 会话过期基准与 usleep 手势/按键节奏。
|
||||||
|
"$CC" -Os -Wall -Wextra -static -I. -I"$I_HDR" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||||
|
src/*.c mp_jpeg.c ../sdk/meow_time32.c -o meowpanel
|
||||||
echo "== meowpanel (mipsel static) =="
|
echo "== meowpanel (mipsel static) =="
|
||||||
ls -la meowpanel
|
ls -la meowpanel
|
||||||
md5sum meowpanel
|
md5sum meowpanel
|
||||||
|
|||||||
Binary file not shown.
@@ -270,6 +270,7 @@ int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen, uint16_t *rtyp
|
|||||||
int api_power_policy_get(int sock);
|
int api_power_policy_get(int sock);
|
||||||
int api_power_policy_set(int sock, const char *body, size_t body_len);
|
int api_power_policy_set(int sock, const char *body, size_t body_len);
|
||||||
int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail);
|
int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail);
|
||||||
|
int power_policy_append_flag(sbuf_t *out, const char *obj); /* 拼 guard③ policy_write_enabled 顶层键(自校验) */
|
||||||
int api_power_history(int sock);
|
int api_power_history(int sock);
|
||||||
int api_power_events(int sock);
|
int api_power_events(int sock);
|
||||||
const char *ams_bin(void);
|
const char *ams_bin(void);
|
||||||
@@ -287,6 +288,10 @@ int api_net_ifaces(int sock);
|
|||||||
void net_policy_parse(const char *txt, sbuf_t *s);
|
void net_policy_parse(const char *txt, sbuf_t *s);
|
||||||
int api_net_policy_get(int sock);
|
int api_net_policy_get(int sock);
|
||||||
int api_net_policy_set(int sock, const char *body, size_t body_len);
|
int api_net_policy_set(int sock, const char *body, size_t body_len);
|
||||||
|
int net_locked_entity(const char *entity); /* 防自锁:系统关键网络/服务组件(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init) */
|
||||||
|
int net_bt_aggregate(const char *st, const char *paired, const char *connected, const char *a2dp, const char *devices, sbuf_t *s); /* F1-③ 0xE6 五读文本聚合(selftest 可测) */
|
||||||
|
int api_net_bt(int sock); /* F1-③ GET /api/net/bt 蓝牙只读详情 */
|
||||||
|
int panel_policy_write_enabled(void); /* policy 默认关 gate:settings.conf `panel.policy_enabled`,缺省 0=关(只读透传) */
|
||||||
void panel_strip_ansi(char *s);
|
void panel_strip_ansi(char *s);
|
||||||
int txt_tokens(char *buf, char **toks, int maxtoks);
|
int txt_tokens(char *buf, char **toks, int maxtoks);
|
||||||
void join_tokens(char **toks, int from, int to, char *out, size_t cap);
|
void join_tokens(char **toks, int from, int to, char *out, size_t cap);
|
||||||
@@ -301,6 +306,7 @@ void ams_history_parse(char *out, sbuf_t *s, const char *pkg);
|
|||||||
void ams_history_fill(sbuf_t *s, const char *pkg);
|
void ams_history_fill(sbuf_t *s, const char *pkg);
|
||||||
int api_ams_history(int sock, const char *pkg);
|
int api_ams_history(int sock, const char *pkg);
|
||||||
int api_ams_ctl(int sock, const char *pkg, int kill);
|
int api_ams_ctl(int sock, const char *pkg, int kill);
|
||||||
|
int api_ams_kill_fg(int sock, const char *pkg); /* 前台强制终止(救命):am kill 优先,launcher 拒→直接 pid 信号 */
|
||||||
void svcd_fill(sbuf_t *s);
|
void svcd_fill(sbuf_t *s);
|
||||||
int api_svcd(int sock);
|
int api_svcd(int sock);
|
||||||
int api_overview(int sock);
|
int api_overview(int sock);
|
||||||
@@ -411,6 +417,8 @@ int api_notification_dismiss_all(int sock);
|
|||||||
int api_files_list(int sock, const req_t *r);
|
int api_files_list(int sock, const req_t *r);
|
||||||
int api_files_read(int sock, const req_t *r);
|
int api_files_read(int sock, const req_t *r);
|
||||||
int api_files_download(int sock, const req_t *r);
|
int api_files_download(int sock, const req_t *r);
|
||||||
|
int files_search_json(const char *abs_start, const char *name, const char *typef, int max, sbuf_t *out); /* F1-④ 搜索核心(selftest 可测) */
|
||||||
|
int api_files_search(int sock, const req_t *r); /* F1-④ GET /api/files/search(只读) */
|
||||||
int api_files_upload(int sock, const req_t *r);
|
int api_files_upload(int sock, const req_t *r);
|
||||||
int rm_recursive(const char *path);
|
int rm_recursive(const char *path);
|
||||||
int api_files_mkdir(int sock, const req_t *r);
|
int api_files_mkdir(int sock, const req_t *r);
|
||||||
@@ -443,7 +451,9 @@ int api_screen_jpeg(int sock, const req_t *r);
|
|||||||
int64_t now_mono_ms(void);
|
int64_t now_mono_ms(void);
|
||||||
int64_t touch_last_local_ms(const char *dev);
|
int64_t touch_last_local_ms(const char *dev);
|
||||||
int inject_tap(const char *dev, int x, int y);
|
int inject_tap(const char *dev, int x, int y);
|
||||||
|
int inject_swipe(const char *dev, int x0, int y0, int x1, int y1);
|
||||||
int api_input_tap(int sock, const req_t *r);
|
int api_input_tap(int sock, const req_t *r);
|
||||||
|
int api_input_gesture(int sock, const req_t *r);
|
||||||
const char *web_root(void);
|
const char *web_root(void);
|
||||||
const char *mime_for(const char *path);
|
const char *mime_for(const char *path);
|
||||||
int serve_static(int sock, const char *path);
|
int serve_static(int sock, const char *path);
|
||||||
|
|||||||
+147
-19
@@ -253,7 +253,32 @@ static void bg_proc_sample(int pid, double *pcpu, long *rss_kb)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行)。 */
|
/* 发射单条 APP 行 JSON(复用于 bg/fg 列表;带 state + is_fg + cpu_pct/rss_kb)。
|
||||||
|
* toks 为已切分的 `APP ...` 行(n>=9);*first 控制数组元素逗号(调用方初始化为 1)。
|
||||||
|
* cpu_pct/rss_kb 诚实原则:pid 读不到/不作为可用进程 → null(前端显「—」),绝不编造。 */
|
||||||
|
static void ams_entry_emit(sbuf_t *out, char **toks, int n, int *first)
|
||||||
|
{
|
||||||
|
const char *st = toks[n - 6]; /* 自右向左:state=tok[n-6], pid=n-5(与 ams_fill 同布局) */
|
||||||
|
char nm[128];
|
||||||
|
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
||||||
|
int pid = atoi(toks[n - 5]);
|
||||||
|
double pcpu = -1.0; long rss = -1;
|
||||||
|
if (pid > 0) bg_proc_sample(pid, &pcpu, &rss);
|
||||||
|
if (!*first) sA(out, ",");
|
||||||
|
*first = 0;
|
||||||
|
sA(out, "{\"pkg\":"); sA_str(out, toks[1]);
|
||||||
|
sA(out, ",\"name\":"); sA_str(out, nm);
|
||||||
|
sA(out, ",\"pid\":%d", pid);
|
||||||
|
sA(out, ",\"state\":"); sA_str(out, st);
|
||||||
|
sA(out, ",\"is_fg\":%s", strcmp(st, "fg") == 0 ? "true" : "false");
|
||||||
|
sA(out, ",\"cpu_pct\":");
|
||||||
|
if (pcpu < 0) sA(out, "null"); else sA(out, "%.1f", pcpu);
|
||||||
|
sA(out, ",\"rss_kb\":");
|
||||||
|
if (rss < 0) sA(out, "null"); else sA(out, "%ld", rss);
|
||||||
|
sA(out, "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行;向后兼容专用,selftest 覆盖)。 */
|
||||||
void ams_bg_parse(char *list_text, sbuf_t *s)
|
void ams_bg_parse(char *list_text, sbuf_t *s)
|
||||||
{
|
{
|
||||||
char *toks[256];
|
char *toks[256];
|
||||||
@@ -266,25 +291,10 @@ void ams_bg_parse(char *list_text, sbuf_t *s)
|
|||||||
if (strncmp(ln, "APP ", 4) == 0) {
|
if (strncmp(ln, "APP ", 4) == 0) {
|
||||||
int n = txt_tokens(ln, toks, 256);
|
int n = txt_tokens(ln, toks, 256);
|
||||||
if (n >= 9) {
|
if (n >= 9) {
|
||||||
/* 自右向左固定(与 ams_fill 同布局):state=tok[n-6], pid=n-5;pkg=tok[1],name=join(tok[2..n-6)) */
|
|
||||||
const char *st = toks[n - 6];
|
const char *st = toks[n - 6];
|
||||||
if (strcmp(st, "bg") == 0) {
|
if (strcmp(st, "bg") == 0) {
|
||||||
char nm[128];
|
count++;
|
||||||
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
ams_entry_emit(&apps, toks, n, &first);
|
||||||
int pid = atoi(toks[n - 5]);
|
|
||||||
double pcpu; long rss;
|
|
||||||
bg_proc_sample(pid, &pcpu, &rss);
|
|
||||||
if (!first) sA(&apps, ",");
|
|
||||||
first = 0; count++;
|
|
||||||
sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]);
|
|
||||||
sA(&apps, ",\"name\":"); sA_str(&apps, nm);
|
|
||||||
sA(&apps, ",\"pid\":%d", pid);
|
|
||||||
sA(&apps, ",\"state\":"); sA_str(&apps, st);
|
|
||||||
sA(&apps, ",\"cpu_pct\":");
|
|
||||||
if (pcpu < 0) sA(&apps, "null"); else sA(&apps, "%.1f", pcpu);
|
|
||||||
sA(&apps, ",\"rss_kb\":");
|
|
||||||
if (rss < 0) sA(&apps, "null"); else sA(&apps, "%ld", rss);
|
|
||||||
sA(&apps, "}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,6 +305,8 @@ void ams_bg_parse(char *list_text, sbuf_t *s)
|
|||||||
sbuf_free(&apps);
|
sbuf_free(&apps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* am list 全文 → bg+fg 双列表 JSON。`apps`(bg)保持向后兼容(count=bg 数量);
|
||||||
|
* 追加 `fg` + `fg_count`(前台 App,前端救命分组)。state=stop 无进程,不在本页(proc 页可见)。 */
|
||||||
void ams_bg_fill(sbuf_t *s)
|
void ams_bg_fill(sbuf_t *s)
|
||||||
{
|
{
|
||||||
char out[16384];
|
char out[16384];
|
||||||
@@ -303,7 +315,28 @@ void ams_bg_fill(sbuf_t *s)
|
|||||||
int code = run_cmd(cmd, out, sizeof out);
|
int code = run_cmd(cmd, out, sizeof out);
|
||||||
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
|
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
|
||||||
panel_strip_ansi(out);
|
panel_strip_ansi(out);
|
||||||
ams_bg_parse(out, s);
|
char *toks[256];
|
||||||
|
sbuf_t bg = {0}, fg = {0};
|
||||||
|
int bg_first = 1, fg_first = 1, bg_count = 0, fg_count = 0;
|
||||||
|
char *ln = out;
|
||||||
|
while (*ln) {
|
||||||
|
char *nl = strchr(ln, '\n');
|
||||||
|
if (nl) *nl = 0;
|
||||||
|
if (strncmp(ln, "APP ", 4) == 0) {
|
||||||
|
int n = txt_tokens(ln, toks, 256);
|
||||||
|
if (n >= 9) {
|
||||||
|
const char *st = toks[n - 6];
|
||||||
|
if (strcmp(st, "bg") == 0) { bg_count++; ams_entry_emit(&bg, toks, n, &bg_first); }
|
||||||
|
else if (strcmp(st, "fg") == 0) { fg_count++; ams_entry_emit(&fg, toks, n, &fg_first); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||||
|
}
|
||||||
|
sA(s, "{\"available\":true,\"count\":%d,\"ts\":%ld,\"apps\":[%s]",
|
||||||
|
bg_count, (long)time(NULL), bg.b ? bg.b : "");
|
||||||
|
sA(s, ",\"fg_count\":%d,\"fg\":[%s]}", fg_count, fg.b ? fg.b : "");
|
||||||
|
sbuf_free(&bg);
|
||||||
|
sbuf_free(&fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
int api_ams_bg(int sock)
|
int api_ams_bg(int sock)
|
||||||
@@ -314,3 +347,98 @@ int api_ams_bg(int sock)
|
|||||||
sbuf_free(&s);
|
sbuf_free(&s);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 从 am list 解析某 pkg 的当前 pid(未注册/无进程 → 0)。 */
|
||||||
|
static int ams_pkg_pid(const char *pkg)
|
||||||
|
{
|
||||||
|
char out[16384];
|
||||||
|
char cmd[300];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin());
|
||||||
|
if (run_cmd(cmd, out, sizeof out) != 0 || !strstr(out, "APP")) return 0;
|
||||||
|
panel_strip_ansi(out);
|
||||||
|
char *toks[256];
|
||||||
|
char *ln = out;
|
||||||
|
while (*ln) {
|
||||||
|
char *nl = strchr(ln, '\n');
|
||||||
|
if (nl) *nl = 0;
|
||||||
|
if (strncmp(ln, "APP ", 4) == 0) {
|
||||||
|
int n = txt_tokens(ln, toks, 256);
|
||||||
|
if (n >= 9 && strcmp(toks[1], pkg) == 0) {
|
||||||
|
int pid = atoi(toks[n - 5]);
|
||||||
|
if (nl) *nl = '\n';
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* /proc/<pid>/stat 进程是否已消失/僵尸:-1=已消失,1=僵尸,0=存活。 */
|
||||||
|
static int proc_zombie(int pid)
|
||||||
|
{
|
||||||
|
char path[64], line[256];
|
||||||
|
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
||||||
|
if (read_small_file(path, line, sizeof line) < 0) return -1;
|
||||||
|
char *rp = strrchr(line, ')');
|
||||||
|
if (!rp) return -1;
|
||||||
|
const char *p = rp + 2;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
return (*p == 'Z') ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 前台强制终止(救命):优先走 `am kill`(launcher 干净 SIGTERM 序列,accepted);若 launcher 拒
|
||||||
|
* (framework / 无前台会话 → "launcher refused"),回退:解析 am list 拿当前 pid → 直接 SIGTERM,
|
||||||
|
* 宽限后仍存活走 SIGKILL(与 launcher 杀序列同语义)。这是用户明示「前台卡死救命」的强通道——
|
||||||
|
* 前端已二次确认 + 标注「前台」。require_auth 在路由层已护。 */
|
||||||
|
int api_ams_kill_fg(int sock, const char *pkg)
|
||||||
|
{
|
||||||
|
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||||
|
|
||||||
|
/* ① 干净通道:am kill(launcher 执行 SIGTERM 序列)。 */
|
||||||
|
{
|
||||||
|
char out[256];
|
||||||
|
char cmd[320];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s kill %s 2>/dev/null", ams_bin(), pkg);
|
||||||
|
int code = run_cmd(cmd, out, sizeof out);
|
||||||
|
if (code == 0 && strstr(out, "OK ")) {
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":");
|
||||||
|
sA_str(&s, pkg);
|
||||||
|
sA(&s, ",\"force\":false,\"message\":"); sA_str(&s, out);
|
||||||
|
sA(&s, "}");
|
||||||
|
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
panel_log("ams kill-fg %s via am refused: %s -> direct signal fallback", pkg,
|
||||||
|
out[0] ? out : "(empty)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ② 回退:直接按 pid 信号(绕过 launcher 门控)。 */
|
||||||
|
int pid = ams_pkg_pid(pkg);
|
||||||
|
if (pid <= 0) return send_err(sock, 409, "CONFLICT", "该 App 当前无进程可终止");
|
||||||
|
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
int killed_hard = 0;
|
||||||
|
for (int i = 0; i < 5; i++) { /* SIGTERM 宽限 ~0.6s(与 launcher 杀序列同语义) */
|
||||||
|
struct timespec ts = {0, 120 * 1000 * 1000};
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
int z = proc_zombie(pid);
|
||||||
|
if (z < 0 || z == 1) break; /* 消失/僵尸 → 已终止 */
|
||||||
|
}
|
||||||
|
if (proc_zombie(pid) == 0) { kill(pid, SIGKILL); killed_hard = 1; }
|
||||||
|
|
||||||
|
char msg[192];
|
||||||
|
snprintf(msg, sizeof msg, "force terminated pid %d via direct signal%s", pid,
|
||||||
|
killed_hard ? " (SIGKILL 强杀)" : " (SIGTERM)");
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":");
|
||||||
|
sA_str(&s, pkg);
|
||||||
|
sA(&s, ",\"force\":%s,\"pid\":%d,\"message\":", killed_hard ? "true" : "false", pid);
|
||||||
|
sA_str(&s, msg);
|
||||||
|
sA(&s, "}");
|
||||||
|
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -571,6 +571,142 @@ int api_files_delete(int sock, const req_t *r)
|
|||||||
return send_json(sock, 200, "{\"removed\":true}");
|
return send_json(sock, 200, "{\"removed\":true}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- F1-④ 文件搜索(FUTURE_MEOWPANEL §2.3,只读):白名单内 readdir 递归(限深 4 / 限量 200 /
|
||||||
|
* 限遍历 8000 条),不 run_cmd("find")(防注入、可控)。name=名子串(ASCII 大小写不敏感);
|
||||||
|
* type=file|dir 可选过滤。不跟符号链接(防循环/逃逸出白名单根)。 ---- */
|
||||||
|
#define SEARCH_MAX_DEPTH 4
|
||||||
|
#define SEARCH_MAX_ITEMS 200
|
||||||
|
#define SEARCH_MAX_VISIT 8000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *needle; /* 已小写化的匹配子串 */
|
||||||
|
int want_type; /* 0=不限 1=仅文件 2=仅目录 */
|
||||||
|
int max;
|
||||||
|
int count;
|
||||||
|
long visited;
|
||||||
|
sbuf_t *s;
|
||||||
|
} search_ctx_t;
|
||||||
|
|
||||||
|
static void name_lower(const char *in, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
for (; in[i] && i + 1 < cap; i++) {
|
||||||
|
unsigned char c = (unsigned char)in[i];
|
||||||
|
out[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : (char)c;
|
||||||
|
}
|
||||||
|
out[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 返回 1=到达上限应全程停止(truncated)。 */
|
||||||
|
static int search_walk(search_ctx_t *c, const char *dir, int depth)
|
||||||
|
{
|
||||||
|
if (c->count >= c->max || c->visited >= SEARCH_MAX_VISIT) return 1;
|
||||||
|
DIR *d = opendir(dir);
|
||||||
|
if (!d) return 0;
|
||||||
|
int stop = 0;
|
||||||
|
struct dirent *e;
|
||||||
|
while ((e = readdir(d))) {
|
||||||
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||||
|
if (++c->visited > SEARCH_MAX_VISIT) { stop = 1; break; }
|
||||||
|
char full[1400];
|
||||||
|
snprintf(full, sizeof full, "%s/%s", dir, e->d_name);
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(full, &st) < 0) continue;
|
||||||
|
if (S_ISLNK(st.st_mode)) continue; /* 不跟符号链接 */
|
||||||
|
int isdir = S_ISDIR(st.st_mode);
|
||||||
|
int type_ok = !c->want_type || (c->want_type == 1 && !isdir) || (c->want_type == 2 && isdir);
|
||||||
|
if (type_ok) {
|
||||||
|
char low[300];
|
||||||
|
name_lower(e->d_name, low, sizeof low);
|
||||||
|
if (strstr(low, c->needle)) {
|
||||||
|
if (c->count >= c->max) { stop = 1; break; }
|
||||||
|
char rel[1200];
|
||||||
|
make_rel(full, rel, sizeof rel);
|
||||||
|
if (c->count) sA(c->s, ",");
|
||||||
|
sA(c->s, "{\"name\":");
|
||||||
|
sA_str(c->s, e->d_name);
|
||||||
|
sA(c->s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld,\"rel\":",
|
||||||
|
isdir ? "dir" : "file", isdir ? 0LL : (long long)st.st_size, (long long)st.st_mtime);
|
||||||
|
sA_str(c->s, rel);
|
||||||
|
sA(c->s, "}");
|
||||||
|
c->count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isdir && depth < SEARCH_MAX_DEPTH && search_walk(c, full, depth + 1)) { stop = 1; break; }
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
return stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0=OK(JSON 入 out);-1 白名单外;-2 路径不存在/非目录;-3 name 空。 */
|
||||||
|
int files_search_json(const char *abs_start, const char *name, const char *typef, int max, sbuf_t *out)
|
||||||
|
{
|
||||||
|
if (!path_in_whitelist(abs_start)) return -1;
|
||||||
|
struct stat st;
|
||||||
|
if (stat(abs_start, &st) < 0 || !S_ISDIR(st.st_mode)) return -2;
|
||||||
|
char needle[300];
|
||||||
|
name_lower(name ? name : "", needle, sizeof needle);
|
||||||
|
if (!needle[0]) return -3;
|
||||||
|
int want_type = 0;
|
||||||
|
if (typef && !strcmp(typef, "file")) want_type = 1;
|
||||||
|
else if (typef && !strcmp(typef, "dir")) want_type = 2;
|
||||||
|
else if (typef && typef[0]) return -4; /* 非法 type 值(api 层映射 422) */
|
||||||
|
if (max <= 0 || max > SEARCH_MAX_ITEMS) max = SEARCH_MAX_ITEMS;
|
||||||
|
search_ctx_t c;
|
||||||
|
memset(&c, 0, sizeof c);
|
||||||
|
c.needle = needle; c.want_type = want_type; c.max = max; c.s = out;
|
||||||
|
char rel[1200];
|
||||||
|
make_rel(abs_start, rel, sizeof rel);
|
||||||
|
sA(out, "{\"path\":");
|
||||||
|
sA_str(out, abs_start);
|
||||||
|
sA(out, ",\"rel\":");
|
||||||
|
sA_str(out, rel);
|
||||||
|
sA(out, ",\"name\":");
|
||||||
|
sA_str(out, name);
|
||||||
|
sA(out, ",\"items\":[");
|
||||||
|
int stopped = search_walk(&c, abs_start, 1);
|
||||||
|
sA(out, "],\"count\":%d,\"truncated\":%s}", c.count,
|
||||||
|
(stopped || c.visited >= SEARCH_MAX_VISIT) ? "true" : "false");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int api_files_search(int sock, const req_t *r)
|
||||||
|
{
|
||||||
|
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||||
|
char path[1024] = "/";
|
||||||
|
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
|
||||||
|
char name[512] = "";
|
||||||
|
if (!qparam(r->query, "name", name, sizeof name) || !name[0])
|
||||||
|
return send_err(sock, 400, "BAD_REQUEST", "缺 name(按名子串搜索)");
|
||||||
|
char typef[16] = "";
|
||||||
|
qparam(r->query, "type", typef, sizeof typef);
|
||||||
|
char maxs[16] = "";
|
||||||
|
int max = SEARCH_MAX_ITEMS;
|
||||||
|
if (qparam(r->query, "max", maxs, sizeof maxs)) { int m = atoi(maxs); if (m > 0) max = m; }
|
||||||
|
char abs[1024];
|
||||||
|
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs))
|
||||||
|
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||||
|
sbuf_t s = {0};
|
||||||
|
int rc = files_search_json(abs, name, typef, max, &s);
|
||||||
|
if (rc == -1) { sbuf_free(&s); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||||||
|
if (rc == -2) { sbuf_free(&s); return send_err(sock, 404, "NOT_FOUND", "路径不存在或非目录"); }
|
||||||
|
if (rc == -4) { sbuf_free(&s); return send_err(sock, 422, "UNSUPPORTED", "type 须为 file|dir"); }
|
||||||
|
if (rc != 0) { sbuf_free(&s); return send_err(sock, 400, "BAD_REQUEST", "name 为空"); }
|
||||||
|
/* 放行前自校验(与 overview 同口径:括号平衡/内嵌 NUL/坏字节,浏览器 JSON.parse 不容忍) */
|
||||||
|
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
|
||||||
|
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
|
||||||
|
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
|
||||||
|
if (chk) jv_free(chk);
|
||||||
|
if (bad) {
|
||||||
|
panel_log("files/search: SELF-CHECK fail (n=%zu)", s.n);
|
||||||
|
sbuf_free(&s);
|
||||||
|
return send_err(sock, 500, "INTERNAL", "搜索结果输出未通过 JSON 自校验");
|
||||||
|
}
|
||||||
|
int out = send_json(sock, 200, s.b);
|
||||||
|
sbuf_free(&s);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
int multipart_extract_file(const req_t *r, const char *outpath)
|
int multipart_extract_file(const req_t *r, const char *outpath)
|
||||||
{
|
{
|
||||||
const char *b = strstr(r->content_type, "boundary=");
|
const char *b = strstr(r->content_type, "boundary=");
|
||||||
|
|||||||
@@ -175,6 +175,160 @@ int api_net_dns_flush(int sock)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- F1-③ 蓝牙只读详情(FUTURE_MEOWPANEL §2.6):netmgrd MSG_NET_BT_CTL(0xE6) 读级五命令
|
||||||
|
* (status/paired/connected/a2dp/devices)聚合为一份 JSON。读级任意调用者(bt_cmd_is_write=读),
|
||||||
|
* 无阻塞(net_ipc 2s 超时兜底);子命令失败/坏字节 → 该子块空数组降级,绝不拖垮整卡、绝不发非法 JSON。 ---- */
|
||||||
|
|
||||||
|
/* 单发 0xE6 读命令;成功返回文本长度(>0),IPC/服务端失败 -1,坏 UTF-8 -2(该子块降级)。 */
|
||||||
|
static int bt_ctl_read(const char *cmd, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
m_net_cmd_t q; memset(&q, 0, sizeof q);
|
||||||
|
snprintf(q.cmd, sizeof q.cmd, "%s", cmd);
|
||||||
|
uint16_t t; uint32_t code;
|
||||||
|
int n = net_ipc(MSG_NET_BT_CTL, &q, sizeof q, &t, &code, NULL, out, cap);
|
||||||
|
if (n <= 0 || t != MSG_NET_BT_RESULT || code != MEOW_OK) return -1;
|
||||||
|
if (!utf8_valid_n(out, (size_t)n)) return -2;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int bt_mac_ok(const char *m)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
if (i % 3 == 2) { if (m[i] != ':') return 0; }
|
||||||
|
else if (!is_hex(m[i])) return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Device <MAC> <Name>" 行集 → JSON 数组项(不合法行跳过;名经 sA_str_n 消毒)。返回条数。 */
|
||||||
|
static int bt_devlines_json(sbuf_t *s, const char *txt, int max)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
const char *ln = txt;
|
||||||
|
while (ln && *ln && n < max) {
|
||||||
|
const char *nl = strchr(ln, '\n');
|
||||||
|
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
|
||||||
|
const char *p = ln;
|
||||||
|
while ((size_t)(p - ln) < ll && (*p == ' ' || *p == '\t')) p++;
|
||||||
|
if ((size_t)(ln + ll - p) >= 7 + 17 && !strncmp(p, "Device ", 7) && bt_mac_ok(p + 7)) {
|
||||||
|
const char *mac = p + 7;
|
||||||
|
const char *nm = mac + 17;
|
||||||
|
while ((size_t)(nm - ln) < ll && (*nm == ' ' || *nm == '\t')) nm++;
|
||||||
|
size_t nlen = ll - (size_t)(nm - ln);
|
||||||
|
while (nlen && (nm[nlen - 1] == ' ' || nm[nlen - 1] == '\t' || nm[nlen - 1] == '\r')) nlen--;
|
||||||
|
if (n) sA(s, ",");
|
||||||
|
sA(s, "{\"mac\":");
|
||||||
|
sA_str_n(s, mac, 17);
|
||||||
|
sA(s, ",\"name\":");
|
||||||
|
sA_str_n(s, nm, nlen);
|
||||||
|
sA(s, "}");
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
ln = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行内 " key=value"(空白/行尾收尾)取值。 */
|
||||||
|
static void bt_line_kv(const char *line, const char *key, char *out, size_t cap)
|
||||||
|
{
|
||||||
|
out[0] = 0;
|
||||||
|
const char *v = strstr(line, key);
|
||||||
|
if (!v) return;
|
||||||
|
v += strlen(key);
|
||||||
|
size_t i = 0;
|
||||||
|
while (v[i] && v[i] != ' ' && v[i] != '\t' && v[i] != '\n' && v[i] != '\r' && i + 1 < cap) { out[i] = v[i]; i++; }
|
||||||
|
out[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=<0|1> role=<..>" 行集 → JSON 数组项。 */
|
||||||
|
static int bt_a2dp_json(sbuf_t *s, const char *txt, int max)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
const char *ln = txt;
|
||||||
|
while (ln && *ln && n < max) {
|
||||||
|
const char *nl = strchr(ln, '\n');
|
||||||
|
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
|
||||||
|
char line[320];
|
||||||
|
if (ll >= sizeof line) ll = sizeof line - 1;
|
||||||
|
memcpy(line, ln, ll); line[ll] = 0;
|
||||||
|
if (!strncmp(line, "A2DP ", 5) && strncmp(line, "A2DP none", 9) && bt_mac_ok(line + 5)) {
|
||||||
|
char codec[24], rate[16], chn[8], bits[8], acq[8], role[8];
|
||||||
|
bt_line_kv(line, " codec=", codec, sizeof codec);
|
||||||
|
bt_line_kv(line, " rate=", rate, sizeof rate);
|
||||||
|
bt_line_kv(line, " channels=", chn, sizeof chn);
|
||||||
|
bt_line_kv(line, " bits=", bits, sizeof bits);
|
||||||
|
bt_line_kv(line, " acquired=", acq, sizeof acq);
|
||||||
|
bt_line_kv(line, " role=", role, sizeof role);
|
||||||
|
if (n) sA(s, ",");
|
||||||
|
sA(s, "{\"mac\":");
|
||||||
|
sA_str_n(s, line + 5, 17);
|
||||||
|
sA(s, ",\"codec\":"); sA_str(s, codec);
|
||||||
|
sA(s, ",\"rate\":"); sA_str(s, rate);
|
||||||
|
sA(s, ",\"channels\":"); sA_str(s, chn);
|
||||||
|
sA(s, ",\"bits\":"); sA_str(s, bits);
|
||||||
|
sA(s, ",\"acquired\":%s", !strcmp(acq, "1") ? "true" : "false");
|
||||||
|
sA(s, ",\"role\":"); sA_str(s, role);
|
||||||
|
sA(s, "}");
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
ln = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 五读文本聚合(任一 NULL=该子块空数组;st=NULL → {"available":false})。 */
|
||||||
|
int net_bt_aggregate(const char *st, const char *paired, const char *connected,
|
||||||
|
const char *a2dp, const char *devices, sbuf_t *s)
|
||||||
|
{
|
||||||
|
if (!st) { sA(s, "{\"available\":false}"); return -1; }
|
||||||
|
int avail = 0, powered = 0; long long paired_n = -1, connected_n = -1;
|
||||||
|
char addr[24] = "";
|
||||||
|
(void)sscanf(st, "bt available=%d powered=%d address=%23s paired=%lld connected=%lld",
|
||||||
|
&avail, &powered, addr, &paired_n, &connected_n);
|
||||||
|
if (!strcmp(addr, "-")) addr[0] = 0;
|
||||||
|
sA(s, "{\"available\":%s", avail ? "true" : "false");
|
||||||
|
sA(s, ",\"powered\":%s", powered ? "true" : "false");
|
||||||
|
sA(s, ",\"address\":"); sA_str(s, addr);
|
||||||
|
if (paired_n >= 0) sA(s, ",\"paired_n\":%lld", paired_n); else sA(s, ",\"paired_n\":null");
|
||||||
|
if (connected_n >= 0) sA(s, ",\"connected_n\":%lld", connected_n); else sA(s, ",\"connected_n\":null");
|
||||||
|
sA(s, ",\"paired\":["); if (paired) bt_devlines_json(s, paired, 32); sA(s, "]");
|
||||||
|
sA(s, ",\"connected\":["); if (connected) bt_devlines_json(s, connected, 16); sA(s, "]");
|
||||||
|
sA(s, ",\"a2dp\":["); if (a2dp) bt_a2dp_json(s, a2dp, 8); sA(s, "]");
|
||||||
|
sA(s, ",\"nearby\":["); if (devices) bt_devlines_json(s, devices, 16); sA(s, "]");
|
||||||
|
sA(s, "}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int api_net_bt(int sock)
|
||||||
|
{
|
||||||
|
char st[MEOW_IPC_MAX], pa[MEOW_IPC_MAX], co[MEOW_IPC_MAX], a2[MEOW_IPC_MAX], dv[MEOW_IPC_MAX];
|
||||||
|
int ns = bt_ctl_read("status", st, sizeof st);
|
||||||
|
if (ns <= 0) /* netmgrd 不可达/status 坏字节 → 整体降级(net 域惯例:200 available:false) */
|
||||||
|
return send_json(sock, 200, "{\"available\":false}");
|
||||||
|
int np = bt_ctl_read("paired", pa, sizeof pa);
|
||||||
|
int nc = bt_ctl_read("connected", co, sizeof co);
|
||||||
|
int na = bt_ctl_read("a2dp", a2, sizeof a2);
|
||||||
|
int nd = bt_ctl_read("devices", dv, sizeof dv);
|
||||||
|
sbuf_t s = {0};
|
||||||
|
net_bt_aggregate(st, np > 0 ? pa : NULL, nc > 0 ? co : NULL, na > 0 ? a2 : NULL, nd > 0 ? dv : NULL, &s);
|
||||||
|
/* 放行前自校验(与 overview 同口径:浏览器 JSON.parse 不容忍尾随垃圾/括号错位/内嵌 NUL/坏字节) */
|
||||||
|
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
|
||||||
|
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
|
||||||
|
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
|
||||||
|
if (chk) jv_free(chk);
|
||||||
|
if (bad) {
|
||||||
|
FILE *df = fopen("/tmp/net_bt_bad.json", "w");
|
||||||
|
if (df && s.b) { fwrite(s.b, 1, s.n, df); fclose(df); }
|
||||||
|
panel_log("net/bt: SELF-CHECK fail, raw dumped /tmp/net_bt_bad.json");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return send_err(sock, 500, "INTERNAL", "蓝牙聚合输出未通过 JSON 自校验(原始数据已留 /tmp/net_bt_bad.json)");
|
||||||
|
}
|
||||||
|
int rc = send_json(sock, 200, s.b);
|
||||||
|
sbuf_free(&s);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
int api_net_log(int sock)
|
int api_net_log(int sock)
|
||||||
{
|
{
|
||||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||||
@@ -285,11 +439,30 @@ int api_net_ifaces(int sock)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 防自锁实体:对本机关键网络/服务组件禁设「阻断」(net=block / set_domain 域名限制),
|
||||||
|
* 否则会把能连上设备/面板/系统核心的网络能力一并锁死(SSH / WiFi / DHCP / DNS / 面板 / init)。
|
||||||
|
* netmgrd 加载时也会剔除自锁项,面板侧抢先硬拒(guard①),保证 0 warning + 如实回错误码。
|
||||||
|
* 匹配口径:取 `svc:|app:|proc:` 之后的裸名精确比对(`svc:netmgrd`/`proc:sysui`/裸 `udhcpc` 均命中)。 */
|
||||||
|
int net_locked_entity(const char *entity)
|
||||||
|
{
|
||||||
|
if (!entity || !entity[0]) return 0;
|
||||||
|
const char *base = entity;
|
||||||
|
const char *colon = strrchr(entity, ':');
|
||||||
|
if (colon) base = colon + 1;
|
||||||
|
static const char *locked[] = {
|
||||||
|
"dropbear", "wpa_supplicant", "udhcpc", "netmgrd", "svcd", "sysui", "init", NULL
|
||||||
|
};
|
||||||
|
for (int i = 0; locked[i]; i++)
|
||||||
|
if (!strcmp(base, locked[i])) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void net_policy_parse(const char *txt, sbuf_t *s)
|
void net_policy_parse(const char *txt, sbuf_t *s)
|
||||||
{
|
{
|
||||||
char lvl[24] = "", enf[24] = "dns_only";
|
char lvl[24] = "", enf[24] = "dns_only";
|
||||||
const char *p = txt;
|
const char *p = txt;
|
||||||
sA(s, "{\"available\":true,\"enforce\":");
|
sA(s, "{\"available\":true,\"policy_write_enabled\":%s,\"enforce\":",
|
||||||
|
panel_policy_write_enabled() ? "true" : "false");
|
||||||
/* 首行找 enforce=... */
|
/* 首行找 enforce=... */
|
||||||
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
|
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
|
||||||
const char *en = strstr(p, "enforce=");
|
const char *en = strstr(p, "enforce=");
|
||||||
@@ -399,6 +572,9 @@ int api_net_policy_get(int sock)
|
|||||||
int api_net_policy_set(int sock, const char *body, size_t body_len)
|
int api_net_policy_set(int sock, const char *body, size_t body_len)
|
||||||
{
|
{
|
||||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||||
|
/* guard③ policy 默认关:写端点默认关闭,须在设置开启 panel.policy_enabled 才放行(只读 GET 不受此限)。 */
|
||||||
|
if (!panel_policy_write_enabled())
|
||||||
|
return send_err(sock, 403, "POLICY_DISABLED", "网络策略写默认关闭:先到面板设置开启 panel.policy_enabled(危险操作)");
|
||||||
jv_t *root = jv_parse(body, (int)body_len);
|
jv_t *root = jv_parse(body, (int)body_len);
|
||||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||||
jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg");
|
jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg");
|
||||||
@@ -415,6 +591,14 @@ int api_net_policy_set(int sock, const char *body, size_t body_len)
|
|||||||
else if (!strcmp(op, "del")) nop = NET_POLICY_DEL;
|
else if (!strcmp(op, "del")) nop = NET_POLICY_DEL;
|
||||||
else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del");
|
else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del");
|
||||||
if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg");
|
if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg");
|
||||||
|
/* guard① 防自锁:对系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)禁设阻断。
|
||||||
|
* 「放行 / 删除规则」不阻断(那是恢复连通),仍放行;仅 set_net=block 与 set_domain(域名限制)命中即拒。 */
|
||||||
|
{
|
||||||
|
int blocking = ((nop == NET_POLICY_SET_NET) && !strcmp(arg, "block")) ||
|
||||||
|
(nop == NET_POLICY_SET_DOMAIN);
|
||||||
|
if (blocking && net_locked_entity(entity))
|
||||||
|
return send_err(sock, 422, "SELF_LOCK", "防自锁:禁止阻断系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)的网络");
|
||||||
|
}
|
||||||
|
|
||||||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||||||
q.op = (uint32_t)nop;
|
q.op = (uint32_t)nop;
|
||||||
|
|||||||
@@ -71,6 +71,18 @@ done:
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 在对象 JSON(已剥尾部 `}`、经 utf8 校验)上拼 guard③ `policy_write_enabled` 顶层键:
|
||||||
|
* 拼完 jv_parse 自校验,绝不发非法 JSON(防 overview 教训);失败返回 -1(调用方回 500,绝不带病下发)。 */
|
||||||
|
int power_policy_append_flag(sbuf_t *out, const char *obj)
|
||||||
|
{
|
||||||
|
sA(out, "%s", obj);
|
||||||
|
sA(out, ",\"policy_write_enabled\":%s}", panel_policy_write_enabled() ? "true" : "false");
|
||||||
|
jv_t *chk = jv_parse(out->b ? out->b : "{}", (int)(out->b ? out->n : 2));
|
||||||
|
if (!chk) return -1;
|
||||||
|
jv_free(chk);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int api_power_policy_get(int sock)
|
int api_power_policy_get(int sock)
|
||||||
{
|
{
|
||||||
char js[MEOW_IPC_MAX];
|
char js[MEOW_IPC_MAX];
|
||||||
@@ -80,12 +92,27 @@ int api_power_policy_get(int sock)
|
|||||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
|
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
|
||||||
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
|
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
|
||||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||||||
return send_json(sock, 200, js);
|
/* 追加顶层 `policy_write_enabled`(面板侧 gate 状态,独立于 powermgr 的 `enacted`):
|
||||||
|
* 稳妥剥外层括号再拼(同 net_block_fill 手法)。上游非对象(数组/文本)时原样透传、不带该键
|
||||||
|
* (前端缺省视为已开启,兼容旧 powermgr)。 */
|
||||||
|
size_t l = strlen(js);
|
||||||
|
while (l && (js[l - 1] == ' ' || js[l - 1] == '\t' || js[l - 1] == '\n' || js[l - 1] == '\r')) js[--l] = 0;
|
||||||
|
if (l < 2 || js[0] != '{' || js[l - 1] != '}') return send_json(sock, 200, js);
|
||||||
|
js[l - 1] = 0; /* 去尾部 } */
|
||||||
|
sbuf_t s = {0};
|
||||||
|
if (power_policy_append_flag(&s, js) != 0) { sbuf_free(&s); return send_err(sock, 500, "INTERNAL", "电源策略聚合输出未通过 JSON 自校验"); }
|
||||||
|
int out = send_json(sock, 200, s.b ? s.b : "{}");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
int api_power_policy_set(int sock, const char *body, size_t body_len)
|
int api_power_policy_set(int sock, const char *body, size_t body_len)
|
||||||
{
|
{
|
||||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||||
|
/* guard③ policy 默认关:写端点默认关闭,须在设置开启 panel.policy_enabled 才放行(只读 GET 不受此限)。
|
||||||
|
* 注意:powermgr 侧另有 `enacted`(策略是否已应用),本 gate 是面板侧写入口门禁,二者独立。 */
|
||||||
|
if (!panel_policy_write_enabled())
|
||||||
|
return send_err(sock, 403, "POLICY_DISABLED", "电源策略写默认关闭:先到面板设置开启 panel.policy_enabled(危险操作)");
|
||||||
jv_t *root = jv_parse(body, (int)body_len);
|
jv_t *root = jv_parse(body, (int)body_len);
|
||||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||||
jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value");
|
jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap。 */
|
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap
|
||||||
|
* 与系统手势注入 /api/input/gesture(back=左缘右滑 / home=底部上滑回桌面)。 */
|
||||||
#include "mp.h"
|
#include "mp.h"
|
||||||
|
|
||||||
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565 */
|
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565 */
|
||||||
@@ -6,6 +7,17 @@ static int g_screen_pw = 0, g_screen_ph = 0;
|
|||||||
static int64_t g_last_self_inject_ms = 0;
|
static int64_t g_last_self_inject_ms = 0;
|
||||||
#define TOUCH_ACTIVE_MS 5000
|
#define TOUCH_ACTIVE_MS 5000
|
||||||
|
|
||||||
|
/* 手势注入阈值(镜像 meow_ui_input.c / np_gesture.h 系统已识别手势,勿新造语义):
|
||||||
|
* 返回=左缘(起)右移 ≥SWIPE_THRESH 且横向主导;回桌面=底部热区(起)上移 ≥HOME_SWIPE
|
||||||
|
* 且竖向主导 + down_x≥EDGE_W(避左缘返回列)。此处位移留容差(≥65)。 */
|
||||||
|
#define GESTURE_EDGE_W 24 /* 左缘宽度:返回手势起点 x 须 < 此值(meow_ui_input.c EDGE_W) */
|
||||||
|
#define GESTURE_SWIPE_DX 78 /* 返回手势右移位移(≥ SDK SWIPE_THRESH 65,留容差) */
|
||||||
|
#define GESTURE_HOME_EDGE_H 28 /* 底部热区高:回桌面起点 y 须 >= fb_h-此值(HOME_EDGE_H) */
|
||||||
|
#define GESTURE_HOME_DY 80 /* 回桌面手势上移位移(≥ SDK HOME_SWIPE 65,留容差) */
|
||||||
|
#define GESTURE_STEPS 10 /* 线性轨迹插值点数(营造真实滑动轨迹) */
|
||||||
|
#define GESTURE_STEP_MS 12 /* 每步间隔毫秒 */
|
||||||
|
#define GESTURE_HOLD_MS 60 /* 末端按住毫秒再抬起(防 fling 误判) */
|
||||||
|
|
||||||
|
|
||||||
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
||||||
{
|
{
|
||||||
@@ -230,6 +242,47 @@ int inject_tap(const char *dev, int x, int y)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 线性滑动注入:按下(原点) → 插值移动 → 末端按住 → 抬起。事件序同 inject_tap
|
||||||
|
* (BTN_TOUCH 边沿先于 ABS,goodix 时序免疫;首帧锁原点),设备手势机据此判
|
||||||
|
* 「左缘右滑=返回」或「底部上滑=回桌面」(复用系统已识别信号,不新造)。 */
|
||||||
|
int inject_swipe(const char *dev, int x0, int y0, int x1, int y1)
|
||||||
|
{
|
||||||
|
int fd = open(dev, O_WRONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
struct input_event ev;
|
||||||
|
#define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \
|
||||||
|
ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0)
|
||||||
|
/* 按下边沿 + 首帧 ABS 锁原点(与 tap 同序) */
|
||||||
|
__WE(EV_KEY, BTN_TOUCH, 1);
|
||||||
|
__WE(EV_ABS, ABS_MT_SLOT, 0);
|
||||||
|
__WE(EV_ABS, ABS_MT_TRACKING_ID, 7);
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_X, x0);
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_Y, y0);
|
||||||
|
__WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6);
|
||||||
|
__WE(EV_ABS, ABS_X, x0);
|
||||||
|
__WE(EV_ABS, ABS_Y, y0);
|
||||||
|
__WE(EV_SYN, SYN_REPORT, 0);
|
||||||
|
/* 插值逐点移动:每步坐标更新 + 同步,营造连续轨迹 */
|
||||||
|
for (int i = 1; i <= GESTURE_STEPS; i++) {
|
||||||
|
int xi = x0 + (x1 - x0) * i / GESTURE_STEPS;
|
||||||
|
int yi = y0 + (y1 - y0) * i / GESTURE_STEPS;
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_X, xi);
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_Y, yi);
|
||||||
|
__WE(EV_ABS, ABS_X, xi);
|
||||||
|
__WE(EV_ABS, ABS_Y, yi);
|
||||||
|
__WE(EV_SYN, SYN_REPORT, 0);
|
||||||
|
usleep((useconds_t)GESTURE_STEP_MS * 1000);
|
||||||
|
}
|
||||||
|
/* 末端短暂按住再抬起(与 tap 同:抬起到按下边沿被手势机视为一次抬手) */
|
||||||
|
usleep((useconds_t)GESTURE_HOLD_MS * 1000);
|
||||||
|
__WE(EV_KEY, BTN_TOUCH, 0);
|
||||||
|
__WE(EV_ABS, ABS_MT_TRACKING_ID, -1);
|
||||||
|
__WE(EV_SYN, SYN_REPORT, 0);
|
||||||
|
#undef __WE
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int api_input_tap(int sock, const req_t *r)
|
int api_input_tap(int sock, const req_t *r)
|
||||||
{
|
{
|
||||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||||
@@ -275,3 +328,71 @@ int api_input_tap(int sock, const req_t *r)
|
|||||||
sbuf_free(&s);
|
sbuf_free(&s);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* /api/input/gesture:注入设备已识别的系统手势(back=左缘右滑 / home=底部上滑回桌面)。
|
||||||
|
* 复用现有触摸注入通道(/dev/input/event1 滑动轨迹),不触碰被物理键红线禁注入的
|
||||||
|
* /api/key 158/172;由设备自身手势机判成 MEOW_SWIPE_LEFT / MEOW_EV_HOME。 */
|
||||||
|
int api_input_gesture(int sock, const req_t *r)
|
||||||
|
{
|
||||||
|
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||||
|
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||||
|
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||||
|
jv_t *jg = jv_obj_get(root, "gesture");
|
||||||
|
if (!jg || jg->type != J_STR) {
|
||||||
|
jv_free(root);
|
||||||
|
return send_err(sock, 400, "BAD_REQUEST", "缺 gesture(back|home)");
|
||||||
|
}
|
||||||
|
const char *gesture = jg->str;
|
||||||
|
int is_back = (strcmp(gesture, "back") == 0);
|
||||||
|
int is_home = (strcmp(gesture, "home") == 0);
|
||||||
|
jv_free(root);
|
||||||
|
if (!is_back && !is_home)
|
||||||
|
return send_err(sock, 400, "BAD_REQUEST", "gesture 非法(仅 back|home)");
|
||||||
|
|
||||||
|
const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV;
|
||||||
|
const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV;
|
||||||
|
int sw = 480, sh = 720;
|
||||||
|
{ int fx = 0, fy = 0; if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } }
|
||||||
|
if (sw < 1) sw = 480;
|
||||||
|
if (sh < 1) sh = 720;
|
||||||
|
|
||||||
|
/* 几何(镜像 SDK 判据 + 留容差,见上 GESTURE_* 宏) */
|
||||||
|
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
||||||
|
if (is_back) {
|
||||||
|
x0 = 4; /* 左缘内(< EDGE_W=24) */
|
||||||
|
if (x0 >= sw) x0 = sw - 1;
|
||||||
|
if (x0 >= GESTURE_EDGE_W) x0 = GESTURE_EDGE_W - 1;
|
||||||
|
y0 = y1 = sh / 2; /* 屏中部,避开顶部下拉/底部上滑热区 */
|
||||||
|
x1 = x0 + GESTURE_SWIPE_DX;
|
||||||
|
if (x1 >= sw) x1 = sw - 1; /* 横向主导 + 右移≥阈值 */
|
||||||
|
} else { /* home */
|
||||||
|
x0 = x1 = sw / 2; /* 屏底中部,避开左缘返回列 */
|
||||||
|
if (x0 < GESTURE_EDGE_W) x0 = x1 = GESTURE_EDGE_W;
|
||||||
|
y0 = sh - 2; /* 底部热区(>= fb_h-HOME_EDGE_H) */
|
||||||
|
if (y0 >= sh) y0 = sh - 1;
|
||||||
|
y1 = y0 - GESTURE_HOME_DY;
|
||||||
|
if (y1 < 0) y1 = 0; /* 竖向主导 + 上移≥阈值 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仲裁:本机最近触摸后 5s 内忽略注入(与 tap 同) */
|
||||||
|
int64_t now = now_mono_ms();
|
||||||
|
int64_t last = touch_last_local_ms(dev);
|
||||||
|
if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) {
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}",
|
||||||
|
(long long)(TOUCH_ACTIVE_MS - (now - last)));
|
||||||
|
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_last_self_inject_ms = now_mono_ms();
|
||||||
|
if (inject_swipe(dev, x0, y0, x1, y1) < 0)
|
||||||
|
return send_err(sock, 500, "INTERNAL", "手势注入失败(无法打开触摸设备)");
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"dispatched\":true,\"gesture\":\"%s\",\"from\":[%d,%d],\"to\":[%d,%d]}",
|
||||||
|
gesture, x0, y0, x1, y1);
|
||||||
|
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
@@ -301,6 +301,7 @@ int handle(int sock, req_t *r)
|
|||||||
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
|
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
|
||||||
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/bt")) return api_net_bt(sock); /* F1-③ 蓝牙只读详情(0xE6 读级聚合) */
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
|
||||||
@@ -339,16 +340,22 @@ int handle(int sock, req_t *r)
|
|||||||
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
|
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
|
||||||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||||||
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
||||||
|
if (!strcmp(p, "/api/input/gesture")) return api_input_gesture(sock, r);
|
||||||
char seg[256];
|
char seg[256];
|
||||||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||||||
return api_key_single(sock, atoi(seg), r);
|
return api_key_single(sock, atoi(seg), r);
|
||||||
/* POST /api/ams/<pkg>/start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */
|
/* POST /api/ams/<pkg>/start|kill|killfg(管启/停;require_auth 已护,kill 前台前端二次确认;
|
||||||
|
* killfg=前台救命强制终止:am kill 优先,launcher 拒→直接 pid 信号,前端二次确认+标注前台) */
|
||||||
if (!strncmp(p, "/api/ams/", 9)) {
|
if (!strncmp(p, "/api/ams/", 9)) {
|
||||||
size_t pl = strlen(p);
|
size_t pl = strlen(p);
|
||||||
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
|
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
|
||||||
char pkg[256];
|
char pkg[256];
|
||||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||||
return api_ams_ctl(sock, pkg, 0);
|
return api_ams_ctl(sock, pkg, 0);
|
||||||
|
} else if (pl > 7 && !strcmp(p + pl - 7, "/killfg")) {
|
||||||
|
char pkg[256];
|
||||||
|
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||||
|
return api_ams_kill_fg(sock, pkg);
|
||||||
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
||||||
char pkg[256];
|
char pkg[256];
|
||||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||||
@@ -375,6 +382,7 @@ int handle(int sock, req_t *r)
|
|||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
|
||||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
|
||||||
|
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/search")) return api_files_search(sock, r); /* F1-④ 搜索(只读) */
|
||||||
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
|
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
|
||||||
|
|
||||||
/* ---- 应用 ---- */
|
/* ---- 应用 ---- */
|
||||||
@@ -517,6 +525,8 @@ int panel_selftest(void)
|
|||||||
if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||||
jv_t *enf = jv_obj_get(pr, "enforce");
|
jv_t *enf = jv_obj_get(pr, "enforce");
|
||||||
if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||||
|
jv_t *pwe = jv_obj_get(pr, "policy_write_enabled"); /* guard③ 状态键必须为布尔,且拼读无括号错位 */
|
||||||
|
if (!pwe || pwe->type != J_BOOL) { printf("SELFTEST FAIL: net_policy_parse 缺/损坏 policy_write_enabled\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||||
jv_free(pr);
|
jv_free(pr);
|
||||||
sbuf_free(&p);
|
sbuf_free(&p);
|
||||||
}
|
}
|
||||||
@@ -567,7 +577,175 @@ int panel_selftest(void)
|
|||||||
sbuf_free(&b);
|
sbuf_free(&b);
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n");
|
/* 7) net_locked_entity:防自锁实体精确匹配(svc:/proc:/裸名);非关键实体放行(guard①)。 */
|
||||||
|
{
|
||||||
|
struct { const char *e; int want; } c[] = {
|
||||||
|
{ "svc:dropbear", 1 }, { "proc:netmgrd", 1 }, { "udhcpc", 1 },
|
||||||
|
{ "svc:sysui", 1 }, { "svc:wpa_supplicant", 1 }, { "svcd", 1 }, { "init", 1 },
|
||||||
|
{ "app:com.example", 0 }, { "svc:avahi", 0 }, { "proc:sh", 0 },
|
||||||
|
{ "app:com.netmgrd.helper", 0 }, /* 含子串但不等于裸名,不得误判 */
|
||||||
|
{ "", 0 }, { NULL, 0 }
|
||||||
|
};
|
||||||
|
for (int i = 0; c[i].e; i++) {
|
||||||
|
int got = net_locked_entity(c[i].e);
|
||||||
|
if (got != c[i].want) { printf("SELFTEST FAIL: net_locked_entity(%s)=%d 期望 %d\n", c[i].e, got, c[i].want); return 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 8) policy 默认关 gate(guard③):无设置文件 → panel_policy_write_enabled()==0(默认关);
|
||||||
|
* settings_write_one("panel.policy_enabled","1") 后 ==1(写临时 settings 文件验证,测完清理)。 */
|
||||||
|
{
|
||||||
|
const char *saved = g_settings_file;
|
||||||
|
char tmpf[] = "/tmp/meowpanel_policy_gate_XXXXXX";
|
||||||
|
int tfd = mkstemp(tmpf);
|
||||||
|
if (tfd < 0) { printf("SELFTEST FAIL: mkstemp 失败\n"); return 1; }
|
||||||
|
close(tfd);
|
||||||
|
g_settings_file = tmpf;
|
||||||
|
int before = panel_policy_write_enabled();
|
||||||
|
if (settings_write_one("panel.policy_enabled", "1") != 0) {
|
||||||
|
printf("SELFTEST FAIL: 写 panel.policy_enabled 失败\n"); unlink(tmpf); g_settings_file = saved; return 1;
|
||||||
|
}
|
||||||
|
int after = panel_policy_write_enabled();
|
||||||
|
unlink(tmpf);
|
||||||
|
g_settings_file = saved;
|
||||||
|
if (before != 0 || after != 1) { printf("SELFTEST FAIL: policy gate 默认关/开启 %d->%d\n", before, after); return 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 9) power_policy_append_flag:对象 JSON 剥尾拼入 policy_write_enabled 后仍可解析、键为布尔、原字段保留
|
||||||
|
* ——防括号错位重演(overview 教训)。测试在无 panel.policy_enabled 的临时 settings 下进行(期望 false=默认关)。 */
|
||||||
|
{
|
||||||
|
const char *saved = g_settings_file;
|
||||||
|
char tmpf[] = "/tmp/meowpanel_policy_append_XXXXXX";
|
||||||
|
int tfd = mkstemp(tmpf);
|
||||||
|
if (tfd >= 0) { close(tfd); g_settings_file = tmpf; }
|
||||||
|
sbuf_t p = {0};
|
||||||
|
if (power_policy_append_flag(&p, "{\"profile\":\"balanced\",\"enacted\":true") != 0) {
|
||||||
|
printf("SELFTEST FAIL: power_policy_append_flag 拼接失败\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
|
||||||
|
}
|
||||||
|
if (!utf8_valid_n(p.b, p.n) || !strstr(p.b, "\"policy_write_enabled\":false")) {
|
||||||
|
printf("SELFTEST FAIL: power_policy_append_flag 产物异常\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
|
||||||
|
}
|
||||||
|
jv_t *pr = jv_parse(p.b, (int)p.n);
|
||||||
|
if (!pr) { printf("SELFTEST FAIL: power_policy_append_flag 产物 JSON 无法解析\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||||||
|
jv_t *pwe = jv_obj_get(pr, "policy_write_enabled");
|
||||||
|
if (!pwe || pwe->type != J_BOOL || pwe->bval != 0) { printf("SELFTEST FAIL: power_policy_append_flag policy_write_enabled 非 false(默认关)\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||||||
|
jv_t *pfl = jv_obj_get(pr, "profile");
|
||||||
|
if (!pfl || pfl->type != J_STR || strcmp(pfl->str, "balanced") != 0) { printf("SELFTEST FAIL: power_policy_append_flag 丢失原字段\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||||||
|
jv_free(pr);
|
||||||
|
sbuf_free(&p);
|
||||||
|
if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 10) net_bt_aggregate(F1-③):0xE6 五读文本聚合 → 合法 JSON + 字段断言;
|
||||||
|
* 设备名含截断坏字节 → sA_str_n 消毒为 '?' 且产物仍合法;子文本 NULL → 空数组;status NULL → available:false。 */
|
||||||
|
{
|
||||||
|
sbuf_t b = {0};
|
||||||
|
const char *st = "bt available=1 powered=1 address=4C:BC:98:B0:98:DB paired=2 connected=1";
|
||||||
|
const char *pa = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 流媒体客户\xE7\n";
|
||||||
|
const char *co = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n";
|
||||||
|
const char *a2 = "A2DP 33:DE:2E:21:AD:30 codec=sbc rate=44100 channels=2 bits=16 acquired=1 role=src\n";
|
||||||
|
const char *dv = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 17OP15\n";
|
||||||
|
if (net_bt_aggregate(st, pa, co, a2, dv, &b) != 0) { printf("SELFTEST FAIL: net_bt_aggregate 正常文本返回非 0\n"); sbuf_free(&b); return 1; }
|
||||||
|
if (!b.b || !utf8_valid_n(b.b, b.n) || !json_strict_balanced(b.b, b.n)) { printf("SELFTEST FAIL: net_bt_aggregate 产物非法字节/括号错位\n"); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *br = jv_parse(b.b, (int)b.n);
|
||||||
|
if (!br) { printf("SELFTEST FAIL: net_bt_aggregate 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *pw = jv_obj_get(br, "powered");
|
||||||
|
if (!pw || pw->type != J_BOOL || !pw->bval) { printf("SELFTEST FAIL: net_bt_aggregate powered 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *pr = jv_obj_get(br, "paired");
|
||||||
|
if (!pr || pr->type != J_ARR || !pr->child || !pr->child->next || pr->child->next->next) { printf("SELFTEST FAIL: net_bt_aggregate paired 应 2 条\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *cn = jv_obj_get(br, "connected");
|
||||||
|
if (!cn || cn->type != J_ARR || !cn->child || cn->child->next) { printf("SELFTEST FAIL: net_bt_aggregate connected 应 1 条\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *a2p = jv_obj_get(br, "a2dp");
|
||||||
|
if (!a2p || a2p->type != J_ARR || !a2p->child) { printf("SELFTEST FAIL: net_bt_aggregate 缺 a2dp 条目\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *cd = jv_obj_get(a2p->child, "codec");
|
||||||
|
if (!cd || cd->type != J_STR || !cd->str || strcmp(cd->str, "sbc") != 0) { printf("SELFTEST FAIL: net_bt_aggregate a2dp codec 非 sbc\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_t *aq = jv_obj_get(a2p->child, "acquired");
|
||||||
|
if (!aq || aq->type != J_BOOL || !aq->bval) { printf("SELFTEST FAIL: net_bt_aggregate a2dp acquired 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
|
||||||
|
jv_free(br);
|
||||||
|
sbuf_free(&b);
|
||||||
|
/* status NULL → available:false;子文本 NULL → 空数组 */
|
||||||
|
sbuf_t b2 = {0};
|
||||||
|
net_bt_aggregate(NULL, NULL, NULL, NULL, NULL, &b2);
|
||||||
|
if (!b2.b || strstr(b2.b, "\"available\":false") == NULL) { printf("SELFTEST FAIL: net_bt_aggregate status NULL 未降级 available:false\n"); sbuf_free(&b2); return 1; }
|
||||||
|
sbuf_free(&b2);
|
||||||
|
sbuf_t b3 = {0};
|
||||||
|
net_bt_aggregate(st, NULL, NULL, NULL, NULL, &b3);
|
||||||
|
jv_t *b3r = b3.b ? jv_parse(b3.b, (int)b3.n) : NULL;
|
||||||
|
jv_t *b3p = b3r ? jv_obj_get(b3r, "paired") : NULL;
|
||||||
|
if (!b3p || b3p->type != J_ARR || b3p->child) { printf("SELFTEST FAIL: net_bt_aggregate 子文本 NULL 应为空数组\n"); if (b3r) jv_free(b3r); sbuf_free(&b3); return 1; }
|
||||||
|
jv_free(b3r);
|
||||||
|
sbuf_free(&b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 11) files_search_json(F1-④):沙盒卡根递归搜索(限深/限量/类型过滤/rel 回显)→ 合法 JSON + 计数断言;
|
||||||
|
* 白名单外起点 → -1;name 空 → -3;非法 type → -4。 */
|
||||||
|
{
|
||||||
|
char tmpl[] = "/tmp/mp_search_XXXXXX";
|
||||||
|
char *root = mkdtemp(tmpl);
|
||||||
|
if (!root) { printf("SELFTEST FAIL: mkdtemp 失败\n"); return 1; }
|
||||||
|
char saved_root[1024];
|
||||||
|
snprintf(saved_root, sizeof saved_root, "%s", g_tf_root_buf);
|
||||||
|
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", root);
|
||||||
|
char d1[1200], d2[1200], f1[1200], f2[1200], f3[1200];
|
||||||
|
snprintf(d1, sizeof d1, "%s/MeowOS", root);
|
||||||
|
snprintf(d2, sizeof d2, "%s/MeowOS/sub", root);
|
||||||
|
snprintf(f1, sizeof f1, "%s/MeowOS/Test Song.mp3", root);
|
||||||
|
snprintf(f2, sizeof f2, "%s/MeowOS/another.txt", root);
|
||||||
|
snprintf(f3, sizeof f3, "%s/MeowOS/sub/x Song2.log", root);
|
||||||
|
int setup_ok = (mkdir(d1, 0755) == 0 && mkdir(d2, 0755) == 0);
|
||||||
|
const char *mk[3] = { f1, f2, f3 };
|
||||||
|
for (int i = 0; i < 3 && setup_ok; i++) {
|
||||||
|
FILE *f = fopen(mk[i], "w");
|
||||||
|
if (!f) { setup_ok = 0; break; }
|
||||||
|
fputs("x", f); fclose(f);
|
||||||
|
}
|
||||||
|
if (!setup_ok) { printf("SELFTEST FAIL: 搜索沙盒构建失败\n"); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root); rm_recursive(root); rmdir(root); return 1; }
|
||||||
|
int fails = 0;
|
||||||
|
sbuf_t s = {0};
|
||||||
|
if (files_search_json(d1, "song", NULL, 0, &s) != 0) { printf("SELFTEST FAIL: files_search_json 正常搜索返回非 0\n"); fails = 1; }
|
||||||
|
jv_t *sr = (!fails && s.b) ? jv_parse(s.b, (int)s.n) : NULL;
|
||||||
|
if (!fails && (!sr || !utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n))) { printf("SELFTEST FAIL: files_search_json 产物非法\n"); fails = 1; }
|
||||||
|
jv_t *cnt = (!fails && sr) ? jv_obj_get(sr, "count") : NULL;
|
||||||
|
if (!fails && (!cnt || cnt->type != J_NUM || cnt->inum != 2)) { printf("SELFTEST FAIL: files_search_json song 应中 2 条(递归命中子目录)\n"); fails = 1; }
|
||||||
|
jv_t *its = (!fails && sr) ? jv_obj_get(sr, "items") : NULL;
|
||||||
|
jv_t *rel0 = (its && its->child) ? jv_obj_get(its->child, "rel") : NULL;
|
||||||
|
if (!fails && (!rel0 || rel0->type != J_STR || !rel0->str || strncmp(rel0->str, "/MeowOS/", 8) != 0)) { printf("SELFTEST FAIL: files_search_json rel 未按卡根相对回显\n"); fails = 1; }
|
||||||
|
if (sr) jv_free(sr);
|
||||||
|
sbuf_free(&s);
|
||||||
|
/* type=dir 过滤:建 Songs 目录应仅中目录 */
|
||||||
|
char d3[1200];
|
||||||
|
snprintf(d3, sizeof d3, "%s/MeowOS/Songs", root);
|
||||||
|
if (!fails && mkdir(d3, 0755) != 0) { printf("SELFTEST FAIL: 沙盒 Songs 目录创建失败\n"); fails = 1; }
|
||||||
|
sbuf_t s2 = {0};
|
||||||
|
if (!fails && files_search_json(d1, "song", "dir", 0, &s2) == 0) {
|
||||||
|
jv_t *sr2 = jv_parse(s2.b, (int)s2.n);
|
||||||
|
jv_t *c2 = sr2 ? jv_obj_get(sr2, "count") : NULL;
|
||||||
|
jv_t *i2 = sr2 ? jv_obj_get(sr2, "items") : NULL;
|
||||||
|
jv_t *t0 = (i2 && i2->child) ? jv_obj_get(i2->child, "type") : NULL;
|
||||||
|
if (!c2 || c2->type != J_NUM || c2->inum != 1 || !t0 || t0->type != J_STR || strcmp(t0->str, "dir") != 0) {
|
||||||
|
printf("SELFTEST FAIL: files_search_json type=dir 应仅中 1 个目录\n"); fails = 1;
|
||||||
|
}
|
||||||
|
if (sr2) jv_free(sr2);
|
||||||
|
} else if (!fails) { printf("SELFTEST FAIL: files_search_json type=dir 搜索失败\n"); fails = 1; }
|
||||||
|
sbuf_free(&s2);
|
||||||
|
/* 无匹配 → count 0;白名单外 → -1;空 name → -3;非法 type → -4 */
|
||||||
|
sbuf_t s3 = {0};
|
||||||
|
if (!fails && files_search_json(d1, "zzzz", NULL, 0, &s3) == 0) {
|
||||||
|
if (!strstr(s3.b, "\"count\":0")) { printf("SELFTEST FAIL: files_search_json 无匹配应 count 0\n"); fails = 1; }
|
||||||
|
} else if (!fails) { printf("SELFTEST FAIL: files_search_json 无匹配搜索失败\n"); fails = 1; }
|
||||||
|
sbuf_free(&s3);
|
||||||
|
sbuf_t s4 = {0};
|
||||||
|
if (!fails && files_search_json("/etc", "x", NULL, 0, &s4) != -1) { printf("SELFTEST FAIL: files_search_json 白名单外未拒\n"); fails = 1; }
|
||||||
|
if (!fails && files_search_json(d1, "", NULL, 0, &s4) != -3) { printf("SELFTEST FAIL: files_search_json 空 name 未拒\n"); fails = 1; }
|
||||||
|
if (!fails && files_search_json(d1, "x", "bogus", 0, &s4) != -4) { printf("SELFTEST FAIL: files_search_json 非法 type 未拒\n"); fails = 1; }
|
||||||
|
sbuf_free(&s4);
|
||||||
|
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root);
|
||||||
|
rm_recursive(root);
|
||||||
|
rmdir(root);
|
||||||
|
if (fails) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 端点池 + 蓝牙 0xE6 聚合 + 文件搜索 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ static const setdef_t g_sets[] = {
|
|||||||
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||||||
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
|
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
|
||||||
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||||||
|
/* panel.policy_enabled — 策略写「默认关」门禁(guard③):POST /api/net/policy 与 /api/power/policy
|
||||||
|
* 默认关闭(缺省 0),置 1 才放行。落 settings.conf(「面板」组,前端设置页可开启),仅作面板侧门禁记录,
|
||||||
|
* 不影响 powermgr 的 `enacted`(那是服务端「策略是否已应用」,二者独立)。 */
|
||||||
|
{ "panel.policy_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
|
||||||
};
|
};
|
||||||
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
|
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
|
||||||
|
|
||||||
@@ -143,6 +147,11 @@ int settings_get_int(const char *key, int defv)
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int panel_policy_write_enabled(void)
|
||||||
|
{
|
||||||
|
return settings_get_int("panel.policy_enabled", 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
int brightness_clamp(int v)
|
int brightness_clamp(int v)
|
||||||
{
|
{
|
||||||
int mn = settings_get_int("display.brightness_min", 1);
|
int mn = settings_get_int("display.brightness_min", 1);
|
||||||
|
|||||||
Loaded 100 of 298 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user