From bc7bafa5dc117a3462b847075ab63ddd9b76071f Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 17:24:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(bootmenu):=20M23=20=E4=BA=AE=E5=BA=A6?= =?UTF-8?q?=E5=8D=95=E4=B8=80=E6=9D=83=E5=A8=81=E6=BA=90=EF=BC=88=E5=8F=AA?= =?UTF-8?q?=E4=BA=A4=E6=BA=90=E7=A0=81=EF=BC=8C=E4=BA=8C=E8=BF=9B=E5=88=B6?= =?UTF-8?q?=E7=94=B1=20M43=20=E7=BB=9F=E4=B8=80=E9=87=8D=E7=BC=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机 0.8.46 两个症状: 1) 开机菜单亮度 = 第一屏的 60(用户设置 1)——根因是 M6.25 的选单亮度写回只改了源码、 二进制没重编(strings 无 display.brightness_min),本次只交源码,重编交给 M43 批次。 2) HibyOS 亮/灭屏瞬间爆亮——supervisor 把启动瞬间面板读数(播放器写的 60)当保底, 且 >60 一律钉 60;扫描窗保底只要读到亮度<=0 就在 400ms 内写回该值,用户主动熄屏 也会被强行点亮。 改动(全部失败也沿用现状行为): - saved_brightness 权威源改为 settings.conf 的 display.brightness(与 watchdog/launcher 同源同 clamp);面板读数只在 settings 不可读时兜底;两者都不可知才写 BM_BRI_SAFE_LOW。 - 旧的 `>60 → 60` 硬钉改为 clamp 进 [display.brightness_min, max_brightness](bm_bri_clamp), 不再用常量顶替用户值;为什么不会重新引入 M2.9 开机闪屏写在 bm_bri_pick_saved 注释里。 - 扫描窗保底加「播放器忙(proc 态 D)+ 面板未下电(bl_power<=0)」两门,窗口结束彻底放手。 - 热键熄屏恢复:面板真实正值优先(HibyOS 内改过亮度),否则用 settings 值,夹进 [min,max]。 - 决策抽纯函数 bm_bri_pick_saved / bm_bri_clamp / bm_bri_floor_decide / bm_bri_hotkey_saved。 - 亮度判定/写回补 bm_log 落卡(stderr 原行全部保留),只在状态跃迁时打,不在高频路径刷日志。 - test_bm_fw.c:宿主单测(settings=1/saved=60/now=0/blank 矩阵 + clamp 矩阵 + 真文件 e2e)。 - -Wall -Wextra 清理:3 处 unsigned/int 比较、未用参数、未用变量、sys/poll.h→poll.h。 不含二进制、不改 fw_content.conf 钉值(保持 4441822d)。 --- patch_v2/bootmenu/meow_bootmenu.c | 247 ++++++++++++++++++++++++----- patch_v2/bootmenu/test_bm_fw.c | 255 ++++++++++++++++++++++++++++++ 2 files changed, 462 insertions(+), 40 deletions(-) create mode 100644 patch_v2/bootmenu/test_bm_fw.c diff --git a/patch_v2/bootmenu/meow_bootmenu.c b/patch_v2/bootmenu/meow_bootmenu.c index 71cab57..e89b7f4 100644 --- a/patch_v2/bootmenu/meow_bootmenu.c +++ b/patch_v2/bootmenu/meow_bootmenu.c @@ -32,6 +32,22 @@ * 说明:/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),面板读数只在 settings 不可读时兜底;旧的 + * `>60 → 60` 改为 clamp 进 [display.brightness_min, max_brightness](为什么不会重新引入 + * M2.9 的开机闪屏见 bm_bri_pick_saved 注释);两者都不可知才写 BM_BRI_SAFE_LOW。 + * 扫描窗保底另加「播放器忙(媒体扫描)+ 面板未下电」两门,窗口结束彻底放手。 + * 决策全部抽成纯函数(bm_bri_pick_saved / bm_bri_floor_decide / bm_bri_hotkey_saved), + * 宿主单测见 test_bm_fw.c。亮度判定改为 bm_log 落卡(stderr 原行保留)。 + * 本单一律**只交源码**:二进制由 M43 rebase 后统一重编并回写 fw_content.conf 钉值。 + * * ============================================================================ * WHY (设计见 BOOT_SELECTOR_DESIGN.md §3) * ============================================================================ @@ -81,7 +97,7 @@ #include #include #include -#include +#include #include #include #include /* M2.29:supervisor 接管关机/升级,直连 reboot(2) syscall */ @@ -111,6 +127,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):按住电源 + 窗口内单击音量减 ---- */ @@ -161,7 +178,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 +346,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 +362,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 +374,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 +453,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; @@ -814,9 +832,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,22 +871,39 @@ 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 内容完整)。 */ +#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 mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */ @@ -873,9 +922,77 @@ static int bootmenu_settings_brightness(void) fclose(f); if (b < mn) b = mn; if (b > 100) b = 100; + if (min_out) *min_out = mn; return b; } +/* ---- M23:亮度决策(纯函数,宿主单测见 test_bm_fw.c)---- + * 与 meow_watchdog wd_bri_decide() 同构:settings 是唯一权威,任何分支都不再写「固定 60」。 */ +#define BM_BRI_MIN_DEFAULT 1 /* display.brightness_min 缺省(与 launcher/watchdog 同值:极暗但可见) */ +#define BM_BRI_SAFE_LOW 20 /* settings/面板都不可知且面板已灭 → 低亮点亮(≈max 的 1/5,绝不 60) */ + +/* 把任意亮度读数夹进面板合法区间 [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])→ 面板读数(>0)→ BM_BRI_SAFE_LOW; + * 后两者再夹进 [brightness_min, max_brightness]。 + * *src 回填来源,原样进日志(settings / panel / safe-low)。 + * 真机症状来源:旧实现把「启动瞬间面板读数」当权威、且 >60 一律钉 60,用户值 1 被 60 顶掉。 + * + * 为什么 clamp[min,max] 不会重新引入 M2.9 Bug 3 的「开机闪一下最高亮度」: + * M2.9 的前置条件是「supervisor 启动时面板上是**播放器启动画面**的瞬时值,还没有人写过用户值」, + * 于是一旦该瞬时值(101)被当作保底、在后续写回时就会跳一次 max。现在三个条件都不成立: + * ① 有 settings 时**根本不看面板值**,写回的就是用户设置本身(写回 == 用户值,不存在跳变); + * ② M21/S22 之后 /usr/data 挂载即写用户值,supervisor 启动时面板上已经不是播放器启动画面的 + * 瞬时值,而是用户值本身 —— 降级路径里「观测值」≈「用户值」,clamp 不会把它抬高; + * ③ 写回只发生在「面板已暗」时(扫描窗保底要求 busy 且 未下电;确认框只在 was_blank 时点亮), + * 即「从暗到用户值」,方向永远是向上回到用户原本的亮度,不是从暗跳到 max。 + * 换句话说:旧写法的问题不是「60 这个数」,而是「用常量顶替用户值」;clamp 保留的是观测/设置到的 + * 合法值,且上限就是面板自己的 max_brightness —— 不会再出现「用户 1、亮屏 60」这种越权抬亮。 */ +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"; return bm_bri_clamp(cur, mn, maxb); } + *src = "safe-low"; + return bm_bri_clamp(BM_BRI_SAFE_LOW, mn, maxb); +} + +/* 扫描窗保底(Bug 4)决策:返回 >0 = 要写的亮度,0 = 不动;*why 回填原因供日志。 + * 旧实现只看「亮度<=0」,于是用户主动熄屏(播放器写 0 + blank)会在 400ms 内被强行写回 + * saved/60 → 「熄屏瞬间爆亮」。现在要求:窗口内 + 面板暗 + 播放器忙(媒体扫描,proc 态 D) + * + 面板未下电(bl_power<=0)。窗口结束 / 播放器不忙 → 完全放手,不再与播放器抢面板。 */ +static int bm_bri_floor_decide(int saved, int cur, int busy, int bl_power, + long long win_left_ms, const char **why) +{ + if (win_left_ms <= 0) { *why = "window-over"; return 0; } + if (cur > 0) { *why = "visible"; return 0; } + if (!busy) { *why = "idle-blank"; return 0; } + if (bl_power > 0) { *why = "panel-off"; 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 +1309,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 +1329,16 @@ 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),并加「播放器忙 + 面板未下电」两门(见 + * 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"; /* 只在判定原因跃迁时打日志(控日志量) */ struct keypoll kp_power, kp_keys; if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "supervisor: WARN no power dev\n"); @@ -1268,16 +1394,35 @@ 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 重做):扫描窗内、播放器**忙**(媒体扫描)且亮度被降到 0 + * → 写回**用户设置**亮度(不再 60)。门条件见 bm_bri_floor_decide: + * 窗口结束 / 面板已亮 / 播放器不忙(用户主动熄屏)/ 面板已下电(bl_power>0)→ 不写。 + * 旧实现只判「亮度<=0」,用户按电源熄屏后 400ms 内被写回 60 → 熄屏瞬间爆亮。 + * (播放器扫描忙时无法响应唤醒,我们只把背光拉回,画面仍为播放器最后帧;不碰 blank。) + * 判定按 400ms 节流(原写入节流同粒度),只在原因跃迁时打日志,避免刷卡。 */ + 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; + int bri = read_brightness(); + int busy = proc_is_busy(player_pid); + int blp = read_file_int(BACKLIGHT_POWER); + const char *why = "?"; + int floor = bm_bri_floor_decide(saved_brightness, bri, busy, blp, win_left, &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); + } + if (strcmp(why, last_floor_why) != 0) { + last_floor_why = why; + bm_log("supervisor: scan-window brightness %s -> %d (user=%d panel=%d busy=%d bl_power=%d left=%lldms)", + (floor > 0) ? "floor" : "skip", floor, bri_user, bri, busy, blp, win_left); + } + /* 窗口结束 → 彻底放手:此后不再读背光/进程态,绝不与播放器抢面板。 */ + if (win_left <= 0) scan_keep_until = 0; } } @@ -1338,10 +1483,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 +1505,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 +1563,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)) { @@ -1628,7 +1783,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 +1830,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 +1894,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 +1914,6 @@ int main(int argc, char **argv) run_menu(sim_sel, do_fork); return 0; } + +/* BM_NO_MAIN(宿主单测)到此为止 */ +#endif diff --git a/patch_v2/bootmenu/test_bm_fw.c b/patch_v2/bootmenu/test_bm_fw.c new file mode 100644 index 0000000..5f13366 --- /dev/null +++ b/patch_v2/bootmenu/test_bm_fw.c @@ -0,0 +1,255 @@ +/* test_bm_fw.c — 宿主单测:meow_bootmenu 的亮度决策(M23 亮度单一权威源)。 + * + * 覆盖真机 0.8.46 两个症状的判定矩阵: + * ① 开机菜单/选单亮度 = 面板继承值 60(用户设置 1)—— 选单路径 bootmenu_settings_brightness() + * ② HibyOS 亮/灭屏瞬间爆亮 —— supervisor 起始 saved、扫描窗保底、热键熄屏恢复三处决策 + * + * 编译(build_bootmenu.sh host;与 meow_watchdog 的 test_wd_fw.c 同款 include 打法): + * gcc -Wall -Wextra -Os -I../meow_launcherd -DBM_NO_MAIN \ + * -Wno-unused-function -Wno-unused-parameter -Wno-unused-variable \ + * test_bm_fw.c ../meow_launcherd/meow_touch.c -o test_bm_fw + * 运行:./test_bm_fw;全 PASS -> exit 0。 + * env MEOW_BACKLIGHT / MEOW_SETTINGS_FILE 把背光与 settings 指到临时文件(真机无这两个 env, + * 走 /sys 与 /usr/data 默认路径),因此这里的读写断言就是真机同一条代码路径。 + */ +#define BM_NO_MAIN +#include "meow_bootmenu.c" + +#include +#include +#include +#include + +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 属 shell 用户、本进程不可写), + * 否则 /tmp(Linux 构建容器)——与 test_wd_fw.c 一致。 */ +static const char *tmp_root(void) +{ + const char *t = getenv("TMPDIR"); + return (t && t[0]) ? t : "/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 不可读(降级路径):沿用面板读数,夹进 [brightness_min, max_brightness](M23 新语义) */ + v = bm_bri_pick_saved(-1, 60, 1, 101, &src); + check("saved: fallback keeps panel 60", v == 60 && !strcmp(src, "panel"), 1); + v = bm_bri_pick_saved(-1, 80, 1, 101, &src); /* 用户真设 80:不再被旧 `>60→60` 顶成 60 */ + check("saved: fallback keeps 80 (old code pinned 60)", v == 80 && !strcmp(src, "panel"), 1); + v = bm_bri_pick_saved(-1, 200, 1, 101, &src); /* 越界读数夹到 max_brightness */ + check("saved: fallback clamps 200 -> max 101", v == 101 && !strcmp(src, "panel"), 1); + v = bm_bri_pick_saved(-1, 37, 1, 32, &src); /* 面板上限 32 时按 32 */ + check("saved: fallback clamps to max_brightness 32", v == 32 && !strcmp(src, "panel"), 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"), 1); + v = bm_bri_pick_saved(0, 3, 1, 101, &src); /* 非正值按住「未知」处理 */ + check("saved: user<=0 treated unknown", v == 3 && !strcmp(src, "panel"), 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); + /* max 读不到(0)时只夹下限,不臆造上限 */ + v = bm_bri_pick_saved(-1, 90, 1, 0, &src); + check("saved: unknown max keeps 90", v == 90 && !strcmp(src, "panel"), 1); +} + +/* ---- 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; + + v = bm_bri_floor_decide(1, 0, 1, 0, 5000, &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, 1, 0, 5000, &why); /* 降级路径 saved=80 照写 80(旧码钉 60) */ + check("floor: degraded saved 80 floors at 80", v == 80 && !strcmp(why, "scan-floor"), 1); + v = bm_bri_floor_decide(0, 0, 1, 0, 5000, &why); /* saved 未知 -> 安全低亮 */ + check("floor: unknown saved -> safe-low", v == BM_BRI_SAFE_LOW && !strcmp(why, "scan-floor"), 1); + + v = bm_bri_floor_decide(1, 5, 1, 0, 5000, &why); /* 面板已亮:不动 */ + check("floor: visible -> no write", v == 0 && !strcmp(why, "visible"), 1); + v = bm_bri_floor_decide(1, 0, 0, 0, 5000, &why); /* ← 用户主动熄屏(播放器不忙):不动 */ + check("floor: user screen-off (idle) -> no write", v == 0 && !strcmp(why, "idle-blank"), 1); + v = bm_bri_floor_decide(1, 0, 1, 1, 5000, &why); /* 面板已下电(bl_power>0):不动 */ + check("floor: panel powered off -> no write", v == 0 && !strcmp(why, "panel-off"), 1); + v = bm_bri_floor_decide(1, 0, 1, 0, 0, &why); /* 90s 窗口结束:放手 */ + check("floor: window over -> no write", v == 0 && !strcmp(why, "window-over"), 1); + v = bm_bri_floor_decide(1, 0, 1, 0, -1, &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 不可读)时才用 60 */ + check("hotkey: blank -> degraded saved 60", 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"); + check("settings: missing key -> 100", bootmenu_settings_brightness(NULL), 100); + + 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(), 1, 0, 5000, &why); + if (floor > 0) write_brightness(floor); + check("e2e: scan floor writes 1 (not 60)", get_file_int(bl), 1); + + /* 用户主动熄屏(不忙)→ 保底不写,面板保持 0 */ + put_file(bl, "0\n"); + floor = bm_bri_floor_decide(saved, read_brightness(), 0, 0, 5000, &why); + if (floor > 0) write_brightness(floor); + check("e2e: user screen-off stays dark", get_file_int(bl), 0); + + /* 热键确认框:面板黑着 → 按 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); + + 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; +} From 8cd6e21cad0ca241c7ae8eb1d2b1d1c08a614f0a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 17:57:39 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(bootmenu):=20M23=20=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=20r1=20=E2=80=94=20=E4=BF=AE=20P2-1=20=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=20+=20P2-2=20Bug4=20=E9=98=B2=E6=8A=A4?= =?UTF-8?q?=E9=80=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P2-1(降级路径把老 bug 请回来):settings 不可读时不再原样采信面板瞬时值。 bm_bri_pick_saved 的 panel 分支改为 min(面板, BM_BRI_SAFE_LOW),src 改名 panel-capped; BM_BRI_SAFE_LOW 20 → 10,与 meow_watchdog 的 WD_BRI_SAFE_LOW 对齐(同一系统同一个安全下限)。 这样 settings 不可读 + 面板 60(播放器启动画面值)时 saved 是 10,扫描窗/确认框不会再写 60。 注释改掉「与 wd_bri_decide 同构」的断言:方向相同,但 bootmenu 没有运行期亮度快照, 降级走安全下限而不是 M20 的「快照 → cur>0 不写」链。 单测:把原先钉死 bug 的 "fallback keeps panel 60" 改成 "caps panel 60 -> SAFE_LOW 10", 并补「比 SAFE_LOW 更暗的真实值保留」「SAFE_LOW 与 M20 对齐」两个断言。 P2-2(busy 门可能让 Bug 4 防护变成空操作):删除 /proc D 态门,触发条件回到 pre-M23 的 「窗口内 + 面板暗(bri<=0)+ 面板未下电(bl_power<=0)」。 避免「熄屏瞬间爆亮」改由两条可观测信号承担:bl_power>0(播放器 FBIOBLANK)与 「电源键按下后 3s 内不补」(supervisor 已在 10ms 轮询 evdev,记录按下边沿)。 单测补 Bug4 防退化回归用例(窗口剩余 89999ms、面板暗、未下电 → 必须补)。 评审非阻塞三条: - 日志节流:同一原因 ≥2s 一条、原因跃迁也 ≥1s(防 visible↔scan-floor 每 400ms 交替刷屏, 90s 窗口最坏 ~90 行);日志字段 busy= 换成 pwr_ago=。 - 单测复跑配方补 TMPDIR;tmp_root() 增加自愈(TMPDIR → 可写 /tmp → $HOME/tmp/m23test)。 - 验收配方(3 次亮/灭屏 + 进一次开机菜单 + 期望日志行)写进报文。 只交源码:不含二进制、不改 fw_content.conf、不动 build_bootmenu.sh。 --- patch_v2/bootmenu/meow_bootmenu.c | 111 ++++++++++++++++---------- patch_v2/bootmenu/test_bm_fw.c | 128 +++++++++++++++++++++--------- 2 files changed, 162 insertions(+), 77 deletions(-) diff --git a/patch_v2/bootmenu/meow_bootmenu.c b/patch_v2/bootmenu/meow_bootmenu.c index e89b7f4..f3d8a12 100644 --- a/patch_v2/bootmenu/meow_bootmenu.c +++ b/patch_v2/bootmenu/meow_bootmenu.c @@ -40,13 +40,16 @@ * - HibyOS 亮/灭屏:supervisor 起始把「启动瞬间面板读数」当保底亮度,且 >60 一律钉 60; * 扫描窗保底只要读到亮度<=0 就 400ms 内写回该值 —— 用户主动熄屏(播放器写 0)会被 * 强行点亮成 60。现改为:settings.conf 的 display.brightness 为唯一权威(与 watchdog - * wd_bri_decide / launcher 同源同 clamp),面板读数只在 settings 不可读时兜底;旧的 - * `>60 → 60` 改为 clamp 进 [display.brightness_min, max_brightness](为什么不会重新引入 - * M2.9 的开机闪屏见 bm_bri_pick_saved 注释);两者都不可知才写 BM_BRI_SAFE_LOW。 - * 扫描窗保底另加「播放器忙(媒体扫描)+ 面板未下电」两门,窗口结束彻底放手。 + * 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、电源键按下 + * 后 3s 内不补;窗口结束彻底停手;日志同原因 ≥2s 一条。 * 决策全部抽成纯函数(bm_bri_pick_saved / bm_bri_floor_decide / bm_bri_hotkey_saved), * 宿主单测见 test_bm_fw.c。亮度判定改为 bm_log 落卡(stderr 原行保留)。 - * 本单一律**只交源码**:二进制由 M43 rebase 后统一重编并回写 fw_content.conf 钉值。 + * 本单一律**只交源码**:二进制由 M43/M51 统一重编并回写 fw_content.conf 钉值。 * * ============================================================================ * WHY (设计见 BOOT_SELECTOR_DESIGN.md §3) @@ -927,9 +930,12 @@ static int bootmenu_settings_brightness(int *min_out) } /* ---- M23:亮度决策(纯函数,宿主单测见 test_bm_fw.c)---- - * 与 meow_watchdog wd_bri_decide() 同构:settings 是唯一权威,任何分支都不再写「固定 60」。 */ + * 方向同 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 20 /* settings/面板都不可知且面板已灭 → 低亮点亮(≈max 的 1/5,绝不 60) */ +#define BM_BRI_SAFE_LOW 10 /* 与 meow_watchdog 的 WD_BRI_SAFE_LOW 对齐(同一系统同一个安全下限) */ +#define BM_BRI_PWR_HOLD_MS 3000 /* 电源键按下后 N ms 内不补亮度(用户主动熄屏信号,见 floor_decide) */ /* 把任意亮度读数夹进面板合法区间 [mn, max_brightness](M23:取代旧 `>60 → 60` 的硬钉)。 * mn < 1 时按 1(防黑屏:亮着总比黑屏好,但也不伪造「比用户下限更亮」); @@ -944,40 +950,52 @@ static int bm_bri_clamp(int v, int mn, int maxb) } /* supervisor 起始「保底亮度」取值。 - * settings(已 clamp 到 [brightness_min,100])→ 面板读数(>0)→ BM_BRI_SAFE_LOW; + * settings(已 clamp 到 [brightness_min,100])→ min(面板读数, SAFE_LOW) → SAFE_LOW; * 后两者再夹进 [brightness_min, max_brightness]。 - * *src 回填来源,原样进日志(settings / panel / safe-low)。 + * *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 启动时面板上是**播放器启动画面**的瞬时值,还没有人写过用户值」, - * 于是一旦该瞬时值(101)被当作保底、在后续写回时就会跳一次 max。现在三个条件都不成立: - * ① 有 settings 时**根本不看面板值**,写回的就是用户设置本身(写回 == 用户值,不存在跳变); - * ② M21/S22 之后 /usr/data 挂载即写用户值,supervisor 启动时面板上已经不是播放器启动画面的 - * 瞬时值,而是用户值本身 —— 降级路径里「观测值」≈「用户值」,clamp 不会把它抬高; - * ③ 写回只发生在「面板已暗」时(扫描窗保底要求 busy 且 未下电;确认框只在 was_blank 时点亮), - * 即「从暗到用户值」,方向永远是向上回到用户原本的亮度,不是从暗跳到 max。 - * 换句话说:旧写法的问题不是「60 这个数」,而是「用常量顶替用户值」;clamp 保留的是观测/设置到的 - * 合法值,且上限就是面板自己的 max_brightness —— 不会再出现「用户 1、亮屏 60」这种越权抬亮。 */ + * 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"; return bm_bri_clamp(cur, 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 回填原因供日志。 - * 旧实现只看「亮度<=0」,于是用户主动熄屏(播放器写 0 + blank)会在 400ms 内被强行写回 - * saved/60 → 「熄屏瞬间爆亮」。现在要求:窗口内 + 面板暗 + 播放器忙(媒体扫描,proc 态 D) - * + 面板未下电(bl_power<=0)。窗口结束 / 播放器不忙 → 完全放手,不再与播放器抢面板。 */ -static int bm_bri_floor_decide(int saved, int cur, int busy, int bl_power, - long long win_left_ms, const char **why) + * 触发条件**回到 pre-M23 的形态**(P2-2 评审):窗口内 + 面板暗(bri<=0)+ 面板未下电 + * (bl_power<=0)→ 补。**不依赖 /proc 的 D 态**:媒体扫描若在子线程,主线程是 S,D 态门会 + * 让 Bug 4 的防护变成空操作(黑屏几分钟回归),那比亮度偏高严重得多。 + * 避免「熄屏瞬间爆亮」改由两条**可观测**信号承担: + * ① bl_power>0 —— 播放器 FBIOBLANK 会置面板下电(bootmenu 确认框 unblank 时就把它写回 0, + * 说明本机确实有这个行为),用户主动熄屏走这条; + * ② 电源键按下后 BM_BRI_PWR_HOLD_MS 内不补 —— 10ms 轮询 evdev 就能看到,媒体扫描不会按电源键; + * 这是「播放器只写 brightness=0 但没碰 bl_power」时的第二道保险。 + * 窗口结束 → *why="window-over",调用方彻底停手。 */ +static int bm_bri_floor_decide(int saved, int cur, int bl_power, + long long win_left_ms, long long since_pwr_ms, + const char **why) { if (win_left_ms <= 0) { *why = "window-over"; return 0; } if (cur > 0) { *why = "visible"; return 0; } - if (!busy) { *why = "idle-blank"; return 0; } if (bl_power > 0) { *why = "panel-off"; return 0; } + if (since_pwr_ms >= 0 && since_pwr_ms < BM_BRI_PWR_HOLD_MS) { *why = "power-key"; return 0; } *why = "scan-floor"; return saved > 0 ? saved : BM_BRI_SAFE_LOW; } @@ -1334,11 +1352,13 @@ static void bootmenu_supervisor(int player_pid) /* Bug 4:HibyOS 开机媒体扫描窗内做亮度保底。播放器忙(媒体扫描)时其背光 * 待机会把亮度降到 0、面板 blank → 黑屏几分钟。**只写 brightness、绝不碰 FBIOBLANK** * (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。 - * M23:补的值改为用户设置(不再 60),并加「播放器忙 + 面板未下电」两门(见 - * bm_bri_floor_decide)—— 用户主动熄屏不再被强行点亮。 */ + * M23:补的值改为用户设置(不再 60);触发条件回到 pre-M23 的「面板暗 + 未下电」, + * 另加「电源键按下后 3s 内不补」(可观测信号,见 bm_bri_floor_decide)。 */ unsigned long long scan_keep_until = now_ms() + SCAN_KEEP_MS; unsigned long long last_bri_floor_at = 0; /* 亮度保底节流:≥400ms 才判定/写一次 */ - const char *last_floor_why = "init"; /* 只在判定原因跃迁时打日志(控日志量) */ + 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=本次开机尚未按过) */ struct keypoll kp_power, kp_keys; if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "supervisor: WARN no power dev\n"); @@ -1394,34 +1414,41 @@ static void bootmenu_supervisor(int player_pid) } } - /* 2a) Bug 4 亮度保底(M23 重做):扫描窗内、播放器**忙**(媒体扫描)且亮度被降到 0 - * → 写回**用户设置**亮度(不再 60)。门条件见 bm_bri_floor_decide: - * 窗口结束 / 面板已亮 / 播放器不忙(用户主动熄屏)/ 面板已下电(bl_power>0)→ 不写。 - * 旧实现只判「亮度<=0」,用户按电源熄屏后 400ms 内被写回 60 → 熄屏瞬间爆亮。 + /* 2a) Bug 4 亮度保底(M23):扫描窗内、面板暗(bri<=0)且**面板未下电**(bl_power<=0) + * → 写回用户设置亮度(不再 60)。触发条件回到 pre-M23 形态,**不依赖 /proc 的 D 态** + * (P2-2:媒体扫描若在子线程,主线程是 S,D 态门会让 Bug 4 防护变成空操作 → 黑屏回归)。 + * 避免「熄屏瞬间爆亮」由两条可观测信号承担:bl_power>0(播放器 FBIOBLANK)与 + * 电源键按下后 3s 内不补(见 bm_bri_floor_decide)。 * (播放器扫描忙时无法响应唤醒,我们只把背光拉回,画面仍为播放器最后帧;不碰 blank。) - * 判定按 400ms 节流(原写入节流同粒度),只在原因跃迁时打日志,避免刷卡。 */ + * 判定按 400ms 节流;日志同一原因 ≥2s 一条、跃迁也 ≥1s(防 `visible↔scan-floor` + * 每 400ms 交替刷屏;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 since_pwr = (last_pwr_edge_at > 0) + ? (long long)(now2 - last_pwr_edge_at) : -1; int bri = read_brightness(); - int busy = proc_is_busy(player_pid); int blp = read_file_int(BACKLIGHT_POWER); const char *why = "?"; - int floor = bm_bri_floor_decide(saved_brightness, bri, busy, blp, win_left, &why); + int floor = bm_bri_floor_decide(saved_brightness, bri, blp, win_left, since_pwr, &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); } - if (strcmp(why, last_floor_why) != 0) { + /* 日志节流:同一原因 ≥2s 一条;原因跃迁也至少隔 1s(防 visible↔scan-floor + * 每 400ms 交替时刷屏)。最坏 ~90 行/90s 窗口。 */ + unsigned long long gap = now2 - last_floor_log_at; + if ((strcmp(why, last_floor_why) != 0 && gap >= 1000) || gap >= 2000) { last_floor_why = why; - bm_log("supervisor: scan-window brightness %s -> %d (user=%d panel=%d busy=%d bl_power=%d left=%lldms)", - (floor > 0) ? "floor" : "skip", floor, bri_user, bri, busy, blp, win_left); + last_floor_log_at = now2; + bm_log("supervisor: scan-window brightness %s -> %d (user=%d panel=%d bl_power=%d pwr_ago=%lldms left=%lldms)", + (floor > 0) ? "floor" : "skip", floor, bri_user, bri, blp, since_pwr, win_left); } - /* 窗口结束 → 彻底放手:此后不再读背光/进程态,绝不与播放器抢面板。 */ + /* 窗口结束 → 彻底放手:此后不再读背光/bl_power,绝不与播放器抢面板。 */ if (win_left <= 0) scan_keep_until = 0; } } @@ -1439,8 +1466,10 @@ 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:亮度保底据此避让「用户主动熄屏」(可观测信号, + * 不依赖 /proc 的 D 态;媒体扫描不会按电源键)。 */ + if (pwr && !prev_pwr) { pwr_hold_armed = 1; pwr_down_at = t; last_pwr_edge_at = t; } 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 */ diff --git a/patch_v2/bootmenu/test_bm_fw.c b/patch_v2/bootmenu/test_bm_fw.c index 5f13366..4f08564 100644 --- a/patch_v2/bootmenu/test_bm_fw.c +++ b/patch_v2/bootmenu/test_bm_fw.c @@ -4,11 +4,13 @@ * ① 开机菜单/选单亮度 = 面板继承值 60(用户设置 1)—— 选单路径 bootmenu_settings_brightness() * ② HibyOS 亮/灭屏瞬间爆亮 —— supervisor 起始 saved、扫描窗保底、热键熄屏恢复三处决策 * - * 编译(build_bootmenu.sh host;与 meow_watchdog 的 test_wd_fw.c 同款 include 打法): - * gcc -Wall -Wextra -Os -I../meow_launcherd -DBM_NO_MAIN \ - * -Wno-unused-function -Wno-unused-parameter -Wno-unused-variable \ - * test_bm_fw.c ../meow_launcherd/meow_touch.c -o test_bm_fw - * 运行:./test_bm_fw;全 PASS -> exit 0。 + * 编译/运行(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 默认路径),因此这里的读写断言就是真机同一条代码路径。 */ @@ -19,6 +21,7 @@ #include #include #include +#include static int g_pass = 0, g_fail = 0; @@ -28,12 +31,29 @@ static void check(const char *name, int got, int want) else { g_fail++; printf("FAIL %s: got %d want %d\n", name, got, want); } } -/* 临时目录根:优先 $TMPDIR(Termux/Android 上 /tmp 属 shell 用户、本进程不可写), - * 否则 /tmp(Linux 构建容器)——与 test_wd_fw.c 一致。 */ +/* 临时目录根(返回可写目录;结果缓存在静态缓冲里): + * ① $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"); - return (t && t[0]) ? t : "/tmp"; + 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) @@ -69,19 +89,27 @@ static void test_pick_saved(void) v = bm_bri_pick_saved(7, 7, 1, 101, &src); check("saved: settings 7", v == 7 && !strcmp(src, "settings"), 1); - /* settings 不可读(降级路径):沿用面板读数,夹进 [brightness_min, max_brightness](M23 新语义) */ + /* settings 不可读(降级路径):**不**原样采信面板瞬时值,封顶 SAFE_LOW=10(P2-1 修复)。 + * 这是 M2.9 闪屏防护的正确形态:宁可用安全低亮,也不把「启动瞬间的面板值」当用户亮度回放。 */ v = bm_bri_pick_saved(-1, 60, 1, 101, &src); - check("saved: fallback keeps panel 60", v == 60 && !strcmp(src, "panel"), 1); - v = bm_bri_pick_saved(-1, 80, 1, 101, &src); /* 用户真设 80:不再被旧 `>60→60` 顶成 60 */ - check("saved: fallback keeps 80 (old code pinned 60)", v == 80 && !strcmp(src, "panel"), 1); - v = bm_bri_pick_saved(-1, 200, 1, 101, &src); /* 越界读数夹到 max_brightness */ - check("saved: fallback clamps 200 -> max 101", v == 101 && !strcmp(src, "panel"), 1); - v = bm_bri_pick_saved(-1, 37, 1, 32, &src); /* 面板上限 32 时按 32 */ - check("saved: fallback clamps to max_brightness 32", v == 32 && !strcmp(src, "panel"), 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"), 1); + 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"), 1); + 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); @@ -89,9 +117,8 @@ static void test_pick_saved(void) 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); - /* max 读不到(0)时只夹下限,不臆造上限 */ - v = bm_bri_pick_saved(-1, 90, 1, 0, &src); - check("saved: unknown max keeps 90", v == 90 && !strcmp(src, "panel"), 1); + /* 与 M20 watchdog 同一安全下限(同一系统不该有两个安全值) */ + check("saved: SAFE_LOW aligned with WD_BRI_SAFE_LOW (10)", BM_BRI_SAFE_LOW, 10); } /* ---- A2) clamp 语义单测:取代旧的 `>60 → 60` 硬钉 ---- */ @@ -111,22 +138,34 @@ static void test_floor_decide(void) const char *why = NULL; int v; - v = bm_bri_floor_decide(1, 0, 1, 0, 5000, &why); /* settings=1 + 扫描忙把面板写 0 */ + /* 触发条件回到 pre-M23:面板暗 + 未下电就补(不依赖 /proc 的 D 态 = P2-2 硬要求) */ + v = bm_bri_floor_decide(1, 0, 0, 5000, -1, &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, 1, 0, 5000, &why); /* 降级路径 saved=80 照写 80(旧码钉 60) */ + v = bm_bri_floor_decide(80, 0, 0, 5000, -1, &why); /* 降级 saved=80 照写 */ check("floor: degraded saved 80 floors at 80", v == 80 && !strcmp(why, "scan-floor"), 1); - v = bm_bri_floor_decide(0, 0, 1, 0, 5000, &why); /* saved 未知 -> 安全低亮 */ + v = bm_bri_floor_decide(0, 0, 0, 5000, -1, &why); /* saved 未知 -> 安全低亮 */ check("floor: unknown saved -> safe-low", v == BM_BRI_SAFE_LOW && !strcmp(why, "scan-floor"), 1); - v = bm_bri_floor_decide(1, 5, 1, 0, 5000, &why); /* 面板已亮:不动 */ + /* Bug 4 防退化回归用例(评审 P2-2):只要面板暗+未下电就必须能补,不能有任何 + * 「播放器看起来不忙」的前提 —— 旧 busy 门会让这两条变成 no-write(黑屏几分钟)。 */ + v = bm_bri_floor_decide(7, 0, 0, 89999, -1, &why); + check("floor: Bug4 regression — dark+unpowered always floors", v == 7 && + !strcmp(why, "scan-floor"), 1); + + v = bm_bri_floor_decide(1, 5, 0, 5000, -1, &why); /* 面板已亮:不动 */ check("floor: visible -> no write", v == 0 && !strcmp(why, "visible"), 1); - v = bm_bri_floor_decide(1, 0, 0, 0, 5000, &why); /* ← 用户主动熄屏(播放器不忙):不动 */ - check("floor: user screen-off (idle) -> no write", v == 0 && !strcmp(why, "idle-blank"), 1); - v = bm_bri_floor_decide(1, 0, 1, 1, 5000, &why); /* 面板已下电(bl_power>0):不动 */ + v = bm_bri_floor_decide(1, 0, 1, 5000, -1, &why); /* 面板已下电(bl_power>0):不动 */ check("floor: panel powered off -> no write", v == 0 && !strcmp(why, "panel-off"), 1); - v = bm_bri_floor_decide(1, 0, 1, 0, 0, &why); /* 90s 窗口结束:放手 */ + /* 电源键刚按下(用户主动熄屏的第二道保险):3s 内不补 */ + v = bm_bri_floor_decide(1, 0, 0, 5000, 1200, &why); + check("floor: recent power key -> no write", v == 0 && !strcmp(why, "power-key"), 1); + v = bm_bri_floor_decide(1, 0, 0, 5000, BM_BRI_PWR_HOLD_MS, &why); /* 到期即恢复补 */ + check("floor: power key expired -> floors", v == 1 && !strcmp(why, "scan-floor"), 1); + v = bm_bri_floor_decide(1, 0, 0, 5000, -1, &why); /* 本次开机没按过电源键 */ + check("floor: no power edge ever -> floors", v == 1 && !strcmp(why, "scan-floor"), 1); + v = bm_bri_floor_decide(1, 0, 0, 0, -1, &why); /* 90s 窗口结束:放手 */ check("floor: window over -> no write", v == 0 && !strcmp(why, "window-over"), 1); - v = bm_bri_floor_decide(1, 0, 1, 0, -1, &why); + v = bm_bri_floor_decide(1, 0, 0, -1, -1, &why); check("floor: negative window -> no write", v == 0 && !strcmp(why, "window-over"), 1); } @@ -213,18 +252,24 @@ static void test_e2e_backlight(void) check("e2e: panel file read 60", panel, 60); check("e2e: saved from settings = 1", saved, 1); - /* 扫描窗:播放器忙把背光写 0 → 保底写用户值 1 */ + /* 扫描窗:播放器待机把背光写 0(面板未下电)→ 保底写用户值 1 */ put_file(bl, "0\n"); const char *why = NULL; - int floor = bm_bri_floor_decide(saved, read_brightness(), 1, 0, 5000, &why); + int floor = bm_bri_floor_decide(saved, read_brightness(), 0, 5000, -1, &why); if (floor > 0) write_brightness(floor); check("e2e: scan floor writes 1 (not 60)", get_file_int(bl), 1); - /* 用户主动熄屏(不忙)→ 保底不写,面板保持 0 */ + /* 用户主动熄屏(播放器已 FBIOBLANK,bl_power>0)→ 保底不写,面板保持 0 */ put_file(bl, "0\n"); - floor = bm_bri_floor_decide(saved, read_brightness(), 0, 0, 5000, &why); + floor = bm_bri_floor_decide(saved, read_brightness(), 1, 5000, -1, &why); if (floor > 0) write_brightness(floor); - check("e2e: user screen-off stays dark", get_file_int(bl), 0); + check("e2e: user screen-off (panel-off) stays dark", get_file_int(bl), 0); + + /* 用户主动熄屏但 bl_power 没置位:电源键按下 1.2s 内也不补(第二道保险) */ + put_file(bl, "0\n"); + floor = bm_bri_floor_decide(saved, read_brightness(), 0, 5000, 1200, &why); + if (floor > 0) write_brightness(floor); + check("e2e: user screen-off (power key) stays dark", get_file_int(bl), 0); /* 热键确认框:面板黑着 → 按 saved(用户值 1)点亮,不是 60 */ int hk = bm_bri_hotkey_saved(saved, read_brightness(), mn_e2e, 101, &src); @@ -237,6 +282,17 @@ static void test_e2e_backlight(void) 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); + unlink(bl); unlink(st); unsetenv("MEOW_BACKLIGHT"); unsetenv("MEOW_SETTINGS_FILE"); From a9281c2d64316aa8a557dee6cf47c96cc8fc3a62 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 18:31:07 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(bootmenu):=20M23=20=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=20r2=20=E2=80=94=20=E7=94=B5=E6=BA=90=E9=94=AE=E9=94=81?= =?UTF-8?q?=E5=AD=98=E5=8F=96=E4=BB=A3=E3=80=8C3s=20=E6=8A=91=E5=88=B6?= =?UTF-8?q?=E3=80=8D=EF=BC=8C=E7=BC=BA=E9=94=AE=E8=A7=86=E5=90=8C=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P2-2(r2):删除「电源键按下后 3s 内不补」的定时抑制 —— 它到期就会把用户刚熄的屏点亮 (窗内可重复),且该行为被单测固定。改为**锁存**:扫描窗内一看到电源键按下边沿就置 scan_pwr_seen=1,本次窗口内此后一律不补(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**,该门在播放器熄屏 场景不生效;真正的门是电源键锁存。(上一轮注释里的「播放器灭屏会置 bl_power>0」是反向推断, 已删。) 顺手关掉评审指出的竞态:亮度保底判定块从主循环前半(keypoll 之前)移到电源键边沿更新之后, 电源按下与播放器写 0 落在同一 poll 间隙时不会再出现一轮「没看到电源键就补亮」。 非阻塞 1(采纳):settings 文件存在但缺 display.brightness 键 → 返回 -1(视同不可信), 不再走 launcher 设置表的 101→100;否则扫描窗待机补 100 比老 bug 的 60 更亮。 单测(52 → 56 条): - 删「power key expired -> floors」(把 bug 固定成期望的那条),换「latch 在窗口内永不过期」 「下一轮窗口(锁存清零)恢复正常补」「latched 时面板暗也不写、why=power-key-latched」; - 「missing key -> 100」改「missing key -> -1 (untrusted)」,并补 e2e「缺键 → saved=10 不是 100」; - 两条名字去 degraded:`floor: saved 80 (from settings) floors at 80`、 `hotkey: blank -> saved 60 (settings value)`(降级路径已封顶 10,80/60 只可能来自 settings)。 只交源码:不含二进制、不改 fw_content.conf、不动 build_bootmenu.sh。 --- patch_v2/bootmenu/meow_bootmenu.c | 141 +++++++++++++++++------------- patch_v2/bootmenu/test_bm_fw.c | 85 +++++++++++------- 2 files changed, 133 insertions(+), 93 deletions(-) diff --git a/patch_v2/bootmenu/meow_bootmenu.c b/patch_v2/bootmenu/meow_bootmenu.c index f3d8a12..432c71f 100644 --- a/patch_v2/bootmenu/meow_bootmenu.c +++ b/patch_v2/bootmenu/meow_bootmenu.c @@ -44,9 +44,10 @@ * [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、电源键按下 - * 后 3s 内不补;窗口结束彻底停手;日志同原因 ≥2s 一条。 + * 扫描窗保底恢复 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 钉值。 @@ -895,7 +896,12 @@ static int read_max_brightness(void) * 值;settings 文件不可读 → 返回 -1,调用方**不动**(退化为现状,保安全——选单阶段只写 brightness、 * 绝不碰 FBIOBLANK/blank,§18.24 纪律)。 * M23:fopen 失败做 BM_SETTINGS_TRIES 次短重试,覆盖 /usr/data 刚挂载 / 原子替换的瞬时窗口 - * (与 M20 的 wd_settings_brightness 同策略);只重试 open(rename 原子,已打开的 fd 内容完整)。 */ + * (与 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 @@ -908,7 +914,7 @@ static int bootmenu_settings_brightness(int *min_out) 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)) { @@ -923,9 +929,10 @@ static int bootmenu_settings_brightness(int *min_out) 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; - if (min_out) *min_out = mn; return b; } @@ -935,7 +942,6 @@ static int bootmenu_settings_brightness(int *min_out) * 安全下限,而不是 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 对齐(同一系统同一个安全下限) */ -#define BM_BRI_PWR_HOLD_MS 3000 /* 电源键按下后 N ms 内不补亮度(用户主动熄屏信号,见 floor_decide) */ /* 把任意亮度读数夹进面板合法区间 [mn, max_brightness](M23:取代旧 `>60 → 60` 的硬钉)。 * mn < 1 时按 1(防黑屏:亮着总比黑屏好,但也不伪造「比用户下限更亮」); @@ -979,23 +985,28 @@ static int bm_bri_pick_saved(int user, int cur, int mn, int maxb, const char **s } /* 扫描窗保底(Bug 4)决策:返回 >0 = 要写的亮度,0 = 不动;*why 回填原因供日志。 - * 触发条件**回到 pre-M23 的形态**(P2-2 评审):窗口内 + 面板暗(bri<=0)+ 面板未下电 - * (bl_power<=0)→ 补。**不依赖 /proc 的 D 态**:媒体扫描若在子线程,主线程是 S,D 态门会 - * 让 Bug 4 的防护变成空操作(黑屏几分钟回归),那比亮度偏高严重得多。 - * 避免「熄屏瞬间爆亮」改由两条**可观测**信号承担: - * ① bl_power>0 —— 播放器 FBIOBLANK 会置面板下电(bootmenu 确认框 unblank 时就把它写回 0, - * 说明本机确实有这个行为),用户主动熄屏走这条; - * ② 电源键按下后 BM_BRI_PWR_HOLD_MS 内不补 —— 10ms 轮询 evdev 就能看到,媒体扫描不会按电源键; - * 这是「播放器只写 brightness=0 但没碰 bl_power」时的第二道保险。 - * 窗口结束 → *why="window-over",调用方彻底停手。 */ + * 触发条件**回到 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, long long since_pwr_ms, + 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 (since_pwr_ms >= 0 && since_pwr_ms < BM_BRI_PWR_HOLD_MS) { *why = "power-key"; return 0; } + if (pwr_latched) { *why = "power-key-latched"; return 0; } *why = "scan-floor"; return saved > 0 ? saved : BM_BRI_SAFE_LOW; } @@ -1352,13 +1363,14 @@ static void bootmenu_supervisor(int player_pid) /* Bug 4:HibyOS 开机媒体扫描窗内做亮度保底。播放器忙(媒体扫描)时其背光 * 待机会把亮度降到 0、面板 blank → 黑屏几分钟。**只写 brightness、绝不碰 FBIOBLANK** * (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。 - * M23:补的值改为用户设置(不再 60);触发条件回到 pre-M23 的「面板暗 + 未下电」, - * 另加「电源键按下后 3s 内不补」(可观测信号,见 bm_bri_floor_decide)。 */ + * 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 才判定/写一次 */ 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"); @@ -1414,45 +1426,10 @@ static void bootmenu_supervisor(int player_pid) } } - /* 2a) Bug 4 亮度保底(M23):扫描窗内、面板暗(bri<=0)且**面板未下电**(bl_power<=0) - * → 写回用户设置亮度(不再 60)。触发条件回到 pre-M23 形态,**不依赖 /proc 的 D 态** - * (P2-2:媒体扫描若在子线程,主线程是 S,D 态门会让 Bug 4 防护变成空操作 → 黑屏回归)。 - * 避免「熄屏瞬间爆亮」由两条可观测信号承担:bl_power>0(播放器 FBIOBLANK)与 - * 电源键按下后 3s 内不补(见 bm_bri_floor_decide)。 - * (播放器扫描忙时无法响应唤醒,我们只把背光拉回,画面仍为播放器最后帧;不碰 blank。) - * 判定按 400ms 节流;日志同一原因 ≥2s 一条、跃迁也 ≥1s(防 `visible↔scan-floor` - * 每 400ms 交替刷屏;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 since_pwr = (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, since_pwr, &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); - } - /* 日志节流:同一原因 ≥2s 一条;原因跃迁也至少隔 1s(防 visible↔scan-floor - * 每 400ms 交替时刷屏)。最坏 ~90 行/90s 窗口。 */ - 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 pwr_ago=%lldms left=%lldms)", - (floor > 0) ? "floor" : "skip", floor, bri_user, bri, blp, since_pwr, win_left); - } - /* 窗口结束 → 彻底放手:此后不再读背光/bl_power,绝不与播放器抢面板。 */ - if (win_left <= 0) scan_keep_until = 0; - } - } - + /* 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); } @@ -1467,13 +1444,53 @@ static void bootmenu_supervisor(int player_pid) } /* 3) 热键:电源按下边沿 arming,窗口内音量减按下边沿触发。 - * 同一边沿也记到 last_pwr_edge_at:亮度保底据此避让「用户主动熄屏」(可观测信号, - * 不依赖 /proc 的 D 态;媒体扫描不会按电源键)。 */ - if (pwr && !prev_pwr) { pwr_hold_armed = 1; pwr_down_at = t; last_pwr_edge_at = t; } + * 同一边沿也记到 last_pwr_edge_at,并**锁存**到本次扫描窗结束:亮度保底据此 + * 不再与用户抢屏(用户已按键介入;需要亮屏时自己再按一次)。 */ + if (pwr && !prev_pwr) { + pwr_hold_armed = 1; pwr_down_at = t; last_pwr_edge_at = t; + if (scan_keep_until) scan_pwr_seen = 1; + } if (!pwr && prev_pwr) pwr_hold_armed = 0; /* 松开:disarm */ if (pwr_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); diff --git a/patch_v2/bootmenu/test_bm_fw.c b/patch_v2/bootmenu/test_bm_fw.c index 4f08564..415d86c 100644 --- a/patch_v2/bootmenu/test_bm_fw.c +++ b/patch_v2/bootmenu/test_bm_fw.c @@ -138,34 +138,41 @@ static void test_floor_decide(void) const char *why = NULL; int v; - /* 触发条件回到 pre-M23:面板暗 + 未下电就补(不依赖 /proc 的 D 态 = P2-2 硬要求) */ - v = bm_bri_floor_decide(1, 0, 0, 5000, -1, &why); /* settings=1,播放器待机写 0 */ + /* 触发条件回到 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, -1, &why); /* 降级 saved=80 照写 */ - check("floor: degraded saved 80 floors at 80", v == 80 && !strcmp(why, "scan-floor"), 1); - v = bm_bri_floor_decide(0, 0, 0, 5000, -1, &why); /* saved 未知 -> 安全低亮 */ + 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 防退化回归用例(评审 P2-2):只要面板暗+未下电就必须能补,不能有任何 - * 「播放器看起来不忙」的前提 —— 旧 busy 门会让这两条变成 no-write(黑屏几分钟)。 */ - v = bm_bri_floor_decide(7, 0, 0, 89999, -1, &why); - check("floor: Bug4 regression — dark+unpowered always floors", v == 7 && - !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, -1, &why); /* 面板已亮:不动 */ + 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, -1, &why); /* 面板已下电(bl_power>0):不动 */ + 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); - /* 电源键刚按下(用户主动熄屏的第二道保险):3s 内不补 */ - v = bm_bri_floor_decide(1, 0, 0, 5000, 1200, &why); - check("floor: recent power key -> no write", v == 0 && !strcmp(why, "power-key"), 1); - v = bm_bri_floor_decide(1, 0, 0, 5000, BM_BRI_PWR_HOLD_MS, &why); /* 到期即恢复补 */ - check("floor: power key expired -> floors", v == 1 && !strcmp(why, "scan-floor"), 1); - v = bm_bri_floor_decide(1, 0, 0, 5000, -1, &why); /* 本次开机没按过电源键 */ - check("floor: no power edge ever -> floors", v == 1 && !strcmp(why, "scan-floor"), 1); - v = bm_bri_floor_decide(1, 0, 0, 0, -1, &why); /* 90s 窗口结束:放手 */ + + /* 评审 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, -1, &why); + v = bm_bri_floor_decide(1, 0, 0, -1, 0, &why); check("floor: negative window -> no write", v == 0 && !strcmp(why, "window-over"), 1); } @@ -177,8 +184,8 @@ static void test_hotkey_saved(void) 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 不可读)时才用 60 */ - check("hotkey: blank -> degraded saved 60", v == 60 && !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 */ @@ -212,7 +219,11 @@ static void test_settings_parse(void) check("settings: clamp down to 100", bootmenu_settings_brightness(NULL), 100); put_file(path, "audio.volume = 10\n"); - check("settings: missing key -> 100", bootmenu_settings_brightness(NULL), 100); + /* 评审 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(); @@ -252,24 +263,26 @@ static void test_e2e_backlight(void) check("e2e: panel file read 60", panel, 60); check("e2e: saved from settings = 1", saved, 1); - /* 扫描窗:播放器待机把背光写 0(面板未下电)→ 保底写用户值 1 */ + /* 扫描窗:播放器待机把背光写 0 → 保底写用户值 1 */ put_file(bl, "0\n"); const char *why = NULL; - int floor = bm_bri_floor_decide(saved, read_brightness(), 0, 5000, -1, &why); + 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); - /* 用户主动熄屏(播放器已 FBIOBLANK,bl_power>0)→ 保底不写,面板保持 0 */ + /* bl_power>0(纯保险分支)→ 保底不写 */ put_file(bl, "0\n"); - floor = bm_bri_floor_decide(saved, read_brightness(), 1, 5000, -1, &why); + 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); - /* 用户主动熄屏但 bl_power 没置位:电源键按下 1.2s 内也不补(第二道保险) */ + /* 评审 r2 核心场景:用户按电源熄屏(bl_power 仍 0)→ 锁存 → 本次窗口内**永久**不补, + * 不会再出现「3s 后自己亮回来」 */ put_file(bl, "0\n"); - floor = bm_bri_floor_decide(saved, read_brightness(), 0, 5000, 1200, &why); + 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) stays dark", get_file_int(bl), 0); + 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); @@ -293,6 +306,16 @@ static void test_e2e_backlight(void) 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");