Compare commits
166
Commits
b364b6371a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08b3e64c8a | ||
|
|
84c7446aa6 | ||
|
|
a5fce76747 | ||
|
|
98de0ab37b | ||
|
|
6bc7e50531 | ||
|
|
9faaf14c86 | ||
|
|
9be447087d | ||
|
|
1ccb3c7a93 | ||
|
|
1936cf7b91 | ||
|
|
008e177307 | ||
|
|
7b32d775c0 | ||
|
|
952a126477 | ||
|
|
4d39c037be | ||
|
|
f7921ba799 | ||
|
|
28fbc6094a | ||
|
|
d34406ac22 | ||
|
|
dd8929e2df | ||
|
|
2c0ec2ba15 | ||
|
|
e37c622597 | ||
|
|
4040b41649 | ||
|
|
0d6c56100f | ||
|
|
166b32d0e5 | ||
|
|
f3d7b386b2 | ||
|
|
1e2604953d | ||
|
|
6e2774ff5c | ||
|
|
26cab3a0ed | ||
|
|
c93814a78b | ||
|
|
24f1d8a682 | ||
|
|
80c0bd4eb4 | ||
|
|
a41b61198e | ||
|
|
0d1b2d4c19 | ||
|
|
31d4c22806 | ||
|
|
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()`)**:
|
||||
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()` 只刷数字。
|
||||
|
||||
**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.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 追加)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> 把 HiBy R3 Pro II 便携播放器(君正 X1600,MIPS32r2 / mipsel / musl 静态软浮点,480×720,无 GPU 软渲染)变成一个可扩展的「第二系统 + 应用框架」。
|
||||
> MeowOS 与官方 HibyOS 共存于同一台设备:开机由引导选单二选一;启动后 MeowOS 作为主控层,提供自己的桌面 UI、应用框架、服务管理与系统组件。
|
||||
>
|
||||
> 版本:**M6.35(0.8.35,已上卡)**。M6.19→M6.35 连续十七版推卡自刷验收——**M6.19**:ncm 后台化 UI 叠层/点击穿透+再进 ncm 桌面叠层+播放页新旧 UI 叠层抢刷脏区(软自抑帧移交不完整,恢复边沿强制整帧重画)+PIN 亮屏亮度跳+通知中心封面显歌单封面;**M6.20**:播放页右滑返回+顶部下滑通知中心+状态栏时钟 1s;**M6.21-22**:播放页返回修复(`np_exit` 退出外层主循环)+方向感知手势;**M6.23**:watchdog 亮屏亮度按用户设置恢复;**M6.24**:App 内按电源键锁屏时序;**M6.25**:bootmenu 开机菜单亮度方案 A;**M6.26**:播放页三键命中+媒体卡片体 FOCUS;**M6.27**:netmgrd 流量统计收编(pub_bytes/去 lo)+面板公网流量卡;**M6.28**:收编桌面 Now Playing 双页歌词页+歌词数据源+蓝牙 P1(netmgrd BT_CTL/bt_page);**M6.29**:播放页任意右滑返回桌面;**M6.30**:收编批5 取色+蓝牙 P2(audiod glibc-dynamic)+面板蓝牙卡;**M6.31**:桌面歌词页封面修复+alsa.conf DEV 自动识别;**M6.32**:重编 meow_ui.a+重链四 App(播放页进度条取色+ncm 列表 gap)+面板透传;**M6.33**:收编 launcher 播放页占位符移除(已有独立歌词页)+封面「空→复原」恢复修复(test_np_cover 14 项 PASS);**M6.34**:收编**批①后台常驻+宽限期**(全 App 入后台表+30min `MEOW_BG_GRACE_MS` 宽限超时回收+声明常驻/媒体活跃无限期保活+槽满 LRU+app.json `background_keepalive` 三权限位+amsd `EV BG/FG` 上报)+**audiod A3/DSD**(24/32bit 高清 byte-exact+独立 `AUDIO_FMT_DOP` DoP DSD 透传+音量收编 v2 双组写)+**meow_img_pixels 访问器上收**+**面板拆分+屏幕·遥控合并页**(前端三层+左屏右遥控 MD3 SVG 矢量按钮);**M6.35**:收编**批③后台管理页**(桌面顶栏「后台」入口+`bg_page` 后台列表+每行「杀」按钮+顶栏「一键清」,`!framework` 门控不杀框架)+**amsd FG/BG 收编**(`57e19cde`)+**i18n settings/lock 补抽**。M6.35 之后工作区已提交(暂未打包,并入 M6.36):批①叠加/穿透根治(`/tmp/meow_notif_active` 内容=前台归属 app_id+SDK present/input 共享判定+launcher 7 处转场)、主界面顶栏后台按钮图标美化、主界面列表圆角卡 8px 外距、后台管理页行 MD3 圆角卡;**MD3 对齐在跑**(agent-435,参考 `third_party/md-material-web` 最新 M3)。M6.10→M6.18:M7 launcher 重写第 1-5 期(M6.10-14:虚拟化宫格/设置页卡顿修复/OOM 通用化/自动切歌根治)+(M6.15)meowpanel jv_free 泄漏修复+(M6.16)Hello demo 滚动卡顿修复+(M6.17)歌词 UI+(M6.18)媒体通知封面+封面分辨率。M6.1→M6.9 全链自刷验收——**M6.1**:netmgrd 期3/powermgr 期2/面板 PIN 鉴权+首页仪表盘/WiFi 录入 UI/服务名统一 meow_*;**M6.2**:锁屏 PIN 全量(`system.pin` 同源面板)+VOL_DOWN×3 框架门控+显示名 name[49](amsd/launcher UTF-8 整序列截断);**M6.3**:设置页 5s 修复(309 网络异步步骤机+跳过字体 sha 校验)+310 锁屏三连(输入穿透/解锁状态栏消失/App 死亡残影);**M6.4**:310 二期 **EVIOCGRAB 锁屏独占触控**(未冻结 App 锁屏输入隔离)+主界面应用名截断;**M6.5**:audiod 315 孤儿会话两段式回收(客户端崩溃/OOM 杀/正常退出后会话完整回收,STATE 回干净无流基线);**M6.6**:锁屏 310 三期——App 内手动息屏也触发锁屏 + 锁屏脏区全屏不透明底 + 300ms 重铺;**M6.7**:锁屏页自睡(空闲超时自动灭屏,294 遗留补;0.8.7 补刷吸收 fw_content 事故修复——拆分 watchdog+meow_audio 两行);**M6.8**:310 四轮 锁屏自睡 min(系统息屏,30s);**M6.9**:netmgrd 期4(SDK 自愿通道/forget/RSSI/AAAA 过滤/iptables 评估)+powermgr 期3(policy/标定工具)固化 + 327 M7.0b launcher 试点(电源菜单/Now Playing 控件化+主题薄壳+i18n 抽串);历史:M2.69→M2.96 全链路自刷验收:控件库/主界面 MD3 双布局/M3 导航栈/M5 通知跳转/媒体会话/Now Playing/SDK nav+media 封装/字库启动竞态修复**/M2.84-88**:installed 图标修复+残帧pan加固/物理键EVIOCGKEY双源/签名鉴权 2B/meow_route 音频路由/amsd 二期(picker 契约)/流媒体客户端二期(封面·歌词后台·大歌单分页)/MeowPanel 实时画面+遥控+通知实时化/中文拼音 IME**/M2.89**:开机选单抢屏回归修复+watchdog SIGUSR1 不误杀框架 App+amsd 失联修复+顶部下滑手势分流**/M2.90**:**meow_audiod 独占音频守护**(AUDIOD_DESIGN,meow_route 并入退役)+MIUIx 顶级主题+流媒体客户端文案中文化+面板字体库 tar.gz 导入导出**/M2.91-96**:audiod 开机武装 hbc3000 FPGA(根治 PCM 启动内核死锁)+svc_power 充电态权威链(mp2731)+amsd 注册自愈(`am rescan`)/Now Line truncated
|
||||
> 版本:下方版本段落记录到 **M6.35(0.8.35,已上卡)**;固件版本号单点 = 板上 `/etc/meowos_version`(由 `build_fw_apps.sh -v` 写入),现行产物示例 `r3proii_0.8.49.upt`。M6.19→M6.35 连续十七版推卡自刷验收——**M6.19**:ncm 后台化 UI 叠层/点击穿透+再进 ncm 桌面叠层+播放页新旧 UI 叠层抢刷脏区(软自抑帧移交不完整,恢复边沿强制整帧重画)+PIN 亮屏亮度跳+通知中心封面显歌单封面;**M6.20**:播放页右滑返回+顶部下滑通知中心+状态栏时钟 1s;**M6.21-22**:播放页返回修复(`np_exit` 退出外层主循环)+方向感知手势;**M6.23**:watchdog 亮屏亮度按用户设置恢复;**M6.24**:App 内按电源键锁屏时序;**M6.25**:bootmenu 开机菜单亮度方案 A;**M6.26**:播放页三键命中+媒体卡片体 FOCUS;**M6.27**:netmgrd 流量统计收编(pub_bytes/去 lo)+面板公网流量卡;**M6.28**:收编桌面 Now Playing 双页歌词页+歌词数据源+蓝牙 P1(netmgrd BT_CTL/bt_page);**M6.29**:播放页任意右滑返回桌面;**M6.30**:收编批5 取色+蓝牙 P2(audiod glibc-dynamic)+面板蓝牙卡;**M6.31**:桌面歌词页封面修复+alsa.conf DEV 自动识别;**M6.32**:重编 meow_ui.a+重链四 App(播放页进度条取色+ncm 列表 gap)+面板透传;**M6.33**:收编 launcher 播放页占位符移除(已有独立歌词页)+封面「空→复原」恢复修复(test_np_cover 14 项 PASS);**M6.34**:收编**批①后台常驻+宽限期**(全 App 入后台表+30min `MEOW_BG_GRACE_MS` 宽限超时回收+声明常驻/媒体活跃无限期保活+槽满 LRU+app.json `background_keepalive` 三权限位+amsd `EV BG/FG` 上报)+**audiod A3/DSD**(24/32bit 高清 byte-exact+独立 `AUDIO_FMT_DOP` DoP DSD 透传+音量收编 v2 双组写)+**meow_img_pixels 访问器上收**+**面板拆分+屏幕·遥控合并页**(前端三层+左屏右遥控 MD3 SVG 矢量按钮);**M6.35**:收编**批③后台管理页**(桌面顶栏「后台」入口+`bg_page` 后台列表+每行「杀」按钮+顶栏「一键清」,`!framework` 门控不杀框架)+**amsd FG/BG 收编**(`57e19cde`)+**i18n settings/lock 补抽**。M6.35 之后工作区已提交(暂未打包,并入 M6.36):批①叠加/穿透根治(`/tmp/meow_notif_active` 内容=前台归属 app_id+SDK present/input 共享判定+launcher 7 处转场)、主界面顶栏后台按钮图标美化、主界面列表圆角卡 8px 外距、后台管理页行 MD3 圆角卡;**MD3 对齐在跑**(agent-435,参考 `third_party/md-material-web` 最新 M3)。M6.10→M6.18:M7 launcher 重写第 1-5 期(M6.10-14:虚拟化宫格/设置页卡顿修复/OOM 通用化/自动切歌根治)+(M6.15)meowpanel jv_free 泄漏修复+(M6.16)Hello demo 滚动卡顿修复+(M6.17)歌词 UI+(M6.18)媒体通知封面+封面分辨率。M6.1→M6.9 全链自刷验收——**M6.1**:netmgrd 期3/powermgr 期2/面板 PIN 鉴权+首页仪表盘/WiFi 录入 UI/服务名统一 meow_*;**M6.2**:锁屏 PIN 全量(`system.pin` 同源面板)+VOL_DOWN×3 框架门控+显示名 name[49](amsd/launcher UTF-8 整序列截断);**M6.3**:设置页 5s 修复(309 网络异步步骤机+跳过字体 sha 校验)+310 锁屏三连(输入穿透/解锁状态栏消失/App 死亡残影);**M6.4**:310 二期 **EVIOCGRAB 锁屏独占触控**(未冻结 App 锁屏输入隔离)+主界面应用名截断;**M6.5**:audiod 315 孤儿会话两段式回收(客户端崩溃/OOM 杀/正常退出后会话完整回收,STATE 回干净无流基线);**M6.6**:锁屏 310 三期——App 内手动息屏也触发锁屏 + 锁屏脏区全屏不透明底 + 300ms 重铺;**M6.7**:锁屏页自睡(空闲超时自动灭屏,294 遗留补;0.8.7 补刷吸收 fw_content 事故修复——拆分 watchdog+meow_audio 两行);**M6.8**:310 四轮 锁屏自睡 min(系统息屏,30s);**M6.9**:netmgrd 期4(SDK 自愿通道/forget/RSSI/AAAA 过滤/iptables 评估)+powermgr 期3(policy/标定工具)固化 + 327 M7.0b launcher 试点(电源菜单/Now Playing 控件化+主题薄壳+i18n 抽串);历史:M2.69→M2.96 全链路自刷验收:控件库/主界面 MD3 双布局/M3 导航栈/M5 通知跳转/媒体会话/Now Playing/SDK nav+media 封装/字库启动竞态修复**/M2.84-88**:installed 图标修复+残帧pan加固/物理键EVIOCGKEY双源/签名鉴权 2B/meow_route 音频路由/amsd 二期(picker 契约)/流媒体客户端二期(封面·歌词后台·大歌单分页)/MeowPanel 实时画面+遥控+通知实时化/中文拼音 IME**/M2.89**:开机选单抢屏回归修复+watchdog SIGUSR1 不误杀框架 App+amsd 失联修复+顶部下滑手势分流**/M2.90**:**meow_audiod 独占音频守护**(AUDIOD_DESIGN,meow_route 并入退役)+MIUIx 顶级主题+流媒体客户端文案中文化+面板字体库 tar.gz 导入导Line truncated
|
||||
>
|
||||
> 许可证:Apache-2.0(见 [`LICENSE`](LICENSE))。本仓库为非官方第三方项目。
|
||||
> 主仓**公开可克隆**:`https://git.yeij.top/AskaEth/MeowOS-For-R3P2.git`(维护者推送走
|
||||
@@ -222,7 +222,7 @@ init (rcS)
|
||||
- **`third_party/`(入库)**—— 第三方参考/依赖归拢目录(与 MeowOS 源码 `patch_v2/` 分离、不混入构建):`CNMPlayer`(流媒体客户端功能对照)、`md-material-web`(**最新 Material 3 参考**,MD3 对齐依据,agent-435 按其对所有组件细化)。
|
||||
- **`LICENSE`** —— Apache-2.0。
|
||||
- **顶层文档(入库)**—— `APPS.md`、`MEOW_APP_SDK.md`、`FRAMEWORK_DESIGN.md`、`SYSTEM_SETTINGS.md`、`NOTIFICATION_SYSTEM.md`、`TF_CARD_LAYOUT.md`、`DEVELOPMENT_PLAN.md`、`GESTURE_BACK_DESIGN.md`、`KEY_CONTROL_DESIGN.md`、`PERF_SCHEDULER.md`、`MEOWOS_API.md`。
|
||||
- **本地构建资产(根目录 gitignore;但 `scripts/build/` 的 `README.md`/`build_fw_apps.sh`/`fw_content.conf` 已 `git add -f` 入库)**—— 其余(`fw_apps/` 固件组装产物、官方固件基线 rootfs、交叉工具链、本地开发文档、设备密钥等)不入库。详见「构建」与「部署」。
|
||||
- **本地构建资产(根目录 gitignore;但 `scripts/build/` 下 `README.md`/`DEVKIT.md`/`build_fw_apps.sh`/`fw_content.conf`/`build_devkit.sh`/`test_build_devkit.sh`/`rebuild_binaries.sh`/`verify_fingerprints.sh`/`devkit-src/*.c` 已 `git add -f` 入库,与 `git ls-files scripts/build` 一致)**—— 其余(`fw_apps/` 固件组装产物、官方固件基线 rootfs、交叉工具链、本地开发文档、设备密钥等)不入库。详见「构建」与「部署」。
|
||||
|
||||
---
|
||||
|
||||
@@ -247,7 +247,7 @@ patch_v2/hello_app/build_hello.sh # 链 meow_ui.a + libz.a + -lpthread → hel
|
||||
固件 = 官方 v1.4 固件基线 rootfs + SSH 工具集 + MeowOS 系统组件(`meow_bootmenu` / `meow_launcherd` / `meow_watchdog` / `meow_svcd` / `meow_mdns` / `meow_sysui` / `nyai` + 相关 init 脚本 + `/etc/meowos_version`)。特点:
|
||||
|
||||
- 打包脚本 `scripts/build/build_fw_apps.sh` 是**配置驱动**的通用脚本(`-c fw_content.conf -v <ver> -o <out.upt> [-b <base>]`):`fw_content.conf` 清单逐条 `src|dst|md5|desc`,**`.upt` 条目 md5 配置化**(`md5` 空=不校验,填了=构建前强校验源文件,不一致即中止);**改固件内容只改 `fw_content.conf`,不动脚本**。历史逐版 `build_fw_apps_m2NN.sh` 已**删除**(每版复制一个、内部硬编码 md5、且共享 `fw_apps/` 互相清空)。工作区 `fw_apps/build/`(每次重建)与产物归档 `fw_apps/out/` 分离防互清。这些脚本/清单已 `git add -f` 入库(`scripts/build/` 根目录仍 gitignore),其余为**本地构建工具(gitignore 不入库)**——本仓库只提交源码与文档,需本地交叉工具链 + 官方固件基线 rootfs 才能复现。详见 `scripts/build/README.md`。
|
||||
- **rootfs 上限 45,088,768 B(≈45MB)**;产物归档 `fw_apps/out/r3proii_m2NN.upt`。
|
||||
- **rootfs 上限 45,088,768 B(≈45MB)**;产物归档 `fw_apps/out/r3proii_<ver>.upt`(如 `r3proii_0.8.49.upt`;`-o` 要显式写进 `out/`,只给文件名会落在仓库根,见 `scripts/build/README.md`)。
|
||||
- `/usr/bin` 各组件为静态 MIPS32r2;版本号以 `/etc/meowos_version` 为单点(`build_fw_apps.sh -v` 写入)。
|
||||
- **`.upt` 容器 md5 不可复现**(repack 时间戳),以**内容级 0 diff 回验**为准。
|
||||
- 外围 CLI(curl/socat/tmux/jq/…)已迁 TF 卡 `MeowOS/opt/bin/`(不进固件);固件**不含** SDK 库(`meow_ui.a` 只随 App 链接),`hiby_player` 保持官方原样。
|
||||
@@ -260,7 +260,7 @@ patch_v2/hello_app/build_hello.sh # 链 meow_ui.a + libz.a + -lpthread → hel
|
||||
|
||||
**刷机(整机刷新 MeowOS 固件)**
|
||||
|
||||
- 打包产物名 `r3proii_m2NN.upt`;要刷机时把它**复制为 TF 卡根目录的 `r3proii.upt`**。
|
||||
- 打包产物名 `r3proii_<ver>.upt`(如 `r3proii_0.8.49.upt`);要刷机时把它**复制为 TF 卡根目录的 `r3proii.upt`**。
|
||||
- 关机(failsafe)后按住**音量加 + 电源**进入升级模式,从卡根 `r3proii.upt` 升级。避免使用播放器 UI 里那个会卡死的升级按钮。
|
||||
- 回退:用官方固件重刷,或从 `backups/`(NAND 全量备份,本地)恢复。
|
||||
|
||||
|
||||
@@ -135,6 +135,10 @@ def build_charset(tier="common", extra=""):
|
||||
add(ord(bytes([hi, lo]).decode("gb2312")))
|
||||
except UnicodeDecodeError:
|
||||
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"):
|
||||
# CJK 统一表意(含常用简繁/生僻):U+4E00-9FFF ≈ 2万+
|
||||
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")"
|
||||
CC="$HOME/Project/R3ProII/.tools/zig-mipsel-cc"
|
||||
MEOW_TOUCH_DIR="../meow_launcherd"
|
||||
"$CC" -Os -static -I "$MEOW_TOUCH_DIR" meow_bootmenu.c "$MEOW_TOUCH_DIR/meow_touch.c" -o meow_bootmenu
|
||||
# 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
|
||||
file meow_bootmenu
|
||||
Binary file not shown.
@@ -32,6 +32,26 @@
|
||||
* 说明:/proc/mtd 只反映分区表不变读,无法观察内容变化,故用轻量读 /dev/mtd5 轮询。
|
||||
* 两根都写 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)
|
||||
* ============================================================================
|
||||
@@ -81,7 +101,7 @@
|
||||
#include <time.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/poll.h>
|
||||
#include <poll.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h> /* M2.29:supervisor 接管关机/升级,直连 reboot(2) syscall */
|
||||
@@ -100,6 +120,8 @@
|
||||
#define FB_DEV "/dev/fb0"
|
||||
#define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs"
|
||||
#define CONF_FILE MEOWAPPS "/meow.conf" /* TF 卡配置:boot_timeout_ms/boot_menu/bg_color;boot_default 权威在板载 /data/meow_boot_default */
|
||||
#define BOOT_DEFAULT_FILE "/data/meow_boot_default" /* 持久默认=设置里的「默认系统」:0=MeowOS,1=HibyOS */
|
||||
#define BOOT_ONCE_FILE "/data/meow_boot_once" /* 一次性指示:1=这次开机进 HibyOS(本程序读一次即删) */
|
||||
#define MODE_FILE "/tmp/meow_boot_mode" /* 开机模式标记(tmpfs,开机清空) */
|
||||
#define PLAYER_BIN "/usr/bin/hiby_player"
|
||||
#define PLAYER_LOG "/usr/data/meow_player.log"
|
||||
@@ -111,6 +133,7 @@
|
||||
#define FBIOBLANK 0x4611
|
||||
#endif
|
||||
#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"
|
||||
|
||||
/* ---- 热键切换(HibyOS→MeowOS):按住电源 + 窗口内单击音量减 ---- */
|
||||
@@ -152,8 +175,11 @@
|
||||
#endif
|
||||
#define KEYBUF_WORDS 32 /* 1024 bits, 覆盖 KEY_MAX 767 */
|
||||
|
||||
/* ---- 编译默认值(可被 /usr/data/mnt/sd_0/MeowAPPs/meow.conf 覆写)---- */
|
||||
#define DEF_BOOT_DEFAULT 1 /* 1=HibyOS 0=MeowOS */
|
||||
/* ---- 编译默认值(可被 /usr/data/mnt/sd_0/MeowAPPs/meow.conf 覆写)----
|
||||
* M69(2026-11-12 定案):默认改 0(MeowOS)。理由:设置页「默认系统」的默认值就是 meow
|
||||
* (meow_settings.c `boot.default` 默认 "meow"),且这是 MeowOS 自己的固件——用户从未设过默认
|
||||
* 时不该被丢进 Hiby(旧值 1 与设置页默认矛盾,也是「每次升级后要手动切」的主因)。 */
|
||||
#define DEF_BOOT_DEFAULT 0 /* 1=HibyOS 0=MeowOS */
|
||||
#define DEF_BOOT_TIMEOUT_MS 5000
|
||||
#define DEF_BOOT_MENU 1
|
||||
#define DEF_BG 0x000000
|
||||
@@ -161,7 +187,7 @@
|
||||
#define POLL_MS 10
|
||||
#define SIM_SELECT_DELAY_MS 2000 /* --sim-select 延迟后模拟选 */
|
||||
#define GHOST_COOLDOWN_MS 800 /* 入口 drain + 短暂冷却防幽灵触摸(M2.1:缩短,不吞首次有效输入) */
|
||||
#define MEOW_BOOTMENU_VER "0.1.0-m2.29-takeover" /* M2.29:supervisor 接管关机(poweroff)/升级(reboot) */
|
||||
#define MEOW_BOOTMENU_VER "0.1.0-m2.30-bri-src" /* M23:亮度单一权威源(settings.conf),二进制随源码重编 */
|
||||
|
||||
/* ---- 版式 ---- */
|
||||
#define STATUS_H 40
|
||||
@@ -329,7 +355,7 @@ static int fb_open(void)
|
||||
fb_bpp = *(uint32_t *)(vinfo + 24);
|
||||
fb_line = *(uint32_t *)(finfo + 44);
|
||||
if (!fb_line) fb_line = fb_xres * (fb_bpp / 8);
|
||||
if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* 单缓冲兜底 */
|
||||
if (fb_yres_virtual < (unsigned)fb_yres) fb_yres_virtual = fb_yres; /* 单缓冲兜底 */
|
||||
fb = mmap(NULL, (size_t)fb_line * fb_yres_virtual,
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (fb == MAP_FAILED) {
|
||||
@@ -345,7 +371,7 @@ static int fb_open(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;
|
||||
fb_orig_yoffset = (int)fb_active_yoffset;
|
||||
fb_snapshot_len = (size_t)fb_line * fb_yres;
|
||||
@@ -357,7 +383,7 @@ static void fb_snapshot_player(void)
|
||||
/* 画前 FBIOPAN_DISPLAY pan 到 yoffset=0(buffer0,选单/app 共同绘制目标) */
|
||||
static void fb_pan_buf0(void)
|
||||
{
|
||||
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
|
||||
if (fb_fd < 0 || fb_yres_virtual <= (unsigned)fb_yres) { fb_active_yoffset = 0; return; }
|
||||
if (fb_active_yoffset == 0) return; /* 已显示 buffer0 */
|
||||
unsigned char v[160];
|
||||
memset(v, 0, sizeof v);
|
||||
@@ -436,6 +462,7 @@ static int amixer_vol(int numid)
|
||||
/* 28px 状态栏:行1=时钟(左) 电量% WIFI BT(右),行2=VOL nn%(左),y=28 分隔线(40→28,用户定) */
|
||||
static void draw_statusbar(uint32_t bg)
|
||||
{
|
||||
(void)bg; /* 传参保留(调用点已传 cfg_bg),本版状态栏用固定配色 */
|
||||
char timebuf[16], right[48];
|
||||
time_t now = time(NULL);
|
||||
struct tm tm;
|
||||
@@ -492,18 +519,30 @@ static void draw_clock(uint32_t bg)
|
||||
|
||||
/* ================================================================== */
|
||||
/* 配置:boot_default / boot_timeout_ms / boot_menu / bg_color */
|
||||
/* boot_default:权威 = 板载 /data/meow_boot_default(O_TRUNC 单值、不写 TF 卡);
|
||||
* meow.conf 的 boot_default= 仅作无板载标记时的兜底(不新增写入,见 set_boot_default_meow)。 */
|
||||
/* boot_default 优先级(高 → 低;M69 定案 2026-11-12):
|
||||
* ① /data/meow_boot_once 一次性指示(「这一次去 HibyOS」,读一次即删)
|
||||
* ② /data/meow_boot_default 持久默认 = 设置里的「默认系统」(权威)
|
||||
* ③ TF 卡 meow.conf 的 boot_default=(无板载标记时的兜底)
|
||||
* ④ 编译默认 DEF_BOOT_DEFAULT(=MeowOS)
|
||||
* 一次性指示必须压过持久默认,否则用户把默认设成 MeowOS 后「切换到 HibyOS」永远失效。 */
|
||||
/* ================================================================== */
|
||||
static int cfg_boot_default = DEF_BOOT_DEFAULT;
|
||||
static long cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS;
|
||||
static int cfg_boot_menu = DEF_BOOT_MENU;
|
||||
static uint32_t cfg_bg = DEF_BG;
|
||||
|
||||
/* 读板载 /data/meow_boot_default(开机默认权威标记)。返回 0/1;-1=不存在/非法。 */
|
||||
/* 配置/标记文件的生产路径;宿主自测可直接重定向这三个指针(只换路径,不换语义)。 */
|
||||
static const char *g_conf_path = CONF_FILE;
|
||||
static const char *g_boot_default_path = BOOT_DEFAULT_FILE;
|
||||
static const char *g_boot_once_path = BOOT_ONCE_FILE;
|
||||
|
||||
/* bm_log 定义在后方(卡日志);load_config 记录一次性指示消费结果要用。 */
|
||||
static void bm_log(const char *fmt, ...);
|
||||
|
||||
/* 读板载 /data/meow_boot_default(=设置里的「默认系统」)。返回 0/1;-1=不存在/非法。 */
|
||||
static int read_boot_default_onboard(void)
|
||||
{
|
||||
int fd = open("/data/meow_boot_default", O_RDONLY);
|
||||
int fd = open(g_boot_default_path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
char buf[8];
|
||||
int n = read(fd, buf, sizeof buf - 1);
|
||||
@@ -514,6 +553,50 @@ static int read_boot_default_onboard(void)
|
||||
return (v == 0 || v == 1) ? v : -1;
|
||||
}
|
||||
|
||||
/* 消费一次性指示 /data/meow_boot_once(meow_svc_power 写,本程序开机读一次即删)。
|
||||
* 返回 0/1;-1=不存在/内容非法。
|
||||
* 删除失败**不许影响开机**:本次照用该值;再尽量把内容清空,保证下一次开机读不到有效值
|
||||
* (两条都失败只记日志继续走,宁可日志脏也不让机器开不了机)。 */
|
||||
static int consume_boot_once(void)
|
||||
{
|
||||
int fd = open(g_boot_once_path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
char buf[8];
|
||||
int n = read(fd, buf, sizeof buf - 1);
|
||||
close(fd);
|
||||
int v = -1;
|
||||
if (n > 0) {
|
||||
/* 严格取值:只接受 "0" / "1"(允许前后空白)。不能直接用 atoi —— "garbage" 会被
|
||||
* atoi 读成 0,等于把垃圾内容当成「进 MeowOS」的指示。 */
|
||||
buf[n] = 0;
|
||||
char *p = buf;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
|
||||
if (*p == '0' || *p == '1') {
|
||||
char c = *p++;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
|
||||
if (*p == 0) v = c - '0';
|
||||
}
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
if (unlink(g_boot_once_path) == 0) {
|
||||
bm_log("once: %s -> %s (used, removed)", g_boot_once_path, v < 0 ? "invalid" : (v ? "hiby" : "meow"));
|
||||
return v;
|
||||
}
|
||||
int uerr = errno;
|
||||
/* 删不掉:退而求其次清空内容(下次 open 读到 0 字节 → 非法 → 回落持久默认)。 */
|
||||
int wf = open(g_boot_once_path, O_WRONLY | O_TRUNC);
|
||||
if (wf >= 0) {
|
||||
close(wf);
|
||||
bm_log("once: %s -> %s (used; unlink failed: %s; truncated so next boot ignores it)",
|
||||
g_boot_once_path, v < 0 ? "invalid" : (v ? "hiby" : "meow"), strerror(uerr));
|
||||
} else {
|
||||
bm_log("once: %s -> %s (used; unlink+truncate failed: %s/%s; file may apply again next boot)",
|
||||
g_boot_once_path, v < 0 ? "invalid" : (v ? "hiby" : "meow"), strerror(uerr), strerror(errno));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static void load_config(void)
|
||||
{
|
||||
cfg_boot_default = DEF_BOOT_DEFAULT;
|
||||
@@ -521,13 +604,13 @@ static void load_config(void)
|
||||
cfg_boot_menu = DEF_BOOT_MENU;
|
||||
cfg_bg = DEF_BG;
|
||||
int has_conf = 1;
|
||||
FILE *f = fopen(CONF_FILE, "r");
|
||||
if (!f) { fprintf(stderr, "bootmenu: no %s, using defaults\n", CONF_FILE); has_conf = 0; }
|
||||
FILE *f = fopen(g_conf_path, "r");
|
||||
if (!f) { fprintf(stderr, "bootmenu: no %s, using defaults\n", g_conf_path); has_conf = 0; }
|
||||
if (has_conf) {
|
||||
char buf[128];
|
||||
while (fgets(buf, sizeof buf, f)) {
|
||||
if (!strncmp(buf, "boot_default=", 13)) {
|
||||
/* 板载 /data/meow_boot_default 为权威;此处仅作无板载标记时的兜底(不再新增写入)。 */
|
||||
/* 板载标记为权威;此处仅作无板载标记时的兜底(不再新增写入)。 */
|
||||
cfg_boot_default = atoi(buf + 13);
|
||||
if (cfg_boot_default != 0 && cfg_boot_default != 1) cfg_boot_default = DEF_BOOT_DEFAULT;
|
||||
} else if (!strncmp(buf, "boot_timeout_ms=", 16)) {
|
||||
@@ -543,13 +626,21 @@ static void load_config(void)
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
/* 板载标记优先(权威):覆盖 meow.conf 兜底/编译默认。 */
|
||||
/* 一次性指示(这次开机专用)压过一切持久来源;读到即删。 */
|
||||
int oc = consume_boot_once();
|
||||
if (oc == 0 || oc == 1) {
|
||||
if (oc != cfg_boot_default)
|
||||
fprintf(stderr, "bootmenu: boot_default once=%d (override %d)\n", oc, cfg_boot_default);
|
||||
cfg_boot_default = oc;
|
||||
} else {
|
||||
/* 持久默认(设置里的「默认系统」)优先:覆盖 meow.conf 兜底/编译默认。 */
|
||||
int ob = read_boot_default_onboard();
|
||||
if (ob == 0 || ob == 1) {
|
||||
if (ob != cfg_boot_default)
|
||||
fprintf(stderr, "bootmenu: boot_default onboard=%d (override %d)\n", ob, cfg_boot_default);
|
||||
cfg_boot_default = ob;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "bootmenu: cfg boot_default=%d timeout_ms=%ld boot_menu=%d bg=%06x\n",
|
||||
cfg_boot_default, cfg_boot_timeout_ms, cfg_boot_menu, cfg_bg);
|
||||
}
|
||||
@@ -814,9 +905,23 @@ static void draw_menu(int cursor, long remaining_ms)
|
||||
/* ================================================================== */
|
||||
/* 面板/背光 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)
|
||||
{
|
||||
int fd = open(BACKLIGHT, O_RDONLY);
|
||||
int fd = open(bm_backlight_path(), O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
char buf[16];
|
||||
int n = read(fd, buf, sizeof buf - 1);
|
||||
@@ -839,24 +944,46 @@ static void write_brightness(int val)
|
||||
{
|
||||
char buf[8];
|
||||
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 (write(fd, buf, l) < 0) fprintf(stderr, "bootmenu: backlight write %d: %s\n", val, strerror(errno));
|
||||
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)。
|
||||
* 根因:选单显示路径从不写背光,开机菜单/第二屏亮度纯继承引导早期面板高值;M2.83 起 launcher
|
||||
* 「开机选单期不抢屏」怠速,移除了 launcher 提前写用户亮度那一步 → 菜单亮在引导高值上。
|
||||
* 与 meow_watchdog wd_settings_brightness() 同源解析(同文件、同 clamp:下限 display.brightness_min
|
||||
* 默认 1=极暗但可见、上限 100=防闪屏),自包含零依赖,不链 launcher meow_settings。返回 clamped
|
||||
* 值;settings 文件不可读 → 返回 -1,调用方**不动**(退化为现状,保安全——选单阶段只写 brightness、
|
||||
* 绝不碰 FBIOBLANK/blank,§18.24 纪律)。 */
|
||||
static int bootmenu_settings_brightness(void)
|
||||
* 绝不碰 FBIOBLANK/blank,§18.24 纪律)。
|
||||
* 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;
|
||||
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
|
||||
int b = -1; /* -1 = 文件里没看到 display.brightness(视为不可信 → 返回 -1) */
|
||||
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
|
||||
char buf[128];
|
||||
while (fgets(buf, sizeof buf, f)) {
|
||||
@@ -871,11 +998,99 @@ static int bootmenu_settings_brightness(void)
|
||||
else if (!strcmp(k, "display.brightness")) { b = iv; }
|
||||
}
|
||||
fclose(f);
|
||||
if (min_out) *min_out = mn;
|
||||
if (b < 0) return -1;
|
||||
if (b < mn) b = mn;
|
||||
if (b > 100) b = 100;
|
||||
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 接管;
|
||||
* 播放器已死,故不涉及「播放器 state machine」一致性(§18.11 关注点)。
|
||||
* bootmenu 选单/自检阶段绝不调用本函数(§18.24 纪律:不碰 panel blank)。 */
|
||||
@@ -1192,14 +1407,19 @@ static int confirm_switch(void)
|
||||
/* ---- HibyOS 分支:父进程监督者/热键守护(M2)---- */
|
||||
static void bootmenu_supervisor(int player_pid)
|
||||
{
|
||||
/* 记录开机后的正常亮度,供「杀播放器后亮度==0 则恢复 saved/60」。
|
||||
* 此刻播放器刚拉起,可能已写入其背光值;过早读到 0 则回退默认 60。 */
|
||||
int saved_brightness = read_brightness();
|
||||
/* M2.9 Bug 3:开机初期播放器启动画面会把背光设为瞬时高值(本机 max=101),
|
||||
* 若直接以其为「保底亮度」,会在扫描窗首次写回时跳一次最高亮度(开机瞬间闪一下)。
|
||||
* 只信任稳定读数:读到 0 或异常高(>稳定上限 60,适中非最大)一律回退稳定值 60,
|
||||
* 确保扫描期写回平滑(只写目标一次、不跳 max)。 */
|
||||
if (saved_brightness <= 0 || saved_brightness > 60) saved_brightness = 60;
|
||||
/* M23:保底亮度(saved_brightness)的权威源 = settings.conf 的 display.brightness
|
||||
* (与 launcher / meow_watchdog 同源同 clamp),不再是「启动瞬间面板读数 + >60 钉 60」。
|
||||
* 旧写法把播放器启动瞬间写入的面板值(真机 60)当权威,用户设置 1 被顶掉,随后扫描窗
|
||||
* 保底 / 熄屏恢复都把 60 写回 → 「亮/灭屏瞬间爆亮」。
|
||||
* 取值优先级见 bm_bri_pick_saved:settings → 面板读数 → BM_BRI_SAFE_LOW,后两者夹进
|
||||
* [brightness_min, max_brightness](取代旧的 >60→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",
|
||||
player_pid, saved_brightness);
|
||||
/* 信号记录 hook(M2 诊断):先装 handler、start 一行写入 TF 卡日志。
|
||||
@@ -1207,12 +1427,19 @@ static void bootmenu_supervisor(int player_pid)
|
||||
bm_sig_install();
|
||||
bm_log("supervisor: start pid=%d saved_brightness=%d mode=hiby ver=%s",
|
||||
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 开机媒体扫描窗内做亮度保底。播放器忙(媒体扫描)时其背光
|
||||
* 待机会把亮度降到 0、面板 blank → 黑屏几分钟。supervisor 在扫描窗内若读到
|
||||
* 亮度 0,写回保底亮度(saved 或 60)。**只写 brightness、绝不碰 FBIOBLANK**
|
||||
* (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。 */
|
||||
* 待机会把亮度降到 0、面板 blank → 黑屏几分钟。**只写 brightness、绝不碰 FBIOBLANK**
|
||||
* (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。
|
||||
* M23:补的值改为用户设置(不再 60);触发回到 pre-M23 的「面板暗」,不抢用户熄屏靠
|
||||
* 「电源键锁存」(见 bm_bri_floor_decide)。 */
|
||||
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;
|
||||
if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "supervisor: WARN no power dev\n");
|
||||
@@ -1268,19 +1495,10 @@ static void bootmenu_supervisor(int player_pid)
|
||||
}
|
||||
}
|
||||
|
||||
/* 2a) Bug 4 亮度保底:扫描窗内、播放器存活且亮度被降到 0 → 写回保底亮度。
|
||||
* (播放器扫描忙时无法响应唤醒,我们只把背光拉回,画面仍为播放器最后帧,
|
||||
* 不再黑屏数分钟;不碰 blank。) */
|
||||
if (player_alive && now_ms() < scan_keep_until) {
|
||||
int bri = read_brightness();
|
||||
if (bri <= 0 && now_ms() - last_bri_floor_at >= 400) {
|
||||
int floor = saved_brightness > 0 ? saved_brightness : 60;
|
||||
write_brightness(floor);
|
||||
last_bri_floor_at = now_ms();
|
||||
fprintf(stderr, "supervisor: scan-window brightness floor -> %d\n", floor);
|
||||
}
|
||||
}
|
||||
|
||||
/* 2a) Bug 4 亮度保底(M23):**已移到主循环后半(按键采样之后)**,见下面
|
||||
* 「2b) 亮度保底」—— 电源键边沿与播放器写 0 落在同一 poll 间隙时,先采键再判定
|
||||
* 才不会出现一轮「未看到电源键就补亮」的竞态(评审 r2 非阻塞建议)。
|
||||
*/
|
||||
/* 2) EVIOCGKEY 采样(event0 电源 / event2 音量) */
|
||||
int pwr = 0, vold = 0;
|
||||
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
|
||||
@@ -1294,12 +1512,54 @@ static void bootmenu_supervisor(int player_pid)
|
||||
if (se >= sim_hotkey_ms + 800 && se < sim_hotkey_ms + 950) vold = 1;
|
||||
}
|
||||
|
||||
/* 3) 热键:电源按下边沿 arming,窗口内音量减按下边沿触发 */
|
||||
if (pwr && !prev_pwr) { pwr_hold_armed = 1; pwr_down_at = t; }
|
||||
/* 3) 热键:电源按下边沿 arming,窗口内音量减按下边沿触发。
|
||||
* 同一边沿也记到 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_hold_armed && pwr && (t - pwr_down_at) > HK_PWRWIN_MAX)
|
||||
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;
|
||||
if (pwr_hold_armed && pwr && vold && !prev_vold) {
|
||||
long since = (long)(t - pwr_down_at);
|
||||
@@ -1338,10 +1598,16 @@ static void bootmenu_supervisor(int player_pid)
|
||||
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
continue;
|
||||
}
|
||||
/* 刷新 saved 亮度:若播放器当前亮度仍为正(如「背光常亮」设置),
|
||||
* 以它为准;若电源按下已触发灭屏(亮度 0),保留此前 saved(≥60)。 */
|
||||
/* 刷新 saved 亮度:若播放器当前亮度仍为正(如「背光常亮」设置 / 用户在 HibyOS 里
|
||||
* 改过亮度但没写 settings.conf),以面板此刻的真实值为准(clamp 到
|
||||
* [brightness_min, max_brightness]);若电源按下已触发灭屏(亮度 0),保留此前 saved
|
||||
* —— M23 起 saved 来自 settings.conf 的用户亮度,不再回退 60。见 bm_bri_hotkey_saved。 */
|
||||
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),防止其在确认框上重绘覆盖;
|
||||
* 同时捕获面板是否已因电源按下而灭屏(was_blank),供较后恢复。 */
|
||||
if (player_alive && !proc_is_stopped(player_pid)) {
|
||||
@@ -1354,7 +1620,10 @@ static void bootmenu_supervisor(int player_pid)
|
||||
|
||||
/* 点亮面板以便看清确认框(播放器已冻结,不会 re-blank)。 */
|
||||
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");
|
||||
|
||||
int ans = confirm_switch();
|
||||
@@ -1409,6 +1678,7 @@ static void bootmenu_supervisor(int player_pid)
|
||||
if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); }
|
||||
} else if (read_brightness() <= 0) {
|
||||
write_brightness(saved_brightness);
|
||||
bm_log("supervisor: cancel restore brightness=%d (panel read 0)", saved_brightness);
|
||||
}
|
||||
fprintf(stderr, "supervisor: hotkey cancelled (stay HibyOS)\n");
|
||||
if (player_alive && proc_is_stopped(player_pid)) {
|
||||
@@ -1443,14 +1713,18 @@ static void write_mode(int choice)
|
||||
choice == 1 ? "hiby" : "meow");
|
||||
}
|
||||
|
||||
/* Bug 1:选 MeowOS → 把它持久为开机默认(写 spec 标记 + meow.conf boot_default=0),与 launcher
|
||||
* «回到HibyOS»(set_boot_default_hiby 写 boot_default=1)对称。效果:本次选中即进 MeowOS 主屏,且
|
||||
* 下一次开机选单默认 MeowOS(不再「选 MeowOS 仍落回 HibyOS」)。
|
||||
* 权威标记 = 板载 /data/meow_boot_default(O_TRUNC 单值);meow.conf 不再写/承载 boot_default。 */
|
||||
/* Bug 1:选 MeowOS → 把它持久为开机默认(写 spec 标记),与设置页「默认系统」/ `set-default-*` 对称。
|
||||
* 效果:本次选中即进 MeowOS 主屏,且下一次开机选单默认 MeowOS(不再「选 MeowOS 仍落回 HibyOS」)。
|
||||
* 权威标记 = 板载 /data/meow_boot_default(O_TRUNC 单值);meow.conf 不再写/承载 boot_default。
|
||||
* 注意:这是**用户在选单里显式选了 MeowOS**(等同设默认),与电源菜单「重启」路径无关。 */
|
||||
static void set_boot_default_meow(void)
|
||||
{
|
||||
int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd >= 0) { write(fd, "0\n", 2); close(fd); fprintf(stderr, "bootmenu: /data/meow_boot_default=0\n"); }
|
||||
int fd = open(g_boot_default_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd >= 0) {
|
||||
write(fd, "0\n", 2);
|
||||
close(fd);
|
||||
fprintf(stderr, "bootmenu: %s=0\n", g_boot_default_path);
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
@@ -1628,7 +1902,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); }
|
||||
/* 每秒刷倒计时(秒值变化才刷) */
|
||||
long cd = (remaining + 999) / 1000;
|
||||
if (el - last_cd_tick >= 1000) { last_cd_tick = el; draw_countdown(cfg_bg, remaining); }
|
||||
|
||||
usleep(POLL_MS * 1000);
|
||||
@@ -1676,6 +1949,9 @@ static void usage(void)
|
||||
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)
|
||||
{
|
||||
long dumpkeys = 0, timeout_override = 0;
|
||||
@@ -1737,10 +2013,17 @@ int main(int argc, char **argv)
|
||||
}
|
||||
|
||||
/* 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();
|
||||
if (b > 0) write_brightness(b);
|
||||
int b = bootmenu_settings_brightness(NULL);
|
||||
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;
|
||||
@@ -1750,3 +2033,6 @@ int main(int argc, char **argv)
|
||||
run_menu(sim_sel, do_fork);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* BM_NO_MAIN(宿主单测)到此为止 */
|
||||
#endif
|
||||
@@ -0,0 +1,197 @@
|
||||
/* test_bm_bootdefault.c — 宿主自测:启动菜单「默认项从哪来」+ 一次性指示(M69)。
|
||||
*
|
||||
* 覆盖的验收推演:
|
||||
* ① 文件全不存在 → 编译默认 MeowOS(DEF_BOOT_DEFAULT=0);
|
||||
* ② /data/meow_boot_default(设置里的「默认系统」)压过 meow.conf 兜底;
|
||||
* ③ 一次性指示 /data/meow_boot_once 压过持久默认,且**读一次即删**;
|
||||
* ④ 删掉之后的下一次开机回落持久默认;
|
||||
* ⑤ 内容非法 → 忽略并删除;
|
||||
* ⑥ 删不掉时「能用就用、不许开机失败」,并尽量清空内容让下一次开机不生效。
|
||||
*
|
||||
* 编译/运行(Termux 上 /tmp 不可写,带 TMPDIR 最稳):
|
||||
* mkdir -p "$HOME/tmp/m69test"
|
||||
* cc -Wall -Wextra -Os -I../meow_launcherd -Wno-unused-function -Wno-unused-parameter \
|
||||
* -Wno-unused-variable test_bm_bootdefault.c ../meow_launcherd/meow_touch.c \
|
||||
* -o "$HOME/tmp/m69test/bm_bt" &&
|
||||
* TMPDIR=$HOME/tmp/m69test "$HOME/tmp/m69test/bm_bt" # 全 PASS -> exit 0
|
||||
*/
|
||||
#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); }
|
||||
}
|
||||
|
||||
static char g_dir[512], g_conf[600], g_def[600], g_once[600];
|
||||
|
||||
/* 临时目录:$TMPDIR → /tmp → $HOME/tmp/m69test(Termux 上 /tmp 属别的用户,不可写)。 */
|
||||
static int mk_tmp_dir(void)
|
||||
{
|
||||
const char *cand[3];
|
||||
char homebuf[512] = "";
|
||||
int n = 0;
|
||||
const char *t = getenv("TMPDIR");
|
||||
if (t && t[0]) cand[n++] = t;
|
||||
cand[n++] = "/tmp";
|
||||
const char *home = getenv("HOME");
|
||||
if (home && home[0]) {
|
||||
snprintf(homebuf, sizeof homebuf, "%s/tmp/m69test", home);
|
||||
mkdir(homebuf, 0700);
|
||||
cand[n++] = homebuf;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
snprintf(g_dir, sizeof g_dir, "%s/m69_bm_XXXXXX", cand[i]);
|
||||
if (!mkdtemp(g_dir)) continue;
|
||||
snprintf(g_conf, sizeof g_conf, "%s/meow.conf", g_dir);
|
||||
snprintf(g_def, sizeof g_def, "%s/boot_default", g_dir);
|
||||
snprintf(g_once, sizeof g_once, "%s/boot_once", g_dir);
|
||||
g_conf_path = g_conf;
|
||||
g_boot_default_path = g_def;
|
||||
g_boot_once_path = g_once;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void put(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 void reset(void)
|
||||
{
|
||||
unlink(g_conf);
|
||||
unlink(g_def);
|
||||
unlink(g_once);
|
||||
}
|
||||
|
||||
static long file_size(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) return -1;
|
||||
return (long)st.st_size;
|
||||
}
|
||||
|
||||
/* ---- ① 什么都没有 → 编译默认 MeowOS ---- */
|
||||
static void case_compile_default(void)
|
||||
{
|
||||
printf("== no files -> compile default ==\n");
|
||||
reset();
|
||||
load_config();
|
||||
check("compile default is MeowOS (0)", cfg_boot_default, 0);
|
||||
check("DEF_BOOT_DEFAULT == 0", DEF_BOOT_DEFAULT, 0);
|
||||
}
|
||||
|
||||
/* ---- ② 持久默认 = 设置里的「默认系统」,压过 conf 兜底 ---- */
|
||||
static void case_persistent_is_authoritative(void)
|
||||
{
|
||||
printf("== persistent boot default ==\n");
|
||||
reset();
|
||||
put(g_def, "1\n");
|
||||
load_config();
|
||||
check("persistent=1 -> Hiby", cfg_boot_default, 1);
|
||||
|
||||
put(g_conf, "boot_default=0\n");
|
||||
load_config();
|
||||
check("persistent=1 overrides conf boot_default=0", cfg_boot_default, 1);
|
||||
|
||||
put(g_def, "0\n");
|
||||
load_config();
|
||||
check("persistent=0 -> MeowOS", cfg_boot_default, 0);
|
||||
}
|
||||
|
||||
/* ---- ③④ 一次性指示:优先、读一次即删、下次回落持久默认 ---- */
|
||||
static void case_once_overrides_and_is_consumed(void)
|
||||
{
|
||||
printf("== one-shot boot_once ==\n");
|
||||
reset();
|
||||
put(g_def, "0\n"); /* 用户设的默认 = MeowOS */
|
||||
put(g_once, "1\n"); /* 「切换到 HibyOS」= 这一次去 Hiby */
|
||||
|
||||
load_config();
|
||||
check("once=1 overrides persistent=0", cfg_boot_default, 1);
|
||||
check("once file removed after use", access(g_once, F_OK), -1);
|
||||
|
||||
load_config(); /* 模拟下一次开机 */
|
||||
check("next boot falls back to persistent 0", cfg_boot_default, 0);
|
||||
}
|
||||
|
||||
/* ---- ⑤ 非法内容:忽略 + 删除(不留垃圾反复干扰) ---- */
|
||||
static void case_invalid_once(void)
|
||||
{
|
||||
printf("== invalid boot_once ==\n");
|
||||
reset();
|
||||
put(g_def, "1\n");
|
||||
put(g_once, "garbage\n");
|
||||
load_config();
|
||||
check("invalid once ignored (persistent=1 wins)", cfg_boot_default, 1);
|
||||
check("invalid once removed", access(g_once, F_OK), -1);
|
||||
}
|
||||
|
||||
/* ---- ⑥ 删不掉:能用就用(不许开机失败),并尽量清空让下次不生效 ----
|
||||
* 构造「目录不可写 + 文件可写」:unlink 需要目录写权限 → 失败;O_TRUNC 只需文件写权限 → 成功。
|
||||
* (以 root 跑时 unlink 会成功,本用例自动 SKIP。) */
|
||||
static void case_unlink_failure(void)
|
||||
{
|
||||
printf("== unlink failure is survivable ==\n");
|
||||
reset();
|
||||
put(g_def, "0\n");
|
||||
put(g_once, "1\n");
|
||||
|
||||
chmod(g_dir, 0555);
|
||||
if (unlink(g_once) == 0) { /* 探测:权限没生效(多半是 root) */
|
||||
chmod(g_dir, 0755);
|
||||
put(g_once, "1\n");
|
||||
printf("SKIP unlink-failure case (unlink succeeded, uid=%d)\n", (int)getuid());
|
||||
return;
|
||||
}
|
||||
|
||||
load_config();
|
||||
check("unremovable once still used this boot", cfg_boot_default, 1);
|
||||
check("once content truncated to empty", (int)file_size(g_once), 0);
|
||||
|
||||
chmod(g_dir, 0755);
|
||||
load_config(); /* 模拟下一次开机 */
|
||||
check("next boot ignores emptied once (persistent=0)", cfg_boot_default, 0);
|
||||
}
|
||||
|
||||
/* ---- 组件契约:两个组件必须用同一对生产路径 ---- */
|
||||
static void case_prod_paths(void)
|
||||
{
|
||||
printf("== production paths ==\n");
|
||||
check("BOOT_DEFAULT_FILE", strcmp(BOOT_DEFAULT_FILE, "/data/meow_boot_default"), 0);
|
||||
check("BOOT_ONCE_FILE", strcmp(BOOT_ONCE_FILE, "/data/meow_boot_once"), 0);
|
||||
check("CONF_FILE on TF card", strcmp(CONF_FILE, "/usr/data/mnt/sd_0/MeowAPPs/meow.conf"), 0);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
if (mk_tmp_dir() != 0) { printf("FATAL: no writable tmp dir\n"); return 1; }
|
||||
|
||||
case_compile_default();
|
||||
case_persistent_is_authoritative();
|
||||
case_once_overrides_and_is_consumed();
|
||||
case_invalid_once();
|
||||
case_unlink_failure();
|
||||
case_prod_paths();
|
||||
|
||||
reset();
|
||||
rmdir(g_dir);
|
||||
|
||||
if (g_fail) { printf("\nBOOTMENU BOOTDEFAULT TEST: %d FAIL (%d pass)\n", g_fail, g_pass); return 1; }
|
||||
printf("\nBOOTMENU BOOTDEFAULT TEST: ALL PASS (%d checks)\n", g_pass);
|
||||
return 0;
|
||||
}
|
||||
@@ -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.c # 路径安全/格式化/枚举排序/虚拟化区间/写操作(可宿主测)
|
||||
└── test/
|
||||
└── test_fsx.c # 宿主单元自测(gcc 直接编,指向临时卡根)
|
||||
├── test_fsx.c # 宿主单元自测(gcc 直接编,指向临时卡根)
|
||||
└── test_row_text_fit.c # 宿主几何断言:列表行高 ≥ 双行文本块(M17)
|
||||
```
|
||||
|
||||
## 构建
|
||||
@@ -55,12 +56,20 @@ v1 无需持久化,暂未使用(启动恒进卡根 `/usr/data/mnt/sd_0/`)
|
||||
cd patch_v2/files_app
|
||||
gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
||||
test/test_fsx.c logic/fsx.c -o /tmp/test_fsx && /tmp/test_fsx
|
||||
|
||||
# 列表行高几何(无需 meow_ui.a;Termux 下产物落 $TMPDIR)
|
||||
gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c \
|
||||
-o "$TMPDIR/test_row_text_fit" && "$TMPDIR/test_row_text_fit"
|
||||
```
|
||||
|
||||
覆盖:路径安全(`within_sd`/`join`/`sanitize`/`dirname`/`basename`)、展示格式化(`fmt_size`/`fmt_time`)、
|
||||
目录枚举排序(`scan_dir`/`sort`)、虚拟化区间(`visible_range`/`max_scroll`)、写操作(`mkdir`/`rename`/
|
||||
`delete`,含卡外/越界拒绝)、非文本写拒绝。宿主全 PASS。
|
||||
|
||||
`test_row_text_fit` 是 M17 行高回归:目录列表行恒为双行档位,行高必须 ≥ 文本块
|
||||
`2*meow_ui_text_height(font_body) + 2`(真机 20px 字库行高 30 → 62),否则 SDK 居中后首行标题
|
||||
`ty < 视口顶` 被裁剪守卫整条跳过。断言矩阵覆盖真机五种字库行高 + 旧固定 56 的 bug 形态。
|
||||
|
||||
## 交互
|
||||
|
||||
- 触摸:行 body=进目录/开文本预览;右侧 `⋯` 或长按→BottomSheet(删除[确认弹窗]/重命名[IME]/新建目录[IME]/属性[大小·mtime]);
|
||||
@@ -78,6 +87,10 @@ gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
||||
位移 ≤ 容忍」→ 经 `meow_ui_set_long_press_cb` 注册的回调带坐标交给本页;`browser_on_long_press`
|
||||
用 `meow_listview_hit` 行命中打开 BottomSheet。App 层长按计时(`browser_tick`/`g_press_ms`)已删除,
|
||||
其抬手 TAP 由 SDK 抑制(长按不再触发点击)。
|
||||
- **行高按字体行高派生(M17)**:`files_row_h()` = `max(APP_ROW_H, 2*行高 + 2)`(当前主题正文
|
||||
`font_body` 的 `meow_ui_text_height`)。目录行是双行档位,行高小于双行文本块时 SDK 居中后首行
|
||||
标题落在视口顶之上被裁剪(20px 字库行高 30 → 块 62,旧的显式 56 首行名称空白)。滚动/命中/
|
||||
最大滚动量由 SDK 按同一行高换算,页脚几何独立(`r_list` 单独扣 `APP_FOOTER_H`)。
|
||||
2. **中文字形**:SDK 默认 `common` 字库(≈4k)不含 CJK 全量;exFAT 中文文件名/中文文本预览在 common 档会显示缺字豆腐。
|
||||
预览页不声明 `fonts`(app.json 未加 `"fonts":"full"`/`"gbk"`),如需中文预览可在 app.json 声明大字号档。
|
||||
3. **预览仅文本**:按扩展名白名单 + 首 512 字节 NUL 嗅探判文本;二进制/图片无预览(提示「not a text file」)。
|
||||
@@ -88,7 +101,7 @@ gcc -O2 -Wall -Wextra -DFSX_SD_ROOT='"/tmp/meowfsx_fsx_test"' -Ilogic \
|
||||
## 验证状态
|
||||
|
||||
- 编译:`-Wall -Wextra` 0 warning/0 error,产物 `files`(MIPS32r2 静态 ELF)。
|
||||
- 宿主:`test/test_fsx.c` 全 PASS。
|
||||
- 宿主:`test/test_fsx.c` 全 PASS;`test/test_row_text_fit.c` 32 断言全 PASS(M17 行高几何)。
|
||||
- qemu-mipsel(无 /dev/fb0 → SDK 离屏 480×720):正常运行到事件循环,导出离屏帧,
|
||||
确认 init/建页/topbar(返回+NEW)/空态(NO STORAGE CARD)渲染正确、SIGTERM 干净退出。
|
||||
- 实机目录列表渲染(有真实 TF 卡目录)需刷机/设备挂载验证——宿主模拟无法造出 `/usr/data/mnt/sd_0`。
|
||||
Binary file not shown.
@@ -37,13 +37,38 @@ void picker_cancel(void); /* 取消:发 code=MEOW_ERR
|
||||
#define APP_W 480
|
||||
#define APP_H 720
|
||||
#define APP_TOPBAR_H 56
|
||||
#define APP_ROW_H 56 /* 列表行高 */
|
||||
#define APP_ROW_H 56 /* 列表行高下限(旧固定值;实际按字体行高派生,见 files_row_h) */
|
||||
#define APP_PAD 16
|
||||
#define APP_ICON_W 26 /* 图标占宽 */
|
||||
#define APP_OP_W 52 /* 右侧操作钮占宽(触达区) */
|
||||
#define APP_FOOTER_H 26 /* 底部信息行高(列表/文本内容在其之上滚动) */
|
||||
#define APP_READ_MAX (256 * 1024) /* 文本预览读取上限 */
|
||||
|
||||
/* ---- 列表行高:按字体行高派生(M17,首行标题不显示根因修复)----
|
||||
* SDK meow_listview 的双行渲染把「标题+副标题」文本块
|
||||
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
|
||||
* 在行内垂直居中(ty = 行顶 + (item - block) / 2),标题只在 ty >= 视口顶时才画。
|
||||
* item < block 时 ty = 行顶 - Δ:第一行(行顶 == 视口顶)标题被整条跳过、只剩副标题。
|
||||
* 真机 source_han_serif 20px(ascent 24 + descent 6)行高 30 → block 62,旧的显式 56 → Δ=3。
|
||||
* 故行高按当前主题正文行高派生:max(APP_ROW_H, 2*line_h + 2)(行高 30 → 62)。
|
||||
* 本页行恒为双行档位(browser_src2 填 title + subtitle)→ 用 lines=2 公式。
|
||||
* 行几何(draw / hit / max_scroll)全由 SDK 按传入的这一个值换算,故滚动与命中天然同步;
|
||||
* 页脚几何与行高无关(r_list 单独扣 APP_FOOTER_H)。 */
|
||||
#define FILES_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行距) */
|
||||
|
||||
static inline int files_row_h_for_line_h(int line_h)
|
||||
{
|
||||
if (line_h < 1) line_h = 1; /* 字体未就绪 → 退回下限 */
|
||||
int h = 2 * line_h + FILES_ROW_SUB_GAP;
|
||||
return (h > APP_ROW_H) ? h : APP_ROW_H;
|
||||
}
|
||||
/* 当前主题正文行高下的两行列表行高(页面 create 时取一次;字体档位切换走页面重建)。 */
|
||||
static inline int files_row_h(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
return files_row_h_for_line_h(th ? meow_ui_text_height(th->font_body) : 0);
|
||||
}
|
||||
|
||||
/* ---- App 级复用矩形/几何 ---- */
|
||||
typedef struct { int x, y, w, h; } app_rect_t;
|
||||
#define AR(x,y,w,h) ((app_rect_t){ (x),(y),(w),(h) })
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* test/test_row_text_fit.c — 宿主回归(M17):目录列表行高必须容纳双行文本块。
|
||||
*
|
||||
* 背景(真机根因,承接 M12/M15):SDK meow_listview 的双行渲染把
|
||||
* block = 2 * meow_ui_text_height(font_body) + 2 (标题行 + 2px 行距 + 副标题行)
|
||||
* 在行内垂直居中:ty = 行顶 + (item - block) / 2;且标题只在 `ty >= 视口顶` 时才画(越界钳制)。
|
||||
* 行高 item < block 时 ty < 行顶 → **第一行(行顶 == 视口顶)标题被整条跳过**、副标题仍在。
|
||||
* 真机字库实测(source_han_serif 20px:ascent 24 + descent 6 → 行高 30)block = 62,旧的
|
||||
* 显式 set_item_h(56) → ty = 行顶 - 3(本 App 目录列表首行名称空白)。
|
||||
*
|
||||
* 本测试只验 app 侧行高派生(files_row_h_for_line_h,纯整数、不链 SDK):
|
||||
* 行高 = max(APP_ROW_H, 2*行高 + FILES_ROW_SUB_GAP) → block ≤ item,首行标题不被钳掉。
|
||||
*
|
||||
* 编译/运行(宿主 gcc,只需 SDK 头,不需要 meow_ui.a):
|
||||
* cd patch_v2/files_app
|
||||
* gcc -O2 -Wall -Wextra -I. -I../sdk test/test_row_text_fit.c -o "$TMPDIR/test_row_text_fit" \
|
||||
* && "$TMPDIR/test_row_text_fit"
|
||||
*
|
||||
* 断言矩阵 = 真机已装字库 20px 面实测行高:source_han_serif 30(当前生效)、
|
||||
* misans/default 27、google_sans/meslo 26、source_han_sans 21;14 = 宿主 5x7 回退。
|
||||
* ① block ≤ item(否则居中偏移为负)
|
||||
* ② 首行 ty ≥ 0 → 绝对 ty_abs ≥ 视口顶(标题不被裁剪守卫跳过)
|
||||
* ③ 副标题底边 ty + block ≤ item(两行都不越行)
|
||||
* ④ item ≥ APP_ROW_H 下限(小字库/字体未就绪不缩回固定小值)
|
||||
* ⑤ 旧固定 56 在行高 30 下 ty < 0(bug 形态复现,防止再写死 56)
|
||||
*/
|
||||
#include "../files_internal.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static int g_checks, g_fail;
|
||||
|
||||
static void chk(int cond, int line_h, const char *what)
|
||||
{
|
||||
g_checks++;
|
||||
if (!cond) {
|
||||
g_fail++;
|
||||
printf(" FAIL lh=%d: %s\n", line_h, what);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const int lh_all[] = { 14, 21, 26, 27, 30 };
|
||||
const int n = (int)(sizeof lh_all / sizeof lh_all[0]);
|
||||
const int list_y = 120; /* 任意列表顶(只验相对几何) */
|
||||
|
||||
for (int k = 0; k < n; k++) {
|
||||
int lh = lh_all[k];
|
||||
int item = files_row_h_for_line_h(lh);
|
||||
int block = 2 * lh + FILES_ROW_SUB_GAP;
|
||||
int ty = (item - block) / 2; /* SDK 双行文本块居中偏移 */
|
||||
|
||||
printf("lh=%2d item=%3d block=%3d ty=%d\n", lh, item, block, ty);
|
||||
|
||||
chk(block <= item, lh, "文本块必须 <= 行高(否则首行标题被视口顶裁剪)");
|
||||
chk(ty >= 0, lh, "首行标题 y 不得高于行顶/视口顶");
|
||||
chk(ty + block <= item, lh, "副标题底边仍在行内");
|
||||
chk(item >= APP_ROW_H, lh, "行高不得小于 APP_ROW_H 下限");
|
||||
|
||||
/* 首行(scroll=0 → 行顶 == 视口顶)的绝对坐标 */
|
||||
chk(list_y + ty >= list_y, lh, "首行标题绝对 y 不得被视口顶裁掉");
|
||||
}
|
||||
|
||||
/* 行高随字体行高单调不减 */
|
||||
chk(files_row_h_for_line_h(30) >= files_row_h_for_line_h(26), 30, "行高随字体行高单调不减");
|
||||
|
||||
/* 真机当前生效字库(source_han_serif 20px,行高 30):block 62 → item 62 */
|
||||
chk(files_row_h_for_line_h(30) == 62, 30, "行高 30 → 派生行高 62");
|
||||
chk(2 * 30 + FILES_ROW_SUB_GAP <= files_row_h_for_line_h(30), 30, "派生行高容纳 62px 文本块");
|
||||
|
||||
/* 下限:小字库 / 字体未就绪(line_h=0)仍不缩回固定小值 */
|
||||
chk(files_row_h_for_line_h(14) == APP_ROW_H, 14, "小字库退回 APP_ROW_H 下限");
|
||||
chk(files_row_h_for_line_h(0) == APP_ROW_H, 0, "字体未就绪(行高 0)退回下限");
|
||||
|
||||
/* 回归哨兵:旧固定行高 56 + 当前默认字体(行高 30)就是 bug 形态 */
|
||||
{
|
||||
const int lh = 30;
|
||||
int block = 2 * lh + FILES_ROW_SUB_GAP; /* 62 */
|
||||
int ty_legacy = (APP_ROW_H - block) / 2; /* -3 */
|
||||
chk(ty_legacy < 0, lh, "旧行高 56 + 行高 30 → ty<0(首行标题被跳过,bug 复现)");
|
||||
chk(files_row_h_for_line_h(lh) > APP_ROW_H, lh, "默认字体下行高必须大于旧值 56");
|
||||
}
|
||||
|
||||
printf("%s: %d checks, %d fail (font line-height matrix: %d)\n",
|
||||
g_fail ? "FAIL" : "PASS", g_checks, g_fail, n);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -421,7 +421,7 @@ static void browser_page_create(meow_view *v, void *ctx)
|
||||
/* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */
|
||||
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
||||
browser_src2, NULL, MEOW_LIST_SEL_SINGLE);
|
||||
meow_listview_set_item_h(g_list, APP_ROW_H);
|
||||
meow_listview_set_item_h(g_list, files_row_h()); /* M17:按字体行高派生,容纳双行文本块 */
|
||||
/* 空态/页脚注册为非控件绘制物(列表视口不含页脚;invalidate/paint_full 自动重画) */
|
||||
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
|
||||
browser_draw_empty, NULL);
|
||||
|
||||
@@ -39,7 +39,27 @@ static int viewer_vp_h(void) { return r_content.h - APP_FOOTER_H; }
|
||||
/* 屏幕行高(px)。 */
|
||||
static int viewer_rowh(const meow_theme *th) { return meow_ui_text_height(th->font_body) + 6; }
|
||||
|
||||
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。 */
|
||||
/* UTF-8 码位字节长(1..4):s[bi] 起、串内有效字节数 lim。
|
||||
* 首字节非法/续字节非法/序列不完整 → 保守按 1(单字节);保证折行绝不断在多字节码位中间,
|
||||
* 也保证宽测量(meow_ui_text_width 按码位测)对中文/混合文本正确。 */
|
||||
static int viewer_cp_len(const unsigned char *s, int bi, int lim)
|
||||
{
|
||||
unsigned char c = s[bi];
|
||||
int n;
|
||||
if (c < 0x80) return 1;
|
||||
else if ((c & 0xE0) == 0xC0) n = 2;
|
||||
else if ((c & 0xF0) == 0xE0) n = 3;
|
||||
else if ((c & 0xF8) == 0xF0) n = 4;
|
||||
else return 1;
|
||||
if (lim - bi < n) return 1;
|
||||
for (int i = 1; i < n; i++)
|
||||
if ((s[bi + i] & 0xC0) != 0x80) return 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 折行:返回逻辑行 li 折成的屏幕行数;若 intra>=0,把该屏幕行文本写入 out(NUL 结尾)。
|
||||
* 按「UTF-8 码位」逐字累加显示宽度(meow_ui_text_width 按码位测>0),到内容宽 colw 即断行;
|
||||
* 多字节(CJK)整字不断开。 */
|
||||
static int viewer_wrap_line(int li, int intra, char *out, int outsz)
|
||||
{
|
||||
if (!g_buf || li < 0 || li >= g_nline) return 0;
|
||||
@@ -49,13 +69,15 @@ static int viewer_wrap_line(int li, int intra, char *out, int outsz)
|
||||
while (len > 0 && (g_buf[start + len - 1] == '\n' || g_buf[start + len - 1] == '\r')) len--;
|
||||
int colw = viewer_colw();
|
||||
int col = 0, row = 0, outlen = 0;
|
||||
char ch[2]; ch[1] = '\0';
|
||||
for (int i = 0; i < len; i++) {
|
||||
ch[0] = g_buf[start + i];
|
||||
if (ch[0] == '\r') continue;
|
||||
int w = meow_ui_text_width(ch, 20);
|
||||
const unsigned char *s = (const unsigned char *)g_buf + start;
|
||||
for (int bi = 0; bi < len; ) {
|
||||
int clen = viewer_cp_len(s, bi, len);
|
||||
if (clen == 1 && s[bi] == '\r') { bi++; continue; }
|
||||
char cp[5]; cp[clen] = '\0';
|
||||
memcpy(cp, s + bi, (size_t)clen); bi += clen;
|
||||
int w = meow_ui_text_width(cp, 20);
|
||||
if (col + w > colw && col > 0) { row++; col = 0; }
|
||||
if (row == intra && out && outlen < outsz - 1) out[outlen++] = ch[0];
|
||||
if (row == intra && out && outlen + clen < outsz) { memcpy(out + outlen, cp, (size_t)clen); outlen += clen; }
|
||||
col += w;
|
||||
}
|
||||
if (out) out[outlen] = '\0';
|
||||
|
||||
Binary file not shown.
@@ -33,6 +33,31 @@ extern meow_sheet *g_sheet; /* BottomSheet(阻塞模态;main 创建
|
||||
typedef struct { int x, y, w, h; } rect_t;
|
||||
#define RX(x,y,w,h) ((rect_t){ (x), (y), (w), (h) })
|
||||
|
||||
/* ---- 两行列表行高:按字体行高派生(M17,首行标题不显示根因修复)----
|
||||
* SDK meow_listview 的双行渲染把「标题+副标题」文本块
|
||||
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
|
||||
* 在行内垂直居中(ty = 行顶 + (item - block) / 2),标题只在 ty >= 视口顶时才画。
|
||||
* item < block 时 ty = 行顶 - Δ:第一行(行顶 == 视口顶)标题被整条跳过、只剩副标题。
|
||||
* 真机 source_han_serif 20px(ascent 24 + descent 6)行高 30 → block 62;demo 的 swipe 列表
|
||||
* 是双行档位(set_item_renderer(2,...)),旧的显式 48 比 block 小 14 → 首行标题必丢。
|
||||
* 故行高按当前主题正文行高派生:max(HELLO_ROW_H_MIN, 2*line_h + 2)(行高 30 → 62)。
|
||||
* 行几何(draw / hit / max_scroll)全由 SDK 按传入的这一个值换算,滚动与命中天然同步。 */
|
||||
#define HELLO_ROW_H_MIN 56
|
||||
#define HELLO_ROW_SUB_GAP 2 /* 与 SDK MEOW_LISTV_SUB_GAP 同口径(标题/副标题行距) */
|
||||
|
||||
static inline int hello_row_h_for_line_h(int line_h)
|
||||
{
|
||||
if (line_h < 1) line_h = 1; /* 字体未就绪 → 退回下限 */
|
||||
int h = 2 * line_h + HELLO_ROW_SUB_GAP;
|
||||
return (h > HELLO_ROW_H_MIN) ? h : HELLO_ROW_H_MIN;
|
||||
}
|
||||
/* 当前主题正文行高下的两行列表行高(建列表时取一次;字体档位切换走内容重建)。 */
|
||||
static inline int hello_row_h(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
return hello_row_h_for_line_h(th ? meow_ui_text_height(th->font_body) : 0);
|
||||
}
|
||||
|
||||
/* ---- 视图控制器(ctx = biz-UI 绑定点)----
|
||||
* 本批控制器为薄壳:持 meow_view 回指针;页面细粒度视图态(g_ds/g_drag_* /scroll 等)留在各页
|
||||
* 模块作用域全局(设计定案 B:demo 视图态不迁 logic)。控制器未来可吸收这些状态。 */
|
||||
|
||||
@@ -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,
|
||||
NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||
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_item_renderer(g_lv, 2, 1, 0); /* 双行、左图标、无右尾件 */
|
||||
meow_listview_set_empty_text(g_lv, "All rows deleted");
|
||||
|
||||
@@ -33,7 +33,10 @@ if [ "$1" = "am" ]; then
|
||||
exit 0
|
||||
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) =="
|
||||
ls -la 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 一帧,读回一帧) ---- */
|
||||
/* 最近一次 ipc_xchg 的失败原因:区分「连不上」与「连上但没及时回」。此前两者都记成
|
||||
* "launcher offline",2026-11-04 真机即误诊——launcher 活着且回了 ACK,只是晚于 1200ms 超时。 */
|
||||
enum { IPC_ERR_NONE = 0, IPC_ERR_CONN, IPC_ERR_IO, IPC_ERR_TIMEOUT, IPC_ERR_PROTO };
|
||||
static int g_ipc_err = IPC_ERR_NONE;
|
||||
|
||||
static const char *ipc_err_str(void)
|
||||
{
|
||||
switch (g_ipc_err) {
|
||||
case IPC_ERR_CONN: return "connect failed";
|
||||
case IPC_ERR_IO: return "io error";
|
||||
case IPC_ERR_TIMEOUT: return "timeout, no reply";
|
||||
case IPC_ERR_PROTO: return "unexpected reply";
|
||||
default: return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
static int ipc_fail(int fd, int err)
|
||||
{
|
||||
if (fd >= 0) close(fd);
|
||||
g_ipc_err = err;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
||||
uint8_t *rbuf, uint32_t rcap)
|
||||
{
|
||||
g_ipc_err = IPC_ERR_NONE;
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
if (fd < 0) { g_ipc_err = IPC_ERR_IO; return -1; }
|
||||
/* 收发加超时:launcher 刚刚重启/主循环未就绪(接受但不回包)时,amsd 的阻塞
|
||||
* read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发
|
||||
* 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。
|
||||
@@ -802,7 +826,7 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, g_ipc_sock, sizeof a.sun_path - 1);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; }
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) return ipc_fail(fd, IPC_ERR_CONN);
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen;
|
||||
uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */
|
||||
@@ -813,31 +837,35 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
|
||||
size_t off = 0;
|
||||
while (off < sizeof hdr) {
|
||||
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
|
||||
if (w <= 0) { close(fd); return -1; }
|
||||
if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
off += (size_t)w;
|
||||
}
|
||||
if (len && payload) {
|
||||
size_t p = 0;
|
||||
while (p < len) {
|
||||
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
|
||||
if (w <= 0) { close(fd); return -1; }
|
||||
if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
p += (size_t)w;
|
||||
}
|
||||
}
|
||||
off = 0;
|
||||
while (off < sizeof hdr) {
|
||||
ssize_t r = read(fd, hdr + off, sizeof hdr - off);
|
||||
if (r <= 0) { close(fd); return -1; }
|
||||
if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
off += (size_t)r;
|
||||
}
|
||||
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2);
|
||||
memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
|
||||
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) { close(fd); return -1; }
|
||||
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) return ipc_fail(fd, IPC_ERR_PROTO);
|
||||
if (rlen > rcap) rlen = rcap;
|
||||
off = 0;
|
||||
while (off < rlen) {
|
||||
ssize_t r = read(fd, rbuf + off, rlen - off);
|
||||
if (r <= 0) { close(fd); return -1; }
|
||||
if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
|
||||
off += (size_t)r;
|
||||
}
|
||||
close(fd);
|
||||
@@ -858,11 +886,12 @@ static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uin
|
||||
if (code) *code = a.code;
|
||||
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。 */
|
||||
if (attempt + 1 < AMS_IPC_RETRY)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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`),非二补。 */
|
||||
snprintf(detail, sizeof detail, "busy (app running)");
|
||||
emit_log("AMS start %s -> BUSY (app running)", pkg);
|
||||
} else if (rt == 0) {
|
||||
snprintf(detail, sizeof detail, "launcher refused code=%d", (int)code);
|
||||
emit_log("AMS start %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
|
||||
} else {
|
||||
snprintf(detail, sizeof detail, "launcher offline");
|
||||
emit_log("AMS start %s -> launcher offline", pkg);
|
||||
/* rt!=0 只代表「这次没拿到回执」,不等于 launcher 死了(同 cmd_kill 注释)。
|
||||
* 响应串保留 "launcher offline"(MeowPanel 据此映射 503),日志补真实原因。 */
|
||||
snprintf(detail, sizeof detail, "launcher offline (%s)", ipc_err_str());
|
||||
emit_log("AMS start %s -> launcher offline (%s)", pkg, ipc_err_str());
|
||||
}
|
||||
snprintf(resp, rn, "%s %s/%s\n", strstr(detail, "ok forwarded") ? "OK" : "ERR",
|
||||
pkg, act[0] ? act : "(default)");
|
||||
@@ -1251,14 +1285,21 @@ static int cmd_kill(const char *pkg, char *resp, int rn)
|
||||
uint32_t code = 0;
|
||||
int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code);
|
||||
if (rt != 0 && a->pid > 0) {
|
||||
/* 兜底:launcher 不在,直接 SIGTERM(与 launcher 的 SIGTERM 语义一致) */
|
||||
/* 兜底:launcher 没给回执 → 直接 SIGTERM(与 launcher 的 SIGTERM 语义一致)。
|
||||
* 注意 rt!=0 **不等于 launcher 死了**:主循环被 IPC 洪泛拖慢时 ACK 会晚于
|
||||
* AMS_IPC_TIMEOUT_MS,故日志写实际原因(connect failed / timeout, no reply / ...),
|
||||
* 而不是笼统的 "launcher offline"(2026-11-04 真机即此误判)。 */
|
||||
kill(a->pid, SIGTERM);
|
||||
emit_log("AMS kill %s -> direct SIGTERM (launcher offline)", pkg);
|
||||
emit_log("AMS kill %s -> direct SIGTERM (launcher %s)", pkg, ipc_err_str());
|
||||
} else if (rt != 0) {
|
||||
return snprintf(resp, rn, "ERR launcher offline: %s\n", pkg);
|
||||
/* 响应串保留 "launcher offline" 字样(MeowPanel api_ams_ctl 依此映射 503 AMS_UNAVAILABLE),
|
||||
* 只在括号里补真实原因,便于区分「连不上」与「连上但超时」。 */
|
||||
return snprintf(resp, rn, "ERR launcher offline (%s): %s\n", ipc_err_str(), pkg);
|
||||
} else if (code != MEOW_OK) {
|
||||
emit_log("AMS kill %s -> launcher refused code=%u", pkg, code);
|
||||
return snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pkg, code);
|
||||
/* launcher 明确拒绝:不改判为直杀(pid 可能已被回收,盲杀会打错进程),如实回错。
|
||||
* code 是负错误码的 uint32 呈现(-2 → 4294967294),括号里给有符号值便于对照。 */
|
||||
emit_log("AMS kill %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
|
||||
return snprintf(resp, rn, "ERR launcher refused: %s (code=%d)\n", pkg, (int)code);
|
||||
}
|
||||
hist_push(a, "ams_kill", "terminate (via launcher)", (long)time(NULL), read_uptime_sec());
|
||||
int m = snprintf(resp, rn, "OK terminate %s\n", pkg);
|
||||
|
||||
+272
-92
@@ -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 负责
|
||||
* 读取 /usr/data/alsa.conf、执行 `bluealsa-cli list-pcms` 及写回。宿主单测零链路重。
|
||||
* 执行 `bluealsa-cli list-pcms`、调用 libasound 及维护运行时状态。
|
||||
*/
|
||||
#include "bt_dev.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.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。
|
||||
* 端点形如 `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
|
||||
* "profile/aptx" 与行 3 "a2dp_source/..." 的经典误判)。 */
|
||||
* "hfphf/source" 与行 2 "a2dpsrc/sink" 的经典误判)。 */
|
||||
int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
||||
{
|
||||
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;
|
||||
const char *p = line;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (line_is_src(line)) {
|
||||
if (parse_dev_seg(p, mac, macsz) == 0) return 0;
|
||||
const char *hit = strstr(p, "a2dpsrc");
|
||||
if (!hit) hit = strstr(p, "a2dp_source");
|
||||
if (hit) {
|
||||
const char *te = hit;
|
||||
while (*te && *te != ' ' && *te != '\t' && *te != '\n') te++;
|
||||
const char *slash = NULL;
|
||||
for (const char *q = hit; q < te; q++) if (*q == '/') slash = q;
|
||||
if (slash && (slash + 1) < te) {
|
||||
const char *start = slash + 1;
|
||||
size_t len = (size_t)(te - start);
|
||||
if (len > 0 && len < macsz) {
|
||||
memcpy(mac, start, len);
|
||||
mac[len] = 0;
|
||||
return 0;
|
||||
if (hit && parse_tail_mac(hit, mac, macsz) == 0) return 0;
|
||||
}
|
||||
if (!nl) break;
|
||||
ln = nl + 1;
|
||||
}
|
||||
mac[0] = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 在 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;
|
||||
ln = nl + 1;
|
||||
@@ -50,89 +199,120 @@ int bt_dev_detect_mac(const char *listpcms, char *mac, size_t macsz)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 在单行内把 anchor("DEV=" / "device")后的 MAC 值替换为 mac,保留引号风格。
|
||||
* 行未含 anchor → 返回 1(out=原样);含 anchor → 写回新行并返回 0。 */
|
||||
static int line_replace_mac(char *out, size_t outsz, const char *line,
|
||||
const char *anchor, const char *mac)
|
||||
/* 关闭决策(纯函数,宿主可单测):当前「设备」该关谁。
|
||||
* bt_dev=1 → 关 BT PCM(句柄在 audiod 的 g_bt_hdl),**不碰 fd**;
|
||||
* 否则 fd>2 才当设备 fd 关;**0/1/2 一律不动**——BT 打开成功时 dev_open() 返回 0,
|
||||
* 任何「提前清 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 (!at) { snprintf(out, outsz, "%s", line); return 1; }
|
||||
const char *vp = at + strlen(anchor);
|
||||
while (*vp == ' ' || *vp == '\t') vp++;
|
||||
if (*vp == '"') vp++; /* 引号风格:值可带双引号 */
|
||||
const char *ve = vp;
|
||||
while (*ve && *ve != ' ' && *ve != '\t' && *ve != '\n' &&
|
||||
*ve != '"' && *ve != '{' && *ve != '}') ve++;
|
||||
size_t prefix = (size_t)(vp - line); /* 前段(含开引号,若带) */
|
||||
size_t tail = (size_t)(ve - line); /* 值终点(不含闭引号,若带) */
|
||||
/* 前段 + mac + 尾段(尾段保留闭引号与其后内容) */
|
||||
snprintf(out, outsz, "%.*s%s%s", (int)prefix, line, mac, line + tail);
|
||||
return 0;
|
||||
if (bt_dev) return 1;
|
||||
return fd > 2 ? 2 : 0;
|
||||
}
|
||||
|
||||
/* 幂等改写 alsa.conf:只把 `pcm.bluealsa` 块内 `DEV=` 与 `device` 后的 MAC 换成 mac。
|
||||
* 逐行处理;无 pcm.bluealsa / 无 DEV= / device token → -1;两端已一致 → 1;改了 → 0。 */
|
||||
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
|
||||
char **out, size_t *out_len)
|
||||
/* 从 `bluealsa-cli info <path>` 输出取实际协商到的 codec("Selected codec: AAC" → "AAC")。
|
||||
* 只认行首 "Selected codec:"("Available codecs: SBC AAC" 那行不能误匹配)。 */
|
||||
int bt_dev_info_codec(const char *info_out, char *out, size_t outsz)
|
||||
{
|
||||
if (!in || !mac || !mac[0] || !out || !out_len || in_len == 0) return -1;
|
||||
if (!strstr(in, "pcm.bluealsa") ||
|
||||
(!strstr(in, "DEV=") && !strstr(in, "device"))) return -1;
|
||||
|
||||
size_t cap = in_len + 128;
|
||||
char *res = malloc(cap ? cap : 1);
|
||||
if (!res) return -1;
|
||||
size_t o = 0, changed = 0;
|
||||
const char *p = in, *end = in + in_len;
|
||||
|
||||
while (p < end) {
|
||||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char *le = nl ? nl : end;
|
||||
size_t linelen = (size_t)(le - p);
|
||||
char line[512], newline[512];
|
||||
if (linelen >= sizeof line) linelen = sizeof line - 1;
|
||||
memcpy(line, p, linelen); line[linelen] = 0;
|
||||
|
||||
const char *k = line;
|
||||
while (*k == ' ' || *k == '\t') k++;
|
||||
int anchored = 0;
|
||||
if (strstr(k, "DEV=")) {
|
||||
line_replace_mac(newline, sizeof newline, line, "DEV=", mac);
|
||||
anchored = 1;
|
||||
} else if (strncmp(k, "device", 6) == 0 &&
|
||||
(k[6] == ' ' || k[6] == '\t' || k[6] == 0)) {
|
||||
line_replace_mac(newline, sizeof newline, line, "device", mac);
|
||||
anchored = 1;
|
||||
}
|
||||
if (anchored) {
|
||||
if (strcmp(line, newline) != 0) changed = 1;
|
||||
size_t n = strlen(newline);
|
||||
if (o + n + 2 > cap) {
|
||||
cap = o + n + 256;
|
||||
char *t = realloc(res, cap);
|
||||
if (!t) { free(res); return -1; }
|
||||
res = t;
|
||||
}
|
||||
memcpy(res + o, newline, n); o += n;
|
||||
if (nl) res[o++] = '\n';
|
||||
} else {
|
||||
size_t n = linelen;
|
||||
if (o + n + 2 > cap) {
|
||||
cap = o + n + 256;
|
||||
char *t = realloc(res, cap);
|
||||
if (!t) { free(res); return -1; }
|
||||
res = t;
|
||||
}
|
||||
memcpy(res + o, p, n); o += n;
|
||||
if (nl) res[o++] = '\n';
|
||||
if (!info_out || !out || outsz < 2) return -1;
|
||||
out[0] = 0;
|
||||
const char *ln = info_out;
|
||||
while (ln && *ln) {
|
||||
const char *nl = strchr(ln, '\n');
|
||||
const char *p = ln;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (strncmp(p, "Selected codec:", 15) == 0) {
|
||||
p += 15;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
size_t n = 0;
|
||||
while (p[n] && p[n] != '\n' && p[n] != '\r' && n + 1 < outsz) { out[n] = p[n]; n++; }
|
||||
while (n > 0 && (out[n-1] == ' ' || out[n-1] == '\t')) n--;
|
||||
out[n] = 0;
|
||||
return n ? 0 : -1;
|
||||
}
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
ln = nl + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!changed) { free(res); return 1; }
|
||||
res[o] = 0;
|
||||
*out = res;
|
||||
*out_len = o;
|
||||
/* bluealsa 4.1.1 src/shared/a2dp-codecs.c 的 canonical 名与别名(strcasecmp 匹配)。
|
||||
* 只列本机可达的音频 codec;auto/unchanged/未知 → unchanged(不干预协商)。 */
|
||||
static const struct {
|
||||
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;
|
||||
}
|
||||
|
||||
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 钉在当前
|
||||
* 连接的 A2DP sink 设备 MAC 上才能正确出声。本模块提供两段纯逻辑(无 meow_* / popen / 文件
|
||||
* I/O —— meow_audiod 负责读文件与执行系统命令):
|
||||
* - bt_dev_detect_mac:从 `bluealsa-cli list-pcms` 输出提取第一个 a2dpsrc 端点的设备 MAC;
|
||||
* - bt_dev_rewrite:幂等改写 alsa.conf 文本,把 `pcm.bluealsa` 块内 `DEV=<mac>` 与
|
||||
* `device <mac>` 两处换成目标 MAC(保留引号风格 / codec / 其余 token)。
|
||||
* 纯 libc(string / stdlib),独立编译,宿主零链接重。
|
||||
* 背景(M50 真机定案):`/usr/data/alsa.conf` **不是** alsa 配置(是 HiBy 播放器的数据文件,
|
||||
* libasound 从不加载),改它对实际解析零影响。真正生效的是 alsa-lib 自带的
|
||||
* `/etc/alsa/conf.d/20-bluealsa.conf`:
|
||||
*
|
||||
* pcm.bluealsa {
|
||||
* @args [ DEV PROFILE CODEC VOL SOFTVOL DELAY SRV ] ← 只有这 7 个名字
|
||||
* ... 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
|
||||
#define BT_DEV_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 解析 bluealsa-cli list-pcms 输出,取第一个 a2dpsrc/a2dp_source 端点的设备 MAC。
|
||||
* 返回 0=找到并写入 mac(NUL 结尾),-1=无 a2dpsrc 端点 / 参数非法。macsz 建议 ≥18。 */
|
||||
/* 从 `hcitool con` 输出收集**所有** ACL 连接的对端 BD_ADDR(按出现顺序去重;MAC 主来源:
|
||||
* 内核 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);
|
||||
|
||||
/* 幂等改写 alsa.conf 文本(in_len 不含 NUL):把 `pcm.bluealsa` 块内 `DEV=<mac>` 与
|
||||
* `device <mac>` 两处 MAC 换成 mac(保留引号风格、codec/其它 token 原样)。
|
||||
* 返回 0=已改动(*out 与 *out_len 给出新文本,调用方负责 free);1=无需改动(两端已一致);
|
||||
* -1=无 pcm.bluealsa / 无 DEV= 或 device token / 参数非法(不动)。 */
|
||||
int bt_dev_rewrite(const char *in, size_t in_len, const char *mac,
|
||||
char **out, size_t *out_len);
|
||||
|
||||
/* 在 list-pcms 里按设备 MAC 精确挑端点路径(换耳机 A→B 后不能拿 A 的路径发 volume/info)。
|
||||
* 返回 0=找到,-1=该 MAC 无端点。 */
|
||||
int bt_dev_pick_endpoint(const char *listpcms, const char *mac, char *out, size_t outsz);
|
||||
|
||||
/* 关闭决策(纯函数):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 */
|
||||
@@ -17,24 +17,65 @@
|
||||
# ./build_audiod.sh # 交叉编译 → meow_audiod(mipsel glibc-dynamic,BT 直链 libasound)
|
||||
# ./build_audiod.sh cli # 交叉编译 meow_audio CLI → meow_audio(mipsel musl-static)
|
||||
# ./build_audiod.sh host # 宿主编译 *_host 并跑 test_audiod_host 回归(FILE sink)
|
||||
#
|
||||
# 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。
|
||||
#
|
||||
# M55 蓝牙音量:BT 下 AVRCP 钉在 127(耳机功放满量程),用户音量由 vol_gain.c 的**软件增益**在
|
||||
# 写路径执行(与 DAC 同一条曲线、全整数)。故 audiod 目标必须一起编 vol_gain.c(下方已带);
|
||||
# host 目标另外单独编 test_vol_gain.c(纯函数回归:曲线/缩放/斜坡/toggle storm)。
|
||||
# ⚠️ `meow_audio_set_volume` 在 BT 下同样生效(写增益 + 持久化),SDK 文档里「BT 时音量由 AVRCP
|
||||
# 侧控」的旧说法已过期——见 vol_gain.h 顶部的 App/SDK 契约段。
|
||||
#
|
||||
# 环境变量覆盖:ROOT / REPO 自动推导,ZIG / CC / LIBBT / ASOUND_SRC / CC_HOST 可显式指定。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
ROOT="$HOME/Project/R3ProII"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
ZIG="$ROOT/.tools/zig-env/bin/zig"
|
||||
INC="-I$ROOT/patch_v2/meow_launcherd"
|
||||
# ---- 路径解析(M50:全部可被环境变量覆盖,不再硬绑某台机器的目录布局)----
|
||||
# ROOT 只用于可选的历史路径(旧 .tools/zig-env、hiby_os_crack 解包树);缺省布局下不依赖它。
|
||||
ROOT="${ROOT:-$HOME/Project/R3ProII}"
|
||||
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
|
||||
HOST_CC="${CC_HOST:-cc}"
|
||||
# BT 目标直链宿主 libasound.so.2(无 asoundlib.h,原型内联于 meow_audiod.c;-l: 精确到版本,
|
||||
# 不依赖 unversioned dev 链接)。删 -ldl:BT 不再用 dlopen。
|
||||
"$HOST_CC" $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod_host -lpthread -l:libasound.so.2
|
||||
# BT 目标直链宿主 libasound(无 asoundlib.h,原型内联于 meow_audiod.c)。删 -ldl:BT 不再用 dlopen。
|
||||
# 链接名先用「trivial 探针」决定:有 -l:libasound.so.2(精确版本)就用它,否则退 -lasound。
|
||||
# 用探针而**不是**「先编一次、失败再编一次 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 vol_gain.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 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_vol_gain.c vol_gain.c -o test_vol_gain -lm
|
||||
echo "== host builds =="
|
||||
ls -la meow_audiod_host meow_audio_host test_audiod_host test_bt_dev
|
||||
file meow_audiod_host meow_audio_host test_audiod_host test_bt_dev
|
||||
ls -la meow_audiod_host meow_audio_host test_audiod_host test_bt_dev test_vol_gain
|
||||
file meow_audiod_host meow_audio_host test_audiod_host test_bt_dev test_vol_gain
|
||||
echo "== host regression (test_vol_gain: BT 增益表/缩放/斜坡) =="
|
||||
./test_vol_gain || { echo "!! test_vol_gain FAILED"; exit 1; }
|
||||
echo "== host regression (test_bt_dev: BT DEV auto-detect) =="
|
||||
./test_bt_dev || { echo "!! test_bt_dev FAILED"; exit 1; }
|
||||
echo "== host regression (test_audiod_host) =="
|
||||
@@ -43,7 +84,13 @@ if [ "$1" = "host" ]; then
|
||||
fi
|
||||
|
||||
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) =="
|
||||
ls -la meow_audio
|
||||
md5sum meow_audio
|
||||
@@ -52,15 +99,43 @@ if [ "$1" = "cli" ]; then
|
||||
fi
|
||||
|
||||
# meow_audiod:mipsel glibc-dynamic,BT 直链 libasound(-Llibbt -lasound)。-lpthread 供播放线程。
|
||||
# 链接源 libbt/libasound.so.2.0.0(设备固件 libasound.so.2 副本);缺失时从固件镜像自拷贝(idempotent)。
|
||||
LIBBT="libbt"
|
||||
ASOUND_SRC="$ROOT/hiby_os_crack/r3proii/squashfs-root-example/usr/lib/libasound.so.2.0.0"
|
||||
# M43 例外:本目标走 glibc-2.22(time_t 32 位、timespec 8 字节)→ 无 musl time64 头/实现错位;
|
||||
# 当前钉在 fw_content.conf 的出货件则是旧工具链静态 musl,其 4403→ENOSYS→4263+宽度转换本身健康。
|
||||
# 两种形态都不需要 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"
|
||||
if [ ! -f "$LIBBT/libasound.so.2.0.0" ]; then cp "$ASOUND_SRC" "$LIBBT/libasound.so.2.0.0"; fi
|
||||
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so"
|
||||
ln -sf libasound.so.2.0.0 "$LIBBT/libasound.so.2"
|
||||
"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c -o meow_audiod \
|
||||
-lpthread -Llibbt -lasound
|
||||
"$ZIG" cc --target=mipsel-linux-gnueabihf.2.22 $INC -Os -Wall -Wextra meow_audiod.c bt_dev.c vol_gain.c -o meow_audiod \
|
||||
-lpthread -L"$LIBBT" -lasound
|
||||
echo "== meow_audiod (mipsel glibc-dynamic, BT links libasound.so.2) =="
|
||||
ls -la meow_audiod
|
||||
md5sum meow_audiod
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+505
-229
File diff suppressed because it is too large.
Load diff
@@ -187,6 +187,24 @@ static int start_audiod_trace(const char *sink, const char *settings_path, const
|
||||
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) */
|
||||
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, m_audio_open_ack_t *ack)
|
||||
{
|
||||
@@ -328,8 +346,58 @@ static int wait_wav_open(const char *path, int tmo_ms)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* S16 立体声 WAV 的 data 段统计(M55 音量阶梯的**客观量化**:不靠耳朵)。
|
||||
* 返回 ok=1 时填 rms / peak / maxdelta(相邻样本最大跳变,用来证明改音量没有硬跳变爆音),
|
||||
* tail_rms 只统计最后 4096 个样本(T20「连续改音量后是否稳定在新音量」用)。 */
|
||||
struct wavstat { int ok; long nsamp; double rms; int peak; int maxdelta; double tail_rms; double head_rms; };
|
||||
static struct wavstat wav_s16_stats(const char *path)
|
||||
{
|
||||
struct wavstat s;
|
||||
memset(&s, 0, sizeof s);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return s;
|
||||
unsigned char h[44];
|
||||
if (fread(h, 1, 44, f) != 44) { fclose(f); return s; }
|
||||
if (memcmp(h, "RIFF", 4) || memcmp(h + 8, "WAVE", 4)) { fclose(f); return s; }
|
||||
uint16_t ch, bits; uint32_t data_len;
|
||||
memcpy(&ch, h + 22, 2); memcpy(&bits, h + 34, 2); memcpy(&data_len, h + 40, 4);
|
||||
if (ch != 2 || bits != 16) { fclose(f); return s; }
|
||||
if (data_len > 1u << 20) data_len = 1u << 20;
|
||||
size_t n = data_len / 2;
|
||||
int16_t *b = malloc(n ? n * 2 : 2);
|
||||
if (!b) { fclose(f); return s; }
|
||||
size_t rd = fread(b, 2, n, f);
|
||||
fclose(f);
|
||||
if (rd == 0) { free(b); return s; }
|
||||
long long sumsq = 0; int peak = 0, maxdelta = 0, prev = 0;
|
||||
for (size_t i = 0; i < rd; i++) {
|
||||
int v = b[i];
|
||||
int a = v < 0 ? -v : v;
|
||||
if (a > peak) peak = a;
|
||||
sumsq += (long long)v * v;
|
||||
if (i) { int d = v - prev; if (d < 0) d = -d; if (d > maxdelta) maxdelta = d; }
|
||||
prev = v;
|
||||
}
|
||||
s.ok = 1;
|
||||
s.nsamp = (long)rd;
|
||||
s.rms = sqrt((double)sumsq / (double)rd);
|
||||
s.peak = peak;
|
||||
s.maxdelta = maxdelta;
|
||||
{ /* 尾部/头部 4096 样本的 RMS(连续改音量后是否稳定在新音量、停手前后差多少) */
|
||||
size_t take = rd < 4096 ? rd : 4096;
|
||||
long long tq = 0, hq = 0;
|
||||
for (size_t i = rd - take; i < rd; i++) tq += (long long)b[i] * b[i];
|
||||
for (size_t i = 0; i < take; i++) hq += (long long)b[i] * b[i];
|
||||
s.tail_rms = take ? sqrt((double)tq / (double)take) : 0.0;
|
||||
s.head_rms = take ? sqrt((double)hq / (double)take) : 0.0;
|
||||
}
|
||||
free(b);
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL); /* 无缓冲:崩溃/卡死时也能看到跑到哪一档(回归排障) */
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
g_audiod_pid = start_audiod();
|
||||
if (g_audiod_pid < 0) { printf("cannot fork audiod\n"); return 1; }
|
||||
@@ -768,19 +836,22 @@ int main(void)
|
||||
check(eok, "fresh OPEN after overflow (table recycled, no exhaustion/wedge)");
|
||||
check(stop_audiod_tmo(5000) != 0, "audiod not wedged by overflow (SIGTERM clean exit)");
|
||||
|
||||
/* ---- T11:P2 蓝牙 输出目标(OUT_BT)+ 音量 AVRCP 隔离 + 格式归一 + codec。
|
||||
/* ---- T11:P2 蓝牙 输出目标(OUT_BT)+ 音量隔离 + 格式归一 + codec。
|
||||
* 宿主以 FILE sink 跑(真 bluealsa 传输留给真机),验证目标选择/音量隔离/格式逻辑:
|
||||
* - settings.conf `audio.output_bt=1` + `bt.codec=SBC` → target=bt / codec=SBC / volctl=avrcp / grant fmt=S16;
|
||||
* - BT 下 MSG_AUDIO_VOLUME 设置:**不下发 DAC 音量反转**——g_vol_pct 不变、不持久化 audio.volume、
|
||||
* volctl 仍 avrcp(隔离成立);
|
||||
* - BT 下 MSG_AUDIO_VOLUME 设置(M55 语义):g_vol_pct 与持久化**必须更新**(旧版提前 return
|
||||
* 什么都不做 → 重启音量跳回去),但**绝不写 DAC numid=1/2/11**(amixer 轨迹必须为空),
|
||||
* volctl 仍 avrcp —— 音量改由写路径的软件增益生效(曲线/单调性/静音见 T18 + test_vol_gain);
|
||||
* - MSG_AUDIO_OUTPUT 切回 DAC:target=dac / volctl=dac;此后音量设置写 DAC + 持久化 audio.volume。
|
||||
* 断言:BT 路径绝不动 DAC 音量(plan §三/§五「两套隔离」);BT 格式归一=S16;codec 读到生效。 ---- */
|
||||
printf("[T11 BT output target + AVRCP vol isolation]\n");
|
||||
printf("[T11 BT output target + vol isolation]\n");
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
const char *tconf = "/tmp/meow_test_settings.conf";
|
||||
const char *trace11 = "/tmp/meow_amixer_trace11.txt";
|
||||
unlink(trace11);
|
||||
FILE *tf = fopen(tconf, "w");
|
||||
if (tf) { fprintf(tf, "# meow-settings 1\naudio.volume = 60\naudio.output_bt = 1\nbt.codec = SBC\n"); fclose(tf); }
|
||||
g_audiod_pid = start_audiod_settings("file", tconf);
|
||||
g_audiod_pid = start_audiod_trace("file", tconf, trace11);
|
||||
usleep(400000);
|
||||
struct cli b11;
|
||||
check(cli_connect(&b11) == 0, "connect B11");
|
||||
@@ -792,10 +863,15 @@ int main(void)
|
||||
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, "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) */
|
||||
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);
|
||||
/* 音量设置 while BT:不下发 DAC——g_vol_pct 不变(60)、不持久化 audio.volume=40、volctl 仍 avrcp */
|
||||
/* 音量设置 while BT(M55):g_vol_pct + 持久化都变,但**不碰 DAC**(amixer 轨迹空) */
|
||||
{
|
||||
int32_t pct = 40;
|
||||
if (sf(b11.fd, MSG_AUDIO_VOLUME, 8, &pct, sizeof pct) == 0) {
|
||||
@@ -805,11 +881,20 @@ int main(void)
|
||||
}
|
||||
check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "volctl=avrcp"),
|
||||
"vol set while bt -> volctl still avrcp (isolation)");
|
||||
check(strstr(q11, "vol=60") != NULL, "vol set while bt -> DAC vol unchanged (60)");
|
||||
check(strstr(q11, "vol=40") != NULL, "bt vol set -> g_vol_pct=40 (was silently dropped before M55)");
|
||||
{
|
||||
FILE *chk = fopen(tconf, "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, "bt vol set does NOT persist audio.volume=40");
|
||||
check(has40, "bt vol set persists audio.volume=40 (survives reboot)");
|
||||
}
|
||||
{
|
||||
/* 隔离断言(M50 T11 不变):BT 真在用时一个 DAC 音量寄存器都没写 */
|
||||
char trb[2048]; int trl = 0;
|
||||
int fd = open(trace11, O_RDONLY);
|
||||
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
|
||||
if (trl > 0) trb[trl] = 0; else trb[0] = 0;
|
||||
check(!strstr(trb, "numid=1 ") && !strstr(trb, "numid=2 ") && !strstr(trb, "numid=11 "),
|
||||
"bt vol set writes NO DAC registers (numid=1/2/11 absent from amixer trace)");
|
||||
}
|
||||
/* MSG_AUDIO_OUTPUT 切回 DAC */
|
||||
{
|
||||
@@ -836,9 +921,19 @@ int main(void)
|
||||
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
|
||||
check(has40, "dac vol set persists audio.volume=40");
|
||||
}
|
||||
{
|
||||
/* 反向断言:切回 DAC 后同一个音量设置**必须**落 DAC(证明上面的空轨迹是隔离而非没写) */
|
||||
char trb[2048]; int trl = 0;
|
||||
int fd = open(trace11, O_RDONLY);
|
||||
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
|
||||
if (trl > 0) trb[trl] = 0; else trb[0] = 0;
|
||||
check(strstr(trb, "numid=1 ") && strstr(trb, "numid=2 ") && strstr(trb, "numid=11 "),
|
||||
"after switch to dac the same vol set DOES write numid=1/2/11");
|
||||
}
|
||||
do_ctrl(&b11, MSG_AUDIO_STOP, ack11.stream_id);
|
||||
close(b11.fd);
|
||||
stop_audiod();
|
||||
unlink(trace11);
|
||||
|
||||
/* ---- T12:24/32bit 高清流 byte-exact(A3 高清实测;FILE sink 按请求保真,S32 升位不降位)。
|
||||
* ① S32@192000:open 应 grant S32(FILE/NULL sink 按 native 高保真透传);写已知 32-bit 交织帧,
|
||||
@@ -1080,6 +1175,276 @@ int main(void)
|
||||
}
|
||||
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);
|
||||
|
||||
/* ---- T18:BT 软件增益(M55)——**客观量化**,不靠耳朵。
|
||||
* target=bt + FILE sink 就是 BT 写路径(grant fmt 恒 S16),捕获的 WAV 就是写进 PCM 的样本。
|
||||
* 逐档起独立实例(settings audio.volume = pct),写同一段 440Hz 正弦(同相位同幅度),
|
||||
* 量 data 段 RMS/峰值:①随 pct 单调下降 ②vol=0 全 0(真静音)③vol=100 与输入逐字节相同
|
||||
* ④实测峰值 = 输入峰值 × 增益表(曲线与 dac_vol_set 同源,不是线性)。 */
|
||||
printf("[T18 BT software gain: volume ladder via wav capture]\n");
|
||||
{
|
||||
const int pcts18[] = { 100, 92, 75, 50, 25, 10, 0 };
|
||||
const int expreg18[] = { 0, 1, 12, 48, 108, 155, 255 };
|
||||
enum { NL = 7, NFR = 4096 };
|
||||
static uint8_t tone18[NFR * 4];
|
||||
static double rms18[NL];
|
||||
static int peak18[NL];
|
||||
const double amp_in = 0.5 * 32767.0;
|
||||
int peak_in = (int)(amp_in + 0.5);
|
||||
for (int i = 0; i < NFR; i++) {
|
||||
double ph = 2.0 * M_PI * 440.0 * (double)i / 44100.0;
|
||||
int16_t v = (int16_t)(sin(ph) * amp_in);
|
||||
memcpy(tone18 + i * 4, &v, 2);
|
||||
memcpy(tone18 + i * 4 + 2, &v, 2);
|
||||
}
|
||||
printf(" input tone: 440Hz S16 stereo, peak=%d (RMS %.1f), %d frames\n",
|
||||
peak_in, peak_in / sqrt(2.0), NFR);
|
||||
printf(" %-5s %-5s %-9s %-6s %-9s %-6s %s\n", "pct", "reg", "curve-dB", "peak", "rms", "gain", "maxdelta");
|
||||
for (int k = 0; k < NL; k++) {
|
||||
int pct = pcts18[k];
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
char conf18[96];
|
||||
snprintf(conf18, sizeof conf18, "/tmp/meow_test_settings18_%d.conf", pct);
|
||||
FILE *f18 = fopen(conf18, "w");
|
||||
if (f18) { fprintf(f18, "# meow-settings 1\naudio.volume = %d\naudio.output_bt = 1\n", pct); fclose(f18); }
|
||||
g_audiod_pid = start_audiod_settings("file", conf18);
|
||||
usleep(350000);
|
||||
struct cli c18;
|
||||
if (cli_connect(&c18) != 0) { check(0, "T18 connect"); break; }
|
||||
m_audio_open_ack_t a18;
|
||||
if (do_open(&c18, 44100, 2, AUDIO_FMT_S16, &a18) != 0 || a18.err != MEOW_OK) {
|
||||
check(0, "T18 open"); close(c18.fd); stop_audiod(); break;
|
||||
}
|
||||
for (size_t off = 0; off < sizeof tone18; off += 4096) {
|
||||
size_t left = sizeof tone18 - off;
|
||||
do_write(&c18, tone18 + off, (uint32_t)(left < 4096 ? left : 4096));
|
||||
}
|
||||
wait_wav_open(TWAV, 3000);
|
||||
usleep(250000);
|
||||
do_ctrl(&c18, MSG_AUDIO_STOP, a18.stream_id);
|
||||
close(c18.fd);
|
||||
stop_audiod();
|
||||
struct wavstat st = wav_s16_stats(TWAV);
|
||||
if (!st.ok) { check(0, "T18 wav readable"); continue; }
|
||||
peak18[k] = st.peak;
|
||||
rms18[k] = st.rms;
|
||||
double g = 32768.0 * pow(10.0, -0.5 * (double)expreg18[k] / 20.0) / 32768.0;
|
||||
printf(" %-5d %-5d %-9.2f %-6d %-9.2f %-6.6f %d\n",
|
||||
pct, expreg18[k], -0.5 * (double)expreg18[k], st.peak, st.rms, g, st.maxdelta);
|
||||
/* 实测峰值必须整落到曲线上(±2 LSB 覆盖整数四舍五入) */
|
||||
int expect_peak = (int)(peak_in * g + 0.5);
|
||||
char nm[96];
|
||||
snprintf(nm, sizeof nm, "vol=%d measured peak == input*curve (%d vs %d)", pct, st.peak, expect_peak);
|
||||
check(abs(st.peak - expect_peak) <= 2, nm);
|
||||
if (pct == 0) check(st.peak == 0 && st.rms == 0.0, "vol=0 -> WAV data all zeros (true mute)");
|
||||
if (pct == 100)
|
||||
check(wav_byte_exact(TWAV, 44100, 2, 16, tone18, sizeof tone18) == 0,
|
||||
"vol=100 -> WAV byte-exact with input (unity, no clipping)");
|
||||
unlink(conf18);
|
||||
}
|
||||
/* 单调:pct 越高越响(严格下降,因为每档差 ≥0.5dB 且在 16bit 可分辨) */
|
||||
int mono = 1;
|
||||
for (int k = 0; k + 1 < NL; k++)
|
||||
if (!(peak18[k] > peak18[k + 1] && rms18[k] > rms18[k + 1])) mono = 0;
|
||||
check(mono, "RMS and peak decrease monotonically with pct (100>92>75>50>25>10>0)");
|
||||
check(peak18[0] > 0 && peak18[NL - 1] == 0, "ladder spans full scale -> silence");
|
||||
}
|
||||
|
||||
/* ---- T19:改音量不爆音 + 不丢帧(M55)。播 100% 中途发 vol=10:增益必须走 512 样本斜坡,
|
||||
* 相邻样本最大跳变不得接近「一步跳到位」的量级(那会听见「叮」)。 */
|
||||
printf("[T19 live volume change: ramp, no click, no drops]\n");
|
||||
{
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
const char *conf19 = "/tmp/meow_test_settings19.conf";
|
||||
FILE *f19 = fopen(conf19, "w");
|
||||
if (f19) { fprintf(f19, "# meow-settings 1\naudio.volume = 100\naudio.output_bt = 1\n"); fclose(f19); }
|
||||
g_audiod_pid = start_audiod_settings("file", conf19);
|
||||
usleep(350000);
|
||||
struct cli c19;
|
||||
if (cli_connect(&c19) != 0) { check(0, "T19 connect"); }
|
||||
else {
|
||||
m_audio_open_ack_t a19;
|
||||
if (do_open(&c19, 44100, 2, AUDIO_FMT_S16, &a19) != 0 || a19.err != MEOW_OK) {
|
||||
check(0, "T19 open");
|
||||
} else {
|
||||
enum { NFR19 = 4096 };
|
||||
static uint8_t t19[NFR19 * 4];
|
||||
static int16_t s19[NFR19 * 2];
|
||||
for (int i = 0; i < NFR19 * 2; i++) { /* 相位连续:中途切音量不制造跳变 */
|
||||
double ph = 2.0 * M_PI * 440.0 * (double)(i / 2) / 44100.0;
|
||||
s19[i] = (int16_t)(sin(ph) * 0.5 * 32767.0);
|
||||
}
|
||||
memcpy(t19, s19, sizeof t19);
|
||||
for (size_t off = 0; off < sizeof t19 / 2; off += 4096) { /* 前半:vol=100 */
|
||||
size_t left = sizeof t19 / 2 - off;
|
||||
do_write(&c19, t19 + off, (uint32_t)(left < 4096 ? left : 4096));
|
||||
}
|
||||
usleep(200000);
|
||||
int32_t pct = 10; /* 播放中途降音量 */
|
||||
if (sf(c19.fd, MSG_AUDIO_VOLUME, 19, &pct, sizeof pct) == 0) {
|
||||
uint8_t ab[64]; uint16_t at; rf(c19.fd, &at, ab, sizeof ab);
|
||||
}
|
||||
usleep(100000);
|
||||
for (size_t off = sizeof t19 / 2; off < sizeof t19; off += 4096) { /* 后半:vol=10 */
|
||||
size_t left = sizeof t19 - off;
|
||||
do_write(&c19, t19 + off, (uint32_t)(left < 4096 ? left : 4096));
|
||||
}
|
||||
char q19[1024];
|
||||
do_query(&c19, q19, sizeof q19);
|
||||
check(strstr(q19, "vol=10") != NULL, "query reflects new vol=10 while streaming");
|
||||
check(strstr(q19, "underrun=0") != NULL && strstr(q19, "dropped=0") != NULL,
|
||||
"no underrun/dropped frames during live volume change");
|
||||
wait_wav_open(TWAV, 3000);
|
||||
usleep(250000);
|
||||
do_ctrl(&c19, MSG_AUDIO_STOP, a19.stream_id);
|
||||
close(c19.fd);
|
||||
stop_audiod();
|
||||
struct wavstat st = wav_s16_stats(TWAV);
|
||||
check(st.ok && st.nsamp > 4000, "T19 wav captured");
|
||||
if (st.ok) {
|
||||
/* 440Hz@44.1k 正弦本身相邻样本差 ≈ 1027;硬跳变会是 ~16380。
|
||||
* 斜坡后应 ≤ ~1100。阈值 2000 只放过正弦自身的斜率。 */
|
||||
printf(" (max adjacent-sample delta = %d; hard gain jump would be ~16380)\n", st.maxdelta);
|
||||
check(st.maxdelta < 2000, "volume change ramps (no hard jump = no click)");
|
||||
check(st.peak > 0, "audio still audible after change");
|
||||
}
|
||||
}
|
||||
}
|
||||
stop_audiod();
|
||||
unlink(conf19);
|
||||
}
|
||||
|
||||
/* ---- T20:连续拖滑块(rapid toggle)的**集成**回归(评审 r1 P2-1 的 daemon 侧等价用例)。
|
||||
* 状态机级断言(cur/left/bump_left、mid-ramp 反向计数)在 test_vol_gain 的 G4b——那里能直接
|
||||
* 看内部字段;这里验真实写路径:通过 IPC 在 vol 100↔10 之间连发 60 次,每次只喂 100 帧
|
||||
* (400B < 512 样本斜坡),让多数改动落在斜坡中段,然后看落盘的 PCM 有没有硬跳变/丢帧。 */
|
||||
printf("[T20 rapid volume toggle (slider drag) through the write path]\n");
|
||||
{
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
const char *conf20 = "/tmp/meow_test_settings20.conf";
|
||||
FILE *f20 = fopen(conf20, "w");
|
||||
if (f20) { fprintf(f20, "# meow-settings 1\naudio.volume = 100\naudio.output_bt = 1\n"); fclose(f20); }
|
||||
g_audiod_pid = start_audiod_settings("file", conf20);
|
||||
usleep(350000);
|
||||
struct cli c20;
|
||||
if (cli_connect(&c20) != 0) { check(0, "T20 connect"); }
|
||||
else {
|
||||
m_audio_open_ack_t a20;
|
||||
if (do_open(&c20, 44100, 2, AUDIO_FMT_S16, &a20) != 0 || a20.err != MEOW_OK) {
|
||||
check(0, "T20 open");
|
||||
} else {
|
||||
enum { NFR20 = 12000 }; /* 6000 帧风暴 + 6000 帧停手后(尾部窗口离过渡区很远) */
|
||||
static uint8_t t20[NFR20 * 4];
|
||||
static int16_t s20[NFR20 * 2];
|
||||
for (int i = 0; i < NFR20 * 2; i++) { /* 相位连续:跳变只可能来自增益 */
|
||||
double ph = 2.0 * M_PI * 440.0 * (double)(i / 2) / 44100.0;
|
||||
s20[i] = (int16_t)(sin(ph) * 0.5 * 32767.0);
|
||||
}
|
||||
memcpy(t20, s20, sizeof t20);
|
||||
size_t off = 0;
|
||||
int ack_ok = 1;
|
||||
for (int k = 0; k < 60; k++) {
|
||||
int32_t pct = (k & 1) ? 100 : 10;
|
||||
if (sf(c20.fd, MSG_AUDIO_VOLUME, (uint32_t)(100 + k), &pct, sizeof pct) == 0) {
|
||||
uint8_t ab[64]; uint16_t at;
|
||||
if (rf(c20.fd, &at, ab, sizeof ab) < 0 || at != MSG_ACK) ack_ok = 0;
|
||||
} else ack_ok = 0;
|
||||
do_write(&c20, t20 + off, 400); /* 100 帧:短于 512 样本斜坡 */
|
||||
off += 400;
|
||||
usleep(3000);
|
||||
}
|
||||
check(ack_ok, "60 rapid vol set all ACKed (async volume path keeps up)");
|
||||
int32_t pct10 = 10; /* 停手:稳定到 10% */
|
||||
if (sf(c20.fd, MSG_AUDIO_VOLUME, 199, &pct10, sizeof pct10) == 0) {
|
||||
uint8_t ab[64]; uint16_t at; rf(c20.fd, &at, ab, sizeof ab);
|
||||
}
|
||||
usleep(80000);
|
||||
for (; off < sizeof t20; off += 4096) { /* 剩余:在 10% 下播完 */
|
||||
size_t left = sizeof t20 - off;
|
||||
do_write(&c20, t20 + off, (uint32_t)(left < 4096 ? left : 4096));
|
||||
}
|
||||
char q20[1024];
|
||||
do_query(&c20, q20, sizeof q20);
|
||||
check(strstr(q20, "vol=10") != NULL, "settled to vol=10 after the toggle storm");
|
||||
check(strstr(q20, "underrun=0") != NULL && strstr(q20, "dropped=0") != NULL,
|
||||
"no underrun/dropped frames during 60 volume toggles");
|
||||
wait_wav_open(TWAV, 3000);
|
||||
usleep(250000);
|
||||
do_ctrl(&c20, MSG_AUDIO_STOP, a20.stream_id);
|
||||
close(c20.fd);
|
||||
stop_audiod();
|
||||
struct wavstat st = wav_s16_stats(TWAV);
|
||||
check(st.ok && st.nsamp > 8000, "T20 wav captured");
|
||||
if (st.ok) {
|
||||
printf(" (nsamp=%ld peak=%d max_delta=%d head_rms=%.2f tail_rms=%.2f; hard jump ~16380)\n",
|
||||
st.nsamp, st.peak, st.maxdelta, st.head_rms, st.tail_rms);
|
||||
check(st.maxdelta < 2000, "rapid toggles never hard-jump the gain (no click)");
|
||||
check(st.peak > 4000, "storm still contains loud passages (toggles took effect)");
|
||||
/* 停手后在 10%:尾部应比头部(满音量段)低 ≥40dB(10% = -77.5dB 理论) */
|
||||
check(st.tail_rms > 0.0 && st.head_rms > st.tail_rms * 100.0,
|
||||
"tail settles >=40dB below head (gain really applied at vol=10)");
|
||||
}
|
||||
}
|
||||
}
|
||||
stop_audiod();
|
||||
unlink(conf20);
|
||||
}
|
||||
|
||||
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||||
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 宿主测试要求):
|
||||
* D1 bt_dev_detect_mac:a2dpsrc/<MAC> 端点 → 提取 MAC(含 a2dp_source、多空行、多设备取第一个、
|
||||
* 无 a2dpsrc / 空输出 / NULL → -1)。
|
||||
* D2 bt_dev_rewrite:真实 alsa.conf 文本 → DEV=、device 两处 MAC 换成目标,保留 codec/其余 token;
|
||||
* 同值 → 1(无需改动);无 pcm.bluealsa / 无 DEV= → -1;带引号风格保留。
|
||||
* 输入全部取自**真机实测原文**(bluealsa v4.1.1;设备 /usr/bin/bluealsa 的
|
||||
* `dev_%.2X_..` + src/ba-transport.c 的 `<dev>/<profile>/<source|sink>`):
|
||||
* /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink ← 实机(本机 a2dp-source)
|
||||
* (同一格式下 /a2dpsrc/source 只是 PCM 模式不同,解析只认 dev_XX_.. 段)
|
||||
*
|
||||
* 覆盖矩阵:
|
||||
* 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 下):
|
||||
* cc -Wall -Wextra -o test_bt_dev test_bt_dev.c bt_dev.c
|
||||
@@ -26,91 +39,250 @@ int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL);
|
||||
|
||||
/* ---- D1 设备列表 → 选中 a2dpsrc 连接设备 ---- */
|
||||
/* ==== D1 设备列表 → 选中 a2dpsrc 连接设备(真实 D-Bus 路径格式) ==== */
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac(
|
||||
"a2dpsrc/E0:42:6D:79:C5:98\na2dpsnk/E0:42:6D:79:C5:98\n", mac, sizeof mac);
|
||||
check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1a a2dpsrc/<MAC> -> extract MAC");
|
||||
"/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,
|
||||
"D1a real D-Bus path -> MAC (dev_ underscores -> colons)");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac(
|
||||
"profile/aptx\n\na2dp_source/33:DE:2E:21:AD:30\n", mac, sizeof mac);
|
||||
check(rc == 0 && strcmp(mac, "33:DE:2E:21:AD:30") == 0, "D1b a2dp_source/<MAC> -> extract MAC");
|
||||
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/hfphf/source\n"
|
||||
"/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] = "";
|
||||
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");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac("\n", mac, sizeof mac);
|
||||
check(rc == -1, "D1d empty output -> -1");
|
||||
int rc = bt_dev_detect_mac("a2dpsrc/E0:42:6D:79:C5:98\n", mac, sizeof mac);
|
||||
check(rc == 0 && strcmp(mac, "E0:42:6D:79:C5:98") == 0, "D1d legacy a2dpsrc/<MAC> still parsed");
|
||||
}
|
||||
{
|
||||
char mac[32] = "";
|
||||
int rc = bt_dev_detect_mac("a2dpsnk/11:22:33:44:55:66\n", mac, sizeof mac);
|
||||
check(rc == -1, "D1e only sink (no a2dpsrc) -> -1");
|
||||
int rc = bt_dev_detect_mac("", mac, sizeof mac);
|
||||
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] = "";
|
||||
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 =
|
||||
"pcm.bluealsa:DEV=E0:42:6D:79:C5:98 {\n"
|
||||
" type plug\n"
|
||||
" slave {\n"
|
||||
" pcm {\n"
|
||||
" type bluealsa\n"
|
||||
" device E0:42:6D:79:C5:98\n"
|
||||
" profile \"a2dp\"\n"
|
||||
" codec \"LDAC\"\n"
|
||||
" ldac_eqmid \"LDAC_ABR\"\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == 0 && out != NULL, "D2a rewrite returns changed");
|
||||
check(out && strstr(out, ":DEV=33:DE:2E:21:AD:30 {"), "D2b DEV= updated");
|
||||
check(out && strstr(out, "device 33:DE:2E:21:AD:30\n"), "D2c device updated");
|
||||
check(out && strstr(out, "codec \"LDAC\"") && strstr(out, "ldac_eqmid \"LDAC_ABR\""),
|
||||
"D2d codec/other tokens preserved");
|
||||
/* 旧 MAC 不再出现 */
|
||||
check(out && !strstr(out, "E0:42:6D:79:C5:98"), "D2e old MAC gone");
|
||||
free(out);
|
||||
struct { const char *in; const char *want; const char *name; } t[] = {
|
||||
{ "LDAC", "LDAC", "D3a LDAC passthrough" },
|
||||
{ "ldac", "LDAC", "D3b lowercase ldac -> canonical" },
|
||||
{ "SBC", "SBC", "D3c SBC" },
|
||||
{ "AAC", "AAC", "D3d AAC" },
|
||||
{ "MPEG24", "AAC", "D3e MPEG24 alias -> AAC" },
|
||||
{ "aptX", "aptX", "D3f aptX" },
|
||||
{ "apt-X", "aptX", "D3g apt-X alias -> aptX" },
|
||||
{ "aptx-hd", "aptX-HD", "D3h aptx-hd -> aptX-HD" },
|
||||
{ "MPEG", "MP3", "D3i MPEG alias -> MP3" },
|
||||
{ "MP3", "MP3", "D3j MP3" },
|
||||
{ "FastStream", "FastStream", "D3k FastStream" },
|
||||
{ "FS", "FastStream", "D3l FS alias -> FastStream" },
|
||||
{ "auto", "unchanged", "D3m auto -> unchanged (do not touch negotiation)" },
|
||||
{ "", "unchanged", "D3n empty -> unchanged" },
|
||||
{ "unchanged", "unchanged", "D3o unchanged -> unchanged" },
|
||||
{ "BOGUS", "unchanged", "D3p unknown -> unchanged (never inject junk)" },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof t / sizeof t[0]; i++) {
|
||||
char tok[16] = "";
|
||||
const char *r = bt_dev_codec_token(t[i].in, tok, sizeof tok);
|
||||
check(r == tok && strcmp(tok, t[i].want) == 0, t[i].name);
|
||||
}
|
||||
{
|
||||
const char *conf =
|
||||
"pcm.bluealsa:DEV=33:DE:2E:21:AD:30 {\n"
|
||||
" type plug\n slave {\n pcm {\n"
|
||||
" type bluealsa\n device 33:DE:2E:21:AD:30\n"
|
||||
" profile \"a2dp\"\n codec \"LDAC\"\n }\n }\n}\n";
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == 1 && out == NULL, "D2f same MAC -> unchanged (idempotent)");
|
||||
}
|
||||
{
|
||||
const char *conf = "pcm.bluealsa {\n type bluealsa\n device \"E0:42:6D:79:C5:98\"\n}\n";
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite(conf, strlen(conf), "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == 0 && out && strstr(out, "device \"33:DE:2E:21:AD:30\""),
|
||||
"D2g quoted device preserved (no DEV= token)");
|
||||
free(out);
|
||||
}
|
||||
{
|
||||
char *out = NULL; size_t olen = 0;
|
||||
int rc = bt_dev_rewrite("pcm.hw {\n type hw\n}\n", 21, "33:DE:2E:21:AD:30", &out, &olen);
|
||||
check(rc == -1 && out == NULL, "D2h no pcm.bluealsa -> -1");
|
||||
char tok[16] = "";
|
||||
check(bt_dev_codec_token(NULL, tok, sizeof tok) == tok && !strcmp(tok, "unchanged"),
|
||||
"D3q NULL setting -> unchanged");
|
||||
}
|
||||
|
||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||
return g_fail;
|
||||
/* ==== D4 PCM 名构造(20-bluealsa.conf @args 只认 DEV/PROFILE/CODEC 这套名字) ==== */
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/* test_vol_gain.c — BT 软件增益(M55)宿主验证:曲线 / 缩放 / 斜坡 / 成本,全纯函数。
|
||||
*
|
||||
* 为什么必须宿主断言(用户真机现象:音量 10 仍满响度):
|
||||
* - `vol_gain_dac_att_reg()` 与 dac_vol_set() 是**同一个函数**(0.5dB/步、平方锥度),
|
||||
* 所以 BT 与 DAC 在同一个百分比上得到同一条听感曲线——这条「同源」必须在测试里钉死,
|
||||
* 否则以后谁改一边就会静默漂移;
|
||||
* - `vol=0` 必须是**真静音**(输出全 0),`vol=100` 必须**逐字节不变**(单位增益),
|
||||
* 中间必须单调(百分比越小越轻);
|
||||
* - 极值输入(±32768/±32767)任何百分比下都不得溢出/削波(增益恒 ≤ 1,32 位中间量);
|
||||
* - 改音量必须走斜坡(512 样本),不能一步跳到位(跳变 = 爆音「叮」)。
|
||||
*
|
||||
* 编译(在 patch_v2/meow_audiod 下):
|
||||
* cc -Os -Wall -Wextra -o test_vol_gain test_vol_gain.c vol_gain.c -lm
|
||||
* 运行:./test_vol_gain
|
||||
*/
|
||||
#include "vol_gain.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void check(int cond, const char *name)
|
||||
{
|
||||
if (cond) { printf(" PASS %s\n", name); g_pass++; }
|
||||
else { printf(" FAIL %s\n", name); g_fail++; }
|
||||
}
|
||||
|
||||
/* 参考实现(**仅测试**用 double,目标机代码零浮点):DAC 寄存器值 → 线性增益。 */
|
||||
static double ref_gain(int reg) { return pow(10.0, -0.5 * (double)reg / 20.0); }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL);
|
||||
vol_gain_init();
|
||||
printf("=== vol_gain host test (M55 BT software gain) ===\n");
|
||||
|
||||
/* ==== G1 衰减寄存器:与 dac_vol_set 的锥度逐点相同(真机 DAC 实测语义) ==== */
|
||||
printf("[G1 DAC attenuation register mapping (shared with dac_vol_set)]\n");
|
||||
check(vol_gain_dac_att_reg(100) == 0, "pct=100 -> reg 0 (0dB, full scale)");
|
||||
check(vol_gain_dac_att_reg(0) == 255, "pct=0 -> reg 255 (-127.5dB)");
|
||||
check(vol_gain_dac_att_reg(50) == 48, "pct=50 -> reg 48 (-24.0dB)");
|
||||
check(vol_gain_dac_att_reg(40) == 69, "pct=40 -> reg 69 (same as dac_vol_set T15)");
|
||||
check(vol_gain_dac_att_reg(10) == 155, "pct=10 -> reg 155 (-77.5dB)");
|
||||
check(vol_gain_dac_att_reg(-5) == 255 && vol_gain_dac_att_reg(140) == 0,
|
||||
"out-of-range pct clamps (<=0 mute, >=100 unity)");
|
||||
{
|
||||
int mono = 1;
|
||||
for (int pct = 100; pct > 0; pct--) /* 百分比下降 → 衰减只增不减 */
|
||||
if (vol_gain_dac_att_reg(pct - 1) < vol_gain_dac_att_reg(pct)) mono = 0;
|
||||
check(mono, "reg monotone non-decreasing as pct decreases");
|
||||
}
|
||||
|
||||
/* ==== G2 增益表:与 10^(-0.5dB*reg/20) 相差 ≤1 LSB ==== */
|
||||
printf("[G2 gain table vs exact pow() reference]\n");
|
||||
{
|
||||
int worst = 0, bad = 0;
|
||||
for (int pct = 1; pct <= 100; pct++) {
|
||||
int reg = vol_gain_dac_att_reg(pct);
|
||||
int got = vol_gain_q15(pct);
|
||||
int err = (int)lround(32768.0 * ref_gain(reg)) - got;
|
||||
if (err < 0) err = -err;
|
||||
if (err > 1) bad++;
|
||||
if (err > worst) worst = err;
|
||||
}
|
||||
check(bad == 0, "every pct within 1 LSB of exact 10^(-0.5dB*reg/20)");
|
||||
printf(" (max |err| = %d LSB over 100 points, 0 worst-case at 1 LSB)\n", worst);
|
||||
}
|
||||
check(vol_gain_q15(100) == VOL_GAIN_UNITY, "pct=100 -> unity (32768)");
|
||||
check(vol_gain_q15(0) == 0, "pct=0 -> 0 (true mute)");
|
||||
{
|
||||
int mono = 1;
|
||||
for (int pct = 100; pct > 0; pct--)
|
||||
if (vol_gain_q15(pct - 1) > vol_gain_q15(pct)) mono = 0;
|
||||
check(mono, "gain monotone non-increasing as pct decreases");
|
||||
}
|
||||
{
|
||||
/* 曲线不是线性:50% 处应远低于 -6dB(线性会正好 -6dB) */
|
||||
double g50 = vol_gain_q15(50) / 32768.0;
|
||||
check(g50 < 0.1, "pct=50 is well below linear -6dB (perceptual taper kept)");
|
||||
printf(" (pct=50 gain = %.4f = %.1f dB)\n", g50, 20.0 * log10(g50));
|
||||
}
|
||||
|
||||
/* ==== G3 单样本缩放:单位增益恒等 / 静音全 0 / 不溢出 ==== */
|
||||
printf("[G3 per-sample scaling]\n");
|
||||
{
|
||||
int bad = 0;
|
||||
for (int v = -32768; v <= 32767; v++) /* 全量程扫一遍:单位增益必须恒等 */
|
||||
if (vol_gain_scale_s16((int16_t)v, VOL_GAIN_UNITY) != (int16_t)v) bad++;
|
||||
check(bad == 0, "unity gain is bit-exact over the whole int16 range");
|
||||
}
|
||||
check(vol_gain_scale_s16(32767, 0) == 0 && vol_gain_scale_s16(-32768, 0) == 0,
|
||||
"gain 0 -> silence (both extremes)");
|
||||
{
|
||||
/* 任何百分比 × 极值输入:不越界、不放大(|out| ≤ |in|) */
|
||||
int bad = 0;
|
||||
const int16_t in[] = { 32767, -32768, 32766, -32767, 1, -1, 0 };
|
||||
for (int pct = 0; pct <= 100; pct++) {
|
||||
int g = vol_gain_q15(pct);
|
||||
for (size_t i = 0; i < sizeof in / sizeof in[0]; i++) {
|
||||
int out = vol_gain_scale_s16(in[i], g);
|
||||
if (out > 32767 || out < -32768) bad++;
|
||||
if (abs(out) > abs((int)in[i])) bad++;
|
||||
}
|
||||
}
|
||||
check(bad == 0, "no clipping/overflow at extremes for every pct (gain <= unity)");
|
||||
}
|
||||
|
||||
/* ==== G4 斜坡:不一步跳到位(防爆音),且终点精确 ==== */
|
||||
printf("[G4 ramp on volume change]\n");
|
||||
{
|
||||
vol_gain_ramp_t r;
|
||||
vol_gain_ramp_snap(&r, 100);
|
||||
check(r.cur_q15 == VOL_GAIN_UNITY && r.left == 0, "snap -> unity, no pending ramp");
|
||||
|
||||
/* 100% → 10%:铺满一个 512 样本块 */
|
||||
int16_t buf[VOL_GAIN_RAMP_SAMPLES * 2];
|
||||
for (int i = 0; i < VOL_GAIN_RAMP_SAMPLES * 2; i++) buf[i] = 16384;
|
||||
vol_gain_ramp_set(&r, 10, VOL_GAIN_RAMP_SAMPLES);
|
||||
check(r.left == VOL_GAIN_RAMP_SAMPLES && r.step_q15 < 0, "ramp armed toward lower gain");
|
||||
vol_gain_ramp_apply(&r, (uint8_t *)buf, sizeof buf);
|
||||
check(r.cur_q15 == vol_gain_q15(10) && r.left == 0, "ramp lands exactly on target q15");
|
||||
check(buf[0] > buf[VOL_GAIN_RAMP_SAMPLES * 2 - 1],
|
||||
"first sample louder than last (no instant jump = no click)");
|
||||
{
|
||||
int mono = 1;
|
||||
for (int i = 1; i < VOL_GAIN_RAMP_SAMPLES * 2; i++)
|
||||
if (buf[i] > buf[i - 1]) mono = 0;
|
||||
check(mono, "per-sample gain decreases monotonically through the ramp");
|
||||
int maxstep = 0;
|
||||
for (int i = 1; i < VOL_GAIN_RAMP_SAMPLES * 2; i++) {
|
||||
int d = buf[i - 1] - buf[i];
|
||||
if (d > maxstep) maxstep = d;
|
||||
}
|
||||
/* 一步跳到位会是 16384×(1-0.000122) ≈ 16382;斜坡每样本 ≤ |step|+1 = 65 →
|
||||
* 输出差 ≤ 65×16384/32768 ≈ 33 LSB(含余额分摊也在这个量级)。 */
|
||||
check(maxstep <= 40, "no hard gain jump (max per-sample delta <= 40 LSB)");
|
||||
printf(" (max per-sample delta during ramp = %d LSB; hard jump would be ~16382)\n", maxstep);
|
||||
}
|
||||
|
||||
/* 跨块连续性:同一段数据分两块推,结果必须与单块一致(斜坡状态不丢) */
|
||||
vol_gain_ramp_t a, b;
|
||||
vol_gain_ramp_snap(&a, 100); vol_gain_ramp_snap(&b, 100);
|
||||
int16_t big[1024], c1[512], c2[512];
|
||||
for (int i = 0; i < 1024; i++) big[i] = 8192;
|
||||
memcpy(c1, big, sizeof c1); memcpy(c2, big + 512, sizeof c2);
|
||||
vol_gain_ramp_set(&a, 25, 700);
|
||||
vol_gain_ramp_apply(&a, (uint8_t *)big, sizeof big);
|
||||
vol_gain_ramp_set(&b, 25, 700);
|
||||
vol_gain_ramp_apply(&b, (uint8_t *)c1, sizeof c1);
|
||||
vol_gain_ramp_apply(&b, (uint8_t *)c2, sizeof c2);
|
||||
check(memcmp(big, c1, sizeof c1) == 0 && memcmp(big + 512, c2, sizeof c2) == 0,
|
||||
"ramp state survives buffer boundaries (chunked == single block)");
|
||||
check(a.cur_q15 == vol_gain_q15(25) && b.cur_q15 == vol_gain_q15(25) && b.left == 0,
|
||||
"ramp longer than one block still reaches target exactly");
|
||||
|
||||
/* 目标重复设置不打断进行中的斜坡(每块都会 set 一次) */
|
||||
vol_gain_ramp_t c;
|
||||
vol_gain_ramp_snap(&c, 100);
|
||||
vol_gain_ramp_set(&c, 10, 512);
|
||||
int32_t step0 = c.step_q15, left0 = c.left;
|
||||
vol_gain_ramp_set(&c, 10, 512);
|
||||
check(c.step_q15 == step0 && c.left == left0, "repeated set() with same pct does not restart ramp");
|
||||
|
||||
/* 增益差小于斜坡长度:斜坡自动缩短(保证每样本至少 1 LSB,终点精确不过冲) */
|
||||
vol_gain_ramp_snap(&c, 100);
|
||||
int32_t tgt = vol_gain_q15(92);
|
||||
int32_t delta = VOL_GAIN_UNITY - tgt; /* 1833 LSB,远小于 4096 */
|
||||
vol_gain_ramp_set(&c, 92, 4096);
|
||||
check(c.left == delta, "small delta -> ramp shortened to |delta| samples");
|
||||
int16_t sm[1024];
|
||||
for (int i = 0; i < 1024; i++) sm[i] = 1000;
|
||||
vol_gain_ramp_apply(&c, (uint8_t *)sm, sizeof sm); /* 第一块:斜坡中段 */
|
||||
check(c.left > 0 && c.cur_q15 > tgt, "mid-ramp after one block (not yet at target)");
|
||||
vol_gain_ramp_apply(&c, (uint8_t *)sm, sizeof sm); /* 第二块:走完(跨块精确到点) */
|
||||
check(c.cur_q15 == tgt && c.left == 0, "short ramp ends exactly at target (no overshoot)");
|
||||
check(c.bump_left == 0, "no residual bump after ramp completes");
|
||||
|
||||
/* ---- G4b:连续拖滑块(rapid toggle)——块长 100 < 斜坡 512,目标中途反复反向 ----
|
||||
* 真实场景是用户按住音量键/拖滑块,每块都改目标、大多数改动落在斜坡中段。
|
||||
* 回归点(评审 P2-1):①每次从**当前** cur 重起斜坡,不许跳到位 ②增益全程在
|
||||
* [q15(10)=4, 32768] 内(不出现 0 或放大)③逐样本增益只朝目标单调走、相邻样本
|
||||
* 跳变 ≤64 LSB(硬跳变会是 ~16380)④停手后精确收敛到目标且无残留余额。 */
|
||||
{
|
||||
const int blk_n = 100; /* < VOL_GAIN_RAMP_SAMPLES:保证落在斜坡中段 */
|
||||
const int toggles = 60;
|
||||
int16_t blk[100];
|
||||
for (int i = 0; i < blk_n; i++) blk[i] = 16384; /* 恒定幅度:输出差 = 增益差/2 */
|
||||
int32_t lo = vol_gain_q15(10), hi = vol_gain_q15(100);
|
||||
int midramp = 0, out_of_band = 0, maxdelta = 0, nonmono = 0;
|
||||
vol_gain_ramp_snap(&c, 100);
|
||||
for (int k = 0; k < toggles; k++) {
|
||||
if (c.left > 0) midramp++; /* 上一次斜坡还没走完就被改目标 */
|
||||
vol_gain_ramp_set(&c, (k & 1) ? 100 : 10, VOL_GAIN_RAMP_SAMPLES);
|
||||
int32_t g_before = c.cur_q15;
|
||||
for (int i = 0; i < blk_n; i++) blk[i] = 16384; /* 每块重铺原始信号 */
|
||||
vol_gain_ramp_apply(&c, (uint8_t *)blk, sizeof blk);
|
||||
if (c.cur_q15 < lo || c.cur_q15 > hi) out_of_band++;
|
||||
/* 块内逐样本:只朝目标走(单调),且相邻样本差 ≤64 */
|
||||
int up = c.tgt_q15 > g_before;
|
||||
for (int i = 1; i < blk_n; i++) {
|
||||
int d = blk[i] - blk[i - 1];
|
||||
if (d < 0) d = -d;
|
||||
if (d > maxdelta) maxdelta = d;
|
||||
if (up ? (blk[i] < blk[i - 1]) : (blk[i] > blk[i - 1])) nonmono++;
|
||||
if (blk[i] > 16384 || blk[i] < 0) out_of_band++; /* 不许放大/越界 */
|
||||
}
|
||||
}
|
||||
check(midramp >= toggles - 2, "toggle storm actually lands mid-ramp (>=58/60)");
|
||||
check(out_of_band == 0, "gain stays within [q15(10)=4, 32768] and never amplifies");
|
||||
check(nonmono == 0, "each block moves monotonically toward its target (no zigzag)");
|
||||
check(maxdelta <= 64, "rapid toggles: per-sample delta <= 64 LSB (hard jump ~16380)");
|
||||
/* 停手:回到 10% 并推几块大缓冲,确认精确收敛、状态无残留 */
|
||||
vol_gain_ramp_set(&c, 10, VOL_GAIN_RAMP_SAMPLES);
|
||||
for (int i = 0; i < blk_n; i++) blk[i] = 16384;
|
||||
for (int k = 0; k < 8; k++) vol_gain_ramp_apply(&c, (uint8_t *)blk, sizeof blk);
|
||||
check(c.cur_q15 == vol_gain_q15(10) && c.cur_q15 == 4 && c.left == 0 && c.bump_left == 0,
|
||||
"after toggle storm: cur=4 (q15 of 10%), left=0, bump_left=0");
|
||||
printf(" (toggle storm: %d/60 mid-ramp reversals, max per-sample delta %d LSB)\n",
|
||||
midramp, maxdelta);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==== G5 稳态快路:vol=100 缓冲原样、vol=0 全 0 ==== */
|
||||
printf("[G5 steady-state fast paths]\n");
|
||||
{
|
||||
vol_gain_ramp_t r;
|
||||
int16_t buf[256];
|
||||
for (int i = 0; i < 256; i++) buf[i] = (int16_t)(i * 97 - 12000);
|
||||
int16_t orig[256];
|
||||
memcpy(orig, buf, sizeof buf);
|
||||
vol_gain_ramp_snap(&r, 100);
|
||||
vol_gain_ramp_apply(&r, (uint8_t *)buf, sizeof buf);
|
||||
check(memcmp(buf, orig, sizeof buf) == 0, "pct=100 steady state: buffer untouched (bit-exact)");
|
||||
vol_gain_ramp_snap(&r, 0);
|
||||
vol_gain_ramp_apply(&r, (uint8_t *)buf, sizeof buf);
|
||||
int allzero = 1;
|
||||
for (int i = 0; i < 256; i++) if (buf[i] != 0) allzero = 0;
|
||||
check(allzero, "pct=0 steady state: buffer all zeros (true mute)");
|
||||
}
|
||||
|
||||
/* ==== G6 成本参考(宿主):逐样本缩放吞吐(目标机另行实测) ==== */
|
||||
printf("[G6 scaling throughput (host reference)]\n");
|
||||
{
|
||||
enum { N = 1 << 20 };
|
||||
int16_t *buf = malloc((size_t)N * 2);
|
||||
vol_gain_ramp_t r;
|
||||
if (buf) {
|
||||
for (int i = 0; i < N; i++) buf[i] = (int16_t)((i * 31) & 0x7FFF);
|
||||
vol_gain_ramp_snap(&r, 50); /* 稳态非快路:每个样本都乘 */
|
||||
struct timespec t0, t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
vol_gain_ramp_apply(&r, (uint8_t *)buf, (size_t)N * 2);
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
double ms = (double)(t1.tv_sec - t0.tv_sec) * 1e3 + (double)(t1.tv_nsec - t0.tv_nsec) / 1e6;
|
||||
/* 44.1k/2ch 每秒 88200 样本 */
|
||||
double us_per_88200 = ms * 1000.0 * 88200.0 / (double)N;
|
||||
printf(" (%d samples in %.3f ms -> %.1f us of every 1000 ms at 44.1k/2ch)\n",
|
||||
N, ms, us_per_88200);
|
||||
check(ms < 1000.0, "host scaling throughput far above real-time (sanity)");
|
||||
free(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==== 附:给报告用的曲线表(百分比 / 衰减寄存器 / dB / Q15) ==== */
|
||||
printf("[curve table pct -> reg / dB / q15 / relative]\n");
|
||||
const int pcts[] = { 100, 92, 75, 60, 50, 40, 25, 10, 5, 1, 0 };
|
||||
for (size_t i = 0; i < sizeof pcts / sizeof pcts[0]; i++) {
|
||||
int p = pcts[i], reg = vol_gain_dac_att_reg(p), g = vol_gain_q15(p);
|
||||
printf(" pct=%3d reg=%3d %7.2f dB q15=%6d x%.6f\n",
|
||||
p, reg, -0.5 * (double)reg, g, (double)g / 32768.0);
|
||||
}
|
||||
|
||||
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/* vol_gain.c — 纯整数增益表 + 缩放/斜坡(宿主与目标机共用)。见 vol_gain.h 的定性说明。 */
|
||||
#include "vol_gain.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* 每衰减步 0.5dB 的振幅系数:10^(-0.5/20) = 10^(-0.025) = 0.9440608762859234。
|
||||
* Q31 = round(0.9440608762859234 × 2^31) = 2027355295(离线算出并钉死;运行期不碰浮点)。 */
|
||||
#define VOL_GAIN_STEP_Q31 2027355295u
|
||||
|
||||
static int32_t s_gain_q15[256]; /* 下标=衰减寄存器值;只读(init 之后) */
|
||||
static int s_gain_ready = 0;
|
||||
|
||||
/* 衰减步 → 增益的整数迭代:acc 以 Q31(15 位整数 + 16 位小数)表示 Q15 增益,
|
||||
* 每步 acc = acc × STEP >> 31。255 步累计相对误差 ~2^-23,实测与 pow() 相差 ≤1 LSB。 */
|
||||
void vol_gain_init(void)
|
||||
{
|
||||
if (s_gain_ready) return;
|
||||
uint64_t acc = (uint64_t)VOL_GAIN_UNITY << 16;
|
||||
for (int reg = 0; reg < 256; reg++) {
|
||||
int32_t g = (int32_t)((acc + 32768u) >> 16);
|
||||
if (g > VOL_GAIN_UNITY) g = VOL_GAIN_UNITY;
|
||||
s_gain_q15[reg] = g;
|
||||
acc = (acc * VOL_GAIN_STEP_Q31 + (1u << 30)) >> 31;
|
||||
}
|
||||
s_gain_ready = 1;
|
||||
}
|
||||
|
||||
/* dac_vol_set() 的锥度原样搬来(平方锥度;255=-127.5dB)。**单一出处**:dac_vol_set 与
|
||||
* BT 增益表都取这个值,谁都不许自己再算一遍,否则 DAC/BT 曲线会各走各的。 */
|
||||
int vol_gain_dac_att_reg(int pct)
|
||||
{
|
||||
if (pct <= 0) return 255; /* 真静音 */
|
||||
if (pct > 100) pct = 100;
|
||||
int reg = (100 - pct) * (100 - pct) * 192 / 10000;
|
||||
if (reg > 255) reg = 255;
|
||||
if (reg < 0) reg = 0;
|
||||
return reg;
|
||||
}
|
||||
|
||||
int vol_gain_q15(int pct)
|
||||
{
|
||||
if (!s_gain_ready) vol_gain_init(); /* 未初始化就查=防御(正常由 main 先 init) */
|
||||
if (pct <= 0) return 0; /* vol=0 必须真静音,不给「很小声」留余地 */
|
||||
return s_gain_q15[vol_gain_dac_att_reg(pct)];
|
||||
}
|
||||
|
||||
int16_t vol_gain_scale_s16(int16_t in, int g_q15)
|
||||
{
|
||||
int32_t v = (int32_t)in * g_q15; /* |in|≤32768 × g≤32768 → |v|≤2^30,int32 不溢出 */
|
||||
v = (v + (1 << 14)) >> 15; /* 四舍五入回 S16 量程(单位增益时恒等) */
|
||||
if (v > 32767) return 32767;
|
||||
if (v < -32768) return -32768;
|
||||
return (int16_t)v;
|
||||
}
|
||||
|
||||
void vol_gain_ramp_snap(vol_gain_ramp_t *r, int pct)
|
||||
{
|
||||
r->cur_q15 = r->tgt_q15 = vol_gain_q15(pct);
|
||||
r->step_q15 = 0;
|
||||
r->bump = 0;
|
||||
r->bump_left = 0;
|
||||
r->left = 0;
|
||||
}
|
||||
|
||||
void vol_gain_ramp_set(vol_gain_ramp_t *r, int pct, int ramp_samples)
|
||||
{
|
||||
int32_t tgt = vol_gain_q15(pct);
|
||||
if (tgt == r->tgt_q15) return; /* 目标没变:不重置进行中的斜坡 */
|
||||
r->tgt_q15 = tgt;
|
||||
int32_t d = tgt - r->cur_q15;
|
||||
if (ramp_samples <= 0 || d == 0) {
|
||||
r->cur_q15 = tgt;
|
||||
r->step_q15 = 0;
|
||||
r->bump = 0;
|
||||
r->bump_left = 0;
|
||||
r->left = 0;
|
||||
return;
|
||||
}
|
||||
/* 线性斜坡 = 固定步进 + 余额分摊(不除法的 Bresenham):
|
||||
* n 个样本走完 d,step=d/n 截断,余数 rem 按 ±1 分摊到前 |rem| 个样本。
|
||||
* 好处:**最后一步不会突然补差**(旧版 step 截断后末尾要一次跳 500+ LSB = 爆音),
|
||||
* 全程每样本增益变化 ≤ |step|+1,且终点精确等于目标(无过冲)。 */
|
||||
int32_t ad = (d > 0) ? d : -d;
|
||||
int32_t n = ramp_samples;
|
||||
if (n > ad) n = ad; /* 增益差比斜坡短:缩短斜坡(步进仍至少 1 LSB) */
|
||||
int32_t step = d / n;
|
||||
int32_t rem = d - step * n; /* |rem| < n,与 d 同号 */
|
||||
r->left = n;
|
||||
r->step_q15 = step;
|
||||
r->bump = (rem > 0) ? 1 : -1;
|
||||
r->bump_left = (rem > 0) ? rem : -rem;
|
||||
}
|
||||
|
||||
void vol_gain_ramp_apply(vol_gain_ramp_t *r, uint8_t *buf, size_t nbytes)
|
||||
{
|
||||
size_t ns = nbytes / 2;
|
||||
if (!buf || !ns) return;
|
||||
if (r->left <= 0) {
|
||||
/* 稳态快路:单位增益→原样返回(vol=100 逐字节不变);静音→memset 全 0(vol=0 真静音) */
|
||||
if (r->cur_q15 == VOL_GAIN_UNITY) return;
|
||||
if (r->cur_q15 == 0) { memset(buf, 0, ns * 2); return; }
|
||||
}
|
||||
for (size_t i = 0; i < ns; i++) {
|
||||
int32_t g = r->cur_q15;
|
||||
if (r->left > 0) {
|
||||
r->cur_q15 += r->step_q15;
|
||||
if (r->bump_left > 0) { r->cur_q15 += r->bump; r->bump_left--; }
|
||||
if (--r->left == 0) r->cur_q15 = r->tgt_q15; /* 兜齐(理论上已精确相等) */
|
||||
g = r->cur_q15;
|
||||
}
|
||||
int16_t s;
|
||||
memcpy(&s, buf + i * 2, 2); /* 字节存取:不假设缓冲对齐、不违反严格别名 */
|
||||
s = vol_gain_scale_s16(s, g);
|
||||
memcpy(buf + i * 2, &s, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/* vol_gain.h — 蓝牙(A2DP)写路径的软件音量增益(M55)。
|
||||
*
|
||||
* 为什么需要:BT 输出时 AVRCP 被钉在 127(耳机功放满量程),`vol_apply()` 旧版在 BT 分支
|
||||
* 直接 return——音量在整条 BT 路径上被跳过,`meow_audio vol <n>` 完全无效(真机现象:
|
||||
* 音量 10 仍是满响度;`bluealsa-cli volume … → L: 127 R: 127`)。
|
||||
* 而每次改音量 fork 一次 `bluealsa-cli volume` 约 25–30ms(单核 1GHz),不能放在滑块每一步;
|
||||
* 大量耳机还忽略 AVRCP absolute volume。→ 定案:AVRCP 保持 127,增益在写路径上做。
|
||||
*
|
||||
* 曲线与 DAC **同源**:CS43198 numid=1/2 是反转衰减(0=0dB,255=-127.5dB,0.5dB/步),
|
||||
* dac_vol_set() 的锥度是「(1-pct)²×96dB」。这里不重写锥度,而是直接取
|
||||
* vol_gain_dac_att_reg(pct)(与 dac_vol_set 同一个函数)→ 增益 = 10^(-0.5dB×reg/20),
|
||||
* 于是「同一百分比」在 DAC 与 BT 上得到同一条听感曲线(非线性、不重排)。
|
||||
*
|
||||
* 全整数:增益表开机算一次(Q31 定点迭代乘法,不引 libm/软浮点),逐样本一次 32 位乘 +
|
||||
* 移位 + 饱和;100% 走「单位增益直接返回」快路(逐字节不变、零开销),0% 走 memset。
|
||||
*
|
||||
* ---- App / SDK 侧契约(M55 定案;MEOW_APP_SDK.md 里「BT 输出目标时音量由 AVRCP 侧控」已过期)----
|
||||
* - `meow_audio_set_volume(pct)` 在**两种输出目标下都生效**:DAC 目标写 CS43198 音量寄存器
|
||||
* (numid=1/2 + DSD_VOLUME 双组),BT 目标写本模块的软件增益(写进 A2DP 的 PCM 样本)。
|
||||
* 同一 pct 在两条路上是**同一条听感曲线**(vol_gain_dac_att_reg),不是「线性 vs 非线性」两套。
|
||||
* - BT 下 AVRCP 被钉在 127(耳机功放满量程)——那是为了让耳机的模拟级不二次衰减,
|
||||
* **不是**「音量交给耳机控」:用户的 vol 值由 audiod 的数字增益执行,`vol=0` 是真正的全 0 静音。
|
||||
* - `pct` 只在 `[0,100]` 有意义:`<=0` 一律真静音,`>100` 一律按 100(满量程)处理。
|
||||
* (`settings.conf` 里被手改成如 `audio.volume = 150` 时表现为满量程,不会超过 100% 的合法响度。)
|
||||
* - 16bit 写路径的固有下限:约 <=-78dB 的档位(pct<=10)只剩 1 LSB 量级,听感上是静音;
|
||||
* 实用区间约 60..100%(与 DAC 相同,因为曲线同源)。
|
||||
*/
|
||||
#ifndef MEOW_VOL_GAIN_H
|
||||
#define MEOW_VOL_GAIN_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define VOL_GAIN_UNITY 32768 /* Q15 单位增益:逐样本 (in*32768+16384)>>15 ≡ in */
|
||||
#define VOL_GAIN_RAMP_SAMPLES 512 /* 增益变更斜坡长度(44.1k ≈ 11.6ms;防「叮」一声爆音) */
|
||||
|
||||
/* 开机调用一次(幂等):生成 256 项增益表。之后查表全只读、无锁。 */
|
||||
void vol_gain_init(void);
|
||||
|
||||
/* pct(0..100) → DAC 衰减寄存器 0..255。**dac_vol_set() 也调它**,保证两条路同一条锥度。
|
||||
* pct<=0 → 255(真静音);pct>=100 → 0(0dB 满量程,**包括 pct>100**:越界按满量程截断,
|
||||
* 不会给出 >100% 的响度)。 */
|
||||
int vol_gain_dac_att_reg(int pct);
|
||||
|
||||
/* pct → 线性增益 Q15(32768=单位增益,0=真静音)。pct 越界自动 clamp。 */
|
||||
int vol_gain_q15(int pct);
|
||||
|
||||
/* 纯函数:单个 S16 样本按 Q15 增益缩放(32 位中间量 + 四舍五入 + 饱和)。
|
||||
* 增益恒 ≤ 32768 → |in*g| ≤ 2^30 不溢出;单位增益时逐样本恒等。 */
|
||||
int16_t vol_gain_scale_s16(int16_t in, int g_q15);
|
||||
|
||||
/* ---- 斜坡状态(**音频线程私有**,调用方持有;不做任何加锁) ----
|
||||
* 目标值由控制线程经单个对齐 int 松散写进来(MIPS32 int 写天然原子),音频线程每
|
||||
* dev_write 读一次即可,最坏晚一帧生效。 */
|
||||
typedef struct {
|
||||
int32_t cur_q15; /* 当前位置 */
|
||||
int32_t tgt_q15; /* 目标增益 */
|
||||
int32_t step_q15; /* 每样本基础步进 */
|
||||
int32_t bump; /* 余额分摊方向(±1,0=无余额) */
|
||||
int32_t bump_left; /* 还要分摊多少样本 */
|
||||
int32_t left; /* 剩余斜坡样本数(0=已到位) */
|
||||
} vol_gain_ramp_t;
|
||||
|
||||
/* 立即跳到 pct 对应增益(开流/换设备/初始化用:新流不该淡入)。 */
|
||||
void vol_gain_ramp_snap(vol_gain_ramp_t *r, int pct);
|
||||
|
||||
/* 目标变为 pct 对应增益;与当前目标相同时不动(不打断进行中的斜坡)。
|
||||
* ramp_samples<=0 → 立即生效。 */
|
||||
void vol_gain_ramp_set(vol_gain_ramp_t *r, int pct, int ramp_samples);
|
||||
|
||||
/* 就地缩放一段 S16_LE 交织 PCM(nbytes 字节;奇数尾字节原样保留)。 */
|
||||
void vol_gain_ramp_apply(vol_gain_ramp_t *r, uint8_t *buf, size_t nbytes);
|
||||
|
||||
#endif
|
||||
@@ -21,7 +21,9 @@ if [ "$1" = "host" ]; then
|
||||
exit 0
|
||||
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) =="
|
||||
ls -la 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 ---- */
|
||||
#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) */
|
||||
/* 传址给应用,不做 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;
|
||||
}
|
||||
|
||||
/* 安装成功后,把原签 .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 版本 == 已装 → 直接复制(不二次签名)。
|
||||
* 成功 0;否则 -1(调用方现场重打包,无签名)。 */
|
||||
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;
|
||||
}
|
||||
|
||||
/* .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> 下无
|
||||
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
|
||||
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 渲染」的通知中心叠加会话。
|
||||
* 复用电源菜单对 App 的冻结模板(show_power_menu 的 SIGSTOP/读回/叠加/接管/恢复):
|
||||
* ① 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();
|
||||
meow_ui_flush();
|
||||
/* M40 F5/P5-P6:先写整帧重画请求,再 SIGCONT/放行——App 恢复后的第一次 present 会
|
||||
* 消费它并 dirty_full(),用自身完整帧盖掉 launcher 快照恢复可能留下的任何残影。 */
|
||||
meow_app_repaint_request();
|
||||
if (!keep_running) {
|
||||
/* §310 输入穿透定案(overlay_gate.h):App 被 SIGSTOP 冻结过 → 在 /tmp/meow_notif_active
|
||||
* 仍未更新时**先 SIGCONT**,让 SDK 输入线程排空冻结期缓冲的 evdev 事件副本 + 对齐
|
||||
@@ -1555,10 +1489,10 @@ static int run_app_lock_overlay(struct app *a, pid_t pid)
|
||||
* 防解锁瞬间把积累的触摸回放成 App 内点击(锁屏穿透→「启动奇怪界面」→ 状态栏消失的触发源)。 */
|
||||
kill(pid, SIGCONT);
|
||||
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);
|
||||
} 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)随之继续自抑
|
||||
* (不叠帧 / 不穿输入——修复「锁屏解锁后 ncm 歌单帧叠加在 Hello 上 + 交互双响应」)。 */
|
||||
@@ -1573,6 +1507,72 @@ static int run_app_lock_overlay(struct app *a, pid_t pid)
|
||||
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 的共享核心。
|
||||
* chain 非空 → 向子进程注入 MEOW_ACTIVITY/MEOW_CHAIN 并初始化 nav_stack(M3);空/NULL → 裸启动。 */
|
||||
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;
|
||||
meow_ams_take_picker(pick_action, sizeof pick_action,
|
||||
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 残留) */
|
||||
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 转过)→ 原进程回前台,
|
||||
* 不 fork/exec(进程仍存无法注入新 env:chain/picker 上下文忽略,记日志)。
|
||||
@@ -1613,7 +1614,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
char dep_missing[32] = "";
|
||||
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);
|
||||
return -1;
|
||||
return launch_fail(a, "missing dep: %s", dep_missing);
|
||||
}
|
||||
}
|
||||
/* 冷启动:标记内容置为前台归属(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
|
||||
* and never masquerades as "app ran and exited". */
|
||||
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);
|
||||
|
||||
/* 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]);
|
||||
if (out_pipe[0] >= 0) close(out_pipe[0]);
|
||||
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) {
|
||||
/* 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;
|
||||
reap_wait(pid, &st); /* reap the failed child(有界) */
|
||||
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/确认) */
|
||||
|
||||
@@ -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 to_background = 0; /* M4:nav-exit 转后台保活(进程保留,跳过退出尾) */
|
||||
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;
|
||||
unsigned long long app_seq_last = 0;
|
||||
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 一并触发
|
||||
* lck_wake_should_lock 锁屏,两条路径对齐。 */
|
||||
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 崩。启动前记录,运行期监测卡移除。
|
||||
* (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */
|
||||
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 的缓存态) */
|
||||
unlink(MEOW_SBAR_MODE_FILE); /* 清可能残留的上一 App 顶条模式(崩溃/被杀未清),防新 App 误收合 */
|
||||
if (!a->fullscreen) app_overlay_statusbar();
|
||||
/* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。 */
|
||||
/* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。
|
||||
* M40 F3:同时清 /tmp/meow_app_activity——上一会话(或启动手势)遗留的标记不能在本会话
|
||||
* 首圈被当成「用户触摸」而触发息屏计时重置/唤醒+锁屏。 */
|
||||
unlink(POWMENU_MARKER);
|
||||
unlink(POWMENU_ACTIVE);
|
||||
unlink(MEOW_APP_ACTIVITY_FILE);
|
||||
/* §10.5:进入 App 前台会话。置位让 ipc_dispatch 的 MSG_NOTIF_OPEN 可入队(否则回 ERR);
|
||||
* 清残留 pending(防上一会话未消费的 OPEN 被本会话误展开)。 */
|
||||
g_app_session_active = 1;
|
||||
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 (;;) {
|
||||
int r = waitpid(pid, &status, WNOHANG);
|
||||
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;
|
||||
if (meow_notif_open_take(&nopen)) {
|
||||
int nr = run_app_notif_overlay(a, pid);
|
||||
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点(activity 标记忽略窗) */
|
||||
overlay_return_valid = 1;
|
||||
/* 输入基线重播(与电源菜单同款):面板期间按键可能已变化,重播防误判 key_edge */
|
||||
kp_poll(&kp_power, 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++) {
|
||||
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 → 息屏;
|
||||
* 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。
|
||||
* M6.5 §310 问题1:watchdog 电源键手动息屏(背光 0、不置 meow_auto_slept)→ 唤醒时按
|
||||
* app_panel_manual_off 一并锁屏,与超时路径(meow_auto_slept)对齐。 */
|
||||
{
|
||||
/* 背光==0 => 面板已关(超时路径已置 auto_slept;手动路径靠此补判)。
|
||||
* read_brightness() 失败返回 -1(≠0),不误置位。 */
|
||||
if (read_brightness() == 0) app_panel_manual_off = 1;
|
||||
/* M40 F3:去掉单向闩锁。旧实现只在 watchdog 手动息屏时置 1、只有本循环唤醒分支清 1;
|
||||
* watchdog 之后把屏点亮(pwr_woke)闩锁仍是 1 → 任何 activity 标记都会再次
|
||||
* 「亮屏+锁屏」(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;
|
||||
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; }
|
||||
@@ -2094,6 +2143,8 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
* (launcher 不崩、run_menu 重绘主屏),同 run_app_notif_overlay 的 died 分支。 */
|
||||
if (lck_wake_should_lock()) {
|
||||
int lr2 = run_app_lock_overlay(a, pid);
|
||||
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点 */
|
||||
overlay_return_valid = 1;
|
||||
if (lr2 != 0) {
|
||||
reap_wait(pid, &status);
|
||||
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 写标记)→ 重置自动息屏
|
||||
* 计时;若已自动息屏,按 key-wake 同款路径唤醒(面板上电 + 落 buffer0)。 */
|
||||
* 计时;若已自动息屏,按 key-wake 同款路径唤醒(面板上电 + 落 buffer0)。
|
||||
* M40 F3:叠加层/电源菜单刚返回的排空窗内,该标记是 App 被 SIGCONT 后重放的冻结期
|
||||
* 输入,只消费不唤醒(否则「亮屏 + 弹 PIN」,P3)。 */
|
||||
if (access(MEOW_APP_ACTIVITY_FILE, F_OK) == 0) {
|
||||
unlink(MEOW_APP_ACTIVITY_FILE);
|
||||
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)",
|
||||
meow_auto_slept, app_panel_manual_off);
|
||||
meow_panel_on();
|
||||
@@ -2129,6 +2184,8 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
* §310:叠加期 App 崩溃 → 回收僵尸走统一退出路径(launcher 不崩、run_menu 重绘主屏)。 */
|
||||
if (lck_wake_should_lock()) {
|
||||
int lr2 = run_app_lock_overlay(a, pid);
|
||||
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点 */
|
||||
overlay_return_valid = 1;
|
||||
if (lr2 != 0) {
|
||||
reap_wait(pid, &status);
|
||||
ams_status_valid = 1;
|
||||
@@ -2169,20 +2226,16 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
app_prev_vold = vold;
|
||||
if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2)
|
||||
app_seq_hits = 0;
|
||||
/* M2.24 全面按键接管:watchdog 检测到电源长按≥3s 写 /tmp/meow_power_menu → 此处消费:
|
||||
* SIGSTOP 应用冻结 → 写 powmenu_active 让 watchdog 让位 → 统一电源菜单(launcher 一处绘制,
|
||||
* 复用 show_power_menu,含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)→ 菜单关 →
|
||||
* 清 active → SIGCONT 应用续跑。 */
|
||||
/* M2.24/M40 全面按键接管:watchdog 检测到电源长按≥power.longpress_menu_ms 写
|
||||
* /tmp/meow_power_menu → 此处消费。M40 F2 起走 run_app_power_menu() 的完整 §10.5
|
||||
* 协议(SIGSTOP + 全抑制标记 + 帧恢复 + SIGCONT→排空→标记翻回前台 + 整帧重画请求),
|
||||
* 不再只 SIGSTOP/show_power_menu/SIGCONT(旧实现关闭后屏幕仍是菜单帧 = P2 主因)。 */
|
||||
if (access(POWMENU_MARKER, F_OK) == 0) {
|
||||
unlink(POWMENU_MARKER);
|
||||
logmsg("watchdog: power menu request (app %s) -> SIGSTOP + menu", a->name);
|
||||
kill(pid, SIGSTOP);
|
||||
int mfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (mfd >= 0) { write(mfd, "1", 1); close(mfd); }
|
||||
int pm_ret = show_power_menu();
|
||||
unlink(POWMENU_ACTIVE);
|
||||
kill(pid, SIGCONT);
|
||||
logmsg("watchdog: power menu closed (ret=%d) -> SIGCONT app", pm_ret);
|
||||
logmsg("watchdog: power menu request (app %s) -> overlay session", a->name);
|
||||
int pm_ret = run_app_power_menu(a, pid);
|
||||
overlay_return_ms = now_ms(); /* M40 F3:排空窗起点(activity 标记忽略窗) */
|
||||
overlay_return_valid = 1;
|
||||
/* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge
|
||||
* (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */
|
||||
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_keys, kp_keys.cur, sizeof save_keys);
|
||||
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,防御性退出 */
|
||||
}
|
||||
/* 「同一个状态栏」闭环:常驻顶条叠加(agent-146 全屏收合接入)。
|
||||
@@ -2223,6 +2281,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
if (to_background) {
|
||||
g_app_session_active = 0; /* 前台会话结束(ipc_dispatch 不再受理 NOTIF_OPEN) */
|
||||
meow_notif_open_clear();
|
||||
meow_volpage_sink_clear(); /* E:离开前台撤翻页 sink(音量键交还系统) */
|
||||
if (wpid > 0) {
|
||||
/* 看门狗随前台会话结束回收(后台无手势监听;有界等防挂死) */
|
||||
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) */
|
||||
g_app_session_active = 0;
|
||||
meow_notif_open_clear();
|
||||
meow_volpage_sink_clear(); /* E:离开前台撤翻页 sink(音量键交还系统) */
|
||||
/* M2.17 兜底:App 会话结束(waitpid 回收/崩溃/被杀,回主界面)清 fg_act ——
|
||||
* App 在 keep_awake activity(如 ncm 播放页)被 OOM/崩溃杀死且未弹回 home 时,g_fg_act 会
|
||||
* 滞留 "<act>",使主页 fg_keep_awake()==1 不再自动息屏。此处兜底清空(App 已退出/死亡,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
*
|
||||
* 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar:
|
||||
* 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action,
|
||||
* act=bgm_top_action → bgmgr_kill_all,**不杀框架/当前前台/非后台**,经 bgmgr_can_kill 门控)。
|
||||
* act=bgm_top_action → bgmgr_kill_all,**不杀受保护/当前前台/非后台**,经 bgmgr_can_kill 门控)。
|
||||
* 列表(live=1 约 2s 重扫,实时增删):
|
||||
* - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮**
|
||||
* (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。
|
||||
@@ -38,8 +38,9 @@ typedef struct {
|
||||
int napps;
|
||||
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
|
||||
int n;
|
||||
char toast[64];
|
||||
long toast_ms;
|
||||
/* 行副文本存储:sub_row_t.sub 被 SDK 行池/自绘在整页会话内持续读取,
|
||||
* 不能指向 bgm_rows 的栈帧(曾用函数内 char sub[80] → 悬垂,页面副标题为乱码)。 */
|
||||
char subs[MEOW_BGMR_ROW_MAX][80];
|
||||
} bgm_ctx_t;
|
||||
|
||||
/* 找 struct app(launcher 应用表)by id */
|
||||
@@ -54,36 +55,44 @@ static struct app *bgm_app_find(struct app *apps, int napps, const char *app_id)
|
||||
/* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */
|
||||
static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid,
|
||||
char *name, int nl, char *icon, int il,
|
||||
char *framework, int *is_fg)
|
||||
char *protected_app, int *is_fg)
|
||||
{
|
||||
(void)pid;
|
||||
bgm_ctx_t *b = ctx;
|
||||
struct app *a = bgm_app_find(b->apps, b->napps, app_id);
|
||||
*framework = 0;
|
||||
*protected_app = 0;
|
||||
*is_fg = 0;
|
||||
if (!a) return -1; /* 未注册 → name 回退 app_id */
|
||||
if (a->name[0]) snprintf(name, nl, "%s", a->name);
|
||||
if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : "");
|
||||
*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;
|
||||
}
|
||||
|
||||
static int bgm_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
||||
{
|
||||
(void)ctx;
|
||||
int n = meow_bg_count();
|
||||
if (n > max) n = max;
|
||||
for (int i = 0; i < n; i++) {
|
||||
/* 逐槽扫描并**压缩**(g_bg 可能有空洞),跳过已发出终止的条目:
|
||||
* meow_bg_terminate 只置 term_at_ms 发 SIGTERM,真正的 reap/摘表在 meow_bg_tick(launcher
|
||||
* 主循环),它在本页 meow_subpage_run 阻塞期间**不跑**——若不在此过滤,kill/清后旧行会一直
|
||||
* 残留(pid 已死/0 CPU),页面上看就是「点不动」。过滤后 kill/一键清当下即从列表消失(可见反馈)。 */
|
||||
int k = 0;
|
||||
for (int i = 0; i < MEOW_BG_MAX && k < max; i++) {
|
||||
const meow_bg_entry_t *e = meow_bg_at(i);
|
||||
if (!e || !e->used || !e->app_id[0]) continue;
|
||||
memset(&rows[i], 0, sizeof rows[i]);
|
||||
snprintf(rows[i].app_id, sizeof rows[i].app_id, "%s", e->app_id);
|
||||
rows[i].pid = e->pid;
|
||||
rows[i].keepalive = (e->keepalive ? 1 : 0);
|
||||
rows[i].on_card = (e->on_card ? 1 : 0);
|
||||
rows[i].bg_since_ms = e->bg_since_ms;
|
||||
if (e->term_at_ms) continue; /* 已进入终止序列(kill/宽限回收/卡拔)→ 行不再显示 */
|
||||
memset(&rows[k], 0, sizeof rows[k]);
|
||||
snprintf(rows[k].app_id, sizeof rows[k].app_id, "%s", e->app_id);
|
||||
rows[k].pid = e->pid;
|
||||
rows[k].keepalive = (e->keepalive ? 1 : 0);
|
||||
rows[k].on_card = (e->on_card ? 1 : 0);
|
||||
rows[k].bg_since_ms = e->bg_since_ms;
|
||||
k++;
|
||||
}
|
||||
return n;
|
||||
return k;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
for (int i = 0; i < b->n && r < max; i++) {
|
||||
char sub[80];
|
||||
snprintf(sub, sizeof sub, "CPU %.1f%% · %s · %s", b->rows[i].pcpu,
|
||||
/* 副文本写进 ctx 的持久缓冲(b->subs):sub_row_t.sub 会被 SDK 行池在整页会话内反复读取,
|
||||
* 指向本函数栈帧的 buffer 会在返回后变成乱码/越界(真机实证:副标题全是乱码字节)。 */
|
||||
snprintf(b->subs[i], sizeof b->subs[i], "CPU %.1f%% · %s · %s", b->rows[i].pcpu,
|
||||
bgmgr_mem_str(b->rows[i].rss_kb), bgmgr_status_str(&b->rows[i]));
|
||||
rbuf[r].kind = SUB_ROW;
|
||||
rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id;
|
||||
rbuf[r].sub = sub;
|
||||
rbuf[r].sub = b->subs[i];
|
||||
rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */
|
||||
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; /* 内容在卡内垂直居中(行中心=卡中心) */
|
||||
if (r->label) meow_ui_text(rx, ry, r->label, 20, mt->onSurface);
|
||||
if (r->sub) meow_ui_text(rx, ry + th + 4, r->sub, 16, mt->onSurfaceVariant);
|
||||
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化 */
|
||||
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化。
|
||||
* 不可杀行(受保护/前台/无进程,bgmgr_can_kill=0)→ 置灰禁点外观,避免误导(按钮可点但不可杀)。 */
|
||||
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);
|
||||
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,行体不响应/不误杀)---- */
|
||||
@@ -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);
|
||||
if (x < bx || x >= bx + bw || y < by || y >= by + bh) return 0; /* 行体/按钮外 → 不杀 */
|
||||
if (!bgmgr_can_kill(&b->rows[i])) {
|
||||
snprintf(b->toast, sizeof b->toast, "该 App 不可杀(框架/前台)");
|
||||
b->toast_ms = (long)now_ms();
|
||||
/* 禁点:受保护/前台/无进程 → 按钮已置灰,不 kill(返回 1=重绘)。
|
||||
* 留一行日志:「按了没反应」当年因为没有这行日志,只能靠翻 app.json 才查出是门控。 */
|
||||
logmsg("bgpage: kill 被门控 app=%s pid=%d fg=%d protected=%d",
|
||||
b->rows[i].app_id, (int)b->rows[i].pid, b->rows[i].is_fg,
|
||||
b->rows[i].protected_app ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
meow_bg_terminate(b->rows[i].app_id);
|
||||
snprintf(b->toast, sizeof b->toast, "已请求终止 %s", b->rows[i].app_id);
|
||||
b->toast_ms = (long)now_ms();
|
||||
int rc = meow_bg_terminate(b->rows[i].app_id); /* 置 term_at_ms → bgm_enumerate 下一跳行消失(可见反馈) */
|
||||
if (rc != 0)
|
||||
logmsg("bgpage: kill app=%s 未命中后台表 rc=%d(列表/后台表不同步)", b->rows[i].app_id, rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -270,11 +287,12 @@ static int bgm_top_action(void *ctx)
|
||||
memset(&ops, 0, sizeof ops);
|
||||
ops.ctx = b;
|
||||
ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */
|
||||
/* kill 只置 term_at_ms(真退/摘表在 launcher 主循环 bgm_tick),此处由 bgm_enumerate 过滤
|
||||
* term_at_ms 让清后的行当下消失(可见反馈;不可杀行本就在上/一键不杀)。 */
|
||||
int k = bgmgr_kill_all(&ops, b->rows, b->n, bgm_kill_fn);
|
||||
if (k > 0) snprintf(b->toast, sizeof b->toast, "已清 %d 个后台 App", k);
|
||||
else snprintf(b->toast, sizeof b->toast, "无后台 App 可清");
|
||||
b->toast_ms = (long)now_ms();
|
||||
return 1;
|
||||
if (k < b->n)
|
||||
logmsg("bgpage: 一键清除 杀 %d/%d(其余为前台/无进程/受保护)", k, b->n);
|
||||
return 1; /* 重绘:重扫行(已清行消失) */
|
||||
}
|
||||
|
||||
/* ---- 入口:运行后台管理页(阻塞到返回)---- */
|
||||
|
||||
@@ -17,8 +17,14 @@
|
||||
*
|
||||
* 【扫描节流(§七)】进页 nm_bt_scan_on、退页/超时 nm_bt_scan_off;7s 节流由 netmgrd 统一控。
|
||||
*
|
||||
* 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。开关写 network.bluetooth_enabled +
|
||||
* netmgrd on/off;连接/断开/配对经 netmgrd(netmgr_ipc.c nm_bt_* 封装)。
|
||||
* 【M47 开关可信化】顶栏开关的显示态只来自真实态查询(daemon `bt status` 的 powered,
|
||||
* 不可达时直连 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 <stdio.h>
|
||||
@@ -42,18 +48,40 @@ typedef struct {
|
||||
meow_net_t near[S_NET_MAX];
|
||||
bt_ep_t a2dp[S_NET_MAX];
|
||||
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 full; /* 1=本次 rows() 全量重扫(conn/paired/a2dp);0=仅附近(缓存)+角色(节流) */
|
||||
char mode[48]; /* bt_role 中文描述(次行摘要) */
|
||||
/* 行副文本池:sub_page_t 行 label/sub 为 const char*,跨 rows() 返回后仍须有效(直到下次
|
||||
* rows() 重填)。本地栈缓冲会悬垂(agent-396 根因同类),故格式化 sub 一律写进本池。 */
|
||||
char pool[512];
|
||||
char pool[768];
|
||||
int pool_off;
|
||||
} 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, ...)
|
||||
{
|
||||
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)
|
||||
{
|
||||
(void)ctx;
|
||||
int v = 0;
|
||||
if (meow_settings_get_bool("network.bluetooth_enabled", &v) != 0) v = 0;
|
||||
*on = v;
|
||||
bt_ctx_t *c = ctx;
|
||||
*on = bt_state_now(c, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 顶栏开关切换:写设置 + on/off(netmgrd IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */
|
||||
/* 顶栏开关切换:落盘意图 → 下发 → 有界等待真实态收敛 → 用真实态定稿/回滚 + 可见错误提示。
|
||||
* 绝不在下发后就直接把 UI 设成用户想要的样子(那是「显示意图」而不是「显示状态」)。 */
|
||||
static void bt_top_sw_set(void *ctx, int on)
|
||||
{
|
||||
(void)ctx;
|
||||
meow_settings_set_bool("network.bluetooth_enabled", on);
|
||||
nm_bt_onoff(on);
|
||||
g_sys.bt_on = on;
|
||||
bt_role_refresh(1);
|
||||
paint_sb_right();
|
||||
bt_ctx_t *c = ctx;
|
||||
unsigned long long t_click = now_ms(); /* 上限从点击起算(含下发耗时,见参数注释) */
|
||||
int want = on ? 1 : 0;
|
||||
int last_real = bt_state_now(c, 0);
|
||||
meow_settings_set_bool("network.bluetooth_enabled", want); /* 意图持久化 */
|
||||
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;
|
||||
int full = c->full; c->full = 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;
|
||||
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(全权威) */
|
||||
@@ -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].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) */
|
||||
{
|
||||
@@ -270,10 +405,14 @@ static int bt_act(void *ctx, int idx, int act)
|
||||
char mac[24] = "";
|
||||
/* 输出目标/编解码器:与本页设备行 act 段(CONN/PAIRED/NEAR base)不冲突,先处理 */
|
||||
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;
|
||||
meow_settings_set_bool("audio.output_bt", c->output_bt);
|
||||
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; /* 重绘 + 重填行 */
|
||||
}
|
||||
if (act == BT_ACT_CODEC) {
|
||||
|
||||
@@ -11,22 +11,76 @@
|
||||
# - launcher 与 SDK 共用同一 meow_font.o(已打进 meow_ui.a)——不再单独链 meow_font.a。
|
||||
# - MEOWOS_VERSION 0.7.21 → 0.7.23(M2.28)。
|
||||
#
|
||||
# Build(在 patch_v2/meow_launcherd 下):
|
||||
# zig-mipsel-cc -I../sdk -Os -static main_menu.c statusbar.c fb_compositor.c settings_page.c \
|
||||
# power_menu.c notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
|
||||
# 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
|
||||
# 用法(在 patch_v2/meow_launcherd 下或任意目录):
|
||||
# bash build_launcher.sh # 交叉编译 meow_launcherd(mipsel 静态)
|
||||
# bash build_launcher.sh test # 宿主 gcc 跑本目录 affected 单测(test_bt_page / test_netmgr_ipc)
|
||||
#
|
||||
# 工具链:优先环境变量 ZIGCC / MIPS_SYSROOT(M47 起),否则退回历史固定路径 $HOME/Project/R3ProII。
|
||||
# export ZIGCC=$HOME/.tools/zig-mipsel-cc
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
ROOT="$HOME/Project/R3ProII"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||
|
||||
CC="${ZIGCC:-$HOME/.tools/zig-mipsel-cc}"
|
||||
[ -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
|
||||
|
||||
# M52:多字体日志跃迁签名(纯头文件逻辑,无设备/无 launcher 依赖)
|
||||
echo "== host test: test_mf_logsig =="
|
||||
if "$HOSTCC" -I. -Wall -Wextra test_mf_logsig.c -o "$TMPD/test_mf_logsig"; then
|
||||
"$TMPD/test_mf_logsig" || rc=1
|
||||
else
|
||||
rc=1
|
||||
fi
|
||||
|
||||
# M65:子进程 stdout 有界读取(popen_tmo.h;launcherd/net.c 与 audiod 共用)
|
||||
# 含反向控制(旧循环在持续输出下永不返回 / 无输出上限 / static 缓冲串写)
|
||||
echo "== host test: test_popen_tmo =="
|
||||
if "$HOSTCC" -I. -Wall -Wextra test_popen_tmo.c -o "$TMPD/test_popen_tmo" -lpthread; then
|
||||
"$TMPD/test_popen_tmo" || 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; }
|
||||
"$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 \
|
||||
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 \
|
||||
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_bgmgr.c \
|
||||
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/* font_theme.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||||
#include "meow_launcher_internal.h"
|
||||
#include "mf_logsig.h" /* M52:多字体日志只在配置签名跃迁时打(纯函数,宿主单测) */
|
||||
|
||||
/* M52 r2:class 缺失 WARN 的跃迁位图是 32 位 unsigned(见 meow_font_reload_multifont)。
|
||||
* 类别数一旦超过 32,高位会被静默丢掉(= 该类的 WARN 不再打)——编译期钉死。 */
|
||||
_Static_assert(MEOW_FCLS_COUNT <= 32, "mf_missing_warn_mask assumes <= 32 font classes");
|
||||
|
||||
/* M2.6 字重结构 + M2.8 三级定案(FONT_SYSTEM):卡内字体 =
|
||||
* <卡根>/MeowOS/fonts/<family>/<weight>/(两级,向后兼容)或
|
||||
@@ -282,6 +287,10 @@ fallback:
|
||||
* meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效;
|
||||
* 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及
|
||||
* 设置页变更 display.multi_font / font.class_* / font.file。 */
|
||||
/* M52:上一次打过日志的配置签名(含 valid 位:签名本身可能恰好是 0,不能拿 0 当「没打过」)。 */
|
||||
static uint64_t mfont_log_sig;
|
||||
static int mfont_log_sig_valid;
|
||||
|
||||
void meow_font_reload_multifont(void)
|
||||
{
|
||||
static const char *cls_keys[MEOW_FCLS_COUNT] = {
|
||||
@@ -299,6 +308,7 @@ void meow_font_reload_multifont(void)
|
||||
static char cpng[MEOW_FCLS_COUNT][400], cidx[MEOW_FCLS_COUNT][400];
|
||||
meow_font_class_cfg cfg[MEOW_FCLS_COUNT];
|
||||
memset(cfg, 0, sizeof cfg);
|
||||
unsigned miss_now = 0; /* 本轮「配置了但缺失/不可读」的类别位图(M52 r2:见下方门控) */
|
||||
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
|
||||
char ref[MEOW_SET_MAX_LEN];
|
||||
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) == 0 && ref[0]) {
|
||||
@@ -312,13 +322,38 @@ void meow_font_reload_multifont(void)
|
||||
}
|
||||
cfg[c].png = cpng[c]; cfg[c].idx = cidx[c];
|
||||
} else {
|
||||
/* M2.58:类别字体配置了但缺失 → 该类别回退全局,并记系统 WARN。 */
|
||||
/* M2.58:类别字体配置了但缺失 → 该类别回退全局(回退行为不变)。
|
||||
* M52 r2(r1 评审 P2-1):这条 WARN 也在这条 2s 热路径上,旧实现每轮每类
|
||||
* 各打一条(最多 6 行/2s = 3 行/s,比刚修掉的 enabled 行凶 6 倍)。改成
|
||||
* 「只记位图、跃迁时才打」——判定在循环外统一做,日志文本与参数不变。 */
|
||||
miss_now |= 1u << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 新出现缺失的类别才打(缺失集合不变 → 静默;恢复后再缺失 → 会再打一次)。 */
|
||||
{
|
||||
static unsigned mfont_miss_logged;
|
||||
unsigned fresh = mf_missing_warn_mask(&mfont_miss_logged, miss_now);
|
||||
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
|
||||
if (!(fresh & (1u << c))) continue;
|
||||
char ref[MEOW_SET_MAX_LEN];
|
||||
/* 跃迁是稀有事件:这里重读一次 settings 复原原始相对路径(不占热路径) */
|
||||
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) != 0) ref[0] = 0;
|
||||
logmsg("multi_font: class %s font %s missing, fallback global", cls_keys[c], ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
int distinct = 0, truncated = 0;
|
||||
if (meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated) != 0)
|
||||
int rc = meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated);
|
||||
/* M52(日志刷屏):本函数被主循环 hotplug 轮询每 2s 调一次,配置绝大多数周期不变。
|
||||
* 旧实现无条件打 "multi_font: enabled..."(真机 6603 行日志里 5297 行 = 80%),
|
||||
* 现在只在「配置签名跃迁」时打 —— 首次、global 字体变、任一类字体变、on 变、
|
||||
* distinct/truncated 变。签名相同 = 行为相同 = 无新信息,静默。 */
|
||||
const char *csig[MEOW_FCLS_COUNT];
|
||||
for (int c = 0; c < MEOW_FCLS_COUNT; c++) csig[c] = cfg[c].png;
|
||||
uint64_t sig = mf_logsig(on, gpng, csig, MEOW_FCLS_COUNT, rc == 0 ? distinct : -1, truncated);
|
||||
/* 无变化(签名相同)= 行为相同 = 无新信息:静默,配置本身已生效。 */
|
||||
if (!mf_logsig_changed(&mfont_log_sig, &mfont_log_sig_valid, sig)) return;
|
||||
if (rc != 0)
|
||||
logmsg("multi_font: config failed -> single-font");
|
||||
else if (truncated)
|
||||
logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* 系统级音量键翻页门(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;
|
||||
}
|
||||
|
||||
/* M7.1 解锁恢复:锁屏是整帧不透明覆盖(lk_draw fill_rect 全屏),退出后底层界面不会自动重绘
|
||||
* —— 只有 App 自绘的控件区域刷新,未刷新区残留锁屏 PIN 最后一帧(解锁后整屏脏区根因)。此处
|
||||
* 在锁屏首扑前把底层帧(SDK back_buf = 进入前的 App 帧/主界面帧)快照下来;解锁(unlocked=1)
|
||||
* 时整帧回写 back_buf + present,强制底层界面完整重绘(app_launcher.c 的 lock-overlay 关闭路径
|
||||
* 随后 pm_capture_app_frame 读 fb0 即得到正确 App 帧,不再读到锁帧)。 */
|
||||
/* M7.1/M40 F5 解锁/熄屏恢复:锁屏是整帧不透明覆盖(lk_draw fill_rect 全屏),退出后底层界面
|
||||
* 不会自动重绘 —— 只有 App 自绘的控件区域刷新,未刷新区残留锁屏 PIN 最后一帧。
|
||||
* 此处把锁屏首扑前的底层帧(SDK back_buf = 进入前的 App 帧/主界面帧)快照下来;
|
||||
* **任何退出路径**(解锁 unlocked=1 或电源键熄屏 unlocked=0)都整帧回写 back_buf + present,
|
||||
* 保证 app_launcher.c 的 lock-overlay 关闭路径随后 pm_capture_app_frame 读 fb0 得到的是
|
||||
* 正确 App 帧,而不是 PIN 帧(后者会被下一次锁屏当成底层帧 → P5 残影链)。 */
|
||||
unsigned char *lk_bg_snap = NULL;
|
||||
size_t lk_bg_len = 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,锁态保持,仍在本循环)。
|
||||
* asleep:电源键按下边沿 / 触摸活动 → meow_panel_on + 重画锁屏页(本循环唤醒链回锁)。
|
||||
* 音量键本页不动(仅视为输入活动,重置计时)。 */
|
||||
@@ -429,9 +430,14 @@ int run_lock_screen(void)
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
/* 电源键=息屏(允许再睡;下次唤醒按 lck_wake_should_lock 再锁) */
|
||||
if (pwr_down) {
|
||||
logmsg("lock: power key -> screen off (stay locked)");
|
||||
/* 电源键=息屏(允许再睡;下次唤醒按 lck_wake_should_lock 再锁)。
|
||||
* M40 F4/P4:必须用**按下边沿**且进场 ≥300ms 才生效。旧实现用电平
|
||||
* (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();
|
||||
break; /* return 0 */
|
||||
}
|
||||
@@ -569,13 +575,17 @@ int run_lock_screen(void)
|
||||
done:
|
||||
meow_ui_activity_pop(); /* M7.1b:锁屏控件 owner 闸门关闭(与进场 push 对称) */
|
||||
g_lk_down_key = LK_DOWN_NONE;
|
||||
/* M7.1 解锁恢复:把锁屏首扑前快照的底层帧整帧回写 back_buf + present,强制底层 App/主界面
|
||||
* 完整重绘(消除「解锁后整屏 = PIN 最后一帧,仅 App 控件自绘区刷新」的脏区残留)。 */
|
||||
if (unlocked && lk_bg_snap && back_buf_ok && fb && lk_bg_len > 0) {
|
||||
/* M7.1/M40 F5:把锁屏首扑前快照的底层帧整帧回写 back_buf + present。
|
||||
* M40 起**不再限定 unlocked**:电源键熄屏退出(lr=0)也必须恢复,否则 fb0 会停留在
|
||||
* 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);
|
||||
meow_dirty_full();
|
||||
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 (tfd >= 0) {
|
||||
|
||||
@@ -16,11 +16,14 @@ int menu_opened_blank = 0;
|
||||
|
||||
|
||||
/* 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)
|
||||
{
|
||||
const char *reason = launch_fail_reason(); /* M6.40:具体失败原因(成功/未失败时为 "") */
|
||||
const char *bd = meow_dep_block_reason();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
* 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
|
||||
* flashes back to the player — Bug 4). We drain the queue immediately after
|
||||
* 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). */
|
||||
* flashes back to the player — Bug 4).
|
||||
*
|
||||
* 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;
|
||||
|
||||
/* 排空本进程触摸队列(非阻塞读到 EAGAIN)+ 复位手势状态机。
|
||||
* 队列/down 边沿都要清:只清队列而保留 t_down,会让「按下已被 drain 掉、抬手稍后才到」的
|
||||
* 这次手势被当成一次有效 tap(ghost touch 重放的另一半)。 */
|
||||
static void menu_drain_touch(void)
|
||||
{
|
||||
if (touch_fd >= 0) {
|
||||
unsigned char ev[16];
|
||||
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)
|
||||
@@ -73,18 +87,19 @@ static void menu_arm_touch(long ms)
|
||||
}
|
||||
|
||||
|
||||
/* Drain the touch queue + arm the post-app cooldown (call right after the
|
||||
* app exits, before the menu resumes polling touch). The drain always runs so
|
||||
* touches queued by the launch-attempt tail are cleared; the cooldown only
|
||||
* arms when an app actually ran (app_ran) so a failed launch doesn't lock the
|
||||
* menu out for 2.5s. */
|
||||
/* Drain the touch queue + arm the post-app guard (call right after the app
|
||||
* exits, before the menu resumes polling touch). The drain always runs so
|
||||
* touches queued by the launch-attempt tail are cleared; the guard only arms
|
||||
* when an app actually ran (app_ran) so a failed launch doesn't lock the menu
|
||||
* out. */
|
||||
static void menu_after_app_exit(int app_ran)
|
||||
{
|
||||
menu_drain_touch();
|
||||
/* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。
|
||||
* 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短,
|
||||
* 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */
|
||||
if (app_ran) menu_touch_cooldown_until = now_ms() + 1200;
|
||||
/* M39:M2.6 的 2500ms → M2.22 的 1200ms → 现在的 MENU_TOUCH_GUARD_MS(150ms)。
|
||||
* 依据:重放的来源是本进程 fd 里 App 运行期的事件,menu_drain_touch() 已一次性清空
|
||||
* (含 t_down/原点,见上),窗口只兜「面板重配后驱动补发的幽灵尾巴」。1200ms 会稳定吞掉
|
||||
* 用户「返回桌面后立即点下一个图标」——正是要修的症状。 */
|
||||
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;
|
||||
unsigned long long t0 = now_ms();
|
||||
int toast_ms = 0;
|
||||
char toast[64] = "";
|
||||
char toast[256] = ""; /* M6.40:失败 toast 带具体原因(CANNOT START: <name>(<reason>))需更大缓冲 */
|
||||
int want_exit = 0;
|
||||
/* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。
|
||||
* 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_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno));
|
||||
/* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */
|
||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||||
/* M39:菜单激活(启动)即 drain + 极短守卫(M2.6 原为 300ms;界面此时已整帧画完,
|
||||
* 窗口只会吞用户看到首页后的第一下,故收窄到 MENU_TOUCH_GUARD_MS) */
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||
|
||||
/* key polling */
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER);
|
||||
kp_open(&kp_keys, DEV_KEYS);
|
||||
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */
|
||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||||
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动。
|
||||
* 音量键防抖/基线已上收 key_router(进程唯一);本页只保留播放/电源键自家防抖。 */
|
||||
struct kd_state kd_play, 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",
|
||||
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
|
||||
* edges; kp.prev cannot be used for this because kp_poll() syncs it
|
||||
* 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++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
@@ -1333,7 +1348,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
||||
sim_exit_tap_done = 1;
|
||||
if (meow_mode) {
|
||||
int rb = confirm_reboot_player();
|
||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||
dirty = 1;
|
||||
logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb);
|
||||
} 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
|
||||
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch
|
||||
* 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_keys, NULL, NULL);
|
||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||||
int play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
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下")。
|
||||
* 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
|
||||
(void)kd_tick(&kd_vold, vold);
|
||||
(void)kd_tick(&kd_volu, volu);
|
||||
* 稳定态 play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
|
||||
(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;
|
||||
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
|
||||
* 单击(短按松开 <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_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */
|
||||
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_* 防抖状态冻结。
|
||||
* 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」,
|
||||
* 而 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
|
||||
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
|
||||
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
continue;
|
||||
}
|
||||
@@ -1527,15 +1534,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
||||
if (lck_wake_should_lock()) {
|
||||
int lr = run_lock_screen();
|
||||
/* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
if (lr == 1)
|
||||
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),
|
||||
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
|
||||
if (meow_mode) {
|
||||
if (vold && !m_prev_vold) {
|
||||
if (kev.vol_dn_edge) {
|
||||
any_input = 1;
|
||||
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
|
||||
logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
|
||||
} else if (volu && !m_prev_volu) {
|
||||
} else if (kev.vol_up_edge) {
|
||||
any_input = 1;
|
||||
settings_volume_adj(+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 {
|
||||
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
|
||||
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 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;
|
||||
cursor++;
|
||||
if (cursor > napps) cursor = 0; /* napps=设置行 */
|
||||
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;
|
||||
cursor--;
|
||||
if (cursor < 0) cursor = napps; /* 回绕到设置行 */
|
||||
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) {
|
||||
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);
|
||||
g_sys.vol = settings_volume_get();
|
||||
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)");
|
||||
}
|
||||
}
|
||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play;
|
||||
m_prev_play = play;
|
||||
|
||||
/* touch (event reads) */
|
||||
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)",
|
||||
t_last_y - loy);
|
||||
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;
|
||||
memset(&click, 0, sizeof click);
|
||||
click.app_idx = -1;
|
||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||
run_notif_center(apps, napps, &click, 1, t_down_x, loy);
|
||||
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
last_input_ms = now_ms();
|
||||
menu_arm_touch(800);
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||
dirty = 1;
|
||||
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
|
||||
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,与左缘互斥。 */
|
||||
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)");
|
||||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||
notif_click_t click;
|
||||
memset(&click, 0, sizeof click);
|
||||
click.app_idx = -1;
|
||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||
run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
|
||||
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
last_input_ms = now_ms();
|
||||
menu_arm_touch(800);
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||
dirty = 1;
|
||||
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
|
||||
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 &&
|
||||
tx >= MEOW_EDGE_W && ty >= SB_BAR_H) {
|
||||
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_menu 误判 */
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
last_input_ms = now_ms();
|
||||
menu_arm_touch(800);
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||
dirty = 1;
|
||||
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)) {
|
||||
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);
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
last_input_ms = now_ms();
|
||||
menu_arm_touch(800);
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS);
|
||||
dirty = 1;
|
||||
continue;
|
||||
}
|
||||
@@ -1878,7 +1876,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
||||
* 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */
|
||||
logmsg("meow mode: BACK TO PLAYER -> confirm reboot");
|
||||
int rb = confirm_reboot_player();
|
||||
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */
|
||||
dirty = 1;
|
||||
if (rb) { logmsg("reboot-to-player: yes, rebooting"); }
|
||||
/* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */
|
||||
@@ -1893,20 +1891,18 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
||||
* 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */
|
||||
else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) {
|
||||
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();
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
last_input_ms = now_ms(); /* 回主界面即视为输入 */
|
||||
menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */
|
||||
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫(原设置页返回 800ms) */
|
||||
dirty = 1; /* 全帧重画应用列表 */
|
||||
}
|
||||
/* 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 */
|
||||
if (lck_wake_should_lock()) {
|
||||
int lr = run_lock_screen();
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
if (lr == 1)
|
||||
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-power-pick N] [--sim-no-exec]\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"
|
||||
" [--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
|
||||
* menu open so edits apply without a daemon restart) */
|
||||
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。
|
||||
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次(verify=0)。 */
|
||||
@@ -2258,10 +2253,12 @@ int main(int argc, char **argv)
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
/* 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;
|
||||
for (int i = 0; i < n; i++) {
|
||||
bgmgr_row_t *r = &rows[i];
|
||||
r->framework = 0;
|
||||
r->protected_app = 0;
|
||||
r->is_fg = 0;
|
||||
r->name[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;
|
||||
ops->app_meta(ops->ctx, r->app_id, r->pid,
|
||||
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 (ops->proc_sample && ops->proc_sample(ops->ctx, r->pid, &r->pcpu, &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)
|
||||
{
|
||||
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->pid <= 0) return 0; /* 非后台/无进程 */
|
||||
return 1;
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法:
|
||||
* 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。
|
||||
*/
|
||||
#ifndef MEOW_BGMGR_H
|
||||
@@ -23,7 +26,9 @@ typedef struct bgmgr_row {
|
||||
char app_id[MEOW_APP_ID_MAX];
|
||||
char name[49]; /* display name(回退 app_id) */
|
||||
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 后台进程 */
|
||||
int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */
|
||||
int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */
|
||||
@@ -39,10 +44,10 @@ typedef struct bgmgr_ops {
|
||||
/* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms,
|
||||
* 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */
|
||||
int (*enumerate)(void *ctx, bgmgr_row_t *rows, int max);
|
||||
/* 按 app_id 补 App 元数据:name/icon/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,
|
||||
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)。 */
|
||||
int (*proc_sample)(void *ctx, pid_t pid, double *pcpu, long *rss_kb);
|
||||
void (*log)(const char *fmt, ...); /* 可空=静默 */
|
||||
@@ -53,11 +58,11 @@ typedef struct bgmgr_ops {
|
||||
* 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */
|
||||
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);
|
||||
|
||||
/* 一键清:遍历 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,
|
||||
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→摘表),
|
||||
* 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。 */
|
||||
* 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。
|
||||
* 调用者:bg_page.c 行内「杀」/「一键清除」+ util.c MSG_AMS_KILL(`am kill`,无前台会话时)
|
||||
* ——两个入口同一通道,故页面按钮与面板/CLI kill 行为对称。 */
|
||||
int meow_bg_terminate(const char *app_id)
|
||||
{
|
||||
if (!app_id || !app_id[0]) return MEOW_ERR_INVAL;
|
||||
@@ -281,7 +283,8 @@ int meow_bg_terminate(const char *app_id)
|
||||
meow_bg_entry_t *e = &g_bg[i];
|
||||
if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) {
|
||||
if (e->term_at_ms == 0) {
|
||||
bg_log("[bgplay] 后台管理页终止后台App: %s pid=%d", app_id, (int)e->pid);
|
||||
bg_log("[bgplay] 用户终止后台App: %s pid=%d(后台管理页/am kill 同通道)",
|
||||
app_id, (int)e->pid);
|
||||
kill(e->pid, SIGTERM);
|
||||
e->term_at_ms = bg_now_ms();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
/* 该 App 是否在后台保活。 */
|
||||
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→摘表。
|
||||
* 返回 0=已受理(在后台表且 pid>0);MEOW_ERR_NOENT=不在后台/无进程;MEOW_ERR_INVAL=app_id 空。 */
|
||||
int meow_bg_terminate(const char *app_id);
|
||||
|
||||
@@ -82,10 +82,19 @@ enum {
|
||||
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
||||
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
||||
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
|
||||
/* ---- 系统翻页信号(READER_UI_DESIGN §E,用户 2026-09-04 拍板):0x0080-0x0081 ----
|
||||
* 语义:launcher 按键层(meow_launcherd/keys.c)在「当前台 App 为其 app.json 声明
|
||||
* volume_page 的前台应用」时,把物理 VOL_UP/VOL_DOWN 转换成系统翻页信号下发(否则照旧
|
||||
* 调音量——音量基本行为/插孔消抖/音量反转绝不破坏)。App 侧(如 reader_app/app/reader_ipc.c)
|
||||
* 持持久连接收本消息 → 上一页/下一页。方向由消息号本身决定(UP=上一页,DOWN=下一页);
|
||||
* 无负载(len=0)。属系统级能力,未来其它 App 可声明复用。 */
|
||||
MSG_KEY_PAGEUP = 0x0080, /* ↓ 系统级:上一页(VOL_UP 语义,仅前台声明 app 收) */
|
||||
MSG_KEY_PAGEDOWN = 0x0081, /* ↓ 系统级:下一页(VOL_DOWN 语义,仅前台声明 app 收) */
|
||||
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
||||
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
||||
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
|
||||
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义) */
|
||||
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义):后台保活表命中→就地真退序列;
|
||||
* 否则有前台会话→置 pending 交会话消费;都没有→ACK -2(MEOW_ERR_NOENT) */
|
||||
/* ---- audiod 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md §2):0x00C0-0x00CA ----
|
||||
* 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。
|
||||
* 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */
|
||||
@@ -455,7 +464,9 @@ typedef struct m_ams_start {
|
||||
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
|
||||
} m_ams_start_t; /* 160 B */
|
||||
|
||||
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。 */
|
||||
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。
|
||||
* 后台保活 App:launcher 就地走 meow_bg_terminate 真退序列(与后台管理页「杀」同一通道);
|
||||
* 前台 App:置 pending 交前台会话消费;两者皆非 → ACK code=(uint32_t)MEOW_ERR_NOENT(-2)。 */
|
||||
typedef struct m_ams_kill {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
} m_ams_kill_t; /* 32 B */
|
||||
|
||||
@@ -28,6 +28,7 @@ extern "C" {
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
|
||||
#include <pthread.h> /* M0:IPC 线程 */
|
||||
#include "key_router.h" /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1) */
|
||||
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
|
||||
#include <sys/socket.h> /* M0:AF_UNIX IPC */
|
||||
#include <sys/un.h> /* M0:sockaddr_un */
|
||||
@@ -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 NCP_DRAG_ENTER_SLOP 16
|
||||
#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_R 50 /* 圆钮半径(直径 100) */
|
||||
#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"
|
||||
/* M2.71-bug5:App 前台触摸/交互 → SDK 写该标记;launcher 前台 app 循环消费(unlink + 重置息屏计时) */
|
||||
#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,拆分后统一放置)---- */
|
||||
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),
|
||||
void *ctx);
|
||||
int launch_app(struct app *a);
|
||||
/* M6.40:最近一次启动失败的具体原因(fork OOM / exec 失败 / 缺依赖 / 路径不存在等;
|
||||
* launch 成功或未失败则 "")。launch_app/launch_activity 返回 -1 前由 app_launcher.c 填好,
|
||||
* main_menu.c 的 launch_fail_toast 据以显示「CANNOT START: %s(%s)」。 */
|
||||
const char *launch_fail_reason(void);
|
||||
/* ---- app.json 二进制依赖(depends)解析与启动前检查(meow_dep.c)---- */
|
||||
int meow_parse_depends(const char *val, struct meow_dep *deps, int max);
|
||||
int meow_check_app_depends(const struct app *a, char *missing, int mlen);
|
||||
@@ -756,6 +769,15 @@ int meow_ipc_conn_alive(int fd);
|
||||
/* M4:ipc_send 公开包装(meow_bgplay send_frame 回调用;同 meow_media 的注入口径)。 */
|
||||
size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id,
|
||||
const void *payload, uint32_t len);
|
||||
/* ---- E:系统级音量翻页(READER_UI_DESIGN §E,用户 2026-09-04 拍板)----
|
||||
* 前台 App 声明 app.json `volume_page` 时,launcher 把物理 VOL_UP/DOWN 转成系统翻页信号
|
||||
* (MSG_KEY_PAGEUP/PAGEDOWN,meow_ipc.h)经 IPC 下发给该 App;否则照旧调音量。 */
|
||||
int meow_volpage_app_allows(const char *app_id); /* keys.c:读 app.json 声明 */
|
||||
void meow_volpage_sink(const char *app_id, int fd); /* keys.c:武装前台翻页 sink */
|
||||
void meow_volpage_sink_clear(void); /* keys.c:撤武装 */
|
||||
int meow_volpage_armed(void); /* keys.c:查询 */
|
||||
int meow_volpage_key(int code, int is_down); /* keys.c:门(1=已消费/已下发) */
|
||||
int meow_ipc_app_fd(const char *app_id); /* util.c:前台 App→持久 IPC fd(-1=无) */
|
||||
/* §10.5:读并清除待处理的 MSG_NOTIF_OPEN(App 前台顶部下滑请求展开通知中心叠加会话)。
|
||||
* 调用方=launch_app waitpid 循环;返回 1 并填 *out(待处理),0=无。 */
|
||||
int meow_notif_open_take(m_notif_open_t *out);
|
||||
@@ -765,7 +787,23 @@ void meow_panel_off(void);
|
||||
void meow_panel_on(void);
|
||||
void meow_parse_app_json(FILE *f, struct app *a);
|
||||
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_refresh(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);
|
||||
void nm_wifi_scan_trigger(void);
|
||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */
|
||||
/* WiFi 设置页 bug2(2026-09-15):hidden=1 走 netmgrd connecth(scan_ssid=1 + 更长 COMPLETED 等待),
|
||||
* 回退直连同步对齐;err 回填 netmgrd/直连的诚实结果文本(成功含 ip / 失败含原因)。 */
|
||||
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen);
|
||||
int nm_wifi_disconnect(char *err, int elen);
|
||||
int nm_wifi_toggle(int on);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
/* mf_logsig.h — 多字体配置的「日志跃迁签名」(纯函数,宿主可单测)。
|
||||
*
|
||||
* 为什么需要它(M52):meow_font_reload_multifont() 由 launcher 主循环的 hotplug 轮询
|
||||
* 每 2s 调一次(main_menu.c),而多字体配置在绝大多数周期里毫无变化。旧实现无条件打
|
||||
* `multi_font: enabled, %d distinct fonts (global=%s)`,真机 meow_20261111_154540.log
|
||||
* 6603 行里 5297 行是它(80%,~291KB/406KB),把事故现场唯一的信号淹没了。
|
||||
*
|
||||
* 修法不是删日志(首次启用/用户改字体必须留痕),而是把它绑到**配置签名**上:签名变了
|
||||
* 才打。签名覆盖所有能改变多字体行为的输入 ——
|
||||
* on(display.multi_font)、全局字体 png(font.file → 生效档)、6 个 font.class_* 的
|
||||
* 解析后绝对路径、distinct(实际驻留套数)、truncated(超预算截断)。
|
||||
* 因此「字体集合或 global font 变化」必然换签名,纯粹的时间流逝不会。
|
||||
*/
|
||||
#ifndef MF_LOGSIG_H
|
||||
#define MF_LOGSIG_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define MF_LOGSIG_SEED 14695981039346656037ULL /* FNV-1a 64 offset basis */
|
||||
#define MF_LOGSIG_PRIME 1099511628211ULL
|
||||
|
||||
/* 把一段字符串折进签名。字段之间补分隔符,防 ("ab","c") 与 ("a","bc") 撞同一值。
|
||||
* NULL 与 "" 视为同一(配置项为空 = 该类别用全局字体)。 */
|
||||
static inline uint64_t mf_logsig_mix(uint64_t h, const char *s)
|
||||
{
|
||||
if (!s) s = "";
|
||||
while (*s) {
|
||||
h ^= (uint64_t)(unsigned char)*s++;
|
||||
h *= MF_LOGSIG_PRIME;
|
||||
}
|
||||
h ^= 0x1f; /* 字段分隔(0x1f = US,不会出现在路径里) */
|
||||
h *= MF_LOGSIG_PRIME;
|
||||
return h;
|
||||
}
|
||||
|
||||
/* 多字体配置签名。ncls = 类别数(MEOW_FCLS_COUNT=6),cpng[c] 可为 NULL。
|
||||
* distinct < 0 表示 config 失败(此时 distinct 无意义,用 -1 编码「失败」)。 */
|
||||
static inline uint64_t mf_logsig(int on, const char *gpng,
|
||||
const char *const *cpng, int ncls,
|
||||
int distinct, int truncated)
|
||||
{
|
||||
char num[24];
|
||||
uint64_t h = MF_LOGSIG_SEED;
|
||||
snprintf(num, sizeof num, "%d", on ? 1 : 0);
|
||||
h = mf_logsig_mix(h, num);
|
||||
h = mf_logsig_mix(h, gpng);
|
||||
for (int c = 0; c < ncls; c++)
|
||||
h = mf_logsig_mix(h, (cpng && cpng[c]) ? cpng[c] : "");
|
||||
snprintf(num, sizeof num, "%d:%d", distinct, truncated ? 1 : 0);
|
||||
h = mf_logsig_mix(h, num);
|
||||
return h;
|
||||
}
|
||||
|
||||
/* 日志门控:首次(*valid=0)或签名变化 → 返回 1 并记下;同一签名重复调用 → 0。
|
||||
* 调用方持有 *last / *valid(文件级静态),因此这就是「只在跃迁时打日志」的全部判定。 */
|
||||
static inline int mf_logsig_changed(uint64_t *last, int *valid, uint64_t sig)
|
||||
{
|
||||
if (*valid && sig == *last) return 0;
|
||||
*last = sig;
|
||||
*valid = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 「class 字体缺失」WARN 的跃迁门控(r1 评审 P2-1)。
|
||||
* mask = 本轮「配置了但缺失/不可读」的类别位图(bit c = MEOW_FCLS_* 第 c 类)。
|
||||
* 返回 mask 里**上一次没报过**的那些位(本轮该打 WARN 的类别),并把 *logged 更新为 mask。
|
||||
* 于是:缺失集合不变 → 返回 0(静默);新类别变缺失 → 只返回新增位;
|
||||
* 某类恢复后再次缺失 → 会重新返回(不是一次性闩锁)。 */
|
||||
static inline unsigned mf_missing_warn_mask(unsigned *logged, unsigned mask)
|
||||
{
|
||||
unsigned fresh = mask & ~(*logged);
|
||||
*logged = mask;
|
||||
return fresh;
|
||||
}
|
||||
|
||||
#endif /* MF_LOGSIG_H */
|
||||
+168
-36
@@ -1,6 +1,7 @@
|
||||
/* net.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||||
#include "meow_launcher_internal.h"
|
||||
#include "popen_tmo.h" /* M65 P1-1:有界读子进程 stdout(deadline 在循环顶部 + 256KB 上限) */
|
||||
|
||||
|
||||
/* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */
|
||||
@@ -76,7 +77,16 @@ int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 p
|
||||
* 避免绘制热点反复 spawn。 */
|
||||
|
||||
/* 带超时跑一条命令并读 stdout(防 wpa_supplicant/BlueZ 未跑时命令挂起)。
|
||||
* 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时被 SIGKILL。 */
|
||||
* 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时/超限被 SIGKILL。
|
||||
*
|
||||
* M65 P1-1(M59 审计:'收益最大、改动最小'的一条):读循环搬进 popen_tmo.h,判据是
|
||||
* ① deadline 在**循环顶部**判 —— 旧实现只在 poll 超时分支里判,而持续输出的子进程
|
||||
* 让 poll 每次立刻返回,deadline 永远轮不到(审计 `yes` 复现:300ms 超时下 8s 不返回);
|
||||
* ② 输出**硬上限 256KB**,超限按失败返回 NULL(杀掉子进程),**不静默截断** ——
|
||||
* 静默截断会让上层以为拿到了完整输出;
|
||||
* ③ 收尸有界(300ms + SIGKILL + 300ms):旧实现 SIGKILL 之后是无条件 waitpid(…, 0),
|
||||
* 子进程若卡在 D 态照样把 UI 线程永久钉住。
|
||||
* 缓冲仍是本函数 malloc、调用方 free(调用面不变,14 个调用点零改动)。 */
|
||||
char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||||
{
|
||||
int fds[2];
|
||||
@@ -96,38 +106,147 @@ char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
char *out = NULL; size_t cap = 0, len = 0;
|
||||
char tmp[256];
|
||||
int done = 0;
|
||||
unsigned long long deadline = now_ms() + tmo_ms;
|
||||
while (!done) {
|
||||
struct pollfd pfd = { fd, POLLIN, 0 };
|
||||
int pr = poll(&pfd, 1, 25);
|
||||
if (pr < 0) break;
|
||||
if (pr == 0) { if (now_ms() > deadline) break; continue; }
|
||||
ssize_t r = read(fd, tmp, sizeof tmp);
|
||||
if (r > 0) {
|
||||
if (len + (size_t)r + 1 > cap) {
|
||||
size_t nc = cap ? cap * 2 : 512;
|
||||
char *nn = realloc(out, nc);
|
||||
if (!nn) { free(out); out = NULL; break; }
|
||||
out = nn; cap = nc;
|
||||
}
|
||||
memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0;
|
||||
} else if (r == 0) {
|
||||
done = 1;
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
/* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时)则 SIGKILL,防僵尸。 */
|
||||
int status = 0;
|
||||
pid_t rp = waitpid(pid, &status, WNOHANG);
|
||||
if (rp == 0) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
|
||||
int rc = popen_tmo_read_all(fd, tmo_ms, &out, &cap, POPEN_TMO_MAX, &len, 1);
|
||||
close(fd);
|
||||
if (rc == POPEN_TMO_OVERFLOW) {
|
||||
/* 超上限 = 子进程在无限吐(或输出异常大):按失败处理,绝不返回半截当完整。 */
|
||||
kill(pid, SIGKILL);
|
||||
free(out);
|
||||
out = NULL;
|
||||
}
|
||||
/* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时/超限)则 SIGKILL,防僵尸。 */
|
||||
if (waitpid(pid, NULL, WNOHANG) == 0) {
|
||||
kill(pid, SIGKILL);
|
||||
if (!popen_tmo_reap_bounded(pid))
|
||||
fprintf(stderr, "[net] popen: child %ld unkillable (D-state) — abandon reap\n", (long)pid);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
/* ===== 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/内存受限)。
|
||||
* M2.40:wpa_cli 对非 ASCII SSID 输出 "\xNN" 转义文字(`\`+x+两位hex 的纯文字,非真字节),
|
||||
* 需先还原成真实 UTF-8 字节才能进界面文本。 */
|
||||
@@ -733,7 +852,11 @@ int s_net_get(const srow_t *r, char *buf, int len)
|
||||
|
||||
|
||||
/* 网络动作开关: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)
|
||||
{
|
||||
if (on) system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
|
||||
@@ -742,24 +865,33 @@ void s_net_toggle_wifi(int on)
|
||||
|
||||
void s_net_toggle_bt(int on)
|
||||
{
|
||||
if (on) system("hciconfig hci0 up 2>/dev/null");
|
||||
else system("hciconfig hci0 down 2>/dev/null");
|
||||
if (on) {
|
||||
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 同步为真实硬件态,
|
||||
* 让开关行反映 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)
|
||||
{
|
||||
int cur = 0;
|
||||
if (meow_settings_get_bool("network.wifi_enabled", &cur) == 0) {
|
||||
int real = wlan_connected() ? 1 : 0;
|
||||
if (real != cur) meow_settings_set_bool("network.wifi_enabled", real);
|
||||
int real = net_wifi_power_probe();
|
||||
if (real >= 0 && real != cur) meow_settings_set_bool("network.wifi_enabled", real);
|
||||
}
|
||||
if (meow_settings_get_bool("network.bluetooth_enabled", &cur) == 0) {
|
||||
int real = hci_powered() ? 1 : 0;
|
||||
if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
|
||||
int real = net_bt_power_probe();
|
||||
if (real >= 0 && real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
|
||||
}
|
||||
/* 只同步开关(快)+ 异步 kick;状态/列表由 run_settings_page 主循环 pump,绝不阻塞入口。 */
|
||||
s_net_refresh_kick();
|
||||
|
||||
@@ -11,10 +11,26 @@
|
||||
* 只读观测(wlan_connected()/hci_powered() 读 /sys 链路态)仍在 net.c;此处只接权威操作。
|
||||
*/
|
||||
#include "meow_launcher_internal.h"
|
||||
/* M64 P1-1:SSID/PSK 校验与 wpa 字面量转义。与 netmgrd 共用同一头文件——回退路径只修一处
|
||||
* 等于没修(netmgrd 不可达时攻击者正是走这里),两边必须同一套规则。 */
|
||||
#include "../meow_netmgrd/nm_wifi_args.h"
|
||||
|
||||
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
|
||||
/* 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 防热循环。 */
|
||||
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
|
||||
static time_t g_nm_ok_ts = 0;
|
||||
@@ -50,7 +66,7 @@ static int nm_r_all(int fd, uint8_t *b, size_t n)
|
||||
return 0;
|
||||
}
|
||||
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
|
||||
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype)
|
||||
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype, int tmo_ms)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
|
||||
@@ -60,7 +76,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 (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
|
||||
struct pollfd pf = { fd, POLLIN, 0 };
|
||||
int pr = poll(&pf, 1, 1500);
|
||||
int pr = poll(&pf, 1, tmo_ms);
|
||||
if (pr <= 0) return -1;
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
|
||||
@@ -81,15 +97,21 @@ static int nm_hello(int fd)
|
||||
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
|
||||
h.version = 1;
|
||||
uint8_t rbuf[256]; uint16_t rtype = 0;
|
||||
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype) < 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;
|
||||
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
|
||||
return (ak.code == MEOW_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。 */
|
||||
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
|
||||
char *text, int tlen)
|
||||
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。
|
||||
* tmo_ms:读超时(connect/connecth/scan 需覆盖 netmgrd 同步等待,见 NM_TMO_*)。
|
||||
* 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;
|
||||
memset(&q, 0, sizeof q);
|
||||
@@ -100,8 +122,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; }
|
||||
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
|
||||
memset(rbuf, 0, sizeof rbuf);
|
||||
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype) < 0 || rtype != res_type) {
|
||||
close(fd); nm_cache(0); return NETMGR_UNREACH;
|
||||
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype, tmo_ms) < 0 || rtype != res_type) {
|
||||
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);
|
||||
close(fd);
|
||||
@@ -112,11 +137,20 @@ static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const c
|
||||
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)
|
||||
{ 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)
|
||||
{ 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)
|
||||
{
|
||||
@@ -361,7 +395,7 @@ int nm_wifi_saved(meow_net_t *out, int max)
|
||||
int nm_wifi_scan(meow_net_t *out, int max)
|
||||
{
|
||||
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 != MEOW_OK) return 0;
|
||||
return nm_parse_scan(text, out, max);
|
||||
@@ -374,49 +408,232 @@ void nm_wifi_scan_trigger(void)
|
||||
if (!netmgr_ok()) net_wifi_scan_trigger();
|
||||
}
|
||||
|
||||
/* —— 直连回退:wpa_cli add/set/enable/select 新网络(复刻 netmgrd wifi_connect 逻辑)—— */
|
||||
static int nm_direct_connect(const char *ssid, const char *psk, char *err, int elen)
|
||||
/* —— 直连回退:与 netmgrd wifi_connect/connecth 对齐(bug1/bug2/bug4)。
|
||||
* 匹配既有(\xNN 还原同表示)→ 新建或(命中且带新 psk)更新凭据 → hidden 补 scan_ssid=1 →
|
||||
* enable+select → 有界等待 wpa_state=COMPLETED → 成功 enable_network all + save_config(校验重试);
|
||||
* 失败时本次新建的网络回收,报文明示原因。仅在 netmgrd 不可达时走此路径。 */
|
||||
static const char NM_WPA_PREFIX[] = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
|
||||
|
||||
static int nm_direct_wpa(char *out, int cap, const char *args, int tmo)
|
||||
{
|
||||
const char *wpa = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof cmd, "%s add_network 2>/dev/null", wpa);
|
||||
char *o = popen_read_tmo(1200, cmd);
|
||||
int id = o ? atoi(o) : -1;
|
||||
if (o) free(o);
|
||||
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
|
||||
char cmd[384];
|
||||
snprintf(cmd, sizeof cmd, "%s %s 2>/dev/null", NM_WPA_PREFIX, args);
|
||||
char *o = popen_read_tmo(tmo, cmd);
|
||||
if (!o) { out[0] = 0; return -1; }
|
||||
int l = (int)strlen(o);
|
||||
if (l >= cap) l = cap - 1;
|
||||
memcpy(out, o, (size_t)l); out[l] = 0;
|
||||
free(o);
|
||||
return l;
|
||||
}
|
||||
|
||||
/* ---- M64 P1-1:外部可控字符串(SSID/PSK)只走这一条,execv 传 argv,不经过 shell ----
|
||||
* 修复前:nm_direct_wpa("set_network %d ssid '\"%s\"'", id, ssid) → popen(sh -c)。
|
||||
* 恶意 AP 广播 SSID=`a' ; id ; '` 即可 root 执行。现在引号/分号/反引号只是 argv 里的字节。 */
|
||||
static int nm_wpa_bin(char *out, size_t cap)
|
||||
{
|
||||
char *r;
|
||||
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]) {
|
||||
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);
|
||||
static const char *dirs[] = { "/sbin", "/usr/sbin", "/bin", "/usr/bin", NULL };
|
||||
for (int i = 0; dirs[i]; i++) {
|
||||
snprintf(out, cap, "%s/wpa_cli", dirs[i]);
|
||||
if (access(out, X_OK) == 0) return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static long nm_mono_ms(void)
|
||||
{
|
||||
struct timespec tp;
|
||||
clock_gettime(CLOCK_MONOTONIC, &tp);
|
||||
return tp.tv_sec * 1000L + tp.tv_nsec / 1000000L;
|
||||
}
|
||||
|
||||
/* 执行 `wpa_cli -i wlan0 <args...>`(execv,无 shell)。args 以 NULL 结尾。
|
||||
* 返回 stdout 字节数(-1=起不来),tmo 内有界收割,语义对齐 nm_direct_wpa。 */
|
||||
static int nm_direct_wpa_exec(char *out, int cap, char *const *args, int tmo)
|
||||
{
|
||||
if (cap > 0) out[0] = 0;
|
||||
char bin[128];
|
||||
if (nm_wpa_bin(bin, sizeof bin) < 0) return -1;
|
||||
char *argv[10];
|
||||
int n = 0;
|
||||
argv[n++] = bin;
|
||||
argv[n++] = "-i";
|
||||
argv[n++] = "wlan0";
|
||||
for (int i = 0; args[i] && n < (int)(sizeof argv / sizeof argv[0]) - 1; i++) argv[n++] = args[i];
|
||||
argv[n] = NULL;
|
||||
|
||||
int pfd[2];
|
||||
if (pipe(pfd) < 0) return -1;
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { close(pfd[0]); close(pfd[1]); return -1; }
|
||||
if (pid == 0) {
|
||||
dup2(pfd[1], 1);
|
||||
int dn = open("/dev/null", O_WRONLY);
|
||||
if (dn >= 0) { dup2(dn, 2); close(dn); }
|
||||
close(pfd[0]); close(pfd[1]);
|
||||
execv(bin, argv);
|
||||
_exit(127);
|
||||
}
|
||||
close(pfd[1]);
|
||||
int off = 0, timed = 0;
|
||||
long deadline = nm_mono_ms() + (tmo > 0 ? tmo : 1);
|
||||
while (off < cap - 1) {
|
||||
long remain = deadline - nm_mono_ms();
|
||||
if (remain <= 0) { timed = 1; break; }
|
||||
struct pollfd pf = { pfd[0], POLLIN, 0 };
|
||||
int pr = poll(&pf, 1, (int)remain);
|
||||
if (pr < 0) { if (errno == EINTR) continue; timed = 1; break; }
|
||||
if (pr == 0) { timed = 1; break; }
|
||||
ssize_t rd = read(pfd[0], out + off, (size_t)(cap - 1 - off));
|
||||
if (rd <= 0) break;
|
||||
off += (int)rd;
|
||||
}
|
||||
out[off] = 0;
|
||||
close(pfd[0]);
|
||||
if (timed) kill(pid, SIGKILL);
|
||||
int st = 0;
|
||||
waitpid(pid, &st, 0);
|
||||
return off;
|
||||
}
|
||||
|
||||
/* set_network <id> <field> <字符串值>:值按 wpa_supplicant 语法加引号(转义 `"`/`\`),
|
||||
* 整条命令以 argv 传入。value 必须先过 nm_wifi_ssid_ok()/nm_wifi_psk_ok()。 */
|
||||
static int nm_direct_wpa_set(char *out, int cap, int id, const char *field, const char *val, int tmo)
|
||||
{
|
||||
char idbuf[16], quoted[128];
|
||||
char *args[5];
|
||||
if (nm_wpa_set_argv(idbuf, sizeof idbuf, quoted, sizeof quoted, id, field, val, args) < 0) {
|
||||
if (cap > 0) out[0] = 0;
|
||||
return -1;
|
||||
}
|
||||
return nm_direct_wpa_exec(out, cap, args, tmo);
|
||||
}
|
||||
|
||||
static int nm_direct_save_config(void)
|
||||
{
|
||||
char out[128];
|
||||
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
|
||||
nm_direct_wpa(out, sizeof out, "set update_config 1", 1500);
|
||||
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int nm_direct_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
|
||||
{
|
||||
char out[1024]; char args[256];
|
||||
/* M64 P1-1:回退路径自己再校验一次(不依赖调用方;两条路径同一套规则)。 */
|
||||
if (!nm_wifi_ssid_ok(ssid)) {
|
||||
if (err) snprintf(err, elen, "ssid 非法(长度 1..32 字节,禁控制字符)");
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
if (!nm_wifi_psk_ok(psk)) {
|
||||
if (err) snprintf(err, elen, "psk 非法(≤63 字符口令或 64 位 hex,禁控制字符)");
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
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(cmd, sizeof cmd, "%s set_network %d key_mgmt NONE 2>/dev/null", wpa, id);
|
||||
if ((r = popen_read_tmo(1200, cmd))) free(r);
|
||||
snprintf(clean, sizeof clean, "%s", ss);
|
||||
}
|
||||
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);
|
||||
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; }
|
||||
}
|
||||
if (err) snprintf(err, elen, "connect: ssid=%s network_id=%d", ssid, id);
|
||||
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s id=%d", ssid, id);
|
||||
ln = strtok(NULL, "\n");
|
||||
}
|
||||
}
|
||||
int created = 0;
|
||||
if (id < 0) {
|
||||
if (nm_direct_wpa(out, sizeof out, "add_network", 1500) <= 0) {
|
||||
if (err) snprintf(err, elen, "add_network 无 id");
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
id = atoi(out);
|
||||
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
|
||||
created = 1;
|
||||
/* M64 P1-1:ssid/psk 走 execv(nm_direct_wpa_set),不再拼 popen(sh -c)。 */
|
||||
nm_direct_wpa_set(out, sizeof out, id, "ssid", ssid, 1500);
|
||||
if (psk && psk[0]) { nm_direct_wpa_set(out, sizeof out, id, "psk", psk, 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]) {
|
||||
nm_direct_wpa_set(out, sizeof out, id, "psk", psk, 1500);
|
||||
}
|
||||
if (hidden) { snprintf(args, sizeof args, "set_network %d scan_ssid 1", id); nm_direct_wpa(out, sizeof out, args, 1500); }
|
||||
snprintf(args, sizeof args, "enable_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
|
||||
snprintf(args, sizeof args, "select_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
|
||||
|
||||
int wait_ms = hidden ? 15000 : 10000;
|
||||
int waited = 0, ok = 0; char ip[40] = "";
|
||||
for (;;) {
|
||||
char st[512];
|
||||
if (nm_direct_wpa(st, sizeof st, "status", 1500) > 0) {
|
||||
char *ws = strstr(st, "wpa_state=");
|
||||
if (ws && !strncmp(ws + 10, "COMPLETED", 9)) {
|
||||
ok = 1;
|
||||
char *ib = strstr(st, "ip_address=");
|
||||
if (ib) { ib += 11; char *ie = ib; while (*ie && *ie != '\n' && *ie != '\r') ie++;
|
||||
int il = (int)(ie - ib); if (il >= (int)sizeof ip) il = (int)sizeof ip - 1;
|
||||
memcpy(ip, ib, (size_t)il); ip[il] = 0; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (waited >= wait_ms) break;
|
||||
usleep(200000);
|
||||
waited += 200;
|
||||
}
|
||||
if (!ok) {
|
||||
if (created) { snprintf(args, sizeof args, "remove_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500); nm_direct_save_config(); }
|
||||
if (err) snprintf(err, elen, "连接失败:密码错误或信号不佳(超时未关联)");
|
||||
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d timeout", ssid, hidden, id);
|
||||
return MEOW_ERR_NODEV;
|
||||
}
|
||||
nm_direct_wpa(out, sizeof out, "enable_network all", 1500);
|
||||
int persisted = nm_direct_save_config();
|
||||
if (err) snprintf(err, elen, "已连接:%s ip=%s%s", ssid, ip[0] ? ip : "-", persisted ? "" : "(记录未持久化)");
|
||||
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d ip=%s", ssid, hidden, id, ip[0] ? ip : "-");
|
||||
return MEOW_OK;
|
||||
}
|
||||
|
||||
int nm_wifi_connect(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; }
|
||||
/* M64 P1-1:IPC 路径与直连回退同一套校验(netmgrd 侧还会再校验一次)。 */
|
||||
if (!nm_wifi_ssid_ok(ssid)) {
|
||||
if (err) snprintf(err, elen, "ssid 非法(长度 1..32 字节,禁控制字符)");
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
if (!nm_wifi_psk_ok(psk)) {
|
||||
if (err) snprintf(err, elen, "psk 非法(≤63 字符口令或 64 位 hex,禁控制字符)");
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
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];
|
||||
int rc = netmgr_wifi("connect", arg, text, sizeof text);
|
||||
if (rc == NETMGR_UNREACH) return nm_direct_connect(ssid, psk, err, elen);
|
||||
int rc = netmgr_wifi_tmo(hidden ? "connecth" : "connect", arg, text, sizeof text,
|
||||
hidden ? NM_TMO_CONNECTH : NM_TMO_CONNECT);
|
||||
if (rc == NETMGR_UNREACH) return nm_direct_connect_ex(ssid, psk, hidden, err, elen);
|
||||
if (err && elen > 0) snprintf(err, elen, "%s", text);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
|
||||
{ return nm_wifi_connect_ex(ssid, psk, 0, err, elen); }
|
||||
|
||||
int nm_wifi_disconnect(char *err, int elen)
|
||||
{
|
||||
char text[128];
|
||||
@@ -456,6 +673,36 @@ int nm_bt_powered(void)
|
||||
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)
|
||||
{
|
||||
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;
|
||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||||
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||
struct kd_state kd_play, kd_pwr;
|
||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||||
int m_prev_play = 0, m_prev_pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
key_router_rebase(); /* 进入通知中心:音量基线对齐当前 raw(防残留边沿) */
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
@@ -1304,12 +1303,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
}
|
||||
if (wak) {
|
||||
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
|
||||
@@ -1331,23 +1329,20 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
}
|
||||
|
||||
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
|
||||
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||||
int play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||
if (wake_suppress) {
|
||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||||
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||
} else {
|
||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (play && !m_prev_play) {
|
||||
logmsg("notif-center: play key caught (media-reserved, no exit)");
|
||||
last_act = now;
|
||||
@@ -1366,12 +1361,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
show_power_menu();
|
||||
last_act = now_ms();
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1; need_paint = 1;
|
||||
goto ncp_keys_seeded;
|
||||
@@ -1390,14 +1384,14 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
}
|
||||
}
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
||||
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||
int going = vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
need_paint = 1;
|
||||
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
|
||||
}
|
||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
||||
m_prev_play = play; m_prev_pwr = pwr;
|
||||
ncp_keys_seeded:
|
||||
;
|
||||
|
||||
@@ -1562,15 +1556,13 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
|
||||
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
|
||||
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
|
||||
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
|
||||
/* 菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL);
|
||||
kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
|
||||
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/* 左对齐 + 超宽截断(UTF-8 安全,近似按字节比例切;歌词页顶部元信息用)。 */
|
||||
static void np_left_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
|
||||
{
|
||||
if (!s) s = "";
|
||||
if (ui_width(s, size) <= maxw) {
|
||||
ui_draw(x, y, s, color, size);
|
||||
return;
|
||||
}
|
||||
char buf[160];
|
||||
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
|
||||
if (keep < 0) keep = 0;
|
||||
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
|
||||
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
|
||||
strcat(buf, "...");
|
||||
ui_draw(x, y, buf, color, size);
|
||||
}
|
||||
|
||||
/* 歌词页顶部元信息(两行居左,复刻 ncm 歌词页):第一行歌名(大字白)、第二行歌手(小字灰)。
|
||||
* 多歌手已由源侧拼串(ncm art_name 即「A / B」),此处原样显示 + 超宽截断。
|
||||
* 左边距与歌词正文一致(LYR_PAD=24);离状态栏留边距(SB_BAR_H + 20);右缘同距截断。 */
|
||||
static void np_draw_lyric_title(const meow_media_session_t *s)
|
||||
{
|
||||
if (!s) return;
|
||||
if (!s->title[0] && !s->artist[0]) return;
|
||||
int pad = 24;
|
||||
int y = SB_BAR_H + 20;
|
||||
int maxw = fb_xres - 2 * pad;
|
||||
if (s->title[0])
|
||||
np_left_text(pad, y, s->title, 28, 0xFFFFFF, maxw);
|
||||
if (s->artist[0])
|
||||
np_left_text(pad, y + meow_font_line_height(28) + 6, s->artist, 20, 0xBBBBBB, maxw);
|
||||
}
|
||||
|
||||
/* 歌词页强制 ncm 配色:活动行恒纯白 + 上下逐级灰阶(对齐 ncm lyr_gray_tbl
|
||||
* {FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}——gray[0]=0 按 cur=0xFFFFFF 自动递减即为该表)。
|
||||
* 背景恒为 np_draw_lyric_page 铺的黑色半透明叠加层,故关掉 lyric_view 自带 scrim
|
||||
* (scrim_alpha=0)避免双重压暗;当前行/灰阶不随主题/封面取色(保黑底对比度,夜间可读)。 */
|
||||
static void np_set_lyric_ncm_colors(void)
|
||||
{
|
||||
if (!g_np_lyric) return;
|
||||
meow_lyric_view_colors c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.scrim = 0x000000;
|
||||
c.cur = 0xFFFFFF;
|
||||
c.scrim_alpha = 0; /* 不画 scrim:由 launcher 黑底铺(单一叠加层) */
|
||||
meow_lyric_view_set_colors(g_np_lyric, &c);
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
|
||||
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
|
||||
@@ -346,28 +394,49 @@ static void np_draw_home_bar(void)
|
||||
NP_HOME_BAR_H / 2, th->on_surface_variant);
|
||||
}
|
||||
|
||||
/* 歌词页整页绘制:背景统一黑色半透明叠加层(用户 2026-09-04,弃封面取色/封面图半透明底,
|
||||
* 保证高亮封面下歌词对比度)→ lyric_view(自绘 scrim + 歌词/空态)。 */
|
||||
/* 网易云式歌词页封面背景(用户 2026-09-05):有封面 → 等比居中放大铺满内容区(cover-fit,
|
||||
* 超出部分由画布裁剪,镜像 ncm plr_draw_lyrics 的 meow_ui_img_scaled 算法);无封面 → 纯黑底
|
||||
* (现状回退)。本函数只铺底;压暗由调用方随后叠黑色半透明叠加层(alpha176)完成。 */
|
||||
static void np_draw_lyric_cover_bg(int W, int H)
|
||||
{
|
||||
int x = 0, y = SB_BAR_H, cw = W, ch = H - SB_BAR_H; /* 只盖内容区(状态栏下),与播放页一致 */
|
||||
if (!g_np_cover || meow_img_w(g_np_cover) <= 0 || meow_img_h(g_np_cover) <= 0) {
|
||||
fill_rect(x, y, cw, ch, 0x000000); /* 无封面 → 纯黑底(回退现状) */
|
||||
return;
|
||||
}
|
||||
int iw = meow_img_w(g_np_cover), ih = meow_img_h(g_np_cover);
|
||||
long dw = cw, dh = (long)ih * cw / iw; /* cover-fit:铺满宽度,高按等比 */
|
||||
if (dh < ch) { dh = ch; dw = (long)iw * ch / ih; } /* 宽度不足再铺满高度(超出裁剪) */
|
||||
meow_ui_img_scaled(x + (cw - (int)dw) / 2, y + (ch - (int)dh) / 2, (int)dw, (int)dh, g_np_cover);
|
||||
}
|
||||
|
||||
/* 歌词页整页绘制:网易云式封面背景(封面图当底 + 黑色半透明叠加层压暗,保证歌词对比度)→
|
||||
* lyric_view(自绘 scrim 关闭,仅歌词/空态)→ 顶部元信息(歌名/歌手)。 */
|
||||
static void np_draw_lyric_page(int redraw_sb)
|
||||
{
|
||||
const meow_ui_theme_t *th = s_ui_theme();
|
||||
int W = fb_xres, H = fb_yres;
|
||||
if (!g_np_init) np_widgets_create();
|
||||
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
||||
/* 批5:封面取色背景(主→次渐变)或主题面;并注入歌词页 set_colors。
|
||||
* 批6:有封面时以封面作整页背景(衬在 lyric_view 半透明 scrim 之下),取色只做附加;
|
||||
* 无封面回退渐变/主题面。
|
||||
/* 批5:np_cover_theme_update 取色(仅为播放页文字/按钮主题;歌词页背景不走取色,恒 ncm 白/灰)。
|
||||
* 批6:有封面时以封面作整页背景(网易云式 cover-fit 铺满,见 np_draw_lyric_cover_bg),
|
||||
* 无封面回退纯黑底;压暗由下方黑色半透明叠加层(alpha176)统一完成,lyric_view 关内置 scrim。
|
||||
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
|
||||
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
|
||||
np_cover_load(meow_media_current());
|
||||
np_cover_theme_update();
|
||||
np_apply_lyric_colors();
|
||||
/* 用户 2026-09-04:歌词页背景统一黑色半透明叠加层(弃封面取色/封面图半透明底)——
|
||||
* 部分高亮度封面对歌词显示不友好,黑底保证对比度。歌词行颜色/布局/滚动不变。 */
|
||||
/* 网易云式封面背景(用户 2026-09-05):先铺封面底(cover-fit 原图铺满),无封面回退纯黑底;
|
||||
* 随后统一叠黑色半透明叠加层(alpha176)压暗,保证歌词对比度。歌词配色仍恒 ncm 白/灰
|
||||
* (不随封面取色——取色仅服务播放页文字/按钮)。歌词行布局/滚动逻辑不变,只换背景。 */
|
||||
np_draw_lyric_cover_bg(W, H);
|
||||
np_set_lyric_ncm_colors();
|
||||
/* 用户 2026-09-04:黑色半透明叠加层(alpha176)——压暗封面底,保证歌词可读;无封面时叠于
|
||||
* 纯黑底之上(仍纯黑,即现状回退)。 */
|
||||
meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176);
|
||||
np_lyric_refresh(meow_media_current());
|
||||
if (g_np_lyric)
|
||||
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
||||
np_draw_lyric_title(meow_media_current()); /* 顶部元信息(歌名/歌手,居左,避状态栏) */
|
||||
np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */
|
||||
if (redraw_sb) draw_statusbar();
|
||||
}
|
||||
@@ -486,6 +555,31 @@ static int64_t np_progress_seek(int tx, int ty)
|
||||
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)
|
||||
{
|
||||
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);
|
||||
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
|
||||
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);
|
||||
/* 读播放键按住态(用于边沿) */
|
||||
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;
|
||||
}
|
||||
int np_prev_power = 0; /* 电源键边沿跟踪 */
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1;
|
||||
key_router_rebase(); /* 进入 Now Playing:音量基线对齐当前 raw(防残留边沿) */
|
||||
unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */
|
||||
|
||||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||||
@@ -521,6 +617,7 @@ void run_now_playing(struct app *apps, int napps)
|
||||
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
||||
* owner==active 闸门——漏 push 则 active()!=0x51,
|
||||
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
||||
key_router_page_enter(NP_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
|
||||
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
|
||||
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
|
||||
@@ -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);
|
||||
int play = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (play && !m_prev_play) {
|
||||
const meow_media_session_t *s = meow_media_current();
|
||||
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
|
||||
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;
|
||||
/* 音量线性渐变(按住长爬;与主界面/子页同语义,状态栏即时刷新)。
|
||||
* Now Playing 用 raw held(kev.vol_held_*,同现状 rvold/rvolu)。 */
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
|
||||
int going = vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
if (!going) logmsg("np: volume ramp end -> %d", g_sys.vol);
|
||||
}
|
||||
|
||||
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
|
||||
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
|
||||
@@ -714,6 +824,7 @@ void run_now_playing(struct app *apps, int napps)
|
||||
usleep(10000);
|
||||
}
|
||||
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
|
||||
key_router_page_exit(NP_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_keys);
|
||||
kp_close(&kp_power);
|
||||
|
||||
@@ -91,4 +91,26 @@ static inline int overlay_lock_wake_now(int pwr_edge, int touch_ready)
|
||||
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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/* popen_tmo.h — 子进程 stdout 的**有界**读取(M65;launcherd/net.c 与 audiod/meow_audiod.c 共用)。
|
||||
*
|
||||
* 三条不变量,都是 M59 内存/资源安全审计里的 P1(原文:
|
||||
* .tower/comms/inbox/20261112-survey-m59-memsafety-tower-survey-summary.md):
|
||||
*
|
||||
* 1. 超时判定在**循环顶部**(P1-1)。子进程持续吐数据时 poll 每次立刻返回,若只在
|
||||
* 「poll 超时」分支里看 deadline,deadline 就永远轮不到 → 调用线程永久挂死。
|
||||
* 审计用 `yes` 定向复现:300ms 超时下 8s 不返回,RSS 236MB→397MB→657MB。
|
||||
* 2. 输出有**硬上限**,超限**报错**而不是静默截断(P1-1 / P2-7)。静默截断会让上层
|
||||
* 以为拿到了完整输出 —— 那是另一种假成功。超限返回 POPEN_TMO_OVERFLOW,调用方必须
|
||||
* 按失败处理并杀掉子进程。
|
||||
* 3. **无 static 状态**(P1-2)。缓冲一律由调用方提供或由本函数用 *buf 持有,多个线程
|
||||
* 各用各的缓冲;旧实现 `static char buf[4096]; … return buf;` 被 audiod 的 4 个线程
|
||||
* 并发使用 → amixer cget 读回值可能被另一个线程改写一半。
|
||||
*
|
||||
* 只依赖 POSIX,便于宿主单测直接 include(patch_v2/meow_launcherd/test_popen_tmo.c)。
|
||||
*/
|
||||
#ifndef MEOW_POPEN_TMO_H
|
||||
#define MEOW_POPEN_TMO_H
|
||||
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* 输出硬上限:256 KB。设备总内存 56 MB;最大的正常输出(wpa_cli scan_results /
|
||||
* bluetoothctl devices)也就几 KB。越过这个量级只可能是子进程在无限吐。 */
|
||||
#define POPEN_TMO_MAX (256 * 1024)
|
||||
|
||||
enum {
|
||||
POPEN_TMO_EOF = 0, /* 读到 EOF:*len 是完整输出 */
|
||||
POPEN_TMO_DEADLINE = 1, /* 到 deadline:*len 是已读到的部分 */
|
||||
POPEN_TMO_OVERFLOW = 2, /* 越过硬上限:*len 是上限内已读到的部分(调用方按失败处理) */
|
||||
POPEN_TMO_ERROR = 3 /* poll/read 硬错误;*len 是已读到的部分 */
|
||||
};
|
||||
|
||||
static long long popen_tmo_now_ms(void)
|
||||
{
|
||||
struct timespec tp;
|
||||
clock_gettime(CLOCK_MONOTONIC, &tp);
|
||||
return (long long)tp.tv_sec * 1000LL + tp.tv_nsec / 1000000LL;
|
||||
}
|
||||
|
||||
/* 从 fd 读子进程 stdout,最多等 tmo_ms 毫秒。
|
||||
* grow=0:定长缓冲 —— *buf 由调用方给,*cap 是容量,最多保留 *cap-1 字节;
|
||||
* grow=1:*buf 由本函数 realloc 增长,*cap 是当前容量,总量不得超过 hard_max;
|
||||
* grow=0 且 *cap==0(或 *buf==NULL):**排空丢弃**模式,只保证子进程不因 pipe 满而阻塞。
|
||||
* hard_max==0 时按 POPEN_TMO_MAX 处理。
|
||||
* 返回 POPEN_TMO_*;*len 是已保留字节数;非丢弃模式下 *buf 末尾总有 NUL。 */
|
||||
static int popen_tmo_read_all(int fd, int tmo_ms, char **buf, size_t *cap,
|
||||
size_t hard_max, size_t *len, int grow)
|
||||
{
|
||||
size_t off = 0;
|
||||
int rc = POPEN_TMO_EOF;
|
||||
int discard = (!grow && (!buf || !*buf || *cap == 0));
|
||||
if (hard_max == 0) hard_max = POPEN_TMO_MAX;
|
||||
if (grow) { *buf = NULL; *cap = 0; }
|
||||
else if (!discard && *buf) (*buf)[0] = 0;
|
||||
long long deadline = popen_tmo_now_ms() + (tmo_ms > 0 ? tmo_ms : 1);
|
||||
char chunk[512];
|
||||
|
||||
for (;;) {
|
||||
/* ① 循环顶部判超时:子进程一直吐数据时也必须到期返回(P1-1 的核心)。 */
|
||||
if (popen_tmo_now_ms() >= deadline) { rc = POPEN_TMO_DEADLINE; break; }
|
||||
long long remain = deadline - popen_tmo_now_ms();
|
||||
struct pollfd pf = { fd, POLLIN, 0 };
|
||||
int wait = (int)(remain > 25 ? 25 : (remain > 0 ? remain : 1)); /* 25ms 一片,保持可打断 */
|
||||
int pr = poll(&pf, 1, wait);
|
||||
if (pr < 0) { if (errno == EINTR) continue; rc = POPEN_TMO_ERROR; break; }
|
||||
if (pr == 0) continue; /* 这一小段没数据:回顶部看 deadline */
|
||||
ssize_t rd = read(fd, chunk, sizeof chunk);
|
||||
if (rd < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) continue;
|
||||
rc = POPEN_TMO_ERROR;
|
||||
break;
|
||||
}
|
||||
if (rd == 0) { rc = POPEN_TMO_EOF; break; }
|
||||
size_t n = (size_t)rd;
|
||||
if (discard) continue; /* 丢弃模式:只排水 */
|
||||
if (grow) {
|
||||
if (off + n + 1 > hard_max) { rc = POPEN_TMO_OVERFLOW; break; } /* ② 硬上限 */
|
||||
if (off + n + 1 > *cap) {
|
||||
size_t nc = *cap ? *cap : 512;
|
||||
while (nc < off + n + 1) nc *= 2;
|
||||
if (nc > hard_max) nc = hard_max;
|
||||
char *nb = realloc(*buf, nc);
|
||||
if (!nb) { rc = POPEN_TMO_ERROR; break; }
|
||||
*buf = nb; *cap = nc;
|
||||
}
|
||||
memcpy(*buf + off, chunk, n);
|
||||
off += n;
|
||||
(*buf)[off] = 0;
|
||||
} else {
|
||||
size_t room = *cap - 1 - off;
|
||||
if (n > room) { /* ② 定长:写满即超限,不静默截断 */
|
||||
memcpy(*buf + off, chunk, room);
|
||||
off += room;
|
||||
(*buf)[off] = 0;
|
||||
rc = POPEN_TMO_OVERFLOW;
|
||||
break;
|
||||
}
|
||||
memcpy(*buf + off, chunk, n);
|
||||
off += n;
|
||||
(*buf)[off] = 0;
|
||||
}
|
||||
}
|
||||
if (len) *len = off;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 有界收尸:先 WNOHANG 等 300ms;不死 SIGKILL 再等 300ms;仍不死(D 态,驱动 wedge)
|
||||
* 放弃收割(泄漏一个子进程换调用线程不被拖死,与 audiod/netmgrd 同权衡)。返回 1=已收。 */
|
||||
static int popen_tmo_reap_bounded(pid_t pid)
|
||||
{
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (waitpid(pid, NULL, WNOHANG) == pid) return 1;
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
kill(pid, SIGKILL);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (waitpid(pid, NULL, WNOHANG) == pid) return 1;
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* MEOW_POPEN_TMO_H */
|
||||
@@ -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 绘制消频闪;输入修复:
|
||||
* - 触摸按下捕获坐标、抬起命中按钮即确认(同 run_menu MT 防抖);
|
||||
* - 音量上/下导航;播放=确认;电源新按(松开长按后)=确认当前钮(非取消);
|
||||
* - 取消=选「取消」钮;8s 持续按住=强制关机(绕过)。
|
||||
* - 取消=选「取消」钮;按住 power.forceoff_ms(缺省 8s,从按下起算)=强制关机(绕过)。
|
||||
* 返回 1=采取动作,0=取消。 */
|
||||
int show_power_menu(void)
|
||||
{
|
||||
int fbfd = open(FB_DEV, O_RDWR);
|
||||
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();
|
||||
/* M2.8 Bug 3-residual:若电源菜单是在「息屏」态打开(按住 3s),此处 FBIOBLANK(0)
|
||||
* 刚上电,LCDC 扫描地址可能非 buffer0;强制 re-pan 让压暗的应用列表 + 菜单落在被显示的
|
||||
* 缓冲,否则菜单画在非显示缓冲上不可见。 */
|
||||
fb_pan_buf0_force();
|
||||
meow_screen_on = 1; /* 菜单打开即视为亮屏 */
|
||||
|
||||
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
|
||||
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
|
||||
@@ -383,29 +420,25 @@ int show_power_menu(void)
|
||||
pm_draw_card(); /* 卡片 + 标题 POWER MENU + 底部提示(SDK 主题色,i18n 文案) */
|
||||
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 pm_rc = 0;
|
||||
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:菜单空闲自动关闭基准(任何互动刷新) */
|
||||
int pm_init = 0, last_sel = -1;
|
||||
/* M2.10 Bug:电源菜单 8s 强关(按住电源≥8s=绕过强关)会覆盖用户的菜单操作 —— 用户按住
|
||||
* 电源打开菜单后(尤其仍按着电源)点选「回到HibyOS/重启」等按钮,8s 一到先触发强关 → 关机
|
||||
/* M2.10 Bug:电源菜单强关(按住电源到 forceoff)会覆盖用户的菜单操作 —— 用户按住
|
||||
* 电源打开菜单后(尤其仍按着电源)点选「回到HibyOS/重启」等按钮,强关一到先触发 → 关机
|
||||
* 而非执行所选动作。修法:一旦用户与菜单互动(音量导航/触摸/播放/电源确认),置 pm_interacted=1
|
||||
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
|
||||
* 8s=强关」。*/
|
||||
* 取消强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
|
||||
* 到 forceoff=强关」。*/
|
||||
int pm_interacted = 0;
|
||||
int pm_press_idx = -1; /* M7.0b:当前按下钮(按压态走 SDK 状态机位) */
|
||||
for (;;) {
|
||||
@@ -488,17 +521,25 @@ int show_power_menu(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
|
||||
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
|
||||
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
|
||||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); meow_ui_activity_pop(); return 1; }
|
||||
logmsg("pm: POWER HOLD 8s -> force poweroff");
|
||||
/* 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
|
||||
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。
|
||||
* M40:阈值接 power.forceoff_ms(从按下起算的剩余时长),不再硬编码 5000/日志谎称 8s。 */
|
||||
if (pwr && !pm_interacted && now_ms() - pm_t0 >= pm_forceoff_after_ms) {
|
||||
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) {
|
||||
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();
|
||||
return 1;
|
||||
pm_rc = 1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* M2.33 菜单空闲超时自动关闭:菜单受 MeowOS screen_off_timeout 管理(系统界面不豁免)。
|
||||
@@ -516,57 +557,64 @@ int show_power_menu(void)
|
||||
if (act >= 0) break;
|
||||
usleep(POLL_MS * 1000);
|
||||
}
|
||||
if (tfd >= 0) close(tfd);
|
||||
meow_ui_activity_pop(); /* M7.0b:退出电源菜单,回主 activity(控件 owner 仍钉哨兵,复用) */
|
||||
|
||||
/* 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==0(关机)/act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才经服务 IPC 执行电源动作。
|
||||
* 只有 act 为 0/1/2/3 才可能发电源动作;power 键本身绝不触发关机。 */
|
||||
logmsg("pm: act=%d -> %s", act,
|
||||
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式"
|
||||
: 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) {
|
||||
logmsg("pm: LOCK SCREEN");
|
||||
meow_lock_screen_engage();
|
||||
return 0; /* 关菜单;run_menu 继续(锁屏页已阻塞到解锁或电源息屏) */
|
||||
pm_rc = 0; /* 关菜单;run_menu 继续(锁屏页已阻塞到解锁或电源息屏) */
|
||||
goto out;
|
||||
}
|
||||
/* 动作统一经电源管理服务 IPC(meow_svc_power);launcher 不再直连 poweroff/reboot(2)/写 mtd5/
|
||||
* 写 boot 默认——关机/重启/升级/回HiBy 的动作原语都在服务内实现(§3.2 单一动作链路)。 */
|
||||
/* 公共服务:执行动作;服务不可达 → 按「取消」返回(launcher 存活,绝不误报已动作卡死)。
|
||||
* meow_power_action 内部已先 meow_notif_before_poweroff()(转储未上卡通知)。 */
|
||||
pm_rc = 1;
|
||||
if (act == 0) {
|
||||
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) {
|
||||
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
||||
return 0;
|
||||
pm_rc = 0;
|
||||
}
|
||||
} else if (act == 1) {
|
||||
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) {
|
||||
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
||||
return 0;
|
||||
pm_rc = 0;
|
||||
}
|
||||
} else if (act == 2) {
|
||||
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) {
|
||||
logmsg("pm: power service unavailable, abort (menu closed, no action)");
|
||||
return 0;
|
||||
pm_rc = 0;
|
||||
}
|
||||
} else if (act == 3) {
|
||||
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) {
|
||||
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;
|
||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||||
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||
struct kd_state kd_play, kd_pwr;
|
||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||||
int m_prev_play = 0, m_prev_pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
key_router_rebase(); /* 进入设置页:音量基线对齐当前 raw(防残留边沿) */
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
@@ -1358,6 +1357,7 @@ void run_settings_page(void)
|
||||
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
|
||||
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
|
||||
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
|
||||
key_router_page_enter(SET_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||
set_paint_page(sel);
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
|
||||
@@ -1396,13 +1396,12 @@ void run_settings_page(void)
|
||||
meow_auto_slept = 0;
|
||||
wake_suppress = 1;
|
||||
last_act = now_ms();
|
||||
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸。
|
||||
* 音量防抖/基线上收 key_router:唤醒时重播音量基线;播放/电源仍本页自理。 */
|
||||
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
|
||||
@@ -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);
|
||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||||
int play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||
if (wake_suppress) {
|
||||
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
|
||||
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
|
||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||||
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||
} else {
|
||||
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
|
||||
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
|
||||
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
|
||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
|
||||
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
|
||||
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
|
||||
@@ -1480,14 +1476,11 @@ void run_settings_page(void)
|
||||
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
|
||||
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
|
||||
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
||||
need_paint = 1;
|
||||
@@ -1514,14 +1507,14 @@ void run_settings_page(void)
|
||||
}
|
||||
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
|
||||
if (vol_ramp_dir != 0) {
|
||||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
||||
int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
|
||||
int going = vol_ramp_tick(held);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
need_paint = 1;
|
||||
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
|
||||
}
|
||||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
||||
m_prev_play = play; m_prev_pwr = pwr;
|
||||
settings_keys_seeded:
|
||||
;
|
||||
|
||||
@@ -1658,14 +1651,10 @@ void run_settings_page(void)
|
||||
scroll_y0 = 0;
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
key_router_rebase(); /* 音量基线重播 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
need_paint = 1;
|
||||
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
|
||||
@@ -1703,14 +1692,10 @@ void run_settings_page(void)
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
/* 重置设置页按键防抖基线,防残边 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
key_router_rebase(); /* 音量基线重播 */
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
need_paint = 1;
|
||||
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
|
||||
@@ -1768,6 +1753,7 @@ void run_settings_page(void)
|
||||
done:
|
||||
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
|
||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||
key_router_page_exit(SET_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_power); kp_close(&kp_keys);
|
||||
logmsg("settings page closed");
|
||||
|
||||
@@ -247,12 +247,11 @@ int meow_subpage_run(sub_page_t *p)
|
||||
meow_auto_slept = 0;
|
||||
wake_suppress = 1;
|
||||
last_act = now;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
|
||||
@@ -302,14 +301,12 @@ int meow_subpage_run(sub_page_t *p)
|
||||
last_act = now_ms();
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||
pwr_suppress_click = 1;
|
||||
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
@@ -456,14 +453,12 @@ int meow_subpage_run(sub_page_t *p)
|
||||
scroll_base = 0; scroll_y0 = 0;
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
@@ -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 int g_nrows = 0; /* 当前行数(listview vsource count 实时读) */
|
||||
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_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)---- */
|
||||
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;
|
||||
int tw = ui_width(g_sp->top_action, 20);
|
||||
if (tw <= 0) return;
|
||||
@@ -670,22 +669,29 @@ static void sp_widgets_create(void)
|
||||
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 行数。 */
|
||||
static void sp_configure_page(const sub_page_t *p, int top_sw_en, int top_on)
|
||||
{
|
||||
if (p->title && g_tb) meow_topappbar_set_title(g_tb, p->title);
|
||||
g_top_sw_en = top_sw_en;
|
||||
g_top_on = top_on;
|
||||
/* 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 (!g_sw) {
|
||||
meow_ui_activity_push(SUB_SENTINEL_ACT); /* owner 钉哨兵(命中闸门) */
|
||||
int swx = fb_xres - SUB_PAD_X - 56; /* meow_switch 固定 56×32 */
|
||||
int swy = SUB_TB_Y + (SUB_TB_H - SUB_SW_H) / 2;
|
||||
g_sw = meow_switch_create(swx, swy, top_on);
|
||||
g_sw = meow_switch_create(swx, swy, g_top_on);
|
||||
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。 */
|
||||
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 */
|
||||
draw_statusbar();
|
||||
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(); /* 批③:顶栏右侧动作按钮 */
|
||||
int 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);
|
||||
}
|
||||
|
||||
/* 顶部开关切换:写设置 + 生效 + 触发重扫 + 重填行。 */
|
||||
/* 顶开关回读真实态(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)
|
||||
{
|
||||
if (!g_sp || !g_sp->top_sw_set) return;
|
||||
g_sp->top_sw_set(g_sp->ctx, !g_top_on);
|
||||
g_top_on = !g_top_on;
|
||||
if (g_sw) meow_switch_set(g_sw, g_top_on);
|
||||
int want = (g_top_on == 1) ? 0 : 1; /* 未知(-1) 视同「关」:点一下表示「请打开」 */
|
||||
sp_paint_switching(want);
|
||||
g_sp->top_sw_set(g_sp->ctx, want); /* 页内:下发 + 有界轮询真实态 + 失败回滚/弹窗 */
|
||||
sp_top_sw_resync(); /* 定稿只认真实态 */
|
||||
sp_top_sw_apply();
|
||||
if (g_sp->rows) {
|
||||
g_nrows = g_sp->rows(g_sp->ctx, g_rows, SUB_ROWS_MAX);
|
||||
if (g_nrows > SUB_ROWS_MAX) g_nrows = SUB_ROWS_MAX;
|
||||
@@ -743,16 +786,15 @@ int meow_subpage_run(sub_page_t *p)
|
||||
{
|
||||
struct keypoll kp_power, kp_keys;
|
||||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||||
/* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
|
||||
struct kd_state kd_play, kd_pwr;
|
||||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||||
int m_prev_play = 0, m_prev_pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||||
}
|
||||
key_router_rebase(); /* 进入子页:音量基线对齐当前 raw(防残留边沿) */
|
||||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||||
@@ -780,6 +822,7 @@ int meow_subpage_run(sub_page_t *p)
|
||||
sp_configure_page(p, top_sw_en, top_on);
|
||||
|
||||
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
|
||||
key_router_page_enter(SUB_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
|
||||
sp_paint_page();
|
||||
meow_dirty_full(); meow_ui_flush();
|
||||
|
||||
@@ -814,12 +857,11 @@ int meow_subpage_run(sub_page_t *p)
|
||||
meow_auto_slept = 0;
|
||||
wake_suppress = 1;
|
||||
last_act = now;
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||||
/* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
td = 0; td_ok = 0; scroll_drag = 0; 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);
|
||||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||||
int play = 0, pwr = 0;
|
||||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||||
}
|
||||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||||
if (wake_suppress) {
|
||||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||||
if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
|
||||
} else {
|
||||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||||
if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||||
if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
|
||||
if (pwr && !m_prev_pwr) {
|
||||
pwr_hold_at = now; pwr_menu_state = 0;
|
||||
@@ -867,14 +906,12 @@ int meow_subpage_run(sub_page_t *p)
|
||||
last_act = now_ms();
|
||||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||||
pwr_suppress_click = 1;
|
||||
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
@@ -899,13 +936,13 @@ int meow_subpage_run(sub_page_t *p)
|
||||
}
|
||||
}
|
||||
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);
|
||||
g_sys.vol = settings_volume_get();
|
||||
paint_sb_right();
|
||||
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:
|
||||
;
|
||||
|
||||
@@ -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)) {
|
||||
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;
|
||||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||||
continue;
|
||||
}
|
||||
/* 列表行命中(SDK 命中:含滚动换算 + 越界/禁用行不响应) */
|
||||
@@ -1021,14 +1080,12 @@ int meow_subpage_run(sub_page_t *p)
|
||||
drag_last_y = 0;
|
||||
tdx = tdy = tlx = tly = -1;
|
||||
touch_cooldown_until = now_ms() + 300;
|
||||
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
|
||||
key_router_rebase();
|
||||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||||
memset(&kd_vold, 0, sizeof kd_vold);
|
||||
memset(&kd_volu, 0, sizeof kd_volu);
|
||||
memset(&kd_play, 0, sizeof kd_play);
|
||||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||||
after_modal = 1;
|
||||
need_paint = 1;
|
||||
@@ -1045,6 +1102,14 @@ int meow_subpage_run(sub_page_t *p)
|
||||
/* ---- 周期性重扫(live 页:蓝牙附近扫描等驻留更新)---- */
|
||||
if (g_sp && g_sp->live && now - last_live >= 2000) {
|
||||
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;
|
||||
if (new_n > SUB_ROWS_MAX) new_n = SUB_ROWS_MAX;
|
||||
if (new_n != g_nrows) {
|
||||
@@ -1087,6 +1152,7 @@ int meow_subpage_run(sub_page_t *p)
|
||||
subdone:
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
|
||||
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
|
||||
key_router_page_exit(SUB_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
|
||||
if (tfd >= 0) close(tfd);
|
||||
kp_close(&kp_power); kp_close(&kp_keys);
|
||||
logmsg("subpage '%s' closed", p->title ? p->title : "");
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* 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。
|
||||
* 2) bgmgr_can_kill 门控矩阵:framework=1 → 0(**框架 App 不 kill**);is_fg=1 → 0(当前前台不 kill);
|
||||
* pid<=0 → 0(非后台/无进程);普通后台(非框架/非前台/pid>0)→ 1。
|
||||
* 3) bgmgr_kill_all 一键清:框架 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。
|
||||
* 2) bgmgr_can_kill 门控矩阵:protected_app=1 → 0(**受保护 App 不 kill**);is_fg=1 → 0(当前前台不 kill);
|
||||
* pid<=0 → 0(非后台/无进程);普通后台(非受保护/非前台/pid>0)→ 1。
|
||||
* 3) bgmgr_kill_all 一键清:受保护 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。
|
||||
* 3b) 回归:app.json framework=1(SDK 框架 App 标志,现有 App 全写 1)**不影响**可杀性。
|
||||
* 4) bgmgr_status_str / bgmgr_mem_str 文案。
|
||||
* 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编:
|
||||
* gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr
|
||||
@@ -17,7 +18,9 @@
|
||||
#include "meow_bgmgr.h"
|
||||
|
||||
static int g_fails = 0;
|
||||
static int g_checks = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
g_checks++; \
|
||||
if (cond) printf("PASS: %s\n", msg); \
|
||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||
|
||||
@@ -33,7 +36,7 @@ static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
||||
{
|
||||
(void)ctx;
|
||||
if (max < 4) return 0;
|
||||
/* 4 个后台行:hello=普通、reader=常驻、sysguard=framework、fgapp=当前前台 */
|
||||
/* 4 个后台行:hello=普通、reader=常驻、sysguard=受保护、fgapp=当前前台 */
|
||||
memset(&rows[0], 0, sizeof rows[0]);
|
||||
snprintf(rows[0].app_id, sizeof rows[0].app_id, "%s", g_meta_app[0]);
|
||||
rows[0].pid = 1001; rows[0].keepalive = 0; rows[0].on_card = 0; rows[0].bg_since_ms = 1000;
|
||||
@@ -52,10 +55,10 @@ static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
||||
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,
|
||||
char *name, int nl, char *icon, int il,
|
||||
char *framework, int *is_fg)
|
||||
char *protected_app, int *is_fg)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放");
|
||||
@@ -64,7 +67,7 @@ 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 return -1; /* 未注册 → name 回退 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 标前台 */
|
||||
return 0;
|
||||
}
|
||||
@@ -112,9 +115,9 @@ int main(void)
|
||||
printf("=== 1) collect ===\n");
|
||||
CHECK(n == 4, "collect returns 4 rows");
|
||||
CHECK(!strcmp(rows[0].name, "播放") && rows[0].pid == 1001, "row0 hello name/pid");
|
||||
CHECK(rows[0].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(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[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");
|
||||
@@ -132,7 +135,7 @@ int main(void)
|
||||
}
|
||||
|
||||
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[0]) == 1, "normal 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.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_bg == 1, "kill_all killed a real bg app");
|
||||
CHECK(bgmgr_kill_all(&ops, rows, n, NULL) == 0, "kill_all NULL kill_fn -> 0");
|
||||
|
||||
/* 回归(2026-11-04 真机):app.json framework=1 是「SDK 框架 App」标志(现有 App 全写 1),
|
||||
* **不是**受保护位。旧实现把它当门控 → dev.player.ncm 等所有后台 App 都不可杀、按钮置灰。 */
|
||||
printf("=== 3b) framework(SDK) ≠ 受保护 ===\n");
|
||||
{
|
||||
bgmgr_row_t sdk; memset(&sdk, 0, sizeof sdk);
|
||||
snprintf(sdk.app_id, sizeof sdk.app_id, "dev.player.ncm");
|
||||
sdk.pid = 7520; sdk.protected_app = 0;
|
||||
CHECK(bgmgr_can_kill(&sdk) == 1, "SDK 框架 App(framework=1)后台仍可杀");
|
||||
}
|
||||
|
||||
printf("=== 4) status/mem str ===\n");
|
||||
CHECK(!strcmp(bgmgr_status_str(&rows[1]), "常驻"), "status_str keepalive=常驻");
|
||||
CHECK(!strcmp(bgmgr_status_str(&rows[0]), "后台"), "status_str normal=后台");
|
||||
@@ -167,7 +180,7 @@ int main(void)
|
||||
CHECK(!strcmp(bgmgr_mem_str(512), "512KB"), "mem_str(<1MB)=KB");
|
||||
CHECK(!strcmp(bgmgr_mem_str(2048), "2.0MB"), "mem_str(>=1MB)=MB");
|
||||
|
||||
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", 20);
|
||||
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
|
||||
else printf("\n%d FAILED\n", g_fails);
|
||||
return g_fails ? 1 : 0;
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
/* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。
|
||||
*
|
||||
* 通过 #include bg_page.c 把其 FILE-STATIC 的 bgm_rows/bgm_btn_rect/bgm_row_tap/bgm_top_action
|
||||
* 平移进本 TU;桩掉 launcher 后台表(meow_bg_count/at/terminate)+ 时钟 + meow_str,
|
||||
* 平移进本 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 定位);
|
||||
* P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;框架 App
|
||||
* 按钮点击 → 不 terminate(!framework 门控);空态行不响应;
|
||||
* P4 bgm_top_action 顶栏清除:kill_all **不杀框架 App**(sysguard 不 terminate),杀普通后台;
|
||||
* P5 门控矩阵:bgmgr_can_kill(framework/fg/pid<=0/普通)。
|
||||
* P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;
|
||||
* **app.json framework=1(SDK 框架 App)照样可杀**(2026-11-04 真机回归);
|
||||
* 受保护/前台行按钮点击 → 不 terminate(门控,留诊断日志);空态行不响应;
|
||||
* P4 bgm_top_action 顶栏清除:kill_all 不杀受保护/前台、杀普通后台(含 framework=1 的);
|
||||
* P5 门控矩阵:bgmgr_can_kill(protected_app/is_fg/pid<=0/普通)。
|
||||
* 不调用行自绘 bgm_row_draw(纯逻辑),故无需桩 SDK 绘制;行自绘段经 -ffunction-sections/--gc-sections 剔除。
|
||||
*
|
||||
* 编译(在 patch_v2/meow_launcherd 下):
|
||||
@@ -44,6 +46,16 @@ int meow_bg_terminate(const char *app_id)
|
||||
}
|
||||
unsigned long long now_ms(void) { return 0; }
|
||||
const char *meow_str(const char *id) { (void)id; return NULL; } /* 未知 id → meow_str_zh 回退 zh */
|
||||
static int g_log_n;
|
||||
void logmsg(const char *fmt, ...) { (void)fmt; g_log_n++; } /* 只计数:门控/未命中应留诊断日志 */
|
||||
|
||||
/* 栈抖动:bgm_rows 返回后调用,用来暴露「sub 指向栈帧」的悬垂指针。 */
|
||||
static void stack_clobber(void)
|
||||
{
|
||||
volatile char buf[256];
|
||||
memset((void *)buf, 'X', sizeof buf);
|
||||
buf[0] = buf[0]; /* 防优化掉 */
|
||||
}
|
||||
|
||||
/* ---------- 测试态:launcher 应用表(app_meta 用) ---------- */
|
||||
static struct app g_apps[4];
|
||||
@@ -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_checks = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
g_checks++; \
|
||||
if (cond) printf("PASS: %s\n", msg); \
|
||||
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
|
||||
|
||||
@@ -86,21 +100,31 @@ int main(void)
|
||||
{
|
||||
static bgm_ctx_t 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(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];
|
||||
|
||||
printf("=== P1) bgm_rows 行骨架 ===\n");
|
||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
||||
printf("=== P1) bgm_rows 行骨架 + 副文本持久化 ===\n");
|
||||
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
|
||||
int rn = bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)");
|
||||
CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0");
|
||||
CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)");
|
||||
CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1");
|
||||
CHECK(!strcmp(rows[1].label, "系统守护"), "row1 label=系统守护");
|
||||
CHECK(!strcmp(rows[1].sub, "") || rows[1].sub[0] != '\0', "row1 sub 非空(CPU/内存/状态)");
|
||||
CHECK(!strcmp(rows[1].label, "流媒体客户端"), "row1 label=流媒体客户端");
|
||||
CHECK(rows[1].sub != NULL && !strncmp(rows[1].sub, "CPU ", 4), "row1 sub=CPU/内存/状态");
|
||||
CHECK(b.n == 2, "bgm_ctx.n=2");
|
||||
/* 悬垂指针回归:sub 必须落在 ctx 缓冲里,且栈被改写后内容不变 */
|
||||
uintptr_t subp = (uintptr_t)rows[1].sub;
|
||||
uintptr_t lob = (uintptr_t)(char *)&b;
|
||||
uintptr_t hib = lob + sizeof b;
|
||||
CHECK(subp >= lob && subp < hib, "sub 指向 ctx 持久缓冲(不是 bgm_rows 栈帧)");
|
||||
char sub_before[80];
|
||||
snprintf(sub_before, sizeof sub_before, "%s", rows[1].sub);
|
||||
stack_clobber();
|
||||
CHECK(!strcmp(rows[1].sub, sub_before), "栈抖动后 sub 内容不变(无悬垂)");
|
||||
|
||||
printf("=== P2) bgm_btn_rect 按钮几何(行右 46×34) ===\n");
|
||||
{
|
||||
@@ -111,7 +135,7 @@ int main(void)
|
||||
}
|
||||
|
||||
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);
|
||||
/* 按钮区命中:行(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(重绘)");
|
||||
@@ -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], 475, 28, 0, 0, 480, 56) == 0, "按钮右侧(475)→不杀");
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀");
|
||||
/* 框架 App:按钮命中但不 terminate(!framework 门控) */
|
||||
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "框架App按钮→返回1(不可杀提示)");
|
||||
CHECK(term_count_app("os.meow.sysguard") == 0, "框架App按钮点击→不 terminate");
|
||||
/* 回归(2026-11-04 真机「杀后台按钮按下去没反应」):dev.player.ncm 的 app.json 写 framework:1
|
||||
* (= 用 SDK 框架,现有 App 全写 1),**必须照杀**——旧实现把它当受保护位,按钮置灰点不动。 */
|
||||
CHECK(b.rows[1].killable == 1, "framework=1(SDK 框架)App 仍判可杀");
|
||||
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "framework=1 App 按钮→返回1");
|
||||
CHECK(term_count_app("dev.player.ncm") == 1, "framework=1 App 按钮→真 terminate(无反应 bug 回归)");
|
||||
/* 真·受保护行(未来系统组件/前台):按钮命中但不 terminate,且留诊断日志 */
|
||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
b.rows[1].protected_app = '1';
|
||||
g_log_n = 0;
|
||||
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "受保护行按钮→返回1(重绘)");
|
||||
CHECK(term_count_app("os.meow.sysguard") == 0, "受保护行按钮→不 terminate");
|
||||
CHECK(g_log_n >= 1, "受保护行按钮→留一行诊断日志(可查「点了没反应」)");
|
||||
|
||||
printf("=== P3b) 空态行不响应 ===\n");
|
||||
bg_reset(0, NULL, 0, NULL, 0);
|
||||
@@ -133,12 +167,19 @@ int main(void)
|
||||
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 0, "空态行点击→返回0(不杀)");
|
||||
g_term_calls = 0;
|
||||
|
||||
printf("=== P4) 顶栏清除(一键清除后台)不杀框架/前台 ===\n");
|
||||
printf("=== P4) 顶栏清除(一键清除后台)不杀受保护/前台 ===\n");
|
||||
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
b.rows[1].protected_app = '1';
|
||||
CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)");
|
||||
CHECK(term_count_app("os.meow.hello") == 1, "清除→terminate 普通后台 hello");
|
||||
CHECK(term_count_app("os.meow.sysguard") == 0, "清除→不杀框架 sysguard");
|
||||
CHECK(term_count_app("os.meow.sysguard") == 0, "清除→不杀受保护 sysguard");
|
||||
/* 回归:framework=1 的 SDK 框架 App 也在「一键清」范围内 */
|
||||
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
g_term_calls = 0;
|
||||
CHECK(bgm_top_action(&b) == 1, "framework=1 App:一键清返回 1");
|
||||
CHECK(term_count_app("dev.player.ncm") == 1, "一键清→terminate framework=1 App(回归)");
|
||||
/* 前台 App(is_fg)不杀:给 hello 标前台再清 */
|
||||
bg_reset(1, "os.meow.hello", 1001, NULL, 0);
|
||||
bgm_rows(&b, rows, SUB_ROWS_MAX);
|
||||
@@ -153,12 +194,12 @@ int main(void)
|
||||
{
|
||||
bgmgr_row_t r; memset(&r, 0, sizeof r);
|
||||
r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀");
|
||||
r.framework = '1'; CHECK(bgmgr_can_kill(&r) == 0, "framework 不可杀");
|
||||
r.framework = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀");
|
||||
r.protected_app = '1'; CHECK(bgmgr_can_kill(&r) == 0, "protected_app 不可杀");
|
||||
r.protected_app = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀");
|
||||
r.is_fg = 0; r.pid = 0; CHECK(bgmgr_can_kill(&r) == 0, "pid<=0 不可杀");
|
||||
}
|
||||
|
||||
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", 32);
|
||||
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
|
||||
else printf("\n%d FAILED\n", g_fails);
|
||||
return g_fails ? 1 : 0;
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
* should_keep=0(停播后再退出=真退);
|
||||
* 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽;
|
||||
* 4) 卡拔例外:on_card=1 + card_present 假 0 → tick SIGTERM 回收(防 SIGBUS);
|
||||
* 4b) meow_bg_terminate(后台管理页「杀」按钮 / `am kill` 共用通道):受理置 term_at_ms →
|
||||
* tick SIGTERM 回收 + on_bg_dead;不在表/空 id 分别 NOENT/INVAL;
|
||||
* 4b) 批①+宽限:未声明(keepalive=0)媒体不活跃超 MEOW_BG_GRACE_MS 回收(含恰等边界不回收);
|
||||
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
|
||||
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
|
||||
@@ -138,11 +140,17 @@ static int cond_svc_gone(void *ctx)
|
||||
return meow_svc_find("dev.test.svc", "daemon") == NULL;
|
||||
}
|
||||
|
||||
/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。 */
|
||||
#define TEST_SVC_PATH "/tmp/meow_bgplay_test_svc.sh"
|
||||
/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。
|
||||
* 路径取 $TMPDIR(Termux/Android 主机没有 /tmp),缺省回退 /tmp。 */
|
||||
static char g_svc_path[256];
|
||||
#define TEST_SVC_PATH g_svc_path
|
||||
static void write_test_svc(void)
|
||||
{
|
||||
const char *tmp = getenv("TMPDIR");
|
||||
if (!tmp || !tmp[0]) tmp = "/tmp";
|
||||
snprintf(g_svc_path, sizeof g_svc_path, "%s/meow_bgplay_test_svc.sh", tmp);
|
||||
FILE *f = fopen(TEST_SVC_PATH, "w");
|
||||
if (!f) { printf("FAIL: 无法创建测试脚本 %s\n", TEST_SVC_PATH); exit(1); }
|
||||
fputs("#!/bin/sh\nexec sleep 300\n", f);
|
||||
fclose(f);
|
||||
chmod(TEST_SVC_PATH, 0755);
|
||||
@@ -271,6 +279,31 @@ int main(void)
|
||||
"卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)");
|
||||
g_card = 1;
|
||||
|
||||
/* ---- 4b) meow_bg_terminate:后台管理页「杀」按钮 / `am kill` 共用的终止通道 ---- */
|
||||
{
|
||||
g_bg_dead = 0;
|
||||
pid_t tk = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.term", tk, -1, 0, 0) == 0, "bg_add(term)");
|
||||
CHECK(meow_bg_terminate("") == MEOW_ERR_INVAL, "terminate 空 app_id → INVAL");
|
||||
CHECK(meow_bg_terminate("dev.test.nothere") == MEOW_ERR_NOENT, "terminate 不在后台表 → NOENT");
|
||||
CHECK(meow_bg_terminate("dev.test.term") == 0, "terminate 受理(在后台表且 pid>0)");
|
||||
{
|
||||
int found = 0;
|
||||
for (int i = 0; i < MEOW_BG_MAX; i++) {
|
||||
const meow_bg_entry_t *be = meow_bg_at(i);
|
||||
if (be && !strcmp(be->app_id, "dev.test.term")) {
|
||||
CHECK(be->term_at_ms > 0, "terminate 置 term_at_ms(bgm_enumerate 据此隐藏行)");
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
CHECK(found == 1, "terminate 后槽仍在(等 tick 回收)");
|
||||
}
|
||||
pump_bg_until(cond_bg_dead, NULL);
|
||||
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.term") == 0,
|
||||
"terminate → tick SIGTERM 回收 + on_bg_dead 上报 amsd");
|
||||
CHECK(meow_bg_count() == 0, "terminate 回收后清槽");
|
||||
}
|
||||
|
||||
/* ---- 5) 表满:LRU 回收最久未激活的未声明槽 / 全常驻满→NOMEM / 重复 / 参数 ---- */
|
||||
{
|
||||
pid_t k[MEOW_BG_MAX];
|
||||
|
||||
@@ -8,16 +8,25 @@
|
||||
* P2 bt_act 命中 → 正确下发 netmgrd 命令(已连接→disconnect、已配对未连→connect、输出目标
|
||||
* 切换、编解码器循环);
|
||||
* P3 附近设备仅列未配对(已配对者归「已配对设备」)。
|
||||
* P4 M47 开关三态:真实态未知(-1) → top_sw 回 -1 + 行内「蓝牙状态未知」(不当已关);
|
||||
* P5 M47 点击收敛:下发成功且真实态达成 → 定稿、不弹窗;下发失败/超时/未知 → 回滚 + 弹窗;
|
||||
* P6 M47 收敛矩阵(纯函数 meow_toggle_*,真实态×下发结果×探测 全组合)。
|
||||
*
|
||||
* 编译(在 patch_v2/meow_launcherd 下):
|
||||
* 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 <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.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 "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 int g_output_bt; /* audio.output_bt 桩值 */
|
||||
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_onoff(int on) { (void)on; return 0; }
|
||||
int nm_bt_power_state(void) { return g_bt_state; }
|
||||
int nm_bt_onoff(int on) { (void)on; return g_onoff_rc; }
|
||||
int nm_bt_connected(meow_net_t *out, int max) {
|
||||
if (max > g_conn_n) max = g_conn_n;
|
||||
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)
|
||||
{
|
||||
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; }
|
||||
return -1;
|
||||
}
|
||||
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, "network.bluetooth_enabled")) return 0;
|
||||
if (!strcmp(k, "network.bluetooth_enabled")) { g_bt_en_setting = v; return 0; }
|
||||
return -1;
|
||||
}
|
||||
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 bt_role_refresh(int force) { (void)force; }
|
||||
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; }
|
||||
|
||||
/* 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;
|
||||
#define CHECK(cond, ...) do { if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
|
||||
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;
|
||||
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)
|
||||
{
|
||||
@@ -230,6 +266,95 @@ int main(void)
|
||||
bt_act(&c, ci, rows[ci].act);
|
||||
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);
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/* test_mf_logsig.c — 宿主单测:多字体「日志跃迁签名」与门控(M52 刷屏整改)。
|
||||
*
|
||||
* 背景:meow_font_reload_multifont() 被 launcher 主循环每 2s 调一次,旧实现每次无条件
|
||||
* 打 `multi_font: enabled, ...`(真机 5297/6603 行 = 80%)。整改后只在配置签名变化时打。
|
||||
* 本测试守两件事:
|
||||
* (1) 签名对**所有**能改变行为的输入敏感(on / global / 各类别 / distinct / truncated);
|
||||
* (2) 输入不变时签名恒定 —— 即 60s 空转(30 次调用)只产生 1 条日志。
|
||||
*
|
||||
* 编译运行:gcc -I. -Wall -Wextra test_mf_logsig.c -o test_mf_logsig && ./test_mf_logsig
|
||||
* (不依赖 launcher 其它模块 / 不依赖设备;纯头文件逻辑)
|
||||
*/
|
||||
#include "mf_logsig.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define NCLS 6
|
||||
|
||||
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); }
|
||||
}
|
||||
|
||||
/* 真机 settings.conf 的多字体形态(2026-11-11):
|
||||
* display.multi_font = 1
|
||||
* font.file = MeowOS/fonts/ztest/regular/font.png → global = ztest
|
||||
* font.class_num / font.class_latin = MeowOS/fonts/google_sans/regular/font.png
|
||||
* → distinct = 2(ztest + google_sans) */
|
||||
#define G_ZTEST "/usr/data/mnt/sd_0/MeowOS/fonts/ztest/regular/font.png"
|
||||
#define C_GOOGLE "/usr/data/mnt/sd_0/MeowOS/fonts/google_sans/regular/font.png"
|
||||
#define C_MISANS "/usr/data/mnt/sd_0/MeowOS/fonts/misans/regular/font.png"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* ---- 1. 真机形态:同输入 → 同签名(1000 次不变,这就是「空转不打日志」的根基)---- */
|
||||
const char *base[NCLS] = { NULL, NULL, C_GOOGLE, C_GOOGLE, NULL, NULL };
|
||||
uint64_t s0 = mf_logsig(1, G_ZTEST, base, NCLS, 2, 0);
|
||||
int stable = 1;
|
||||
for (int i = 0; i < 1000; i++)
|
||||
if (mf_logsig(1, G_ZTEST, base, NCLS, 2, 0) != s0) stable = 0;
|
||||
check("same config -> same sig (1000x)", stable, 1);
|
||||
|
||||
/* ---- 2. 每一条输入变化都必须换签名 ---- */
|
||||
check("on 1->0 changes sig", mf_logsig(0, G_ZTEST, base, NCLS, 2, 0) != s0, 1);
|
||||
check("global font changes sig",
|
||||
mf_logsig(1, "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.png",
|
||||
base, NCLS, 2, 0) != s0, 1);
|
||||
{
|
||||
const char *c[NCLS] = { NULL, NULL, C_MISANS, C_GOOGLE, NULL, NULL };
|
||||
check("class_num changes sig", mf_logsig(1, G_ZTEST, c, NCLS, 2, 0) != s0, 1);
|
||||
}
|
||||
check("distinct 2->1 changes sig", mf_logsig(1, G_ZTEST, base, NCLS, 1, 0) != s0, 1);
|
||||
check("truncated 0->1 changes sig", mf_logsig(1, G_ZTEST, base, NCLS, 2, 1) != s0, 1);
|
||||
check("config-fail (-1) changes sig", mf_logsig(1, G_ZTEST, base, NCLS, -1, 0) != s0, 1);
|
||||
|
||||
/* ---- 3. 空配置项形态:NULL 与 "" 等价(settings 空值 = 该类回退全局)---- */
|
||||
{
|
||||
const char *a[NCLS] = { NULL, NULL, NULL, NULL, NULL, NULL };
|
||||
const char *b[NCLS] = { "", "", "", "", "", "" };
|
||||
check("NULL == \"\"", mf_logsig(1, G_ZTEST, a, NCLS, 1, 0) ==
|
||||
mf_logsig(1, G_ZTEST, b, NCLS, 1, 0), 1);
|
||||
check("null cpng array tolerated", mf_logsig(1, G_ZTEST, NULL, NCLS, 1, 0) > 0, 1);
|
||||
}
|
||||
|
||||
/* ---- 4. 字段边界不串味:("ab","c") 与 ("a","bc") 不能撞 ---- */
|
||||
{
|
||||
const char *x[NCLS] = { "ab", "c", NULL, NULL, NULL, NULL };
|
||||
const char *y[NCLS] = { "a", "bc", NULL, NULL, NULL, NULL };
|
||||
check("field boundary no collision",
|
||||
mf_logsig(1, G_ZTEST, x, NCLS, 2, 0) != mf_logsig(1, G_ZTEST, y, NCLS, 2, 0), 1);
|
||||
}
|
||||
|
||||
/* ---- 5. 门控:60s 空转(2s 一次 × 30)只打 1 条;改配置再打 1 条;改回来再打 1 条 ---- */
|
||||
{
|
||||
uint64_t last = 0;
|
||||
int valid = 0, emits = 0;
|
||||
for (int i = 0; i < 30; i++) { /* 首轮 + 29 轮空转 */
|
||||
uint64_t sig = mf_logsig(1, G_ZTEST, base, NCLS, 2, 0);
|
||||
if (mf_logsig_changed(&last, &valid, sig)) emits++;
|
||||
}
|
||||
check("60s idle @2s -> exactly 1 log line", emits, 1);
|
||||
{
|
||||
const char *c[NCLS] = { NULL, NULL, C_MISANS, C_GOOGLE, NULL, NULL };
|
||||
uint64_t sig = mf_logsig(1, G_ZTEST, c, NCLS, 2, 0);
|
||||
if (mf_logsig_changed(&last, &valid, sig)) emits++;
|
||||
}
|
||||
check("class change -> 2nd log line", emits, 2);
|
||||
for (int i = 0; i < 30; i++) { /* 再空转 60s:不应再增长 */
|
||||
uint64_t sig = mf_logsig(1, G_ZTEST, base, NCLS, 2, 0);
|
||||
if (mf_logsig_changed(&last, &valid, sig)) emits++;
|
||||
}
|
||||
check("back to original -> 3rd line, then silent", emits, 3);
|
||||
/* 真实行数:旧实现 60s = 30 行(每 2s 一条),新实现 60s = 0 行(稳定后) */
|
||||
printf(" (旧实现 60s 空转 = 30 行;新实现稳态 = 0 行,本用例全程 %d 行)\n", emits);
|
||||
}
|
||||
|
||||
/* ---- 6. 「class 字体缺失」WARN 的跃迁门控(r1 评审 P2-1 的验收断言)----
|
||||
* 场景:某个 font.class_* 配置了但字体文件缺失 → 该类别回退全局(行为不变),
|
||||
* 旧实现每 2s 每类各打一条(最多 6 行/2s = 3 行/s)。要求:
|
||||
* 缺失集合不变 → 60s(30 次) 只出现 1 次;
|
||||
* 缺失的类别变了(另一个 class 也缺失)→ 再出现 1 次(不是一次性闩锁);
|
||||
* 某类恢复后再缺失 → 也算新跃迁,再 1 次。 */
|
||||
{
|
||||
unsigned logged = 0;
|
||||
int warns = 0;
|
||||
/* (a) 只有 class_num(2) 缺失:连续 30 次(60s @2s) */
|
||||
for (int i = 0; i < 30; i++)
|
||||
if (mf_missing_warn_mask(&logged, 1u << 2)) warns++;
|
||||
check("class missing 60s @2s -> exactly 1 WARN", warns, 1);
|
||||
/* (b) 缺失集合不变(再 30 次):不增长 */
|
||||
for (int i = 0; i < 30; i++)
|
||||
if (mf_missing_warn_mask(&logged, 1u << 2)) warns++;
|
||||
check("class missing unchanged -> no new WARN", warns, 1);
|
||||
/* (c) class_latin(3) 也变缺失:只多 1 次(且只针对新缺失的那一类) */
|
||||
{
|
||||
unsigned fresh = mf_missing_warn_mask(&logged, (1u << 2) | (1u << 3));
|
||||
warns += fresh ? 1 : 0;
|
||||
check("new missing class -> exactly 1 more WARN", fresh, 1u << 3);
|
||||
}
|
||||
check("total WARN after set change", warns, 2);
|
||||
/* (d) class_num(2) 恢复 → 位图缩小但不再打;再次缺失 → 重新打一次 */
|
||||
check("recovered class -> no WARN", mf_missing_warn_mask(&logged, 1u << 3), 0);
|
||||
check("re-missing after recovery -> WARN again",
|
||||
mf_missing_warn_mask(&logged, (1u << 2) | (1u << 3)), 1u << 2);
|
||||
/* (e) 全部恢复 → 位图清零、静默 */
|
||||
check("none missing -> no WARN", mf_missing_warn_mask(&logged, 0), 0);
|
||||
check("logged mask reset", logged, 0);
|
||||
printf(" (旧实现 60s @2s 单类缺失 = 30 行;新实现 = 1 行)\n");
|
||||
}
|
||||
|
||||
printf("\n%d pass, %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 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_bt(int on) { (void)on; }
|
||||
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)
|
||||
{ (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 链接所需;测试不触碰这些路径) ---------- */
|
||||
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)
|
||||
{
|
||||
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;
|
||||
memset(&f, 0, sizeof f);
|
||||
@@ -236,7 +277,26 @@ int main(void)
|
||||
assert(strstr(err, "add_network") && "connect 失败带回原因");
|
||||
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()。 */
|
||||
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");
|
||||
@@ -254,6 +314,21 @@ int main(void)
|
||||
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");
|
||||
unlink(sock);
|
||||
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)
|
||||
* (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 下,无其它依赖):
|
||||
* 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, 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 死亡触发主屏重绘:崩溃必须走统一退出路径 ---- */
|
||||
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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/* test_popen_tmo.c — M65 P1-1 / P1-2 宿主验证(本程序非生产代码)。
|
||||
*
|
||||
* 被测对象是 popen_tmo.h(生产代码 launcherd/net.c 的 popen_read_tmo 与 audiod/meow_audiod.c
|
||||
* 的 popen_read_tmo 都只用这一个读循环)。每组断言都配**反向控制**:把 M59 审计里的旧实现
|
||||
* 原样抄进来(old_read / old_static_read_cmd),证明旧代码在同样输入下确实失败 ——
|
||||
* 只看「新代码返回了」不算证明,必须同时证明「旧代码不会返回」。
|
||||
*
|
||||
* 反向控制 A(P1-1 永不超时):持续输出但**非常慢**的生产者(2B/20ms)。旧循环的 deadline
|
||||
* 只在 poll 超时分支检查 → 永不触发;新循环在**顶部**检查 → 到期就返回。
|
||||
* 用慢生产者而不是 `yes`:审计里 `yes` 版 5s 就涨到 657MB(设备 56MB),在这里会打死宿主。
|
||||
* 反向控制 B(P1-1 无上限):让子进程输出 300KB(有界,宿主安全)。旧循环照单全收
|
||||
* (>256KB 也不管),新实现在硬上限处**主动**返回 OVERFLOW —— 上限确实是新加的。
|
||||
* 反向控制 C(P1-2 共享缓冲):两个线程用旧式 static 缓冲交错读,A 读到的是 B 的内容
|
||||
* (这就是 audiod 里 amixer cget numid=9 被改写一半的模型);同样的交错用调用方缓冲
|
||||
* 则各读各的。
|
||||
*
|
||||
* 编译(在 patch_v2/meow_launcherd 下):cc -Wall -Wextra test_popen_tmo.c -o test_popen_tmo -lpthread
|
||||
* 运行:./test_popen_tmo
|
||||
*/
|
||||
#include "popen_tmo.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if (!(cond)) { printf(" FAIL: %s (line %d)\n", #cond, __LINE__); g_fail++; } \
|
||||
else { printf(" ok : %s\n", #cond); } } while (0)
|
||||
|
||||
/* 持续输出且**永不退出**的生产者:先一次性灌 32KB 把 pipe 填满(保证 poll 每次都有数据,
|
||||
* 旧循环因此永远进不到 deadline 分支),之后 1B/10ms 慢慢滴(内存涨不起来,反向控制可以在
|
||||
* 宿主上安全地跑到被外部杀掉)。注意不能用 `yes`:审计里 5s 就 657MB,宿主会被打死。 */
|
||||
static char g_slow_cmd[512];
|
||||
static void build_slow_producer(void)
|
||||
{
|
||||
char chunk[129];
|
||||
memset(chunk, 'x', 128);
|
||||
chunk[128] = 0;
|
||||
snprintf(g_slow_cmd, sizeof g_slow_cmd,
|
||||
"c='%s'; i=0; while [ $i -lt 256 ]; do printf '%%s' \"$c\"; i=$((i+1)); done; "
|
||||
"while :; do printf 'y'; sleep 0.01; done", chunk);
|
||||
}
|
||||
|
||||
/* fork 一个生产者,返回读端 fd(非阻塞),*pid 是子进程。失败返回 -1。 */
|
||||
static int spawn_cmd(const char *cmd, pid_t *pid)
|
||||
{
|
||||
int p[2];
|
||||
if (pipe(p) != 0) return -1;
|
||||
pid_t c = fork();
|
||||
if (c < 0) { close(p[0]); close(p[1]); return -1; }
|
||||
if (c == 0) {
|
||||
close(p[0]);
|
||||
dup2(p[1], 1);
|
||||
close(p[1]);
|
||||
execl("/bin/sh", "sh", "-c", cmd, (char *)0);
|
||||
_exit(127);
|
||||
}
|
||||
close(p[1]);
|
||||
fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK);
|
||||
*pid = c;
|
||||
return p[0];
|
||||
}
|
||||
|
||||
static void kill_reap(pid_t pid)
|
||||
{
|
||||
if (pid <= 0) return;
|
||||
kill(pid, SIGKILL);
|
||||
(void)popen_tmo_reap_bounded(pid);
|
||||
}
|
||||
|
||||
/* ---------------- 反向控制:旧 launcherd/net.c 的读循环(原文,仅 now_ms→popen_tmo_now_ms) ---------------- */
|
||||
static char *old_read(int fd, int tmo_ms)
|
||||
{
|
||||
char *out = NULL; size_t cap = 0, len = 0;
|
||||
char tmp[256];
|
||||
int done = 0;
|
||||
long long deadline = popen_tmo_now_ms() + tmo_ms;
|
||||
while (!done) {
|
||||
struct pollfd pfd = { fd, POLLIN, 0 };
|
||||
int pr = poll(&pfd, 1, 25);
|
||||
if (pr < 0) break;
|
||||
if (pr == 0) { if (popen_tmo_now_ms() > deadline) break; continue; } /* ← 只在 poll 超时分支看 deadline */
|
||||
ssize_t r = read(fd, tmp, sizeof tmp);
|
||||
if (r > 0) {
|
||||
if (len + (size_t)r + 1 > cap) {
|
||||
size_t nc = cap ? cap * 2 : 512; /* ← realloc 没有字节上限 */
|
||||
char *nn = realloc(out, nc);
|
||||
if (!nn) { free(out); return NULL; }
|
||||
out = nn; cap = nc;
|
||||
}
|
||||
memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0;
|
||||
} else if (r == 0) {
|
||||
done = 1;
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* 在子进程里跑旧循环(旧循环可能吃很多内存,隔一层进程隔离)。返回 1=wait_ms 内自己退出,
|
||||
* 0=超时仍在跑(已被 kill+reap)。*out_len = 旧循环读回的字节数(-1=返回 NULL/没跑完)。 */
|
||||
static int old_loop_in_child(const char *cmd, int tmo_ms, int wait_ms, long *out_len)
|
||||
{
|
||||
int rp[2];
|
||||
if (pipe(rp) != 0) return -1;
|
||||
fflush(NULL);
|
||||
pid_t c = fork();
|
||||
if (c < 0) { close(rp[0]); close(rp[1]); return -1; }
|
||||
if (c == 0) {
|
||||
close(rp[0]);
|
||||
pid_t pp = 0;
|
||||
int fd = spawn_cmd(cmd, &pp);
|
||||
char *o = (fd >= 0) ? old_read(fd, tmo_ms) : NULL;
|
||||
if (fd >= 0) close(fd);
|
||||
if (pp > 0) kill_reap(pp);
|
||||
long l = o ? (long)strlen(o) : -1;
|
||||
if (write(rp[1], &l, sizeof l) < 0) { }
|
||||
_exit(o ? 0 : 7);
|
||||
}
|
||||
close(rp[1]);
|
||||
int st = 0, finished = 0;
|
||||
for (int i = 0; i < wait_ms / 10; i++) {
|
||||
if (waitpid(c, &st, WNOHANG) == c) { finished = 1; break; }
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
if (!finished) kill_reap(c);
|
||||
long l = -1;
|
||||
if (read(rp[0], &l, sizeof l) != (ssize_t)sizeof l) l = -1;
|
||||
close(rp[0]);
|
||||
if (out_len) *out_len = l;
|
||||
return finished;
|
||||
}
|
||||
|
||||
/* ---------------- T1:deadline 在循环顶部(P1-1 主案) ---------------- */
|
||||
static void t1_deadline_at_top(void)
|
||||
{
|
||||
printf("== T1 持续输出子进程:deadline 必须在循环顶部(tmo=300ms) ==\n");
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd(g_slow_cmd, &pid);
|
||||
CHECK(fd >= 0);
|
||||
char *buf = NULL; size_t cap = 0, len = 0;
|
||||
long long t0 = popen_tmo_now_ms();
|
||||
int rc = popen_tmo_read_all(fd, 300, &buf, &cap, POPEN_TMO_MAX, &len, 1);
|
||||
long long el = popen_tmo_now_ms() - t0;
|
||||
close(fd);
|
||||
kill_reap(pid);
|
||||
printf(" [info] 新实现:rc=%d len=%zu 滞回=%lldms\n", rc, len, el);
|
||||
CHECK(rc == POPEN_TMO_DEADLINE);
|
||||
CHECK(el >= 300);
|
||||
CHECK(el < 1200); /* 到期就回,不再等子进程 EOF */
|
||||
CHECK(len < POPEN_TMO_MAX); /* 有界:32KB 灌入 + 滴流,不会无限涨 */
|
||||
CHECK(buf != NULL && strlen(buf) == len);
|
||||
free(buf);
|
||||
|
||||
/* 反向控制:同一个输入,旧循环永不返回。 */
|
||||
long oldlen = 0;
|
||||
int finished = old_loop_in_child(g_slow_cmd, 300, 1500, &oldlen);
|
||||
printf(" [info] 旧实现:1500ms 内%s退出(永不返回=BUG)\n", finished ? "已" : "未");
|
||||
CHECK(finished == 0); /* ← 反向控制:旧代码不返回 */
|
||||
}
|
||||
|
||||
/* ---------------- T2:输出硬上限(P1-1 后半 / P2-7) ---------------- */
|
||||
static void t2_hard_cap(void)
|
||||
{
|
||||
printf("== T2 无限输出:grow 模式 64KB 硬上限 / 定长模式写满即超限 ==\n");
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("yes", &pid);
|
||||
CHECK(fd >= 0);
|
||||
char *buf = NULL; size_t cap = 0, len = 0;
|
||||
long long t0 = popen_tmo_now_ms();
|
||||
int rc = popen_tmo_read_all(fd, 5000, &buf, &cap, 64 * 1024, &len, 1); /* tmo 远大于触发点 */
|
||||
long long el = popen_tmo_now_ms() - t0;
|
||||
close(fd);
|
||||
kill_reap(pid);
|
||||
printf(" [info] grow: rc=%d len=%zu cap=%zu 滞回=%lldms\n", rc, len, cap, el);
|
||||
CHECK(rc == POPEN_TMO_OVERFLOW); /* 超限**报错**,不是静默截断 */
|
||||
CHECK(len > 0 && len < 64 * 1024);
|
||||
CHECK(cap <= 64 * 1024);
|
||||
CHECK(el < 1500); /* 是「上限触发」而不是「超时触发」 */
|
||||
free(buf);
|
||||
|
||||
pid_t pid2 = 0;
|
||||
int fd2 = spawn_cmd("yes", &pid2);
|
||||
CHECK(fd2 >= 0);
|
||||
char fixed[1024];
|
||||
size_t len2 = 0;
|
||||
char *p = fixed; size_t cp = sizeof fixed;
|
||||
rc = popen_tmo_read_all(fd2, 5000, &p, &cp, 0, &len2, 0);
|
||||
close(fd2);
|
||||
kill_reap(pid2);
|
||||
printf(" [info] fixed: rc=%d len=%zu\n", rc, len2);
|
||||
CHECK(rc == POPEN_TMO_OVERFLOW);
|
||||
CHECK(len2 == sizeof fixed - 1);
|
||||
CHECK(fixed[sizeof fixed - 1] == 0); /* 总是 NUL 结尾,解析不会越界 */
|
||||
|
||||
/* 反向控制:同一份 300KB 有界输出,旧循环照单全收(>256KB 也返回),没有上限。 */
|
||||
long oldlen = 0;
|
||||
int finished = old_loop_in_child("yes | head -c 307200", 5000, 8000, &oldlen);
|
||||
printf(" [info] 旧实现读到 %ld 字节(上限 %d)\n", oldlen, POPEN_TMO_MAX);
|
||||
CHECK(finished == 1);
|
||||
CHECK(oldlen > POPEN_TMO_MAX); /* ← 反向控制:旧实现没有上限 */
|
||||
/* 同一输入走新实现的**生产上限**(256KB):必须报 OVERFLOW 而不是带回 300KB。 */
|
||||
pid_t pid3 = 0;
|
||||
int fd3 = spawn_cmd("yes | head -c 307200", &pid3);
|
||||
CHECK(fd3 >= 0);
|
||||
char *buf3 = NULL; size_t cap3 = 0, len3 = 0;
|
||||
rc = popen_tmo_read_all(fd3, 5000, &buf3, &cap3, POPEN_TMO_MAX, &len3, 1);
|
||||
close(fd3);
|
||||
kill_reap(pid3);
|
||||
printf(" [info] 新实现同输入:rc=%d len=%zu\n", rc, len3);
|
||||
CHECK(rc == POPEN_TMO_OVERFLOW);
|
||||
CHECK(len3 < POPEN_TMO_MAX);
|
||||
free(buf3);
|
||||
}
|
||||
|
||||
/* ---------------- T3:调用方缓冲 vs static 缓冲(P1-2) ---------------- */
|
||||
static pthread_barrier_t g_b1, g_b2;
|
||||
static char g_shared[4096]; /* 旧实现的 static 缓冲(模型) */
|
||||
static char g_seen_old[64], g_seen_new[64];
|
||||
|
||||
/* 旧 audiod 形态:static 缓冲 + return 指针。 */
|
||||
static char *old_static_read_cmd(const char *cmd)
|
||||
{
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd(cmd, &pid);
|
||||
if (fd < 0) return NULL;
|
||||
char *b = g_shared; size_t cap = sizeof g_shared, len = 0;
|
||||
popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0);
|
||||
close(fd);
|
||||
kill_reap(pid);
|
||||
return g_shared;
|
||||
}
|
||||
|
||||
static void *thr_old_a(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char *o = old_static_read_cmd("echo AAAAAAAAAA");
|
||||
pthread_barrier_wait(&g_b1);
|
||||
pthread_barrier_wait(&g_b2); /* 等 B 读完(旧代码里这就是「A 还没解析」的窗口) */
|
||||
snprintf(g_seen_old, sizeof g_seen_old, "%s", o ? o : "");
|
||||
return NULL;
|
||||
}
|
||||
static void *thr_old_b(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
pthread_barrier_wait(&g_b1);
|
||||
old_static_read_cmd("echo BBBBBBBBBB");
|
||||
pthread_barrier_wait(&g_b2);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *thr_new_a(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char buf[256];
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("echo AAAAAAAAAA", &pid);
|
||||
size_t len = 0;
|
||||
char *b = buf; size_t cap = sizeof buf;
|
||||
if (fd >= 0) { popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0); close(fd); kill_reap(pid); }
|
||||
pthread_barrier_wait(&g_b1);
|
||||
pthread_barrier_wait(&g_b2);
|
||||
snprintf(g_seen_new, sizeof g_seen_new, "%s", buf);
|
||||
return NULL;
|
||||
}
|
||||
static void *thr_new_b(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
pthread_barrier_wait(&g_b1);
|
||||
char buf[256];
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("echo BBBBBBBBBB", &pid);
|
||||
size_t len = 0;
|
||||
char *b = buf; size_t cap = sizeof buf;
|
||||
if (fd >= 0) { popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0); close(fd); kill_reap(pid); }
|
||||
pthread_barrier_wait(&g_b2);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void t3_caller_buffer(void)
|
||||
{
|
||||
printf("== T3 并发读:旧 static 缓冲会串(P1-2),调用方缓冲各归各 ==\n");
|
||||
pthread_t a, b;
|
||||
pthread_barrier_init(&g_b1, NULL, 2);
|
||||
pthread_barrier_init(&g_b2, NULL, 2);
|
||||
g_seen_old[0] = g_seen_new[0] = 0;
|
||||
|
||||
pthread_create(&a, NULL, thr_old_a, NULL);
|
||||
pthread_create(&b, NULL, thr_old_b, NULL);
|
||||
pthread_join(a, NULL); pthread_join(b, NULL);
|
||||
printf(" [info] 旧实现 A 读到:%s\n", g_seen_old);
|
||||
CHECK(strncmp(g_seen_old, "AAAAAAAAAA", 10) != 0); /* ← 反向控制:A 读到的是 B 的内容 */
|
||||
|
||||
pthread_barrier_destroy(&g_b1);
|
||||
pthread_barrier_destroy(&g_b2);
|
||||
pthread_barrier_init(&g_b1, NULL, 2);
|
||||
pthread_barrier_init(&g_b2, NULL, 2);
|
||||
pthread_create(&a, NULL, thr_new_a, NULL);
|
||||
pthread_create(&b, NULL, thr_new_b, NULL);
|
||||
pthread_join(a, NULL); pthread_join(b, NULL);
|
||||
printf(" [info] 新实现 A 读到:%s", g_seen_new);
|
||||
CHECK(strncmp(g_seen_new, "AAAAAAAAAA", 10) == 0); /* 调用方缓冲:不串 */
|
||||
pthread_barrier_destroy(&g_b1);
|
||||
pthread_barrier_destroy(&g_b2);
|
||||
}
|
||||
|
||||
/* ---------------- T4:有界收尸 ---------------- */
|
||||
static void t4_reap_bounded(void)
|
||||
{
|
||||
printf("== T4 收尸有界:挂住的子进程 SIGKILL 后 ≤700ms 收完 ==\n");
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("sleep 60", &pid);
|
||||
CHECK(fd >= 0);
|
||||
long long t0 = popen_tmo_now_ms();
|
||||
int ok = popen_tmo_reap_bounded(pid);
|
||||
long long el = popen_tmo_now_ms() - t0;
|
||||
close(fd);
|
||||
printf(" [info] reap_bounded=%d 滞回=%lldms\n", ok, el);
|
||||
CHECK(ok == 1);
|
||||
CHECK(el < 700);
|
||||
errno = 0;
|
||||
CHECK(waitpid(-1, NULL, WNOHANG) == -1 && errno == ECHILD); /* 没有残留子进程 */
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL);
|
||||
build_slow_producer();
|
||||
t1_deadline_at_top();
|
||||
t2_hard_cap();
|
||||
t3_caller_buffer();
|
||||
t4_reap_bounded();
|
||||
if (g_fail) { printf("\nPOPEN_TMO TEST: %d FAIL\n", g_fail); return 1; }
|
||||
printf("\nPOPEN_TMO TEST: ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -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 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) {
|
||||
meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh);
|
||||
int ty = y + (SET_ITEM_H - thh) / 2;
|
||||
if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary);
|
||||
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
|
||||
if (ty >= 0) meow_ui_text(cx + 16, ty, r->label, mt->font_body, mt->primary);
|
||||
return;
|
||||
}
|
||||
if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer);
|
||||
else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26);
|
||||
meow_gfx_fill_round(cx, cy, cw, ch, 12, mt->surfaceContainer);
|
||||
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 rgt = cx + cw - 16;
|
||||
switch (r->ctrl) {
|
||||
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();
|
||||
if (sw) {
|
||||
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_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: {
|
||||
/* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由
|
||||
* 真机 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);
|
||||
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;
|
||||
}
|
||||
case SC_LIST: {
|
||||
meow_ui_text(x + 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(cx + 16, ly, r->label, mt->font_body, textcol);
|
||||
meow_ui_text(rgt - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol);
|
||||
break;
|
||||
}
|
||||
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);
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
|
||||
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90);
|
||||
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_gfx_fill_round_alpha(cx, cy, cw, ch, 12, mt->onSurface, 90);
|
||||
}
|
||||
|
||||
static void mk_widgets(void)
|
||||
@@ -247,7 +252,7 @@ static void draw_all(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
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); }
|
||||
}
|
||||
|
||||
@@ -305,16 +310,17 @@ int main(void)
|
||||
meow_ui_activity_push(SET_SENTINEL);
|
||||
const meow_theme *th = meow_theme_get();
|
||||
draw_all();
|
||||
/* 段头行整体填 surfaceContainerHigh(覆盖列表底 surfaceVariant)→ 对列表底背景即非背景;
|
||||
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「段头底 vs 列表底」判渲染而非文字像素。 */
|
||||
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
|
||||
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant);
|
||||
CHECK(hdrcnt > 0, "section header band renders (hdr=%d)", hdrcnt);
|
||||
CHECK(rowcnt > 40, "row band renders (title/value/switch) cnt=%d", rowcnt);
|
||||
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景 */
|
||||
/* MD3 容器化(P0):段头=surface 上的 primary 段标签(无满幅 band/卡,宿主文本为 no-op →
|
||||
* 段头区应回 surface);普通行=surfaceContainer 圆角卡(region_nonbg 应见大量非 surface 像素)。
|
||||
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「卡/段头 vs 列表底」判渲染而非文字像素。 */
|
||||
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
|
||||
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
|
||||
CHECK(hdrcnt == 0, "section header no-band(label-on-surface) nb=%d", hdrcnt);
|
||||
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 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ static void mk_widgets(void)
|
||||
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_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);
|
||||
meow_ui_activity_pop();
|
||||
}
|
||||
@@ -153,12 +154,14 @@ int main(void)
|
||||
meow_ui_activity_push(SUB_SENTINEL);
|
||||
const meow_theme *th = meow_theme_get();
|
||||
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 rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
|
||||
char nm[80];
|
||||
snprintf(nm, sizeof nm, "section header renders (band=%d)", hdrcnt);
|
||||
CHECK(hdrcnt > 0, "%s", nm);
|
||||
snprintf(nm, sizeof nm, "row renders (title hdr/switch/arrow) cnt=%d", rowcnt);
|
||||
snprintf(nm, sizeof nm, "section header no-band(label-on-surface) nb=%d", hdrcnt);
|
||||
CHECK(hdrcnt == 0, "%s", nm);
|
||||
snprintf(nm, sizeof nm, "row renders (card) cnt=%d", rowcnt);
|
||||
CHECK(rowcnt > 40, "%s", nm);
|
||||
|
||||
/* ============ P2 listview 命中(行号 + right 操作区) ============ */
|
||||
@@ -190,6 +193,7 @@ int main(void)
|
||||
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_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 */
|
||||
CHECK(s != NULL, "meow_scroll_wrap(listview)");
|
||||
int maxs = meow_listview_max_scroll(scroll_lv);
|
||||
|
||||
@@ -907,6 +907,52 @@ int meow_ipc_conn_alive(int fd)
|
||||
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
|
||||
* 循环 meow_notif_open_take 读取并清除)。仅当 g_app_session_active(App 前台会话)期间才置位,
|
||||
* 否则回 ERR(无 App 前台,不展开)。 */
|
||||
@@ -1099,6 +1145,7 @@ static void *ipc_thread(void *arg)
|
||||
if (ipc_conn_read(cc) < 0) {
|
||||
logmsg("ipc: conn %d closed", cc->fd);
|
||||
close(cc->fd);
|
||||
volpage_fd_clear_fd(cc->fd); /* E:清已断连接的翻页 fd 映射 */
|
||||
for (int j = i; j < nfds - 1; j++) {
|
||||
fds[j] = fds[j + 1];
|
||||
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);
|
||||
h.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||
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;
|
||||
a.code = MEOW_OK;
|
||||
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);
|
||||
k.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||
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);
|
||||
a.code = MEOW_OK;
|
||||
} 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);
|
||||
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;
|
||||
}
|
||||
/* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
* 【M6.0 迁移】全部查询/操作走 netmgr IPC(netmgr_ipc.c 的 nm_* 封装),netmgrd 是 WiFi 权威;
|
||||
* 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"
|
||||
|
||||
@@ -19,6 +24,7 @@
|
||||
#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */
|
||||
#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */
|
||||
#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */
|
||||
#define WF_ACT_MANUAL 2000 /* 「手动添加隐藏网络」动作行(SUB_ACT,connecth) */
|
||||
|
||||
typedef struct {
|
||||
meow_net_t saved[S_NET_MAX];
|
||||
@@ -26,32 +32,96 @@ typedef struct {
|
||||
int saved_n, scan_n;
|
||||
char ssid[64];
|
||||
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; /* 扫描触发节流 */
|
||||
} 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)
|
||||
{
|
||||
(void)ctx;
|
||||
int v = 1;
|
||||
if (meow_settings_get_bool("network.wifi_enabled", &v) != 0) v = 1;
|
||||
*on = v;
|
||||
wifi_ctx_t *c = ctx;
|
||||
*on = wifi_state_now(c, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/* 顶栏开关切换:写设置 + 开/关网卡(netmgr on/off,回退直连)+ 触发重扫 + 同步状态栏 */
|
||||
/* 顶栏开关切换:落盘意图 → 开/关网卡(netmgr on/off,回退直连)→ 有界等待真实态收敛 →
|
||||
* 真实态定稿/回滚 + 可见错误提示。 */
|
||||
static void wifi_top_sw_set(void *ctx, int on)
|
||||
{
|
||||
wifi_ctx_t *c = ctx;
|
||||
meow_settings_set_bool("network.wifi_enabled", on);
|
||||
nm_wifi_toggle(on);
|
||||
g_sys.wifi_on = on;
|
||||
paint_sb_right(); /* 状态栏 WiFi 图标随开关即时刷新 */
|
||||
unsigned long long t_click = now_ms(); /* 上限从点击起算(含下发耗时,见参数注释) */
|
||||
int want = on ? 1 : 0;
|
||||
int last_real = wifi_state_now(c, 0);
|
||||
meow_settings_set_bool("network.wifi_enabled", want); /* 意图持久化 */
|
||||
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)
|
||||
{
|
||||
wifi_ctx_t *c = ctx;
|
||||
c->sw_state = wifi_state_now(c, 0); /* 真实射频态(三态);未知时下面显式说明 */
|
||||
c->connected = nm_wifi_connected();
|
||||
if (c->connected) {
|
||||
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].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) {
|
||||
@@ -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;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/* SDK IME 输密码(PW 模式=ASCII 软键盘输入,现有输入法复用;回车提交/ESC或点背景取消)。
|
||||
/* SDK IME 通用文本输入(MEOW_IME_ASCII:QWERTY,顶条「中」可切拼音输中文 SSID)。
|
||||
* 返回 1=已输入;0=取消。 */
|
||||
static int wifi_ask_psk(char *psk, int maxlen)
|
||||
static int wifi_ask_text(char *out, int maxlen)
|
||||
{
|
||||
meow_ime_ctx ic;
|
||||
memset(&ic, 0, sizeof ic);
|
||||
@@ -136,36 +222,72 @@ static int wifi_ask_psk(char *psk, int maxlen)
|
||||
ic.maxlen = maxlen;
|
||||
ic.buf[0] = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/* 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=重绘。 */
|
||||
static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx)
|
||||
{
|
||||
if (scan_idx < 0 || scan_idx >= c->scan_n) return 0;
|
||||
meow_net_t *s = &c->scan[scan_idx];
|
||||
if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) {
|
||||
const char *txt[1]; txt[0] = "隐藏网络无法直接连接";
|
||||
s_dd_modal("附近网络", txt, 1, -1);
|
||||
wifi_manual_add(c); /* bug2:占位隐藏网络 → 手动录入(SSID 待输) */
|
||||
return 1;
|
||||
}
|
||||
int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open"));
|
||||
char psk[96] = "";
|
||||
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;
|
||||
wifi_connecting_banner(s->name); /* bug1:阻塞前横幅,避免「点完无反馈」 */
|
||||
int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err);
|
||||
char line[192];
|
||||
const char *txt[1]; txt[0] = line;
|
||||
if (rc == MEOW_OK) {
|
||||
snprintf(line, sizeof line, "连接成功:%s%s", s->name,
|
||||
err[0] ? "" : "(已启用)");
|
||||
} else {
|
||||
snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误");
|
||||
}
|
||||
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
|
||||
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
|
||||
logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err);
|
||||
/* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */
|
||||
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)
|
||||
{
|
||||
(void)idx;
|
||||
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),不走这里 */
|
||||
int sidx = act - WF_ACT_SAVED;
|
||||
if (sidx < 0 || sidx >= c->saved_n) return 0;
|
||||
if (c->saved[sidx].id < 0) return 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);
|
||||
char line[192]; const char *txt[1]; txt[0] = line;
|
||||
if (rc == MEOW_OK) snprintf(line, sizeof line, "已连接:%s", c->saved[sidx].name);
|
||||
else snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误");
|
||||
/* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
|
||||
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);
|
||||
s_dd_modal(c->saved[sidx].name, txt, 1, -1);
|
||||
nm_wifi_scan_trigger();
|
||||
|
||||
@@ -9,25 +9,44 @@
|
||||
# 用法:
|
||||
# ./build_netmgrd.sh # 交叉编译 → patch_v2/meow_netmgrd/meow_netmgrd(mipsel 静态)
|
||||
# ./build_netmgrd.sh host # 宿主编译 → patch_v2/meow_netmgrd/meow_netmgrd_host
|
||||
# OUT=/path/to/out ./build_netmgrd.sh # 产物另存(M49 起:仓库里不提交二进制,验证产物放仓库外)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
ROOT="$HOME/Project/R3ProII"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
# 仓库根按本脚本位置推导(<root>/patch_v2/meow_netmgrd/),MEOW_ROOT 可覆盖。
|
||||
# 旧版硬编码 $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_*)
|
||||
OUT="${OUT:-.}" # 产物目录:默认脚本目录;设了则在交叉编译后把产物移过去
|
||||
[ -d "$OUT" ] || mkdir -p "$OUT"
|
||||
|
||||
if [ "$1" = "host" ]; then
|
||||
HOST_CC="${CC_HOST:-cc}"
|
||||
# -Wno-format-truncation:宿主 glibc 对固定缓冲 snprintf 的「安全截断」误报(musl 交叉不报)。
|
||||
"$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 =="
|
||||
ls -la meow_netmgrd_host
|
||||
file meow_netmgrd_host
|
||||
ls -la "$OUT/meow_netmgrd_host"
|
||||
file "$OUT/meow_netmgrd_host"
|
||||
exit 0
|
||||
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) =="
|
||||
ls -la meow_netmgrd
|
||||
md5sum meow_netmgrd
|
||||
file meow_netmgrd
|
||||
ls -la "$OUT/meow_netmgrd"
|
||||
md5sum "$OUT/meow_netmgrd"
|
||||
file "$OUT/meow_netmgrd"
|
||||
Binary file not shown.
+1298
-286
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,108 @@
|
||||
/* nm_wifi_args.h — wpa_cli 参数安全(M64 P1-1:SSID/PSK → root 命令注入)。
|
||||
*
|
||||
* 修复前:SSID/PSK 被拼成 `set_network <id> ssid '"<ssid>"'`,再交给
|
||||
* `execl("/bin/sh","sh","-c",cmd)` 执行(meow_netmgrd.c:3307/3154/325;同一实现复制在
|
||||
* meow_launcherd/netmgr_ipc.c:475 的直连回退)。单引号里套双引号,SSID 自己带 `'` 就能
|
||||
* 闭合引号——附近恶意 AP 广播 SSID=`a' ; id ; '`,用户点连接即 root 执行。
|
||||
*
|
||||
* 修复分两层,本头只放「可在宿主上直接单测」的纯函数:
|
||||
* 1) 彻底层:调用方改用 fork+execv 传 argv(见 meow_netmgrd.c:wpa_exec /
|
||||
* netmgr_ipc.c:nm_direct_wpa_exec)。不再有 shell,`' ; ` $ & | < >` 全部只是
|
||||
* 普通字节,既不需要也不敢拒绝("Bob's WiFi" 是合法 SSID)。
|
||||
* 2) 约束层:SSID/PSK 按 802.11 与 wpa_supplicant 语法收口(长度、控制字符);
|
||||
* 拼 wpa 字面量时转义 `"` 与 `\`——这两个字符破坏的是 supplicant 的引号解析,
|
||||
* 与 shell 无关,属于正确性要求。
|
||||
*
|
||||
* 两份实现(netmgrd 与 launcherd 回退)共用本头:只修一处等于没修——launcherd 那条
|
||||
* 路径正是 netmgrd 不可达时的回退,攻击者可以制造 netmgrd 不可达(M64 任务书 P1-1)。
|
||||
*/
|
||||
#ifndef NM_WIFI_ARGS_H
|
||||
#define NM_WIFI_ARGS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define NM_WIFI_SSID_MAX 32 /* 802.11 SSID 字节上限 */
|
||||
#define NM_WIFI_PSK_MAX 63 /* WPA passphrase 上限(8..63 字符);64 位 hex PSK 另算 */
|
||||
|
||||
/* wpa_supplicant 的 ctrl_iface 是行协议:值里出现 \n/\r 会截断或伪造命令,
|
||||
* 其它控制字符也进不了 SSID/PSK 的合法字符集。 */
|
||||
static inline int nm_wifi_ctl_free(const char *s, size_t n)
|
||||
{
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (c < 0x20 || c == 0x7f) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int nm_wifi_ssid_ok(const char *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
size_t n = strlen(s);
|
||||
if (n == 0 || n > NM_WIFI_SSID_MAX) return 0;
|
||||
return nm_wifi_ctl_free(s, n);
|
||||
}
|
||||
|
||||
/* 空串 = 开放网络(调用方按 key_mgmt NONE 处理);≤63 字节口令;64 位 hex PSK。
|
||||
* 不强制 8 字符下限:WPA 会自行拒绝过短口令,而面板/设置页对「密码错」的既有文案依赖
|
||||
* 把它交给 supplicant 判定(M64 只收口注入面与行协议,不改失败语义)。 */
|
||||
static inline int nm_wifi_psk_ok(const char *s)
|
||||
{
|
||||
if (!s || !s[0]) return 1;
|
||||
size_t n = strlen(s);
|
||||
if (!nm_wifi_ctl_free(s, n)) return 0;
|
||||
if (n <= NM_WIFI_PSK_MAX) return 1;
|
||||
if (n == 64) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char c = s[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 把值拼成 wpa_supplicant 的字面量 `"..."`(ssid/psk 这两个字段要求带引号)。
|
||||
* 只转义 `"` 与 `\`——它们在 supplicant 的 parse_string 里有特殊含义。
|
||||
* 返回 0 成功;-1 = 含控制字符或缓冲不足。 */
|
||||
static inline int nm_wpa_quote(const char *val, char *out, size_t cap)
|
||||
{
|
||||
if (!val || !out) return -1;
|
||||
size_t n = strlen(val);
|
||||
if (!nm_wifi_ctl_free(val, n)) return -1;
|
||||
size_t need = 3; /* 开引号 + 闭引号 + NUL */
|
||||
for (size_t i = 0; i < n; i++) need += (val[i] == '"' || val[i] == '\\') ? 2 : 1;
|
||||
if (need > cap) return -1;
|
||||
size_t o = 0;
|
||||
out[o++] = '"';
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char c = val[i];
|
||||
if (c == '"' || c == '\\') out[o++] = '\\';
|
||||
out[o++] = c;
|
||||
}
|
||||
out[o++] = '"';
|
||||
out[o] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 组装 `set_network <id> <field> <"value">` 的 argv(argv[0] 由调用方补 wpa_cli 路径)。
|
||||
* 生产代码(netmgrd wpa_set_string / launcherd nm_direct_wpa_set)与宿主测试共用这一份,
|
||||
* 保证测试断言的就是实际下发的那条命令。返回 0 成功;-1 = 值不合法/缓冲不足。 */
|
||||
static inline int nm_wpa_set_argv(char *idbuf, size_t idcap, char *quoted, size_t qcap,
|
||||
int id, const char *field, const char *val, char *argv[5])
|
||||
{
|
||||
if (!field || !argv) return -1;
|
||||
if (nm_wpa_quote(val, quoted, qcap) < 0) return -1;
|
||||
snprintf(idbuf, idcap, "%d", id);
|
||||
argv[0] = (char *)"set_network";
|
||||
argv[1] = idbuf;
|
||||
argv[2] = (char *)field;
|
||||
argv[3] = quoted;
|
||||
argv[4] = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* NM_WIFI_ARGS_H */
|
||||
@@ -1,12 +1,20 @@
|
||||
/* test_bt_ctl.c — meow_netmgrd 蓝牙 P1 权威收口 纯逻辑宿主自测(本程序非生产代码)。
|
||||
/* test_bt_ctl.c — meow_netmgrd 蓝牙控制面 纯逻辑宿主自测(本程序非生产代码)。
|
||||
*
|
||||
* 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问
|
||||
* FILE-STATIC 函数/全局:bt_cmd_is_write / bt_write_authorize / bt_ctl_execute(connect/disconnect/
|
||||
* pair/unpair/connected/a2dp/devices/status)。工具经桩脚本(g_bluetoothctl/g_hcitool/g_bluealsa_cli/
|
||||
* g_bt_scan_cache 指向 $T/桩)覆盖,不依赖真实蓝牙栈。
|
||||
* FILE-STATIC 函数/全局:三个纯解析器(bt_scan_line_parse / bt_pcm_paths_parse+bt_pcm_info_parse /
|
||||
* bt_acl_parse)、run_cmd_ex 四态、bt_cmd_is_write / bt_write_authorize、bt_ctl_execute
|
||||
* (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
|
||||
*/
|
||||
/* M65:扫描窗口在单测里缩短 —— pipeline 的 dwell 取 0(整条 ~1s 结束),超时压到 1200ms。
|
||||
* 这样既能验「超时强杀 + 不漏孙进程」,又不用为每个用例等生产的 6.5s。 */
|
||||
#define BT_SCAN_TMO_MS 1200
|
||||
#define BT_SCAN_DWELL_S 0
|
||||
#define main meow_netmgrd_prod_main_unused
|
||||
#include "meow_netmgrd.c"
|
||||
#undef main
|
||||
@@ -27,46 +35,503 @@ static void wf(const char *p, const char *s)
|
||||
if (f) { fputs(s, f); fclose(f); }
|
||||
}
|
||||
|
||||
/* ---- M65 单测辅助:可控时钟 + 在飞 scan worker 的确定性收尾 ---- */
|
||||
static long long g_fake_now_ms = 0;
|
||||
static long long fake_clock_ms(void) { return g_fake_now_ms; }
|
||||
|
||||
/* 立刻收掉在飞 scan worker(整组杀 + 阻塞收尸):单测要确定性,不等生产的 ~5s 窗口。 */
|
||||
static void kill_scan_worker(void)
|
||||
{
|
||||
if (g_bt_scan_pid > 0) {
|
||||
kill(-g_bt_scan_pid, SIGKILL);
|
||||
kill(g_bt_scan_pid, SIGKILL);
|
||||
int st = 0;
|
||||
while (waitpid(g_bt_scan_pid, &st, 0) < 0 && errno == EINTR) { }
|
||||
}
|
||||
g_bt_scan_pid = 0;
|
||||
g_bt_scan_deadline = 0;
|
||||
}
|
||||
|
||||
/* 等 worker 由 bt_scan_reap() 自己收完(超时/正常都算),返回耗时 ms;超时返回 -1。 */
|
||||
static long drain_scan_worker(int max_ms)
|
||||
{
|
||||
long t0 = (long)bt_scan_now_ms();
|
||||
while (g_bt_scan_pid > 0) {
|
||||
bt_scan_reap();
|
||||
if (g_bt_scan_pid <= 0) break;
|
||||
if ((long)bt_scan_now_ms() - t0 > max_ms) return -1;
|
||||
struct timespec ts = { 0, 20 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
return (long)bt_scan_now_ms() - t0;
|
||||
}
|
||||
|
||||
/* ---- 真机原始字节样本(含 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";
|
||||
|
||||
/* ---- 真机 `hcitool con` 原始字节样本(两条都可从仓库内自证)----
|
||||
*
|
||||
* S_CON_INBOUND —— 0.8.50 / 设备 10.58.215.50 / 2026-11-11 20:53 / **耳机自主回连**现场,
|
||||
* `hcitool con 2>&1 | od -c` 的原样输出(每行 16 字节;偏移是八进制,末行 0000120 = 80 字节,
|
||||
* 与本字面量长度一致;每字节右侧的空格是 od 的字段填充,不是数据):
|
||||
* 0000000 C o n n e c t i o n s : \n \t >
|
||||
* 0000020 A C L 3 3 : D E : 2 E : 2 1 :
|
||||
* 0000040 A D : 3 0 h a n d l e 1 1
|
||||
* 0000060 s t a t e 1 l m M A S T E
|
||||
* 0000100 R A U T H E N C R Y P T \n
|
||||
* 0000120
|
||||
* 逐字节读法:`Connections:` + LF + TAB + `>` + SPC + `ACL` + SPC + 17 字符 MAC +
|
||||
* ` handle 11 ` + `state 1 lm MASTER AUTH ENCRYPT` + SPC + LF ⇒ 字面量见下方 S_CON_INBOUND。
|
||||
* 复核方法:`printf '<上面的字面量>' | od -c`,与本块除「行尾字段填充空格」外逐行相同
|
||||
* (本块为免源码尾随空格删掉了那点填充;数据以 80 字节 / 末偏移 0000120 为准)。
|
||||
* 这次采集没有单独落盘成证据文件(`devdocs/evidence/` 只有 m50 那一份),上面这块 od 输出
|
||||
* 就是它的原样记录;同一条命令的「`Connections:` 表头 + 行首 TAB + 行尾 SPC 后 LF」框架字节
|
||||
* 可与 S_CON_OUTBOUND 的来源(下面那份 `cat -A` 证据)互相印证。
|
||||
*
|
||||
* S_CON_OUTBOUND —— devdocs/evidence/m50_bt_result_20261111.log:7-8(同机同耳机,本端发起)。
|
||||
* `cat -A` 核为 `Connections:$` + `^I< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER $`,
|
||||
* 与下面的字面量逐字节一致(行首 TAB、行尾一个空格再 LF)。 */
|
||||
static const char *S_CON_INBOUND =
|
||||
"Connections:\n"
|
||||
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n";
|
||||
static const char *S_CON_OUTBOUND =
|
||||
"Connections:\n"
|
||||
"\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n";
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const char *T = "/tmp/nmg_bt";
|
||||
char p[192];
|
||||
mkdir(T, 0755);
|
||||
/* 宿主临时目录:Termux/Android 上 /tmp 不可写,优先 $TMPDIR(mktemp 同规则) */
|
||||
const char *base = getenv("TMPDIR");
|
||||
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) */
|
||||
{ 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/address", T); wf(p, "00:1A:7D:DA:71:13\n");
|
||||
snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "on\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, "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、扫描缓存 */
|
||||
snprintf(p, sizeof p, "%s/bluetoothctl", T); wf(p,
|
||||
"#!/bin/sh\ncase \"$1\" in\n"
|
||||
" paired|paired-devices|devices) echo 'Device 33:DE:2E:21:AD:30 Pudding'; echo 'Device 11:22:33:44:55:66 Speaker';;\n"
|
||||
" 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);
|
||||
snprintf(p, sizeof p, "%s/hcitool", T); wf(p,
|
||||
"#!/bin/sh\necho '\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER'\n"
|
||||
"echo '\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE'\nexit 0\n"); chmod(p, 0755);
|
||||
snprintf(p, sizeof p, "%s/bluealsa-cli", T); wf(p,
|
||||
"#!/bin/sh\nif [ \"${1:-}\" = \"a2dp\" ]; then\n"
|
||||
"echo '/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:'\n"
|
||||
"echo '\tCodec: SBC'\necho '\tSampling: 44100 Hz'\necho '\tChannels: 2'\n"
|
||||
"echo '\tBits per Sample: 16'\necho '\tState: RUNNING'\necho '\tAcquired: yes'\nfi\nexit 0\n"); chmod(p, 0755);
|
||||
snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p,
|
||||
"[NEW] Device 33:DE:2E:21:AD:30 Pudding\n[NEW] Device AA:BB:CC:DD:EE:FF Earbuds\n");
|
||||
/* 桩:bluetoothctl(paired-devices/devices/info/connect/disconnect/pair/remove,靠状态文件变迁)
|
||||
* 注意:生产代码给后端命令前置 PATH=/bin:/sbin:/usr/bin:/usr/sbin,Termux 上 cat/basename/grep
|
||||
* 都不在这几个目录里 —— 桩脚本只能用 shell 内建(read/echo/[ /参数展开),否则测试会假失败。 */
|
||||
snprintf(p, sizeof p, "%s/bluetoothctl", 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"
|
||||
/* M65:scan 的 wedge 桩。交互形态(无参数,scan pipeline 喂 stdin)与 `scan off`
|
||||
* 两种入口都要挂住;用 exec 让 sleep 就是那个进程本身(杀掉父 shell 不会留孤儿)。 */
|
||||
"if [ $# -eq 0 ] && [ -f \"$S/hang_scan\" ]; then exec sleep 60; fi\n"
|
||||
"case \"$1\" in\n"
|
||||
" paired-devices) catf \"$S/paired_list.txt\"; exit 0;;\n"
|
||||
" devices) catf \"$S/dev_list.txt\"; exit 0;;\n"
|
||||
" info)\n"
|
||||
" mac=\"$2\"\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"
|
||||
/* 已入站连接时 connect 是幂等 no-op:真机上内核 ACL 仍是 `>` 行,别把方向改成 `<` */
|
||||
" prev=; [ -f \"$S/conn_$2\" ] && IFS= read -r prev < \"$S/conn_$2\"\n"
|
||||
" [ \"$prev\" = in ] || echo yes > \"$S/conn_$2\"\n"
|
||||
" 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"
|
||||
/* M65:scan 的 wedge 桩 —— 真的挂住(exec 让 sleep 就是那个进程本身,模拟 bluetoothctl
|
||||
* 卡在 HCI/D-Bus 里;若改成子进程 sleep,杀父进程会留下孤儿,断言就不诚实了)。 */
|
||||
" scan)\n"
|
||||
" if [ -f \"$S/hang_scan\" ]; then exec sleep 60; fi\n"
|
||||
" if [ \"$2\" = on ]; then echo 'Discovery started'; else echo 'Discovery stopped'; fi\n"
|
||||
" exit 0;;\n"
|
||||
"esac\n"
|
||||
"exit 0\n", T);
|
||||
wf(p, sh); chmod(p, 0755);
|
||||
}
|
||||
/* 桩:hcitool con —— 按 conn_<mac> 文件内容选方向/链路类型(真机字节形态):
|
||||
* in → "\t> ACL … "(远端发起,耳机自主回连,M54 真机 od -c 原文)
|
||||
* sco → "\t< SCO … "(SCO 链路,不是 ACL)
|
||||
* 其它非空值(yes)→ "\t< ACL … "(本端发起) */
|
||||
snprintf(p, sizeof p, "%s/hcitool", T);
|
||||
{
|
||||
char sh[1200];
|
||||
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"
|
||||
" case \"$v\" in yes|in|sco) ;; *) continue;; esac\n"
|
||||
" m=\"${f##*/}\"; m=\"${m#conn_}\"\n"
|
||||
" case \"$v\" in\n"
|
||||
" in) echo \"\t> ACL $m handle 11 state 1 lm MASTER AUTH ENCRYPT \";;\n"
|
||||
" sco) echo \"\t< SCO $m handle 13 state 1 lm MASTER \";;\n"
|
||||
" *) echo \"\t< ACL $m handle 12 state 1 lm MASTER \";;\n"
|
||||
" esac\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];
|
||||
snprintf(s, sizeof s, "%s", x); g_bluetoothctl = s; }
|
||||
g_hcitool = ""; { char x[160]; snprintf(x, sizeof x, "%s/hcitool", T); static char s[160];
|
||||
snprintf(s, sizeof s, "%s", x); g_hcitool = s; }
|
||||
g_bluealsa_cli = ""; { char x[160]; snprintf(x, sizeof x, "%s/bluealsa-cli", T); static char s[160];
|
||||
snprintf(s, sizeof s, "%s", x); g_bluealsa_cli = s; }
|
||||
g_bt_scan_cache = ""; { char x[160]; snprintf(x, sizeof x, "%s/bt_scan.txt", T); static char s[160];
|
||||
snprintf(s, sizeof s, "%s", x); g_bt_scan_cache = s; }
|
||||
/* 指向桩(g_* 是 const char*,用静态缓冲承载) */
|
||||
static char sb_bt[256], sb_hci[256], sb_ba[256], sb_hc[256], sb_cache[256];
|
||||
snprintf(sb_bt, sizeof sb_bt, "%s/bluetoothctl", T);
|
||||
snprintf(sb_hci, sizeof sb_hci, "%s/hcitool", T);
|
||||
snprintf(sb_ba, sizeof sb_ba, "%s/bluealsa-cli", T);
|
||||
snprintf(sb_hc, sizeof sb_hc, "%s/hciconfig", T);
|
||||
snprintf(sb_cache, sizeof sb_cache, "%s/bt_scan.txt", T);
|
||||
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_rfkill_dir[0] = 0; snprintf(g_rfkill_dir, sizeof g_rfkill_dir, "%s/rfkill", T);
|
||||
|
||||
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 真机字节:> / < / SCO / 表头 / 空 / 截断 / 大小写) ==\n");
|
||||
{
|
||||
char macs[8][24];
|
||||
/* 入站(远端发起):M54 的 bug 现场 —— 只认 "< ACL" 时这里返回 0 */
|
||||
CHECK(bt_acl_parse(S_CON_INBOUND, macs, 8) == 1);
|
||||
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||
/* 出站(本端发起):M49 起就该过,修入站时不许回归 */
|
||||
CHECK(bt_acl_parse(S_CON_OUTBOUND, macs, 8) == 1);
|
||||
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||
/* 两副耳机混排(一副自主回连、一副本端发起)→ 按输出顺序全收 */
|
||||
int n = bt_acl_parse("Connections:\n"
|
||||
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
|
||||
"\t< ACL 11:22:33:44:55:66 handle 12 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:两个方向都不收 */
|
||||
CHECK(bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
|
||||
CHECK(bt_acl_parse("\t> SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
|
||||
/* 表头 / 空 / NULL / 无方向符(裸 "ACL" 不能被当连接) */
|
||||
CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0);
|
||||
CHECK(bt_acl_parse("", macs, 8) == 0);
|
||||
CHECK(bt_acl_parse(NULL, macs, 8) == 0);
|
||||
CHECK(bt_acl_parse("\tACL 33:DE:2E:21:AD:30 handle 1 state 1 lm MASTER \n", macs, 8) == 0);
|
||||
CHECK(bt_acl_parse("no ACL connection\n", macs, 8) == 0);
|
||||
/* 截断 MAC(行中 / 行尾都算)与「MAC 后面还跟 hex/':'」的伪 MAC */
|
||||
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:A\n", macs, 8) == 0);
|
||||
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:3", macs, 8) == 0);
|
||||
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30:99 handle 1 state 1\n", macs, 8) == 0);
|
||||
/* 大小写归一(hcitool 理论上恒大写,别让下游 join 因大小写错过) */
|
||||
n = bt_acl_parse("\t> ACL 33:de:2e:21:ad:30 handle 11 state 1 lm MASTER \n", macs, 8);
|
||||
CHECK(n == 1);
|
||||
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||
/* CRLF 行尾(终端/串口转发形态) */
|
||||
n = bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \r\n", macs, 8);
|
||||
CHECK(n == 1);
|
||||
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||
/* max 上限裁剪 */
|
||||
n = bt_acl_parse("Connections:\n"
|
||||
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \n"
|
||||
"\t< ACL 11:22:33:44:55:66 handle 12 state 1 lm SLAVE \n"
|
||||
"\t> ACL 22:33:44:55:66:77 handle 13 state 1 lm SLAVE \n", macs, 2);
|
||||
CHECK(n == 2);
|
||||
}
|
||||
|
||||
/* ================= 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");
|
||||
CHECK(bt_cmd_is_write("status") == 0);
|
||||
CHECK(bt_cmd_is_write("paired") == 0);
|
||||
@@ -81,45 +546,398 @@ int main(void)
|
||||
CHECK(bt_cmd_is_write("unpair") == 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("== 入站回连(hcitool con 的 `> ACL`):connected / status / connect 复查都必须认 ==\n");
|
||||
{ /* 真机复现场景(M54):耳机自己回连,行首是 '>'。修前:connected 空、connected=0、
|
||||
* connect 报「未生效(hcitool con 无该 ACL)」。桩按 conn 文件内容生成方向。 */
|
||||
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "in\n");
|
||||
bt_ctl_execute("connected", NULL, &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
|
||||
bt_ctl_execute("status", NULL, &r);
|
||||
CHECK(strstr(r.text, "connected=1") != NULL);
|
||||
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 1); /* 写后复查走的也是这个解析器 */
|
||||
/* 已连(入站)时再点连接:ACL 仍是 '>' 行,必须报已连接,不许报「未生效」 */
|
||||
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(strstr(r.text, "Connected") != NULL);
|
||||
CHECK(strstr(r.text, "已连接") != NULL);
|
||||
CHECK(strstr(r.text, "未生效") == NULL);
|
||||
/* 只改了链路类型(SCO 行)→ 不算连接:防止「把 SCO 当 ACL」的宽匹配回归 */
|
||||
wf(p, "sco\n");
|
||||
bt_ctl_execute("status", NULL, &r);
|
||||
CHECK(strstr(r.text, "connected=0") != NULL);
|
||||
bt_ctl_execute("connected", NULL, &r);
|
||||
CHECK(r.text[0] == 0);
|
||||
unlink(p);
|
||||
}
|
||||
|
||||
printf("== bt connect:成功路径以 hcitool con 复查 ==\n");
|
||||
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
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);
|
||||
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, "in\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); /* 入站 ACL 残留也必须被看见 */
|
||||
CHECK(r.code == MEOW_ERR_NODEV);
|
||||
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
|
||||
snprintf(p, sizeof p, "%s/state/conn_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);
|
||||
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(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);
|
||||
CHECK(r.code == MEOW_ERR_INVAL); /* 缺 mac → INVAL */
|
||||
|
||||
printf("== bt scan on/off(写级;桩 bluetoothctl 现在被 scan 真正调用) ==\n");
|
||||
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);
|
||||
bt_ctl_execute("scan", "off", &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
kill_scan_worker(); /* 1s 窗口不必等,直接收 */
|
||||
|
||||
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);
|
||||
/* ================= M65 P2-1:扫描节流单位(7s / 7.1s;可控时钟) ================= */
|
||||
/* 回归防线:旧实现 `mono_now()`(秒)比 `7000`(毫秒)→ 实际 7000 秒 ≈ 1.94 小时一次。
|
||||
* 两个入口都验:①纯判定 bt_scan_throttle(入参毫秒)②真实入口 bt_scan_on(只认
|
||||
* g_bt_scan_clock_ms 钩子 —— 若有人把时钟改回 mono_now(),钩子失效,这里的 +6999 用例
|
||||
* 会看到真实时间几乎没动 → 判「放行」→ 断言失败)。 */
|
||||
printf("== M65 P2-1: bt scan 节流按毫秒(7s 放行 / 6.999s 拒 / 7.1s 放行) ==\n");
|
||||
{
|
||||
g_bt_scan_ms = 0;
|
||||
g_bt_scan_pid = 0; g_bt_scan_deadline = 0;
|
||||
g_bt_tmo_streak = 0; g_bt_tmo_until = 0;
|
||||
|
||||
CHECK(bt_scan_throttle(1000000) == 1); /* 首次:放行 */
|
||||
CHECK(bt_scan_throttle(1000000 + 6999) == 0); /* 6.999s:拒 */
|
||||
CHECK(bt_scan_throttle(1000000 + 7000) == 1); /* 7.000s:放行(秒混毫秒会等到 7000s) */
|
||||
CHECK(bt_scan_throttle(1000000 + 7000 + 99) == 0); /* 0.099s:拒 */
|
||||
CHECK(bt_scan_throttle(1000000 + 7000 + 7100) == 1); /* 7.1s:放行 */
|
||||
|
||||
/* 真实入口:时钟走钩子 */
|
||||
g_bt_scan_clock_ms = fake_clock_ms;
|
||||
g_bt_scan_ms = 0; g_bt_scan_pid = 0; g_bt_scan_deadline = 0;
|
||||
g_fake_now_ms = 3000000;
|
||||
CHECK(bt_scan_on() == 1); /* t=3.000s:放行并起 worker */
|
||||
kill_scan_worker();
|
||||
g_fake_now_ms = 3000000 + 6999;
|
||||
CHECK(bt_scan_on() == 0); /* +6.999s:节流(不起 worker) */
|
||||
CHECK(g_bt_scan_pid <= 0);
|
||||
g_fake_now_ms = 3000000 + 7000;
|
||||
CHECK(bt_scan_on() == 1); /* +7.000s:放行 */
|
||||
kill_scan_worker();
|
||||
g_fake_now_ms = 3000000 + 7000 + 7100;
|
||||
CHECK(bt_scan_on() == 1); /* +7.1s:再放行 */
|
||||
kill_scan_worker();
|
||||
g_bt_scan_clock_ms = bt_scan_now_ms; /* 复原真时钟 */
|
||||
g_bt_scan_ms = 0;
|
||||
}
|
||||
|
||||
/* ================= M65 P1-3:scan 有界收割(不挂死、不漏子进程) ================= */
|
||||
printf("== M65 P1-3: scan on/off 对「永不返回」的 bluetoothctl 有界收割、不漏子进程 ==\n");
|
||||
{
|
||||
snprintf(p, sizeof p, "%s/state/hang_scan", T); wf(p, "1\n"); /* 桩:scan → exec sleep 60 */
|
||||
g_bt_scan_ms = 0; g_bt_scan_pid = 0; g_bt_scan_deadline = 0;
|
||||
g_bt_tmo_streak = 0; g_bt_tmo_until = 0;
|
||||
|
||||
/* ① scan off 是同步路径:旧实现 system() 会永远阻塞在这个 wedge 上。 */
|
||||
long t0 = (long)bt_scan_now_ms();
|
||||
int rc = bt_scan_off();
|
||||
long el = (long)bt_scan_now_ms() - t0;
|
||||
printf(" [info] bt_scan_off 滞回 %ldms (上限 %dms)\n", el, BT_SCAN_OFF_TMO_MS);
|
||||
CHECK(rc == -1); /* 如实报失败,不假成功 */
|
||||
CHECK(el >= BT_SCAN_OFF_TMO_MS - 50); /* 确实等到了超时点 */
|
||||
CHECK(el < BT_SCAN_OFF_TMO_MS + 1200); /* 但有界(旧 system() 无界) */
|
||||
{ int st2 = 0; errno = 0;
|
||||
CHECK(waitpid(-1, &st2, WNOHANG) == -1 && errno == ECHILD); } /* 无残留子进程 */
|
||||
|
||||
/* ② scan on:worker 在飞,主循环不被阻塞;过 deadline 整组强杀(含 pipeline 孙进程)。 */
|
||||
g_bt_scan_ms = 0;
|
||||
t0 = (long)bt_scan_now_ms();
|
||||
CHECK(bt_scan_on() == 1);
|
||||
long fork_el = (long)bt_scan_now_ms() - t0;
|
||||
CHECK(fork_el < 300); /* 起扫描本身绝不阻塞 */
|
||||
CHECK(g_bt_scan_pid > 0);
|
||||
CHECK(bt_scan_on() == 0); /* 在飞上限 1:不重复 fork */
|
||||
long dr = drain_scan_worker(BT_SCAN_TMO_MS + 3000);
|
||||
el = (long)bt_scan_now_ms() - t0; /* 从起扫描算起,含 fork + 窗口 */
|
||||
printf(" [info] scan worker 从起扫描到收割 %ldms(drain 轮询 %ldms;窗口 %dms)\n",
|
||||
el, dr, BT_SCAN_TMO_MS);
|
||||
CHECK(dr >= 0); /* 有界收完(没有永久卡住) */
|
||||
CHECK(el >= BT_SCAN_TMO_MS); /* 是「等到超时才收」而不是立刻杀 */
|
||||
CHECK(el < BT_SCAN_TMO_MS + 2500);
|
||||
CHECK(g_bt_scan_pid == 0); /* 槽位已释放 */
|
||||
CHECK(g_bt_tmo_streak >= 1); /* 超时计入既有熔断计数 */
|
||||
{ int st2 = 0; errno = 0;
|
||||
CHECK(waitpid(-1, &st2, WNOHANG) == -1 && errno == ECHILD); } /* 孙进程也被整组收掉 */
|
||||
|
||||
/* ③ 熔断联动:再来一次超时就把 streak 顶到上限 → bt_circuit_open(),不再 fork。 */
|
||||
g_bt_tmo_streak = BT_TMO_STREAK_MAX - 1;
|
||||
g_bt_tmo_until = 0;
|
||||
g_bt_scan_ms = 0;
|
||||
CHECK(bt_scan_on() == 1);
|
||||
CHECK(drain_scan_worker(BT_SCAN_TMO_MS + 3000) >= 0);
|
||||
CHECK(g_bt_tmo_streak >= BT_TMO_STREAK_MAX);
|
||||
CHECK(bt_circuit_open() == 1);
|
||||
g_bt_scan_ms = 0;
|
||||
CHECK(bt_scan_on() == -1); /* 熔断中:不再 fork(不烧进程槽) */
|
||||
CHECK(g_bt_scan_pid <= 0);
|
||||
|
||||
/* 复原:摘掉 wedge 钩子,熔断器清零 */
|
||||
{ char ub[320]; snprintf(ub, sizeof ub, "rm -f '%s/state/hang_scan'", T); (void)!system(ub); }
|
||||
g_bt_tmo_streak = 0; g_bt_tmo_until = 0; g_bt_scan_ms = 0;
|
||||
bt_scan_off(); /* 桩恢复正常:现在能返回 0 */
|
||||
}
|
||||
|
||||
printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n");
|
||||
caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER;
|
||||
@@ -130,6 +948,8 @@ int main(void)
|
||||
CHECK(bt_write_authorize(&sys, 0, "connect") == MEOW_ERR_PERM); /* 系统+hiby → 拒 */
|
||||
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; }
|
||||
printf("\nBT_CTL TEST: ALL PASS\n");
|
||||
return 0;
|
||||
|
||||
@@ -6,7 +6,7 @@ set -u
|
||||
cd "$(dirname "$0")"
|
||||
BIN="${1:-./meow_netmgrd_host}"
|
||||
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
|
||||
PASS=0; FAIL=0
|
||||
ck() { # ck <名> <期望子串> <实际>
|
||||
@@ -97,16 +97,29 @@ exit 0
|
||||
EOF
|
||||
cat > "$T/bluealsa-cli" <<'EOF'
|
||||
#!/bin/sh
|
||||
if [ "${1:-}" = "a2dp" ]; then
|
||||
echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:"
|
||||
echo " Type: A2DP Source"
|
||||
echo " Codec: SBC"
|
||||
echo " Sampling: 44100 Hz"
|
||||
# v4.1.1 实际命令表:list-services/list-pcms/status/info/codec/volume/...(没有 a2dp 子命令)
|
||||
case "${1:-}" in
|
||||
list-pcms)
|
||||
# 真机路径带末端段,且结尾字面量不可依赖(首连实测是 /a2dpsrc/sink):
|
||||
# /org/bluealsa/hci0/dev_XX/a2dpsrc/sink
|
||||
echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink";;
|
||||
info)
|
||||
# 真机 v4.1.1 原文(M50 2026-11-11 18:26 抓,PCM 被 audiod 撑住时);此处取「正在传音频」态
|
||||
echo "Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30"
|
||||
echo "Sequence: 3"
|
||||
echo "Transport: A2DP-source"
|
||||
echo "Mode: sink"
|
||||
echo "Running: true"
|
||||
echo "Format: S16_LE"
|
||||
echo "Channels: 2"
|
||||
echo " Bits per Sample: 16"
|
||||
echo " State: RUNNING"
|
||||
echo " Acquired: yes"
|
||||
fi
|
||||
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
|
||||
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 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)
|
||||
ck T5f "codec=SBC" "$OUT"
|
||||
ck T5f2 "rate=44100" "$OUT"
|
||||
ck T5f3 "acquired=1" "$OUT"
|
||||
ck T5f4 "role=src" "$OUT"
|
||||
ck T5f "A2DP 33:DE:2E:21:AD:30" "$OUT"
|
||||
ck T5f2 "codec=AAC" "$OUT" # 真机 Selected codec(不是 Available codecs 列表)
|
||||
ck T5f3 "rate=48000 Hz" "$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 权威扫描缓存)----
|
||||
cat > "$T/bt_scan.txt" <<'EOF'
|
||||
[NEW] Device 33:DE:2E:21:AD:30 Pudding
|
||||
[NEW] Device AA:BB:CC:DD:EE:FF Earbuds
|
||||
[DEL] Device 00:11:22:33:44:55
|
||||
EOF
|
||||
# ---- T5g: bt devices(P1:netmgrd 权威扫描缓存;M49:实时 [NEW] 行带 ANSI ESC 也要能解析)----
|
||||
# 真机 /tmp/meow_bt_scan.txt 的实时行以 ESC[0;94m[bluetooth]ESC[0m# \rESC[K[001 ESC[0;92m 002NEW 001 ESC[0m 002] 开头
|
||||
{ printf '%s\n' 'Waiting to connect to bluetoothd...'
|
||||
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'
|
||||
printf '\033[0;94m[bluetooth]\033[0m# scan off\n'
|
||||
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)
|
||||
ck T5g "Device 33:DE:2E:21:AD:30 Pudding" "$OUT"
|
||||
ck T5g2 "Device AA:BB:CC:DD:EE:FF Earbuds" "$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" # 尾部 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)----
|
||||
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"
|
||||
|
||||
# ---- 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}')
|
||||
[ -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'
|
||||
global domain=-ads.example.com dns.filter_aaaa=1
|
||||
EOF
|
||||
@@ -422,6 +450,26 @@ ck T14e "deny=1" "$OUT"
|
||||
kill $D2PID 2>/dev/null; wait $D2PID 2>/dev/null
|
||||
# 恢复守护 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"; }
|
||||
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 "PASS=$PASS FAIL=$FAIL"
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/* test_nm_wifi_args.c — M64 P1-1 宿主回归:SSID/PSK 参数安全 + 反向控制。
|
||||
*
|
||||
* 纯宿主测试:不连设备、不启服务、不发 /api/key/<code>,只用本进程 + sh + execv。
|
||||
*
|
||||
* 编译运行(在 patch_v2/meow_netmgrd 下):
|
||||
* cc -O1 -Wall -Wextra -I. test_nm_wifi_args.c -o ~/tmp/test_nm_wifi_args
|
||||
* ~/tmp/test_nm_wifi_args
|
||||
*
|
||||
* 反向控制:同一套用例分别喂给
|
||||
* ① 「修复前实现」复刻(legacy_build:`set_network %d ssid '"%s"'` → sh -c)→ 必须注入成功;
|
||||
* ② 修复后实现(nm_wifi_ssid_ok/nm_wpa_set_argv + execv 传 argv)→ 必须不执行任何 shell。
|
||||
* 只有 ①成立、②不成立,才证明这套改动真的拦住了东西(而不是「本来就不会中」)。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include "nm_wifi_args.h"
|
||||
|
||||
static char g_self[512];
|
||||
static int g_fail;
|
||||
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (!(cond)) { printf(" FAIL: "); printf(__VA_ARGS__); printf("\n"); g_fail = 1; } \
|
||||
else { printf(" PASS: "); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
/* 复刻修复前 meow_netmgrd.c:3154 + :3307 的拼法:SSID 塞进单引号内的双引号,整行交给 sh -c。
|
||||
* (值是 `a' ; id ; '` 时,第一个 `'` 就把单引号闭合了。) */
|
||||
static void legacy_build(const char *field, const char *val, char *out, size_t cap)
|
||||
{
|
||||
char args[256];
|
||||
snprintf(args, sizeof args, "set_network 0 %s '\"%s\"'", field, val);
|
||||
snprintf(out, cap, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli %s 2>/dev/null", args);
|
||||
}
|
||||
|
||||
static int run_shell(const char *cmd, char *out, int cap)
|
||||
{
|
||||
out[0] = 0;
|
||||
FILE *f = popen(cmd, "r");
|
||||
if (!f) return -1;
|
||||
size_t off = fread(out, 1, (size_t)cap - 1, f);
|
||||
out[off] = 0;
|
||||
return pclose(f);
|
||||
}
|
||||
|
||||
/* 用 execv 跑一条 argv,子进程是本程序的 --stub-argv 形态(把收到的 argv 原样打印)。
|
||||
* 这就是修复后的下发方式:wpa_cli 收到的是 argv,不经 shell。 */
|
||||
static int run_argv_stub(char *const *args, char *out, int cap)
|
||||
{
|
||||
out[0] = 0;
|
||||
int pfd[2];
|
||||
if (pipe(pfd) < 0) return -1;
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { close(pfd[0]); close(pfd[1]); return -1; }
|
||||
if (pid == 0) {
|
||||
dup2(pfd[1], 1);
|
||||
dup2(pfd[1], 2);
|
||||
close(pfd[0]); close(pfd[1]);
|
||||
char *av[10];
|
||||
int n = 0;
|
||||
av[n++] = (char *)"test_nm_wifi_args";
|
||||
av[n++] = (char *)"--stub-argv";
|
||||
for (int i = 0; args[i] && n < 9; i++) av[n++] = args[i];
|
||||
av[n] = NULL;
|
||||
execv(g_self, av);
|
||||
_exit(127);
|
||||
}
|
||||
close(pfd[1]);
|
||||
size_t off = 0;
|
||||
for (;;) {
|
||||
size_t room = (off + 1 < (size_t)cap) ? (size_t)cap - 1 - off : 0;
|
||||
if (!room) break;
|
||||
ssize_t rd = read(pfd[0], out + off, room);
|
||||
if (rd <= 0) break;
|
||||
off += (size_t)rd;
|
||||
}
|
||||
out[off] = 0;
|
||||
close(pfd[0]);
|
||||
int st = 0;
|
||||
waitpid(pid, &st, 0);
|
||||
return st;
|
||||
}
|
||||
|
||||
/* 旧实现「接受」判定:str_capsafe(meow_netmgrd.c:3140)只 trim 空白,不校验字符。 */
|
||||
static int legacy_ssid_accepted(const char *s)
|
||||
{
|
||||
return s && s[0];
|
||||
}
|
||||
|
||||
/* 一条用例的双跑:旧实现必须中招,新实现必须只把值当字节。 */
|
||||
static void case_attack(const char *label, const char *field, const char *val)
|
||||
{
|
||||
char cmd[512], out[4096];
|
||||
printf("== %s ==\n value=%s\n", label, val);
|
||||
|
||||
/* ① 反向控制:修复前 */
|
||||
CHECK(legacy_ssid_accepted(val), "修复前判定放行该值(无字符校验)");
|
||||
legacy_build(field, val, cmd, sizeof cmd);
|
||||
printf(" 修复前命令:%s\n", cmd);
|
||||
run_shell(cmd, out, sizeof out);
|
||||
CHECK(strstr(out, "uid=") != NULL, "修复前被注入:值里的 `id` 在 root 语境执行(stdout 含 uid=)");
|
||||
|
||||
/* ② 修复后 */
|
||||
char idbuf[16], quoted[160];
|
||||
char *args[5];
|
||||
int rc = nm_wpa_set_argv(idbuf, sizeof idbuf, quoted, sizeof quoted, 0, field, val, args);
|
||||
CHECK(rc == 0, "修复后可构造 argv(execv 传参,值里的元字符无 shell 语义)");
|
||||
char *full[7] = { (char *)"wpa_cli", args[0], args[1], args[2], args[3], NULL };
|
||||
run_argv_stub(full, out, sizeof out);
|
||||
printf(" 修复后子进程收到的 argv:\n%s", out);
|
||||
CHECK(strstr(out, "uid=") == NULL, "修复后没有 shell:值里的 `id` 不执行");
|
||||
CHECK(strstr(out, quoted) != NULL, "值原样作为**单个** argv 元素到达(未断词)");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc > 1 && !strcmp(argv[1], "--stub-argv")) { /* execv 桩:打印收到的 argv */
|
||||
for (int i = 2; i < argc; i++) printf("ARG[%d]=%s\n", i - 2, argv[i]);
|
||||
return 0;
|
||||
}
|
||||
ssize_t l = readlink("/proc/self/exe", g_self, sizeof g_self - 1);
|
||||
if (l <= 0) snprintf(g_self, sizeof g_self, "%s", argv[0]);
|
||||
else g_self[l] = 0;
|
||||
|
||||
printf("== M64 P1-1 反向控制:修复前注入 / 修复后不注入 ==\n");
|
||||
/* 审计给出的真实恶意串:附近 AP 广播的 SSID 与用户输入的 PSK。 */
|
||||
case_attack("SSID 注入(审计原串)", "ssid", "a' ; id ; '");
|
||||
case_attack("PSK 注入(审计原串)", "psk", "x';id;'");
|
||||
|
||||
printf("== 约束层:长度 / 控制字符 ==\n");
|
||||
CHECK(nm_wifi_ssid_ok("a' ; id ; '") == 1, "`a' ; id ; '` 通过校验(11B<32B,是合法 SSID;靠 execv 中立化)");
|
||||
CHECK(nm_wifi_ssid_ok("Home2G") == 1, "普通 SSID 通过");
|
||||
CHECK(nm_wifi_ssid_ok("中文网络") == 1, "UTF-8 中文 SSID 通过(12B)");
|
||||
CHECK(nm_wifi_ssid_ok("") == 0, "空 SSID 拒绝");
|
||||
CHECK(nm_wifi_ssid_ok("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") == 0, "33 字节 SSID 拒绝(802.11 上限 32)");
|
||||
CHECK(nm_wifi_ssid_ok("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") == 1, "32 字节 SSID 通过");
|
||||
CHECK(nm_wifi_ssid_ok("a\nset_network 0 psk \"x\"") == 0, "含 \\n 的 SSID 拒绝(wpa ctrl_iface 行协议)");
|
||||
CHECK(nm_wifi_ssid_ok("a\x7f" "b") == 0, "含 DEL 的 SSID 拒绝");
|
||||
CHECK(nm_wifi_ssid_ok("a\tb") == 0, "含 TAB 的 SSID 拒绝");
|
||||
|
||||
CHECK(nm_wifi_psk_ok("") == 1, "空 PSK(开放网络)通过");
|
||||
CHECK(nm_wifi_psk_ok("pass1234") == 1, "普通口令通过");
|
||||
CHECK(nm_wifi_psk_ok("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") == 1,
|
||||
"64 位 hex PSK 通过");
|
||||
CHECK(nm_wifi_psk_ok("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz") == 0,
|
||||
"64 字符非 hex 拒绝(≠ 合法 PSK)");
|
||||
CHECK(nm_wifi_psk_ok("x';id;'") == 1, "`x';id;'` 通过(=合法口令字节,靠 execv 中立化)");
|
||||
CHECK(nm_wifi_psk_ok("a\nb") == 0, "含 \\n 的 PSK 拒绝");
|
||||
|
||||
printf("== wpa_supplicant 字面量:只转义 \" 与 \\ ==\n");
|
||||
{
|
||||
char q[64];
|
||||
CHECK(nm_wpa_quote("a\"b\\c", q, sizeof q) == 0 && !strcmp(q, "\"a\\\"b\\\\c\""),
|
||||
"`a\"b\\c` → %s", q);
|
||||
CHECK(nm_wpa_quote("a' ; id ; '", q, sizeof q) == 0 && !strcmp(q, "\"a' ; id ; '\""),
|
||||
"shell 元字符原样保留 → %s", q);
|
||||
CHECK(nm_wpa_quote("a\nb", q, sizeof q) == -1, "控制字符拒绝(不落盘、不进 ctrl_iface)");
|
||||
CHECK(nm_wpa_quote("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", q, 8) == -1, "缓冲不足返回 -1");
|
||||
}
|
||||
|
||||
printf("== 两处实现同步(只修一处等于没修)==\n");
|
||||
{
|
||||
static const char *files[] = { "meow_netmgrd.c", "../meow_launcherd/netmgr_ipc.c" };
|
||||
for (int i = 0; i < 2; i++) {
|
||||
FILE *f = fopen(files[i], "r");
|
||||
if (!f) { printf(" SKIP: 读不到 %s(从 patch_v2/meow_netmgrd 下运行才检查)\n", files[i]); continue; }
|
||||
char buf[400000];
|
||||
size_t n = fread(buf, 1, sizeof buf - 1, f);
|
||||
buf[n] = 0;
|
||||
fclose(f);
|
||||
CHECK(strstr(buf, "nm_wpa_set_argv") != NULL, "%s 使用共享的 nm_wpa_set_argv", files[i]);
|
||||
CHECK(strstr(buf, "ssid '\"") == NULL, "%s 不再有 `ssid '\\\"` 拼法", files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
printf(g_fail ? "\nRESULT: FAIL\n" : "\nRESULT: ALL PASS\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -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
|
||||
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) =="
|
||||
ls -la meow_powermgr
|
||||
md5sum meow_powermgr
|
||||
|
||||
Binary file not shown.
@@ -22,7 +22,10 @@ if [ "$1" = "host" ]; then
|
||||
exit 0
|
||||
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) =="
|
||||
ls -la meow_svc_perf
|
||||
md5sum meow_svc_perf
|
||||
|
||||
Binary file not shown.
@@ -10,6 +10,12 @@
|
||||
* 动作逻辑与 launcher `power_menu.c` 的 `sys_do_reboot`/`set_boot_default_hiby`/`bootmode.sh Recovery`
|
||||
* **同源 0-diff 搬移**,仅入口(CLI vs IPC)与鉴权层不同。
|
||||
*
|
||||
* 开机默认语义(M69,用户定案 2026-11-12):`/data/meow_boot_default` 是「设置里的默认系统」,
|
||||
* **只有显式设默认(POWER_SET_DEFAULT_* / CLI set-default-*)才能写**;普通重启(POWER_REBOOT /
|
||||
* CLI reboot)与 OTA 升级后的重启(POWER_UPGRADE)都不碰它——重启后选单按配置默认(含倒计时)。
|
||||
* 「这一次去 Hiby」(POWER_BACK_HIBY / CLI `reboot --hiby`)只写一次性指示 `/data/meow_boot_once`,
|
||||
* 由 `meow_bootmenu` 开机读一次即删,不改持久默认。
|
||||
*
|
||||
* 鉴权(II 期,MPK_SIGNATURE_DESIGN §6):IPC 服务端按【连接级身份】分级——`SO_PEERCRED` 取 pid →
|
||||
* `readlink /proc/pid/exe` → 命中 `MeowAPPs/<pkg>/` 判定包名,或命中系统二进制 allowlist
|
||||
* (/usr/bin/meow_*、/usr/data/meow/meow*、MeowPanel 后端)判定为系统身份;其余为 OTHER。
|
||||
@@ -51,7 +57,8 @@
|
||||
#define FBIOBLANK 0x4611
|
||||
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
|
||||
#define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power"
|
||||
#define BOOT_DEFAULT "/data/meow_boot_default" /* 0=MeowOS(默认),1=HibyOS */
|
||||
#define BOOT_DEFAULT "/data/meow_boot_default" /* 持久默认=设置里的「默认系统」:0=MeowOS,1=HibyOS */
|
||||
#define BOOT_ONCE "/data/meow_boot_once" /* 一次性指示:1=这次开机进 HibyOS(bootmenu 读一次即删) */
|
||||
#define COLD_BOOT "/data/meow_cold_boot" /* 冷启标记:1=上次关机,0=上次重启 */
|
||||
#define MTD_OTA "/dev/mtd5" /* ota 标记分区(8M) */
|
||||
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
|
||||
@@ -72,6 +79,10 @@
|
||||
#define CHG_AC_ONLINE MEOW_PS_DIR "/ac/online"
|
||||
#define UPGRADE_FW "/usr/data/mnt/sd_0/r3proii.upt" /* 卡根升级固件(写 mtd5 前完整性校验) */
|
||||
|
||||
/* 启动默认两个文件的生产路径(语义见文件头);宿主自测可直接重定向这两个指针,不改语义。 */
|
||||
static const char *g_boot_default_path = BOOT_DEFAULT;
|
||||
static const char *g_boot_once_path = BOOT_ONCE;
|
||||
|
||||
/* 音量语义(与 launcher MEOW_VOL_DEFAULT/MEOW_VOL_MAX_DEFAULT、SDK MEOW_UI_VOL_* 一致):
|
||||
* audio.volume 默认 60(写 DAC 映射值 0-100);audio.volume_max 默认 100(软上限 clamp)。 */
|
||||
#ifndef MEOW_VOL_DEFAULT
|
||||
@@ -279,14 +290,57 @@ static void sys_reboot_do(int cmd)
|
||||
for (;;) usleep(1000);
|
||||
}
|
||||
|
||||
/* ---------------- boot 默认 ---------------- */
|
||||
/* 写 /data/meow_boot_default(0=MeowOS 默认 / 1=HibyOS 默认)。 */
|
||||
static void write_boot_default(int v)
|
||||
/* ---------------- boot 默认 ----------------
|
||||
* 两个文件,两种语义(M69,用户定案 2026-11-12):
|
||||
* · /data/meow_boot_default = 设置里的「默认系统」(持久)。**只有** SET_DEFAULT_* / set-default-*
|
||||
* 写它;普通重启/升级/关机都不写,重启后由选单读它决定默认项。
|
||||
* · /data/meow_boot_once = 「这一次去 Hiby」一次性指示。bootmenu 开机读一次即删、优先级更高。
|
||||
* 写入策略集中在 boot_default_intent()(纯函数,宿主可测),CLI 与 IPC 共用同一张表。 */
|
||||
static int write_boot_default(int v)
|
||||
{
|
||||
char b[8];
|
||||
snprintf(b, sizeof b, "%d\n", v);
|
||||
if (write_file_str(BOOT_DEFAULT, b, 0644) < 0) svc_log("write %s fail: %s", BOOT_DEFAULT, strerror(errno));
|
||||
int rc = write_file_str(g_boot_default_path, b, 0644);
|
||||
if (rc < 0) svc_log("write %s fail: %s", g_boot_default_path, strerror(errno));
|
||||
else svc_log("boot_default=%d", v);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int write_boot_once(int v)
|
||||
{
|
||||
char b[8];
|
||||
snprintf(b, sizeof b, "%d\n", v);
|
||||
int rc = write_file_str(g_boot_once_path, b, 0644);
|
||||
if (rc < 0) svc_log("write %s fail: %s", g_boot_once_path, strerror(errno));
|
||||
else svc_log("boot_once=%d (one-shot; persistent default untouched)", v);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 动作 → 开机默认写入意图(纯函数):
|
||||
* *persist: -1=不写持久默认;0/1=把 /data/meow_boot_default 写成该值
|
||||
* *once: -1=不写一次性指示;0/1=把 /data/meow_boot_once 写成该值
|
||||
* 表外动作(POWER_REBOOT/POWER_UPGRADE/POWER_SHUTDOWN/读级…)两个都不写。 */
|
||||
static void boot_default_intent(uint32_t cmd, int *persist, int *once)
|
||||
{
|
||||
*persist = -1;
|
||||
*once = -1;
|
||||
switch (cmd) {
|
||||
case POWER_SET_DEFAULT_MEOW: *persist = 0; break; /* 用户显式设默认 */
|
||||
case POWER_SET_DEFAULT_HIBY: *persist = 1; break; /* 用户显式设默认 */
|
||||
case POWER_BACK_HIBY: *once = 1; break; /* 这一次去 Hiby(临时) */
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/* 按意图落盘(CLI 与 IPC 共用,防两条入口语义漂移)。
|
||||
* 返回 0;-1=某次写入失败(动作仍照常继续:重启请求不该因标记写失败就不重启)。 */
|
||||
static int apply_boot_default_intent(uint32_t cmd)
|
||||
{
|
||||
int persist = -1, once = -1, rc = 0;
|
||||
boot_default_intent(cmd, &persist, &once);
|
||||
if (persist >= 0 && write_boot_default(persist) < 0) rc = -1;
|
||||
if (once >= 0 && write_boot_once(once) < 0) rc = -1;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---------------- 关机 logo ---------------- */
|
||||
@@ -358,7 +412,7 @@ static void battery_fill(char *out, int cap)
|
||||
|
||||
static const char *boot_default_str(void)
|
||||
{
|
||||
int v = read_file_int_def(BOOT_DEFAULT, -1);
|
||||
int v = read_file_int_def(g_boot_default_path, -1);
|
||||
if (v < 0) return "unknown";
|
||||
return v ? "hiby" : "meow";
|
||||
}
|
||||
@@ -455,10 +509,11 @@ static int execute_action(const m_power_action_t *a, m_power_status_t *st)
|
||||
snprintf(st->msg, sizeof st->msg, "shutdown accepted");
|
||||
return 1; /* 应答后:logo + poweroff(不返回) */
|
||||
case POWER_REBOOT:
|
||||
write_boot_default(0); /* 普通重启恒回 MeowOS(无视之前) */
|
||||
/* 普通重启:**不写**持久默认、也不写一次性指示。重启后选单按配置默认(5s 倒计时)。
|
||||
* 用户定案(2026-11-12):POWER_REBOOT 不该碰 /data/meow_boot_default。 */
|
||||
sync();
|
||||
st->code = MEOW_OK;
|
||||
snprintf(st->msg, sizeof st->msg, "reboot accepted (boot=meow)");
|
||||
snprintf(st->msg, sizeof st->msg, "reboot accepted (boot menu uses configured default)");
|
||||
return 1;
|
||||
case POWER_UPGRADE:
|
||||
{ /* 写 mtd5 前先校验卡上升级固件(升级写保护);校验失败→非 OK、绝不写 mtd5/不 reboot。 */
|
||||
@@ -474,18 +529,25 @@ static int execute_action(const m_power_action_t *a, m_power_status_t *st)
|
||||
snprintf(st->msg, sizeof st->msg, "upgrade accepted");
|
||||
return 1; }
|
||||
case POWER_BACK_HIBY:
|
||||
write_boot_default(1);
|
||||
{ /* 「切换到 HibyOS」= 这一次去 Hiby:只写一次性指示,持久默认不动。
|
||||
* 与 CLI `reboot --hiby` 走同一张意图表(boot_default_intent),不写持久默认。 */
|
||||
int rc = apply_boot_default_intent(POWER_BACK_HIBY);
|
||||
sync();
|
||||
st->code = MEOW_OK;
|
||||
snprintf(st->msg, sizeof st->msg, "reboot -> hiby accepted");
|
||||
return 1;
|
||||
if (rc < 0)
|
||||
snprintf(st->msg, sizeof st->msg,
|
||||
"reboot -> hiby accepted; once flag write FAILED (will boot configured default)");
|
||||
else
|
||||
snprintf(st->msg, sizeof st->msg,
|
||||
"reboot -> hiby accepted (once; persistent default unchanged)");
|
||||
return 1; }
|
||||
case POWER_SET_DEFAULT_MEOW:
|
||||
write_boot_default(0);
|
||||
apply_boot_default_intent(POWER_SET_DEFAULT_MEOW);
|
||||
st->code = MEOW_OK;
|
||||
snprintf(st->msg, sizeof st->msg, "boot_default=meow (next boot)");
|
||||
return 0;
|
||||
case POWER_SET_DEFAULT_HIBY:
|
||||
write_boot_default(1);
|
||||
apply_boot_default_intent(POWER_SET_DEFAULT_HIBY);
|
||||
st->code = MEOW_OK;
|
||||
snprintf(st->msg, sizeof st->msg, "boot_default=hiby (next boot)");
|
||||
return 0;
|
||||
@@ -846,8 +908,15 @@ static int cli_run(int n, char **v)
|
||||
return 0; /* 不返回 */
|
||||
}
|
||||
if (!strcmp(cmd, "reboot")) {
|
||||
if (n > 1 && !strcmp(v[1], "--hiby")) { write_boot_default(1); }
|
||||
else write_boot_default(0); /* 普通重启恒回 MeowOS */
|
||||
if (n > 1 && !strcmp(v[1], "--hiby")) {
|
||||
/* 「这一次去 Hiby」:与 IPC POWER_BACK_HIBY 同一张意图表 —— 只写一次性指示,
|
||||
* 持久默认(设置里的「默认系统」)不动。 */
|
||||
if (apply_boot_default_intent(POWER_BACK_HIBY) < 0)
|
||||
fprintf(stderr, "warning: %s write failed; next boot uses configured default\n", BOOT_ONCE);
|
||||
else
|
||||
printf("reboot -> hiby (once; persistent default unchanged)\n");
|
||||
}
|
||||
/* 普通重启:不写持久默认、不写一次性指示 —— 重启后选单读配置默认。 */
|
||||
sys_reboot_do(LINUX_REBOOT_CMD_RESTART);
|
||||
return 0;
|
||||
}
|
||||
@@ -863,12 +932,12 @@ static int cli_run(int n, char **v)
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(cmd, "set-default-hiby")) {
|
||||
write_boot_default(1);
|
||||
apply_boot_default_intent(POWER_SET_DEFAULT_HIBY);
|
||||
printf("boot_default=hiby (next boot)\n");
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(cmd, "set-default-meow")) {
|
||||
write_boot_default(0);
|
||||
apply_boot_default_intent(POWER_SET_DEFAULT_MEOW);
|
||||
printf("boot_default=meow (next boot)\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -921,7 +990,7 @@ static int cli_run(int n, char **v)
|
||||
|
||||
static const char *cli_usage(void)
|
||||
{
|
||||
return "shutdown|reboot[--hiby]|upgrade|set-default-hiby|set-default-meow|"
|
||||
return "shutdown|reboot[--hiby=一次性去Hiby,不改默认]|upgrade|set-default-hiby|set-default-meow|"
|
||||
"status|battery|reboot-reason|upgrade-status|screen on|off|timeout<秒>|dac on|off|status";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/* svc_power_bootdefault_test.c — 宿主自测:电源动作对「开机默认」的写入语义(M69)。
|
||||
*
|
||||
* 被测对象(诊断见文件头 M69 段):
|
||||
* /data/meow_boot_default 持久默认 = 设置里的「默认系统」,只有显式设默认才写;
|
||||
* /data/meow_boot_once 一次性「这一次去 Hiby」,POWER_BACK_HIBY / CLI `reboot --hiby` 写。
|
||||
*
|
||||
* 做法:#include 生产源(#define main 改名),把 g_boot_default_path / g_boot_once_path 重定向到
|
||||
* 临时目录,直接断言**落盘副作用**。不调用 execute_action / cli_run —— 那两个会真的 reboot 宿主。
|
||||
* 覆盖是靠「写入只有一条路径」保证的:全文件 write_boot_default/write_boot_once 的唯一调用点是
|
||||
* apply_boot_default_intent(),CLI 与 IPC 共用它 + boot_default_intent() 这张表。
|
||||
*
|
||||
* 编译/运行(Termux 上 /tmp 不可写,带 TMPDIR 最稳):
|
||||
* mkdir -p "$HOME/tmp/m69test"
|
||||
* cc -Wall -Wextra -I../meow_launcherd svc_power_bootdefault_test.c -o "$HOME/tmp/m69test/svc_bt" &&
|
||||
* TMPDIR=$HOME/tmp/m69test "$HOME/tmp/m69test/svc_bt" # 全 PASS -> exit 0
|
||||
*/
|
||||
#define main meow_svc_power_prod_main_unused
|
||||
#include "meow_svc_power.c"
|
||||
#undef main
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if (!(cond)) { printf(" FAIL: %s (line %d)\n", #cond, __LINE__); g_fail++; } \
|
||||
else { printf(" ok : %s\n", #cond); } } while (0)
|
||||
|
||||
static char g_dir[512], g_persist[600], g_once[600];
|
||||
|
||||
/* 临时目录:$TMPDIR → /tmp → $HOME/tmp/m69test(Termux 上 /tmp 属别的用户,不可写)。 */
|
||||
static int mk_tmp_dir(void)
|
||||
{
|
||||
const char *cand[3];
|
||||
char homebuf[512] = "";
|
||||
int n = 0;
|
||||
const char *t = getenv("TMPDIR");
|
||||
if (t && t[0]) cand[n++] = t;
|
||||
cand[n++] = "/tmp";
|
||||
const char *home = getenv("HOME");
|
||||
if (home && home[0]) {
|
||||
snprintf(homebuf, sizeof homebuf, "%s/tmp/m69test", home);
|
||||
mkdir(homebuf, 0700);
|
||||
cand[n++] = homebuf;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
snprintf(g_dir, sizeof g_dir, "%s/m69_svcp_XXXXXX", cand[i]);
|
||||
if (!mkdtemp(g_dir)) continue;
|
||||
snprintf(g_persist, sizeof g_persist, "%s/boot_default", g_dir);
|
||||
snprintf(g_once, sizeof g_once, "%s/boot_once", g_dir);
|
||||
g_boot_default_path = g_persist;
|
||||
g_boot_once_path = g_once;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void put(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);
|
||||
}
|
||||
|
||||
/* 读文件首个整数;文件不存在/空 → -1。 */
|
||||
static int get_int(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "r");
|
||||
int v = -1;
|
||||
if (!f) return -1;
|
||||
if (fscanf(f, "%d", &v) != 1) v = -1;
|
||||
fclose(f);
|
||||
return v;
|
||||
}
|
||||
|
||||
static int exists(const char *path) { return access(path, F_OK) == 0 ? 1 : 0; }
|
||||
|
||||
static void reset(void)
|
||||
{
|
||||
unlink(g_persist);
|
||||
unlink(g_once);
|
||||
}
|
||||
|
||||
/* ---- A) 意图表:谁是「设持久默认」,谁是「一次性」,谁两个都不写 ---- */
|
||||
static void test_intent_table(void)
|
||||
{
|
||||
int p, o;
|
||||
|
||||
printf("== boot_default_intent ==\n");
|
||||
|
||||
/* 普通重启 / OTA 升级重启 / 关机:两个文件都不写(本次修的病根) */
|
||||
boot_default_intent(POWER_REBOOT, &p, &o); CHECK(p == -1 && o == -1);
|
||||
boot_default_intent(POWER_UPGRADE, &p, &o); CHECK(p == -1 && o == -1);
|
||||
boot_default_intent(POWER_SHUTDOWN, &p, &o); CHECK(p == -1 && o == -1);
|
||||
|
||||
/* 「这一次去 Hiby」:只写一次性指示,绝不写持久默认 */
|
||||
boot_default_intent(POWER_BACK_HIBY, &p, &o); CHECK(p == -1 && o == 1);
|
||||
|
||||
/* 用户显式设默认:写持久默认,不写一次性指示 */
|
||||
boot_default_intent(POWER_SET_DEFAULT_MEOW, &p, &o); CHECK(p == 0 && o == -1);
|
||||
boot_default_intent(POWER_SET_DEFAULT_HIBY, &p, &o); CHECK(p == 1 && o == -1);
|
||||
|
||||
/* 只读/其它动作:都不写 */
|
||||
boot_default_intent(POWER_STATUS, &p, &o); CHECK(p == -1 && o == -1);
|
||||
boot_default_intent(POWER_SCREEN, &p, &o); CHECK(p == -1 && o == -1);
|
||||
boot_default_intent(POWER_DAC, &p, &o); CHECK(p == -1 && o == -1);
|
||||
boot_default_intent(9999, &p, &o); CHECK(p == -1 && o == -1);
|
||||
}
|
||||
|
||||
/* ---- B) 普通重启 / OTA 重启:落盘副作用 = 没有 ---- */
|
||||
static void test_reboot_and_upgrade_write_nothing(void)
|
||||
{
|
||||
printf("== POWER_REBOOT / POWER_UPGRADE leave boot default alone ==\n");
|
||||
|
||||
/* 用户把默认设成 HiByOS(设置里的「默认系统」) */
|
||||
reset();
|
||||
put(g_persist, "1\n");
|
||||
|
||||
/* 改后:普通重启不写任何启动默认 */
|
||||
CHECK(apply_boot_default_intent(POWER_REBOOT) == 0);
|
||||
CHECK(get_int(g_persist) == 1);
|
||||
CHECK(exists(g_once) == 0);
|
||||
|
||||
/* 反向控制(= 改前 POWER_REBOOT 干的活):直接写持久默认会真的改文件,
|
||||
* 说明上面的断言不是空转、文件路径重定向也确实生效。 */
|
||||
write_boot_default(0);
|
||||
CHECK(get_int(g_persist) == 0);
|
||||
put(g_persist, "1\n"); /* 复原用户设置 */
|
||||
|
||||
/* OTA 升级后的那次重启:同样两个文件都不碰 */
|
||||
CHECK(apply_boot_default_intent(POWER_UPGRADE) == 0);
|
||||
CHECK(get_int(g_persist) == 1);
|
||||
CHECK(exists(g_once) == 0);
|
||||
|
||||
/* 关机同理 */
|
||||
CHECK(apply_boot_default_intent(POWER_SHUTDOWN) == 0);
|
||||
CHECK(get_int(g_persist) == 1);
|
||||
CHECK(exists(g_once) == 0);
|
||||
}
|
||||
|
||||
/* ---- C) 「切换到 HibyOS」= 一次性:持久默认不变、只留一次性指示 ---- */
|
||||
static void test_back_hiby_is_one_shot(void)
|
||||
{
|
||||
printf("== POWER_BACK_HIBY is one-shot ==\n");
|
||||
|
||||
reset();
|
||||
put(g_persist, "0\n"); /* 用户默认 MeowOS */
|
||||
|
||||
CHECK(apply_boot_default_intent(POWER_BACK_HIBY) == 0);
|
||||
CHECK(get_int(g_persist) == 0); /* 持久默认没被改成 1 —— 这就是本次病根 */
|
||||
CHECK(get_int(g_once) == 1); /* 一次性指示已写 */
|
||||
|
||||
/* 反向控制(= 改前 POWER_BACK_HIBY 干的活):写持久默认 1。 */
|
||||
write_boot_default(1);
|
||||
CHECK(get_int(g_persist) == 1);
|
||||
}
|
||||
|
||||
/* ---- D) 显式设默认仍然持久写 ---- */
|
||||
static void test_set_default_still_persists(void)
|
||||
{
|
||||
printf("== POWER_SET_DEFAULT_* still persist ==\n");
|
||||
|
||||
reset();
|
||||
CHECK(apply_boot_default_intent(POWER_SET_DEFAULT_HIBY) == 0);
|
||||
CHECK(get_int(g_persist) == 1);
|
||||
CHECK(exists(g_once) == 0); /* 设默认不该产生一次性指示 */
|
||||
|
||||
CHECK(apply_boot_default_intent(POWER_SET_DEFAULT_MEOW) == 0);
|
||||
CHECK(get_int(g_persist) == 0);
|
||||
CHECK(exists(g_once) == 0);
|
||||
}
|
||||
|
||||
/* ---- E) 生产路径常量:两组件必须写/读同一对文件 ---- */
|
||||
static void test_prod_paths(void)
|
||||
{
|
||||
printf("== production paths ==\n");
|
||||
CHECK(strcmp(BOOT_DEFAULT, "/data/meow_boot_default") == 0);
|
||||
CHECK(strcmp(BOOT_ONCE, "/data/meow_boot_once") == 0);
|
||||
CHECK(strcmp(g_boot_default_path, g_persist) == 0); /* 自测确实重定向了 */
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
if (mk_tmp_dir() != 0) { printf("FATAL: no writable tmp dir\n"); return 1; }
|
||||
|
||||
test_intent_table();
|
||||
test_reboot_and_upgrade_write_nothing();
|
||||
test_back_hiby_is_one_shot();
|
||||
test_set_default_still_persists();
|
||||
test_prod_paths();
|
||||
|
||||
reset();
|
||||
rmdir(g_dir);
|
||||
|
||||
if (g_fail) { printf("\nBOOTDEFAULT TEST: %d FAIL\n", g_fail); return 1; }
|
||||
printf("\nBOOTDEFAULT TEST: ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -21,7 +21,10 @@ if [ "$1" = "host" ]; then
|
||||
exit 0
|
||||
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) =="
|
||||
ls -la meow_svcd
|
||||
md5sum meow_svcd
|
||||
|
||||
Binary file not shown.
@@ -21,7 +21,10 @@ if [ "$1" = "host" ]; then
|
||||
exit 0
|
||||
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) =="
|
||||
ls -la meow_sysui
|
||||
md5sum meow_sysui
|
||||
|
||||
Binary file not shown.
@@ -7,17 +7,24 @@
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
ROOT=../..
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
# 交叉编译器:ZIGCC env 优先(与 build_netmgrd.sh 同口径;干净 worktree 里 <root>/.tools
|
||||
# 这个符号链接通常不存在,重编驱动会直接传 ZIGCC 进来),其次 <root>/.tools/,最后 $HOME/.tools/
|
||||
CC="${ZIGCC:-$ROOT/.tools/zig-mipsel-cc}"
|
||||
[ -x "$CC" ] || CC="$HOME/.tools/zig-mipsel-cc"
|
||||
INC="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(音量收编 v2:MSG_AUDIO_VOLUME/MSG_ACK)
|
||||
|
||||
if [ "$1" = "host" ]; then
|
||||
gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
||||
test_wd_fw.c -o test_wd_fw -lm
|
||||
gcc -Wall -Wextra -Os -I"$INC" -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
|
||||
-Wno-unused-variable test_wd_fw.c ../sdk/meow_time32.c -o test_wd_fw -lm
|
||||
./test_wd_fw
|
||||
exit $?
|
||||
fi
|
||||
|
||||
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
|
||||
md5sum meow_watchdog
|
||||
file meow_watchdog
|
||||
Binary file not shown.
Loaded 100 of 326 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user