diff --git a/KEY_CONTROL_DESIGN.md b/KEY_CONTROL_DESIGN.md index 7797e92..d29a168 100644 --- a/KEY_CONTROL_DESIGN.md +++ b/KEY_CONTROL_DESIGN.md @@ -97,5 +97,6 @@ watchdog 主循环(`for(;;)`)加按键轮询(`read /dev/input/event0/event 2. **音量跳步**:watchdog 每按下边沿按 `audio.volume_step` 一步(不做 launcher 的线性渐变);短按与 launcher 菜单一致,长按不连续爬(媒体落地后可补)。launcher 应用返回时 `settings_load()` 同步。 3. **左右滑切屏**:**已落地(M2.81 Now Playing 横划页)**——主界面左滑进 Now Playing 大页;左缘返回手势保留。此处旧注「未实现切换逻辑」作废。 4. **媒体键**:**已落地(M2.81 起路由最近活跃媒体会话)**——播放/上下曲路由到最近活跃媒体会话(MSG_PLAYCTL),无会话时「抓住」不动作。此处旧注「路由待阶段 D」作废。 +5. **插孔消抖(2026-09-01,真机日志实证)**:4.4/3.5 插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生 VOL_UP 幽灵按住,launcher vol_ramp 曾从 24% 一路爬到上限 50(日志 225391 ramp 起 → 227213 爬满 → 紧随 audiod「jack changed」)。修:launcher 与 watchdog 音量键处理统一读 `/sys/class/switch/{headset,balance}/state`(节流读缓存:launcher 1s / watchdog 500ms,绝不在热路径每圈读文件),插孔状态变化后 **800ms 内忽略音量键边沿**(vol_ramp_begin 不起、在爬 ramp 中止、watchdog 单步忽略);另有 **ramp 总时长上限 VOL_RAMP_MAX_MS=3000** 兜底,防任何「持续按住」假象一次打满。SDK 输入层(框架 App 前台时的音量路径)需同款消抖——sdk/ 归其主,另行落地。 真机验收要点(§7):应用内电源单击/长按菜单/8s 强关、音量±调音量、播放/上下曲无反应(被抓住)、launcher 菜单同样一致、电源菜单任何界面可弹(含应用内)、息屏后任意键唤醒。 diff --git a/patch_v2/meow_launcherd/audio.c b/patch_v2/meow_launcherd/audio.c index 52c8a94..08c35b6 100644 --- a/patch_v2/meow_launcherd/audio.c +++ b/patch_v2/meow_launcherd/audio.c @@ -153,8 +153,65 @@ static unsigned long long vol_ramp_next_at = 0; static unsigned long long vol_ramp_press_at = 0; /* M2.18 Bug 4:本方向键按下时刻(长按判据) */ +/* ---- 插孔消抖(2026-09-01,真机日志实证)---- + * 4.4/3.5 耳机插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生 VOL_UP 幽灵按住 + * (EVIOCGKEY 轮询到持续 down),vol_ramp 一路爬到上限:日志 225391「volume UP <- ramp」 + * → 227213「ramp end -> 50」,紧接着 audiod 才报「route: jack changed -> 4.4 balance」。 + * 修法:/sys/class/switch/{headset,balance}/state 任一变化后 JACK_DEBOUNCE_MS 内忽略音量键 + * 边沿(vol_ramp_begin 不起、vol_ramp_tick 在爬也中止)。插拔耳机时没人按音量,误伤可忽略。 + * 检测走 1s 节流读缓存——绝不在 10ms 主循环热路径上每圈读文件。 */ +/* 宿主测试可 -D 覆盖(同 meow_settings.c MEOW_SETTINGS_FILE 惯例) */ +#ifndef JACK_HS_STATE +#define JACK_HS_STATE "/sys/class/switch/headset/state" +#endif +#ifndef JACK_BAL_STATE +#define JACK_BAL_STATE "/sys/class/switch/balance/state" +#endif + +static int jack_combo_cached = -1; /* hs*2+bal;-1=未初始化/节点缺失 */ + +static unsigned long long jack_poll_at = 0; /* 上次节流读时刻 */ + +static unsigned long long jack_changed_at = 0; /* 最近一次检测到插孔变化的时刻(0=无) */ + + +/* 读两个插孔开关节点合成 hs*2+bal;两节点都不可读返回 -1(不更新缓存,不误判变化)。 */ +static int jack_combo_read(void) +{ + int hs = read_file_int(JACK_HS_STATE); + int bal = read_file_int(JACK_BAL_STATE); + if (hs < 0 && bal < 0) return -1; + return (hs > 0 ? 2 : 0) | (bal > 0 ? 1 : 0); +} + + +/* 返回 1=插孔变化后消抖窗口内(音量键边沿应忽略/在爬 ramp 应中止)。 + * 自节流:每 JACK_POLL_MS 才真正读一次 sysfs。 */ +static int jack_debounce_active(void) +{ + unsigned long long now = now_ms(); + if (now - jack_poll_at >= JACK_POLL_MS) { + jack_poll_at = now; + int c = jack_combo_read(); + if (c >= 0) { + if (jack_combo_cached >= 0 && c != jack_combo_cached) { + jack_changed_at = now; + logmsg("jack state changed (%d -> %d), vol-key debounce %dms", + jack_combo_cached, c, JACK_DEBOUNCE_MS); + } + jack_combo_cached = c; + } + } + return jack_changed_at && (now - jack_changed_at < (unsigned long long)JACK_DEBOUNCE_MS); +} + + static void vol_ramp_begin(int delta) { + if (jack_debounce_active()) { /* 插孔瞬态期:忽略音量键边沿(幽灵按下) */ + logmsg("volume adj %+d ignored (jack debounce)", delta); + return; + } int target = vol_clamp(settings_volume + delta); if (target == settings_volume) return; vol_ramp_dir = (target > settings_volume) ? 1 : -1; @@ -175,6 +232,22 @@ int vol_ramp_tick(int key_held) unsigned long long now = now_ms(); if (now < vol_ramp_next_at) return 1; vol_ramp_next_at = now + VOL_RAMP_MS; + /* 插孔消抖:插拔瞬态的幽灵按住正在驱动 ramp(真机实证 24%→上限 50)—— + * 一旦检测到插孔变化(1s 节流轮询),立即中止在爬的 ramp。 */ + if (jack_debounce_active()) { + vol_ramp_dir = 0; vol_ramp_target = -1; + meow_settings_save(); + logmsg("volume ramp aborted (jack debounce) -> %d", settings_volume); + return 0; + } + /* 兜底:ramp 总时长上限 VOL_RAMP_MAX_MS——防任何「持续按住」假象(ADC 噪声卡 key_held=1) + * 一次打满/打到底。正常长按 0→上限(通常 ≤2.7s)不受影响;到顶想再调需松手重按。 */ + if (now - vol_ramp_press_at >= (unsigned long long)VOL_RAMP_MAX_MS) { + vol_ramp_dir = 0; vol_ramp_target = -1; + meow_settings_save(); + logmsg("volume ramp force-stop (%dms cap) -> %d", VOL_RAMP_MAX_MS, settings_volume); + return 0; + } /* 先看是否已到「当前目标值」(settings_volume 等于 target,而非候选 v)—— * 因为一旦在 target 再取 v=settings_volume+dir 便越过 target,导致 hold_wait 失效继续爬。 */ if (settings_volume == vol_ramp_target) { diff --git a/patch_v2/meow_launcherd/backlight.c b/patch_v2/meow_launcherd/backlight.c index a7305dc..9d75f8d 100644 --- a/patch_v2/meow_launcherd/backlight.c +++ b/patch_v2/meow_launcherd/backlight.c @@ -41,7 +41,7 @@ void apply_user_brightness(void) } int v = meow_settings_brightness_clamped(); if (v <= 0) v = 60; /* 防御:clamp 后不可能 ≤0,仅兜底 */ - write_brightness(v); + write_brightness(v, "apply_user_brightness"); g_sys.brightness = v; meow_brightness_pending = 0; logmsg("apply user brightness (MeowOS takeover) -> %d", v); @@ -73,13 +73,16 @@ static void write_file(const char *path, const char *val) } -void write_brightness(int val) +void write_brightness(int val, const char *src) { + /* 亮度幽灵排查(2026-09-01:用户报告插耳机瞬间亮度自己跳到 raw 上限,写者未知): + * 每次写背光都留值+调用源线索,下次「亮度自己跳」可直接定位写者。 */ char buf[8]; int l = snprintf(buf, sizeof buf, "%d\n", val); int fd = open(BACKLIGHT, O_WRONLY); - if (fd < 0) { logmsg("backlight open fail: %s", strerror(errno)); return; } - if (write(fd, buf, l) < 0) logmsg("backlight write %d: %s", val, strerror(errno)); + if (fd < 0) { logmsg("backlight <- %d (src=%s) open fail: %s", val, src, strerror(errno)); return; } + if (write(fd, buf, l) < 0) logmsg("backlight <- %d (src=%s): %s", val, src, strerror(errno)); + else logmsg("backlight <- %d (src=%s)", val, src); close(fd); } @@ -107,7 +110,7 @@ void ensure_backlight(void) } int target = (saved_brightness > 0) ? saved_brightness : 60; logmsg("backlight off (cur=%d bl_power=%d) -> %d", cur, blp, target); - if (cur <= 0) write_brightness(target); + if (cur <= 0) write_brightness(target, "ensure_backlight"); if (blp > 0) write_file(BACKLIGHT_POWER, "0\n"); } @@ -126,7 +129,7 @@ void restore_backlight(void) { if (menu_opened_blank) { logmsg("restore: panel was blanked at menu open, re-blank (0 + FBIOBLANK 4)"); - write_brightness(0); + write_brightness(0, "restore_backlight(re-blank)"); int fbfd = open(FB_DEV, O_RDWR); if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 4); close(fbfd); } menu_opened_blank = 0; @@ -138,7 +141,7 @@ void restore_backlight(void) int target = (saved_brightness > 0) ? saved_brightness : 60; logmsg("restore backlight (cur=%d bl_power=%d) -> %d (saved=%d)", cur, blp, target, saved_brightness); - if (cur <= 0) write_brightness(target); + if (cur <= 0) write_brightness(target, "restore_backlight"); if (blp > 0) write_file(BACKLIGHT_POWER, "0\n"); } } diff --git a/patch_v2/meow_launcherd/keys.c b/patch_v2/meow_launcherd/keys.c index 3c22771..4c568ba 100644 --- a/patch_v2/meow_launcherd/keys.c +++ b/patch_v2/meow_launcherd/keys.c @@ -40,7 +40,7 @@ void meow_panel_off(void) { meow_saved_brightness = read_brightness(); if (meow_saved_brightness <= 0) meow_saved_brightness = 60; - write_brightness(0); + write_brightness(0, "meow_panel_off"); if (screen_off_mode == 0) { /* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */ int f = open(FB_DEV, O_RDWR); @@ -58,7 +58,7 @@ void meow_panel_on(void) { int f = open(FB_DEV, O_RDWR); if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); } - write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60); + write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60, "meow_panel_on"); meow_screen_on = 1; /* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在 * 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位 diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index f20944f..36e946a 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -140,6 +140,9 @@ extern "C" { #define MEOW_VOL_MAX_DEFAULT 100 /* 默认最大音量上限(尊重上限;可配 audio.volume_max) */ #define VOL_DRAG_DAC_MS 60 /* 滑条拖动期间两次真实 DAC 写入的最短墙钟间隔 */ #define VOL_RAMP_MS 15 /* 每 1% 步进的最短墙钟间隔 */ +#define VOL_RAMP_MAX_MS 3000 /* ramp 总时长上限(防 ADC 噪声假按住一次打满;正常长按到上限 <3s) */ +#define JACK_POLL_MS 1000 /* 插孔状态节流读取间隔(绝不在 10ms 热路径每圈读文件) */ +#define JACK_DEBOUNCE_MS 800 /* 插孔变化后忽略音量键边沿的窗口(插拔时没人按音量,误伤可忽略) */ #define BT_ROLE_OFF 0 /* 关 */ #define BT_ROLE_ON_NO_CONN 1 /* 开未连接 */ #define BT_ROLE_MASTER 2 /* 主模式:连了外围(如耳机) */ @@ -759,7 +762,7 @@ void vol_restore(struct vol_snap s); struct vol_snap vol_snapshot(void); int wlan_connected(void); int wlan_radio_up(void); -void write_brightness(int val); +void write_brightness(int val, const char *src); /* src=调用源线索,随值一并 logmsg(亮度幽灵排查) */ #ifdef __cplusplus } diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 1121cec..9e01344 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/notif_center.c b/patch_v2/meow_launcherd/notif_center.c index bb84551..62e6c5d 100644 --- a/patch_v2/meow_launcherd/notif_center.c +++ b/patch_v2/meow_launcherd/notif_center.c @@ -386,11 +386,11 @@ static int ncp_chip_apply(int chip) int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin); if (mnp_brightness_clamp(cur, vmin) < 100) { meow_settings_set_int("display.brightness", 100); - write_brightness(100); + write_brightness(100, "notif_center bright_chip"); g_sys.brightness = 100; } else { meow_settings_set_int("display.brightness", vmin); - write_brightness(vmin); + write_brightness(vmin, "notif_center bright_chip"); g_sys.brightness = vmin; } paint_sb_right(); @@ -865,7 +865,7 @@ static int ncp_slider_apply(int which, int tx, int save_now) int cur = 0; meow_settings_get_int("display.brightness", &cur); if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */ meow_settings_set_int("display.brightness", v); - write_brightness(v); + write_brightness(v, "notif_center bright_slider"); g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */ paint_sb_right(); if (save_now) meow_settings_save(); diff --git a/patch_v2/meow_launcherd/settings_page.c b/patch_v2/meow_launcherd/settings_page.c index 3fd47cf..4b73b1a 100644 --- a/patch_v2/meow_launcherd/settings_page.c +++ b/patch_v2/meow_launcherd/settings_page.c @@ -373,7 +373,7 @@ static void s_commit(const srow_t *r, int save_now) * 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。 * 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */ int v; if (meow_settings_get_int("display.brightness", &v) == 0) { - write_brightness(v); + write_brightness(v, "settings_page bright_slider"); g_sys.brightness = v; paint_sb_right(); } diff --git a/patch_v2/meow_launcherd/test_vol_ramp_jack b/patch_v2/meow_launcherd/test_vol_ramp_jack new file mode 100755 index 0000000..1684324 Binary files /dev/null and b/patch_v2/meow_launcherd/test_vol_ramp_jack differ diff --git a/patch_v2/meow_launcherd/test_vol_ramp_jack.c b/patch_v2/meow_launcherd/test_vol_ramp_jack.c new file mode 100644 index 0000000..e28dbd9 --- /dev/null +++ b/patch_v2/meow_launcherd/test_vol_ramp_jack.c @@ -0,0 +1,189 @@ +/* test_vol_ramp_jack.c — 宿主单测:音量 ramp 的插孔消抖 + 时长上限(2026-09-01)。 + * + * 背景(真机日志实证):4.4 耳机插入瞬间,插孔电气瞬态在音量键 ADC(jz_adc_aux, + * event2)上产生 VOL_UP 幽灵按住,launcher vol_ramp 从 24% 一路爬到上限 50 + * (225391「volume UP <- ramp」→ 227213「ramp end -> 50」,紧随 audiod「jack changed」)。 + * + * 编译运行(在 patch_v2/meow_launcherd 下): + * gcc -Wall -Wextra -Os -I../sdk \ + * -DJACK_HS_STATE='"/tmp/tvrj_hs"' -DJACK_BAL_STATE='"/tmp/tvrj_bal"' \ + * test_vol_ramp_jack.c -o test_vol_ramp_jack && ./test_vol_ramp_jack + * 全 PASS -> exit 0。 + * + * 手法:#include "audio.c" 同 TU 平移(同 test_notif_tf.c 惯例);now_ms 用假时钟 + * (手动推进,3s 上限与 1s 节流都可瞬时测);meow_settings_* 打内存桩。 */ + +/* ---- 桩(先定义,audio.c 经内部头拿到声明) ---- */ +#include +#include +#include +#include + +static unsigned long long g_fake_now = 100000; /* 假时钟(ms),手动推进 */ + +unsigned long long now_ms(void); /* 前置声明,防与头文件顺序问题 */ +unsigned long long now_ms(void) { return g_fake_now; } +static void advance_ms(unsigned long long d) { g_fake_now += d; } + +void logmsg(const char *fmt, ...); +void logmsg(const char *fmt, ...) +{ + va_list ap; va_start(ap, fmt); + fprintf(stderr, " [log] "); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); + va_end(ap); +} + +/* meow_settings 内存桩:audio.c 只用 get_int("audio.volume*")/set_int/save */ +static int g_store_volume = 24; +static int g_store_max = 50; +static int g_store_step = 1; +static int g_save_count = 0; + +extern int settings_volume; /* audio.c 的缓存全局(同 TU #include 后链接可见) */ +extern int settings_volume_max; +extern int settings_volume_step; + +int meow_settings_get_int(const char *key, int *out) +{ + if (!strcmp(key, "audio.volume")) { *out = g_store_volume; return 0; } + if (!strcmp(key, "audio.volume_max")) { *out = g_store_max; return 0; } + if (!strcmp(key, "audio.volume_step")) { *out = g_store_step; return 0; } + return -1; +} +int meow_settings_get_enum(const char *key, int *out) { (void)key; (void)out; return -1; } +int meow_settings_get_bool(const char *key, int *out) { (void)key; (void)out; return -1; } +int meow_settings_set_int(const char *key, int v) +{ + /* 真代码经 settings_sync_cache_listener 把 backend 值同步到 audio.c 的 settings_volume + * 缓存全局;桩里直接同步,保持同一语义。 */ + if (!strcmp(key, "audio.volume")) { g_store_volume = v; settings_volume = v; return 0; } + return -1; +} +int meow_settings_save(void) { g_save_count++; return 0; } + +/* util.c 符号桩(audio.c 引用,宿主无 util.c) */ +int read_file_int(const char *path); +int read_file_int(const char *path) +{ + FILE *f = fopen(path, "r"); + if (!f) return -1; + int v = -1; + if (fscanf(f, "%d", &v) != 1) v = -1; + fclose(f); + return v; +} + +/* 其它模块全局桩(settings_sync_cache_* 引用;本测试不走该路径,仅满足链接) */ +int screen_off_timeout = 0; +int screen_off_mode = 0; +int power_longpress_menu_ms = 0; +int power_forceoff_ms = 0; +int refresh_rate_cfg = 60; +int refresh_always_max = 0; + +#include "audio.c" + +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 void write_file(const char *path, int v) +{ + FILE *f = fopen(path, "w"); + if (!f) { perror(path); exit(2); } + fprintf(f, "%d\n", v); + fclose(f); +} + +/* 驱动 ramp 直到停止或步数超限;held=key_held 恒值;每步推进 40ms + * (模拟真机每步 ~52ms 的 amixer system() 开销:0→100 全程 ~4s,必然越过 3s 上限, + * 而常见 24→50 窗口 ~1s 不受影响)。 */ +static int drive_ramp(int held, int max_steps) +{ + int steps = 0; + while (vol_ramp_dir != 0 && steps < max_steps) { + advance_ms(40); + vol_ramp_tick(held); + steps++; + } + return steps; +} + +/* 场景起始音量:backend 桩与 audio.c 缓存全局一起置(真代码由 settings_load 同步)。 */ +static void set_vol(int v) { g_store_volume = v; settings_volume = v; } + +/* 音量上限/跳步同理:vol_clamp 读的是 audio.c 的 settings_volume_max/settings_volume_step。 */ +static void set_max(int v) { g_store_max = v; settings_volume_max = v; } + +int main(void) +{ + /* T0 基线回归:无插孔节点(文件不存在)→ 单击起 ramp,到 target 即停(只跳 step 格)。 */ + unlink(JACK_HS_STATE); unlink(JACK_BAL_STATE); + set_vol(24); set_max(50); g_store_step = 1; g_save_count = 0; + settings_volume_adj(+1); + check("T0 ramp started", vol_ramp_dir != 0, 1); + drive_ramp(0, 100); + check("T0 click stops at target 25", g_store_volume, 25); + check("T0 ramp stopped", vol_ramp_dir, 0); + + /* T1 长按续爬到上限 50(无插孔干扰,正常长按体验不受消抖影响)。 */ + set_vol(24); g_save_count = 0; + settings_volume_adj(+1); + drive_ramp(1, 5000); /* 24→50=26 步 ×40ms≈1s,远未到 3s 上限 */ + check("T1 long-press reaches max 50", g_store_volume, 50); + check("T1 stopped at max", vol_ramp_dir, 0); + + /* T2 时长上限:max 提到 100(0→100 需 ~100 步 >3s 窗)→ 3s 强制停,不打满。 + * 假时钟 40ms/步 → 3s=75 步 <100 步(4s);drive 300 步必触发 cap。 */ + set_vol(0); set_max(100); g_save_count = 0; + settings_volume_adj(+1); + drive_ramp(1, 300); + check("T2 ramp force-stopped by cap", vol_ramp_dir, 0); + check("T2 volume below max (not pinned)", g_store_volume < 100, 1); + check("T2 volume climbed (>0)", g_store_volume > 0, 1); + printf(" (T2 stopped at volume=%d after cap)\n", g_store_volume); + + /* T3 插孔消抖:prime 插孔状态(hs=0,bal=0),随后模拟插入 4.4(bal=1)。 + * 变化后 800ms 内:vol_ramp_begin 被忽略;在爬的 ramp 被 tick 中止。 */ + set_max(50); set_vol(24); + write_file(JACK_HS_STATE, 0); write_file(JACK_BAL_STATE, 0); + advance_ms(JACK_POLL_MS + 10); + settings_volume_adj(+1); /* 这次调用顺带 prime 插孔缓存(无变化→不消抖) */ + drive_ramp(0, 100); + check("T3 prime: normal click still works", g_store_volume, 25); + + write_file(JACK_BAL_STATE, 1); /* 模拟 4.4 插入 */ + advance_ms(JACK_POLL_MS + 10); /* 过节流窗,下次检测即发现变化 */ + set_vol(24); g_save_count = 0; + settings_volume_adj(+1); /* 此调用检测变化 → 边沿被忽略 */ + check("T3 press ignored in debounce window", vol_ramp_dir, 0); + check("T3 volume untouched", g_store_volume, 24); + + /* T3b 在爬的 ramp 被插孔变化中止(幽灵按住场景:ramp 已起,随后检测到插孔变化)。 */ + /* 先让消抖窗过期:推进 >800ms,再正常起 ramp(长按)。 */ + advance_ms(JACK_DEBOUNCE_MS + 100); + set_vol(24); + settings_volume_adj(+1); + check("T3b ramp restarted after window", vol_ramp_dir, 1); + vol_ramp_tick(1); advance_ms(20); vol_ramp_tick(1); /* 爬了两步 */ + int before = g_store_volume; + write_file(JACK_HS_STATE, 1); /* 又插 3.5(变化) */ + advance_ms(JACK_POLL_MS + 10); /* 过节流窗 */ + vol_ramp_tick(1); /* 检测到变化 → 中止 */ + check("T3b in-flight ramp aborted by jack change", vol_ramp_dir, 0); + check("T3b volume froze at abort point", g_store_volume, before); + printf(" (T3b aborted at volume=%d)\n", g_store_volume); + + /* T3c 消抖窗过后恢复正常(误伤可忽略验证)。 */ + advance_ms(JACK_DEBOUNCE_MS + 100); + settings_volume_adj(+1); + check("T3c press works again after window", vol_ramp_dir, 1); + drive_ramp(0, 100); + + unlink(JACK_HS_STATE); unlink(JACK_BAL_STATE); + printf("\n%d pass, %d fail\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/patch_v2/meow_watchdog/meow_watchdog b/patch_v2/meow_watchdog/meow_watchdog index 49f5d80..96566b5 100755 Binary files a/patch_v2/meow_watchdog/meow_watchdog and b/patch_v2/meow_watchdog/meow_watchdog differ diff --git a/patch_v2/meow_watchdog/meow_watchdog.c b/patch_v2/meow_watchdog/meow_watchdog.c index c70b403..d2a1b57 100644 --- a/patch_v2/meow_watchdog/meow_watchdog.c +++ b/patch_v2/meow_watchdog/meow_watchdog.c @@ -20,6 +20,8 @@ * 让位,绝不与 launcher 抢屏/抢键);launcher 画菜单,本进程不自绘。 * 电源长按 ≥8s -> 强制关机兜底(仅当菜单未生效/launcher 未接管)。 * 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。 + * 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿 + * (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。 * 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。 * (8s 强关的「防误触 / pm_interacted」逻辑在 launcher 的 show_power_menu 内,此进程只做兜底。) * - M2.86(226) SDK 框架 App 让位:启动时经 /proc//exe 读同目录 app.json 的 framework @@ -46,7 +48,7 @@ #include #include #include -#include +#include #include #include #include @@ -379,6 +381,55 @@ static void wd_vol_save(void) wd_log("vol: saved volume=%d(max=%d,step=%d)", wd_vol, wd_vol_max, wd_vol_step); } +/* ---- 插孔消抖(2026-09-01,与 launcher audio.c 同款,真机日志实证)---- + * 4.4/3.5 耳机插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生幽灵按下 + * (launcher 侧实证:VOL_UP 假按住 → ramp 24%→上限 50,紧随 audiod「jack changed」)。 + * /sys/class/switch/{headset,balance}/state 任一变化后 WD_JACK_DEBOUNCE_MS 内忽略音量键 + * 边沿(插拔耳机时没人按音量,误伤可忽略)。读文件走节流缓存,不每圈读。 */ +#define WD_JACK_HS_STATE "/sys/class/switch/headset/state" +#define WD_JACK_BAL_STATE "/sys/class/switch/balance/state" +#define WD_JACK_POLL_MS 500 /* 节流读取间隔(主循环 100ms,5 圈一读) */ +#define WD_JACK_DEBOUNCE_MS 800 /* 插孔变化后忽略音量键边沿的窗口 */ + +static int wd_jack_combo = -1; /* hs*2+bal;-1=未初始化/节点缺失 */ +static unsigned long long wd_jack_poll_at = 0; +static unsigned long long wd_jack_changed_at = 0; /* 0=未检测到变化 */ + +static int wd_read_int_file(const char *path) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + char buf[16]; + int n = read(fd, buf, sizeof buf - 1); + close(fd); + if (n <= 0) return -1; + buf[n] = 0; + return atoi(buf); +} + +static void wd_jack_tick(void) +{ + unsigned long long now = now_ms(); + if (now - wd_jack_poll_at < WD_JACK_POLL_MS) return; + wd_jack_poll_at = now; + int hs = wd_read_int_file(WD_JACK_HS_STATE); + int bal = wd_read_int_file(WD_JACK_BAL_STATE); + if (hs < 0 && bal < 0) return; /* 节点缺失:不更新缓存,不误判变化 */ + int c = (hs > 0 ? 2 : 0) | (bal > 0 ? 1 : 0); + if (wd_jack_combo >= 0 && c != wd_jack_combo) { + wd_jack_changed_at = now; + wd_log("jack state changed (%d -> %d), vol-key debounce %dms", + wd_jack_combo, c, WD_JACK_DEBOUNCE_MS); + } + wd_jack_combo = c; +} + +static int wd_jack_debounce(void) +{ + return wd_jack_changed_at && + (now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS); +} + /* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。 */ static void wd_vol_adj(int dir) { @@ -612,10 +663,17 @@ int main(int argc, char **argv) } else if (type == EV_KEY && code == KEY_POWER) { handle_power(val); /* M2.22(A)/M2.24:电源单击=息屏/亮屏,长按 tick 里判菜单 */ } else if (type == EV_KEY && code == KEY_VOLUMEUP) { - /* M2.86(226):SDK 框架 App 前台 -> 音量键让位 SDK(防双步进)。 */ - if (val == 1 && !g_fw_app) wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */ + /* M2.86(226):SDK 框架 App 前台 -> 音量键让位 SDK(防双步进)。 + * 插孔消抖:插拔瞬态的 ADC 幽灵按下直接忽略。 */ + if (val == 1 && !g_fw_app) { + if (wd_jack_debounce()) wd_log("volume UP ignored (jack debounce)"); + else wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */ + } } else if (type == EV_KEY && code == KEY_VOLUMEDOWN) { - if (val == 1 && !g_fw_app) wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */ + if (val == 1 && !g_fw_app) { + if (wd_jack_debounce()) wd_log("volume DOWN ignored (jack debounce)"); + else wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */ + } } else if (type == EV_KEY && (code == KEY_PLAYPAUSE || code == KEY_PREVIOUSSONG || code == KEY_NEXTSONG || code == KEY_PREVIOUS)) { /* M2.86(226):框架 App 前台 -> 媒体键让位 SDK(接媒体会话路由)。 */ @@ -626,6 +684,7 @@ int main(int argc, char **argv) } } wd_power_tick(); /* M2.24:电源长按检测(3s 菜单 / 8s 强关 / 菜单期让位) */ + wd_jack_tick(); /* 插孔状态节流轮询(消抖音量键幽灵按下) */ elapsed += 100; } return 0; diff --git a/scripts/build/fw_content.conf b/scripts/build/fw_content.conf index 5f6f203..0be19e3 100644 --- a/scripts/build/fw_content.conf +++ b/scripts/build/fw_content.conf @@ -28,8 +28,8 @@ # ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)---- patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|4441822d87e76bfce2ce584604ac3830|引导选单 M2.85(sysui让位 agent-216) -patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|2aa6178090837793957b815ff2f108ba|主屏 launcher(+充电态权威源 mp2731-charger/status,修 battery/status 撒谎恒 Discharging) -patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|6373d8e5cac62a34ee468a344b267761|应用看门狗 M2.89(SIGUSR1 仅裸沉浸 App,框架 App 不误杀 agent-236) +patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|7be1d4eb786ff08f4b20f3be775a8151|主屏 launcher(插孔消抖+ramp 3s 上限+亮度写日志;耳机插拔 ADC 幽灵音量键修复) +patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|baa997ea9100b04e1cc96e25c16f7e4b|应用看门狗(插孔消抖:插拔瞬态忽略音量键 800ms;M2.89 SIGUSR1 仅裸沉浸 App) patch_v2/meow_svcd/meow_svcd|/usr/bin/meow_svcd|605646c4cc93f2baf29071ea6bc870ea|服务管理器 M2.77(+崩溃上报 meow_crash agent-187) mips-repo/bins/meow_mdns/bin/meow_mdns|/usr/bin/meow_mdns|d6b0151bda26768f19952dae1342610d|mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194) patch_v2/meow_sysui/meow_sysui|/usr/bin/meow_sysui|173013cc8f0899b3001d0b894ac0d9e6|系统 UI(sysui)