/* main_menu.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" /* set by run_menu: the player's panel was blanked (screen timeout) when the * menu opened. fb_open() unblanks it so the menu is visible; on session end * restore_backlight() re-blanks (brightness 0 + FBIOBLANK(4)) to put the * physical panel back into the state the player's display state machine * assumes — otherwise the player resumes believing the panel is blanked * while the panel is actually lit (frozen frame). Blanking is idempotent * and matches what the player itself did, so it cannot wedge the player. */ int menu_opened_blank = 0; /* ---- config (MeowAPPs/meow.conf): pure black background by default ---- */ uint32_t menu_bg = 0x000000; static int touch_fd = -1; static int t_last_x = -1, t_last_y = -1; static int t_down = 0; static int t_down_x = 0, t_down_y = 0; /* M2.23 手势原点锁定:BTN_TOUCH=1 只标「已按下」,坐标(goodix)不保证先于按下边沿, * 故 t_down_x/y 延迟到「本次按下之后」的首个有效 ABS 才锁定,避免把上一手势残留 * (-1/旧坐标) 当本次原点。t_down_valid=0 表示本次尚未锁到有效坐标。 */ static int t_down_valid = 0; /* ⑨ 顶部热区「跟手下拉」进入通知中心的 per-gesture latch:进入后置 1,本次手势不再二次进入; * 由下次 touch-down 复位。防止「拖动中切入 run_notif_center 后返回」把同次手势再判成打开/滑动。 */ static int menu_live_open_pending = 0; /* Bug 2/4 (2026-08-28): after an app exits we return to the menu, but touch * events that were generated DURING the app run (the goodix ghost touch the * v6.4 watchdog guards against, plus the back-tap/press that actually exited * the app) are still queued on the launcher's touch fd and get replayed as * fresh menu taps. A replayed tap on an app row relaunches the app (symptom: * BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO * PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen * flashes back to the player — Bug 4). We drain the queue immediately after * the app exits and ignore touches for a short cooldown that covers the * measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */ static unsigned long long menu_touch_cooldown_until = 0; static void menu_drain_touch(void) { if (touch_fd >= 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } } } static void menu_arm_touch(long ms) { menu_drain_touch(); menu_touch_cooldown_until = now_ms() + ms; } /* Drain the touch queue + arm the post-app cooldown (call right after the * app exits, before the menu resumes polling touch). The drain always runs so * touches queued by the launch-attempt tail are cleared; the cooldown only * arms when an app actually ran (app_ran) so a failed launch doesn't lock the * menu out for 2.5s. */ static void menu_after_app_exit(int app_ran) { menu_drain_touch(); /* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。 * 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短, * 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */ if (app_ran) menu_touch_cooldown_until = now_ms() + 1200; } /* Header block (status bar + MEOWAPPS title + thin divider) covering the * region y=0..58, kept separate from the app-list body so the ~1s clock tick * (draw_clock) never redraws or tears the list. draw_menu fills the whole * background then calls draw_header_content. The menu is pre-drawn before * fb_norm_buf0 so the initial pan shows a complete frame. */ static void draw_header_content(void) { draw_statusbar(); /* M2.6 标题真字体(28px;回退点阵3x) */ if (mfont_ready) { meow_font_drawtext((uint16_t *)fb, 24, 48, "MeowOS 应用", rgb565(0xFFE066), 28); fill_rect(0, 48 + meow_font_line_height(28) + 2, fb_xres, 2, 0x3A3A5C); } else { draw_text(24, 28, "MEOWAPPS", 3, 0xFFE066); fill_rect(0, 56, fb_xres, 2, 0x3A3A5C); } } /* 每秒一次刷新状态栏时钟(顶部 HH:MM:SS,唯一每秒都变的值)。Stage-One 状态栏收敛后, * launcher 不再自绘状态栏——draw_clock 只经 paint_sb_clock() → sb_sdk_refresh() 把权威 * 状态喂进 SDK 全局状态栏 + 标脏顶条,由 SDK present() 顶条重绘(时钟/电量/音量等一并刷新)。 * 绝不动应用列表(仅顶条 28px 脏),故列表不重绘/不撕裂;无 amixer/sysfs 每帧开销。 */ void draw_clock(void) { paint_sb_clock(); /* → sb_sdk_refresh():喂 SDK + 标脏顶条,present() 时 SDK 重绘状态栏 */ } /* M2.36 右向小箭头(进入/下拉提示)。点阵后备字库(5x7)缺 '>' 字形(glyph_index 落到空 * 格),故用像素画一个 4x7 的 '>';顶点在 (x,y) 指向右。设置页选择器/设置项行复用。 */ void s_draw_chevron(int x, int y, uint32_t color) { for (int i = 0; i < 4; i++) { put_px(x - i, y - i, color); put_px(x - i, y + i, color); } } /* M2.39 左向小箭头(返回提示)。点阵后备字库同样缺 '<' 字形('‹' 会渲染成方框), * 故用像素画一个指向左的 4x7 '<';顶点在 (x,y)。设置页底部「返回」/任何左向提示复用。 */ void s_draw_chevron_left(int x, int y, uint32_t color) { for (int i = 0; i < 4; i++) { put_px(x + i, y - i, color); put_px(x + i, y + i, color); } } static void draw_menu(struct app apps[MAX_APPS], int napps, int state, int cursor, int toast_ms, const char *toast) { fill_rect(0, 0, fb_xres, fb_yres, menu_bg); draw_header_content(); char line[64]; int y = ROW_TOP; if (state == 1) { if (mfont_ready) { ui_draw(30, 200, "没有存储卡", 0xFFFFFF, 20); ui_draw(30, 234, "请插入TF卡后重开菜单", 0x9FB8D9, 20); } else { draw_text(30, 200, "NO STORAGE CARD", 2, 0xFFFFFF); draw_text(30, 240, "INSERT TF CARD", 2, 0x9FB8D9); draw_text(30, 272, "THEN RE-OPEN MENU", 2, 0x9FB8D9); } } else if (state == 2 || napps == 0) { if (mfont_ready) { ui_draw(30, 200, "没有找到应用", 0xFFFFFF, 20); ui_draw(30, 234, "请放入应用到", 0x9FB8D9, 20); ui_draw(30, 268, "MeowAPPs//app.json", 0x9FB8D9, 20); } else { draw_text(30, 200, "NO APPS FOUND", 2, 0xFFFFFF); draw_text(30, 240, "PUT APP IN", 2, 0x9FB8D9); draw_text(30, 272, "MeowAPPs//app.json", 2, 0x9FB8D9); } } else { for (int i = 0; i < napps; i++) { int sel = (i == cursor); if (sel) { fill_rect(8, y - 4, fb_xres - 16, ROW_H - 8, 0x2A3A6A); snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name); ui_draw(24, y, line, 0xFFFFFF, 20); if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0xFF6B6B, 20); } else { snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name); ui_draw(24, y, line, 0xC8D0E8, 20); if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0x8A4A4A, 20); } y += ROW_H; } } /* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前; * 不依赖 .mpk(图标/文案就地绘制)。音量导航可把光标移到此处(cursor==napps), * 触摸点击该行进入设置页(run_menu 分发到 run_settings_page)。 */ { int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */ int selset = (cursor == napps); /* 光标已移到设置行 */ fill_rect(8, sy - 4, fb_xres - 16, ROW_H - 8, selset ? 0x2A3A6A : 0x12142A); if (mfont_ready) ui_draw(24, sy, "系统设置", selset ? 0xFFFFFF : 0xC8D0E8, 20); else draw_text(24, sy, "SETTINGS", 2, selset ? 0xFFFFFF : 0xC8D0E8); s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, 0x9FB8D9); /* 右向进入提示 */ } /* exit row */ fill_rect(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 0x1A1A3A); /* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */ if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", 0x8FBFD9, 20); else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, 0x8FBFD9); /* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。屏幕高 fb_yres=720,字号 20 真实行高 * meow_font_line_height(20)=27(ascent 21 + descent 6);原 y=700 使字底 = 700+27 = 727 * > 720,溢出 7px ≈ 1/5 字符高被裁。改为 y = fb_yres - 行高 - 底部留白。非 mfont 回退 * 分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */ #define FOOTY_PAD 8 /* dev version, bottom-right: single source MEOWOS_VERSION. */ { char ver[32]; snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version); int foot_y = fb_yres - meow_font_line_height(20) - FOOTY_PAD; ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, 0x666688, 20); } if (toast_ms > 0 && toast && toast[0]) { fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, 0x2A0A0A); ui_draw(30, TOAST_Y, toast, 0xFF8080, 20); } } /* ================================================================== */ /* M2.3A 电源菜单(Android 11 样式):长按电源 3s 弹出;8s 强制关机 */ /* 半透明深色遮罩 + 居中 5 个大按钮(2 列×3 行,ASCII 点阵,不耦合字体) */ /* 与「单击=息屏」「电源+音量减=切MeowOS」按电源按住时长区分。 */ /* ================================================================== */ /* M2.3A sim 钩子(电源菜单验证,物理留用户): * --sim-power-hold N —— 主循环 N ms 后强制 pwr=1(模拟按住电源),驱动 * 3s 弹电源菜单 / 8s 强制关机两条路径。 * --sim-power-pick N —— 电源菜单打开 2s 后自动选中按钮 N(0..4;M2.29 起 5 钮)。 * --sim-no-exec —— 免执行:电源动作(关机/重启/回HibyOS)/8s强关仅打 log, * 不真正 poweroff/reboot(安全 sim 用)。 */ static long sim_power_hold_ms = 0; int sim_power_pick = -1; int sim_no_exec = 0; void fill_circle(int cx, int cy, int r, uint32_t color) { for (int dy = -r; dy <= r; dy++) for (int dx = -r; dx <= r; dx++) if (dx * dx + dy * dy <= r * r) put_px(cx + dx, cy + dy, color); } /* 圆角矩形 */ void fill_round_rect(int x0, int y0, int w, int h, int r, uint32_t color) { fill_rect(x0 + r, y0, w - 2 * r, h, color); fill_rect(x0, y0 + r, w, h - 2 * r, color); fill_circle(x0 + r, y0 + r, r, color); fill_circle(x0 + w - r, y0 + r, r, color); fill_circle(x0 + r, y0 + h - r, r, color); fill_circle(x0 + w - r, y0 + h - r, r, color); } /* returns: 0 = stay in menu, 1 = exit menu (BACK TO PLAYER / sim-exit), * 2 = launch request */ /* M2.8 Bug 3-residual「统一重绘」:把当前 MeowOS 界面内容整体重绘到 buffer0 并强制 pan 到 * buffer0。息屏唤醒(电源单击亮屏)时调用 —— 唤醒不是 draw 事件:FBIOBLANK(4) 下电 -> * FBIOBLANK(0) 上电后,LCDC 指向的内容可能已失效/为空,必须把当前真实界面(应用列表主界面, * 含「没有存储卡/没有找到应用」态)重新画一遍再强制 re-pan,而不是仅翻页到陈旧/空缓冲。 * signature 传入当前界面状态(由 run_menu 持有),不把应用列表局部状态全局化。 */ static void meow_repaint_surface(struct app apps[MAX_APPS], int napps, int state, int cursor) { /* M2.11 E:记录唤醒上下文(时刻/亮度/当前 screen_on/待 yoffset),便于定位偶发「唤醒后黑屏 * 又自愈」的竞态。 */ int pre_bri = read_brightness(); int pre_screen = meow_screen_on; logmsg("wake repaint: pre bri=%d screen_on=%d (state=%d napps=%d cursor=%d)", pre_bri, pre_screen, state, napps, cursor); draw_menu(apps, napps, state, cursor, 0, ""); /* 完整重绘当前视图到 back_buf */ fb_pan_buf0_force(); /* 确保落在被显示的缓冲(buffer0) */ meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit) */ int post_bri = read_brightness(); logmsg("wake repaint done: post bri=%d (after force-pan buffer0)", post_bri); } /* set by on_sigterm; run_menu breaks on it so the menu closes cleanly and * the main loop restores the player (must be declared before run_menu) */ volatile sig_atomic_t sigterm_flag = 0; /* M2.2 sim 钩子:--sim-exit-tap N —— 菜单打开 N ms 后模拟「点 BACK TO PLAYER 行」, * 用于验证 MeowOS 模式下 BACK TO PLAYER 显示需重启提示(而非恢复死帧)。 */ static long sim_exit_tap_ms = 0; static int sim_exit_tap_done = 0; /* real menu loop */ static void run_menu(struct app apps[MAX_APPS], int napps, int state, long sim_tap_ms, long sim_exit_ms) { int cursor = 0; unsigned long long t0 = now_ms(); int toast_ms = 0; char toast[64] = ""; int want_exit = 0; /* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。 * meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */ unsigned long long last_input_ms = now_ms(); int any_input = 0; int fbfd = -1; int had_player = 0; /* M2.2:菜单打开时是否存在存活播放器 */ long sim_tap_done = 0, sim_exit_done = 0; long meow_last_poll = 0; /* M2.2:MeowOS 后台拔营(卡/网)轮询时间戳 */ load_config(); /* meow.conf on the card: bg_color etc. */ /* A-1/A-2:launcher 用 SDK 渲染栈 —— 初始化 SDK(自建 back_buf/读几何/载字库), * 并把刷新率档位/恒满速开关交给 SDK present 帧率门控(去中心化:launcher 不再自带一套)。 * set_statusbar(1)(Stage-One 状态栏收敛):launcher 系统界面(主界面/设置页/电源菜单/ * 通知中心)顶部状态栏统一由 SDK 全局状态栏绘制(MUI_SBAR_H=28),launcher 不再自绘 * (draw_statusbar/sb_redraw 退役,改由 sb_sdk_refresh() 喂权威状态 + 标脏顶条驱动)。 * 与 App 同一实现/同一主题/同日/夜。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ meow_ui_init(NULL); meow_ui_set_statusbar(1); /* 「同一个状态栏」闭环(FRAMEWORK_DESIGN §10.2 / MEOW_APP_SDK §3.1):SDK 全局状态栏 * 仅系统进程(launcher)渲染。launcher 是系统 UI 渲染宿主 → 置 system_mode=1 让 SDK 渲染顶条 * /提示线;App 进程默认 0(不渲染、只 inset,由 launcher 叠加)。详见 devdocs/plan/LAUNCHER_SDK_REWRITE.md。 */ meow_ui_sys_set_system_mode(1); meow_ui_set_refresh_rate(refresh_rate_cfg); meow_ui_set_refresh_always_max(refresh_always_max); /* record the brightness BEFORE we touch it, so any menu exit can * restore the player's pre-menu value (0 = player standby -> default 60) */ int raw_brightness = read_brightness(); saved_brightness = (raw_brightness > 0) ? raw_brightness : 60; logmsg("menu saved brightness = %d", saved_brightness); /* remember whether the player had blanked the panel: fb_open() below * unblanks so the menu is visible, and restore_backlight() re-blanks * on exit to keep the player's display state machine consistent */ menu_opened_blank = (raw_brightness <= 0); fbfd = fb_open(); meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */ /* M2.6 加载字库(卡上 MeowOS/fonts/default/,卡片已挂载则该步成功;未挂载由下方轮询补载) */ meow_font_ensure(); meow_font_reload_multifont(); /* M2.5x 多字体(读 settings display.multi_font + 6 类;关=单字体) */ /* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份 * 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才 * 冻结 + 备份,退出时恢复其帧。 */ had_player = (find_hiby_pid() > 0); meow_mode = had_player ? 0 : 1; /* 权威判定:每次按播放器是否存活重置 */ /* M2.11 A:每次进 MeowOS(热键切换/开机选 MeowOS/覆盖菜单)都全量重抓系统状态快照, * HibyOS 里改过(蓝牙/WiFi/音量/亮度等)也同步。主循环 10ms 绝不读系统态。 */ meow_sys_refresh(); g_sys.mode = meow_mode; g_sys.napps = napps; if (had_player) { stop_player(); /* freeze the player so the menu owns fb0 */ } else { logmsg("meow mode: hiby_player dead, self-drawn home (no stop/backup)"); } ensure_backlight(); /* only if the panel is dark, never yank brightness */ unblank_panel(); /* player frozen: it can no longer re-blank */ /* M2.53:MeowOS 主屏接管(播放器已死,had_player==0)时强制把亮度压回用户 * display.brightness 设定值。HibyOS 播放器运行/恢复画面会把它自己的背光下限 * (16/25)写进 sysfs;若以它为「当前亮度」,用户设的低亮度(<25)会被覆盖。 * 覆盖菜单(had_player==1,播放器会话中)不压回——播电影时亮度归播放器管。 */ if (!had_player) apply_user_brightness(); if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */ draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */ fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */ meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit 到 fb0) */ /* touch: event reads (event1 is not grabbed) */ touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno)); /* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */ menu_arm_touch(TAP_ARM_DELAY_MS); /* key polling */ struct keypoll kp_power, kp_keys; kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); /* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */ struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); logmsg("menu open: napps=%d state=%d cursor=%d", napps, state, cursor); /* key edge baselines: sample the initial state so keys already held * when the menu opens (e.g. the hotkey release tail) do not fire * edges; kp.prev cannot be used for this because kp_poll() syncs it * to cur before we compare */ int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; } /* M2.3A 电源按住计时:≥8s 强制关机;≥3s 弹电源菜单;与单击息屏/热键按时长区分 */ unsigned long long pwr_hold_at = 0; int pwr_menu_state = 0; /* M2.18 Bug 2: 一次性标志——本次电源按下已触发「自动息屏唤醒」时置位,其释放 * 只唤醒不 toggle(防同一次按键被唤醒路径与单单击 toggle 各消费一次 → 闪黑)。 */ int pwr_woke = 0; /* M2.34: 一次性标志——电源菜单刚关闭(无论确认/取消/超时)。返回主循环后,本次 * 电源按下-松开不得被 3130「松开→单击」路径误判为「电源单击」→ toggle 息屏,否则: * ①菜单点关机/重启:关机/重启画面被息屏覆盖;②焦点在取消键单击电源:取消同时熄屏。 * 清除时机:电源松开(3130 松开分支末尾)或下一次电源按下边沿。 */ int pwr_suppress_click = 0; /* The menu was already drawn into buffer0 above (pre-drawn before the pan * to buffer0), so a redraw is only needed when something actually changes: * cursor movement (input), app exit/enter, toast start/end, or a future * notification-badge change. NEVER on a periodic timer — see the big * comment on the redraw block below. */ int dirty = 0; /* 结构变化(app 进出/state/唤醒/热键…)→ 全帧重画 */ int need_menu_paint = 0; /* M2.16:需 draw_menu 全画 back_buf(光标/toast/test 变化) */ int last_cursor = 0; /* pre-drawn frame showed cursor 0 */ int toast_active = 0; /* tracks toast_ms>0 for start/end redraw edges */ long last_status_tick = 0; /* ~1s status-bar-only refresh (clock) */ for (;;) { unsigned long long t = now_ms(); long el = (long)(t - t0); any_input = 0; /* M2.17:本圈是否有输入(重置 auto screen-off 计时) */ /* SIGTERM during the menu: close it cleanly (restore + resume happen * in the hotkey block / main loop after we return) */ if (sigterm_flag) { logmsg("menu: sigterm, closing"); want_exit = 1; break; } /* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */ meow_ipc_process(); meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ /* sim hooks */ if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) { logmsg("SIM TAP row 0"); sim_tap_done = 1; if (napps > 0 && state == 0) { /* fall through: select row 0 */ int ret = launch_app(&apps[0]); /* app exited: RETURN TO THE MENU (v6.3) — closing the menu * here would leave the app's last frame (e.g. BYE) on fb0, * and the player only redraws on input events (none arrive, * injection is not delivered), so the screen would stay * dark until a key press. Staying in the menu keeps the * menu visible; BACK TO PLAYER then resumes the player. * Bug 2/3/4: a queued touch from the app run must NOT be * replayed as a fresh menu tap (relaunch / flash-to-player), * and the "CANNOT START" toast only fires on a real launch * failure (ret != 0), never after a clean app exit. */ menu_after_app_exit(ret == 0); last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ dirty = 1; /* app wrote buffer0 during its run -> redraw menu */ if (ret != 0) { snprintf(toast, sizeof toast, "CANNOT START: %s", apps[0].name); toast_ms = TOAST_MS; } } else { snprintf(toast, sizeof toast, "NO APPS"); toast_ms = TOAST_MS; } } if (sim_exit_ms > 0 && !sim_exit_done && el >= sim_exit_ms) { logmsg("SIM EXIT"); sim_exit_done = 1; want_exit = 1; break; } /* M2.6 sim:点 BACK TO PLAYER 行 → MeowOS 模式弹「需重启」确认框并留下; * 会话覆盖模式则正常退出。 */ if (sim_exit_tap_ms > 0 && !sim_exit_tap_done && el >= sim_exit_tap_ms) { logmsg("SIM TAP exit row"); sim_exit_tap_done = 1; if (meow_mode) { int rb = confirm_reboot_player(); menu_arm_touch(TAP_ARM_DELAY_MS); dirty = 1; logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb); } else { want_exit = 1; break; } } /* M2.2 MeowOS 系统拔营:无播放器时由 launcher 接管官方交给 * hiby_player/sys_server 的两件事 —— TF 卡挂载 + WiFi 联网。 * 后台每 2s 重试,不阻塞主界面;卡片就绪后重扫应用列表并就地刷新。 * 仅 meow 常驻模式需要(HibyOS 覆盖模式播放器仍在,/或其管理)。 * M2.11 C:TF 卡热插拔最佳努力 —— 拔出检测(节点消失→无卡)、 * 插入检测(节点出现→自动挂载+重扫+重载字库)。不阻塞、不崩。 */ if (meow_mode && (long)(t - t0) - meow_last_poll >= 2000) { meow_last_poll = (long)(t - t0); ensure_wifi(); if (!sd_device_present()) { /* 卡被拔出:设备节点消失 → 无卡态。停止扫描;留 state=1(NO STORAGE CARD)。 * 若此前是正常态,切到无卡并重绘。不杀正在跑的应用(其由 D 的监测处理)。 */ if (state == 0 || state == 2) { state = 1; dirty = 1; logmsg("hotplug: card removed -> state=1"); } g_sys.sd_mounted = 0; log_poll_card(); /* 卡日志不可用时回退 /tmp 缓冲 */ } else if (ensure_card_mounted()) { /* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */ int was_nocard = (state == 1); if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); } if (mfont_ready) meow_font_reload_multifont(); /* M2.5x:卡挂载后重配多字体(此前无全局可配) */ if (was_nocard || state != 0 || napps == 0) { struct app fresh[MAX_APPS]; int fn = scan_apps(fresh); int fstate = (fn < 0) ? (fn == -1 ? 1 : 2) : (fn == 0 ? 2 : 0); if (fstate == 0 && fn > 0) { for (int i = 0; i < fn; i++) apps[i] = fresh[i]; napps = fn; state = 0; logmsg("hotplug: card ready -> %d apps loaded", napps); dirty = 1; } else if (fstate != state) { state = fstate; logmsg("hotplug: state -> %d", state); dirty = 1; } } g_sys.sd_mounted = 1; log_poll_card(); } else if (!sd_mounted()) { /* 卡在但未挂载(正挂载/失败):保持当前显示不误判;不崩。 */ g_sys.sd_mounted = 0; } } /* keys: poll and handle edges. Power is NOT handled here anymore * (v6.5): inside the menu the power key is a pure screen-off/on * toggle handled by sys_server/kernel (the player being SIGSTOPped * does not affect it) — the launcher must NOT touch brightness/panel * or exit the menu on it. Exit is only via the BACK TO PLAYER touch * row (below) or sim-exit. */ kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); int vold = 0, volu = 0, play = 0, pwr = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; } if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */ /* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */ int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */ (void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */ /* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。 * 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */ (void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); vold = kd_vold.stable; volu = kd_volu.stable; play = kd_play.stable; pwr = kd_pwr.stable; /* M2.6 电源语义(MeowOS 自持面板,无播放器拔河): * 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */ if (meow_mode) { if (pwr && !m_prev_pwr) { pwr_hold_at = t; pwr_menu_state = 0; any_input = 1; pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */ /* M2.18 Bug 2: 若这次按下正好落在「自动息屏」态(auto_slept 且屏已关), * 本次电源键只做唤醒(下方通用醒屏分支 meow_panel_on()),其释放不再走 * 单单击 toggle —— 否则同一次按下先被唤醒点亮、又在释放时被 toggle 关掉 * (症状:自动息屏后点电源「闪一下黑、再点才亮」)。 */ if (meow_auto_slept && !meow_screen_on) pwr_woke = 1; } if (pwr && pwr_hold_at) { /* M2.10/M2.11 B:8s 强关只在该「按住」从未打开过电源菜单时才触发 * (!pwr_menu_state)。一旦打开过菜单(用户已互动/取消),run_menu 的 * 8s 强关永远不在此 hold 上再触发 —— 由菜单自身的「按当前项确认」/取消 * 决定动作。避免「按住电源开菜单->确认取消->仍按住->run_menu 8s 强关」残留。 * M2.37:阈值接 power.forceoff_ms(默认 8000),强关保持 > 弹菜单上限。 */ if (t - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) { if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); } else { logmsg("pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } m_prev_pwr = pwr; continue; } else if (t - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) { pwr_menu_state = 1; logmsg("pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms); show_power_menu(); /* 动作在函数内 system(); 取消则续循环 */ /* M2.31:菜单打开期间主循环阻塞,last_input_ms 不会刷新;若停留超过自动息屏 * 超时,取消返回后会被误判「空闲」而立即熄屏(用户点取消瞬间黑屏)。 * 菜单返回即视为「刚有输入」,重置自动息屏计时。 */ last_input_ms = now_ms(); /* M2.11 B:菜单已关闭(取消)。清空按住计时与菜单态:仍按住/随后松开的电源 * 按「新按下」处理,绝不把已互动的 hold 再累计到 8s 强关。 */ pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */ dirty = 1; /* 取消后重绘应用列表 */ menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */ /* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。 * 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」, * 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at, * 随后稳定值转 0 时又被误判「单击」→ meow_toggle_panel 把面板关掉(黑屏且 * 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw * 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw * 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */ memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); continue; } } if (!pwr && m_prev_pwr) { /* 松开:短按(<3s 且菜单未开)→ 单击 = 息屏/亮屏切换 */ if (pwr_suppress_click) { /* M2.34:本次电源按下来自「电源菜单内确认/取消」,其松开不得 toggle 息屏 * (否则关机/重启画面被息屏覆盖,或焦点在取消键单击电源导致熄屏)。 */ logmsg("pm: power release after power-menu -> no toggle (suppress)"); } else if (pwr_hold_at && (t - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) { logmsg("pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off"); meow_toggle_panel(); if (meow_screen_on) { /* M2.8 Bug 3-residual:唤醒不是 draw 事件,LCDC 在 FBIOBLANK(4)->(0) * 后指向的内容可能已失效/为空;必须把当前界面(应用列表)整体重绘到 * buffer0 并强制 re-pan,而非仅翻页/仅 dirty 重绘(M2.7 仍黑屏根因)。 */ meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */ last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟(防 t 过期回绕) */ meow_repaint_surface(apps, napps, state, cursor); } else { dirty = 1; /* 息屏:面板已黑,留 dirty 供下次亮屏重绘 */ last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟 */ } } else if (pwr_woke) { /* M2.18 Bug 2:本次电源按下已由自动息屏唤醒分支消费,释放只记录、不 toggle。 */ logmsg("pm: power release after auto screen-off wake -> no toggle (wake only)"); } pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */ } m_prev_pwr = pwr; } /* volume/play edges against the launcher-maintained baselines * (previously dead code: prev was re-seeded from cur each pass, so * no edge could ever fire and the menu keys could not navigate or * launch). * M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp), * 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */ if (meow_mode) { if (vold && !m_prev_vold) { any_input = 1; settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */ logmsg("volume DOWN <- ramp step=%d", settings_volume_step); } else if (volu && !m_prev_volu) { any_input = 1; settings_volume_adj(+settings_volume_step); logmsg("volume UP <- ramp step=%d", settings_volume_step); } } else { /* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。 * 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */ if (vold && !m_prev_vold && napps > 0 && state == 0) { any_input = 1; cursor++; if (cursor > napps) cursor = 0; /* napps=设置行 */ logmsg("menu cursor -> %d", cursor); } if (volu && !m_prev_volu && napps > 0 && state == 0) { any_input = 1; cursor--; if (cursor < 0) cursor = napps; /* 回绕到设置行 */ logmsg("menu cursor -> %d", cursor); } } /* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */ if (meow_mode && vol_ramp_dir != 0) { int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */ int going = vol_ramp_tick(held); g_sys.vol = settings_volume_get(); paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */ if (!going) logmsg("volume ramp end -> %d", g_sys.vol); } /* M2.19:播放键不再是「应用列表的确认/打开」——它让位给全局媒体控制 * (播放/暂停,路由到当前活跃媒体会话)。菜单/列表的确认只走触摸点选 * (见下方 touch 段),物理确认交给电源键(power 菜单/确认框),播放键 * 在 MeowOS 主循环里不再触发 launch_app。媒体预留:后续媒体会话接管, * 此处仅作 any_input(any_input 仍用于重置自动息屏计时)。(原播放键=打开 * 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */ if (play && !m_prev_play) { any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */ logmsg("play key: media-reserved (no menu confirm)"); } m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; /* touch (event reads) */ if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */ struct pollfd pfd = { touch_fd, POLLIN, 0 }; if (poll(&pfd, 1, 0) > 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == 16) { uint16_t type = *(uint16_t *)(ev + 8); uint16_t code = *(uint16_t *)(ev + 10); int32_t val = *(int32_t *)(ev + 12); if (type == EV_ABS) { if (code == ABS_X || code == ABS_MT_POSITION_X) t_last_x = val; else if (code == ABS_Y || code == ABS_MT_POSITION_Y) t_last_y = val; /* M2.23 原点锁定:goodix 事件时序不保证 ABS 与 BTN_TOUCH 先后, * 按下边沿只标 t_down,原点延迟到「本次按下之后」的首个有效坐标才锁定, * 杜绝把上一手势残留(-1/旧坐标)当本次手势起点。 */ if (t_down && !t_down_valid && t_last_x >= 0 && t_last_y >= 0) { t_down_x = t_last_x; t_down_y = t_last_y; t_down_valid = 1; } /* ⑨ 顶部热区「跟手下拉」打开通知中心(拖动中即进,真跟手): * 原点锁定 + 起点在顶部热区(y= 0 && t_down_y < SB_BAR_H && (t_last_y - t_down_y) >= NCP_DRAG_ENTER_SLOP) { menu_live_open_pending = 1; /* 本次手势仅触发一次 */ int loy = t_down_y; logmsg("menu: top-edge live drag-down -> notification center (dy=%d)", t_last_y - loy); t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */ menu_arm_touch(TAP_ARM_DELAY_MS); int want_launch = -1; run_notif_center(apps, napps, &want_launch, 1, t_down_x, loy); /* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */ memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); menu_arm_touch(800); dirty = 1; if (want_launch >= 0 && want_launch < napps) { logmsg("menu: notification launch app idx=%d", want_launch); int ret = launch_app(&apps[want_launch]); menu_after_app_exit(ret == 0); last_input_ms = now_ms(); dirty = 1; if (ret != 0) { snprintf(toast, sizeof toast, "CANNOT START: %s", apps[want_launch].name); toast_ms = TOAST_MS; } } continue; /* 本圈不再按 tap/其余处理(剩余事件因 t_down=0 成 no-op) */ } } else if (type == EV_KEY && code == BTN_TOUCH) { if (val && !t_down) { /* Bug 2/4: ignore touches inside the post-app * cooldown (goodix ghost + back-tap replay). */ if (now_ms() < menu_touch_cooldown_until) { /* M2.18 排查乱跳:记录被冷却丢弃的按下(坐标 + 剩余冷却), * 便于定位「概率进 Hello」是否 ghost/重放越过冷却。 */ logmsg("menu touch dropped by cooldown (at %d,%d, rem %llu ms)", t_last_x, t_last_y, menu_touch_cooldown_until - now_ms()); } else { any_input = 1; /* M2.17:触摸重置自动息屏计时 */ t_down = 1; t_down_valid = 0; /* 原点待首个有效坐标锁定(防残留 -1/旧值) */ t_last_x = -1; t_last_y = -1; /* 本次以「无坐标」起步(防跨手势残留) */ menu_live_open_pending = 0; /* ⑨ 新触摸:复位 live-open latch */ } } else if (!val && t_down) { t_down = 0; if (!t_down_valid) { /* 本次按下从未锁到有效坐标(坐标滞后/ghost 空按下):丢本次。 */ logmsg("menu tap dropped: no valid origin (last %d,%d)", t_last_x, t_last_y); t_down_x = t_down_y = -1; } else { int tx = t_down_x, ty = t_down_y; int dx = t_last_x - tx, dy = t_last_y - ty; int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; t_down_valid = 0; logmsg("menu tap at %d,%d (up %d,%d, d=(%d,%d))", tx, ty, t_last_x, t_last_y, dx, dy); /* 阶段B:顶部热区下滑打开通知中心。判定在「滑动」分支内先于左缘返回—— * 左缘(down_x notification center (release edge, fallback)"); menu_arm_touch(TAP_ARM_DELAY_MS); int want_launch = -1; run_notif_center(apps, napps, &want_launch, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */ /* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */ memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); menu_arm_touch(800); dirty = 1; /* 点击通知命中可启动 app → 走标准启动链路(与触摸行同路径)。 */ if (want_launch >= 0 && want_launch < napps) { logmsg("menu: notification launch app idx=%d", want_launch); int ret = launch_app(&apps[want_launch]); menu_after_app_exit(ret == 0); last_input_ms = now_ms(); dirty = 1; if (ret != 0) { snprintf(toast, sizeof toast, "CANNOT START: %s", apps[want_launch].name); toast_ms = TOAST_MS; } } continue; /* 本圈不再按 tap/其余处理 */ } /* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、 * 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */ if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) { logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy); } else if (tx >= 0 && ty >= 0) { if (ty >= EXIT_ROW - 4 && ty < EXIT_ROW + ROW_H - 8) { logmsg("menu: touch exit row"); if (meow_mode) { /* M2.6 G:播放器已死,弹出「返回播放器需重启」确认框。 * 是=写选单默认HibyOS + reboot;否=留在 MeowOS。 * 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */ logmsg("meow mode: BACK TO PLAYER -> confirm reboot"); int rb = confirm_reboot_player(); menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */ dirty = 1; if (rb) { logmsg("reboot-to-player: yes, rebooting"); } /* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */ } else { want_exit = 1; break; } } /* M2.36 B2b:系统设置行(位于 Exit 行上方,y ∈ [588-4, 588+52-8))。 * 命中 → 进设置页。阻塞式 run_settings_page 返回后,重播 kd_* 并以 * 当前 raw 对齐 m_prev_*(M2.25 黑屏修复同款),杜绝「设置页内按电源 * 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */ else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) { logmsg("menu: touch settings row -> settings page"); menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */ run_settings_page(); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); /* 回主界面即视为输入 */ menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */ dirty = 1; /* 全帧重画应用列表 */ } /* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与 * 应用行动作非互斥。应用很多(napps*ROW_H 上探到 EXIT 带)时, * 一个 tap 会同时命中退出行(<~684)与应用行带,触发确认重启又启动 * 应用(或直接 break 走人)→ 概率乱跳。退出行优先,命中即不再 * 落到应用行。普通小列表两区间不重叠,行为不变。 */ else if (ty >= ROW_TOP && ty < ROW_TOP + napps * ROW_H && napps > 0 && state == 0) { int idx = (ty - ROW_TOP) / ROW_H; if (idx < napps) { logmsg("menu: touch row %d", idx); int ret = launch_app(&apps[idx]); /* app exited: return to menu (v6.3). * menu_after_app_exit() drains queued * app-run touches so they are not * replayed as row/tap gestures. */ menu_after_app_exit(ret == 0); last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */ dirty = 1; /* app wrote buffer0 -> redraw menu */ if (ret != 0) { snprintf(toast, sizeof toast, "CANNOT START: %s", apps[idx].name); toast_ms = TOAST_MS; } } } } t_down_x = t_down_y = -1; /* 复位,下一手势从零开始 */ } } } } if (want_exit) break; } } /* ---- M2.17 超时自动息屏(主界面;主循环 10ms 低频检查,不阻塞)---- * 任何输入重置 last_input;空闲 > screen_off_timeout(0=常亮)且未在 keep_awake 页 → 息屏。 * 自动息屏后任意输入唤醒(电源单击路径已在 meow_mode 块处理;这里处理其他输入/触摸)。 * black 模式:面板仍上电、触摸未挂起,故在屏下额外探测一次 BTN_TOUCH 按下 → 触发唤醒 * (不处理点击;唤醒后下一圈正常触摸处理)。panel 模式触摸已被 FBIOBLANK(4) 挂起,仅按键唤醒。 */ if (meow_auto_slept && !meow_screen_on && screen_off_mode == 1 && touch_fd >= 0) { struct pollfd spfd = { touch_fd, POLLIN, 0 }; if (poll(&spfd, 1, 0) > 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == 16) { uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10); int32_t val = *(int32_t *)(ev + 12); if (type == EV_KEY && code == BTN_TOUCH && val) any_input = 1; } } } /* M2.18 Bug 1: 用 now_ms() 而非本圈顶部捕获的 t —— launch_app() 阻塞期间 t 已过期 * (停在 app 启动前小数),若仍写 t 会把 last_input 倒拨到 app 启动时刻;写 now_ms() * 才是「输入发生」的真实墙钟。配合下方 idle_ms_since 的回绕保护,杜绝误判空闲。 */ if (any_input) last_input_ms = now_ms(); if (meow_auto_slept && any_input && !meow_screen_on) { logmsg("auto screen-off: input wake (meow_mode=%d keep_awake=%d)", meow_mode, fg_keep_awake()); meow_panel_on(); meow_auto_slept = 0; last_input_ms = now_ms(); meow_repaint_surface(apps, napps, state, cursor); /* M2.8 唤醒重绘 */ } if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) { /* M2.18 Bug 1: 用 idle_ms_since 取差,杜绝 t= (unsigned long long)screen_off_timeout * 1000) { meow_panel_off(); meow_auto_slept = 1; logmsg("auto screen-off after %ds idle (mode=%s keep_awake=%d, t=%llu last=%llu)", screen_off_timeout, screen_off_mode ? "black" : "panel", fg_keep_awake(), t, last_input_ms); } } /* Redraw only when something actually changed: cursor movement (an * input event), app exit/enter (which rewrote buffer0), toast * start/end, or a future notification-badge change. NEVER on a * periodic timer. * * v6.8 BUG (the ~3fps list flicker): the v6.8 "500ms safety redraw" * rewrote the ENTIRE menu into the live-displayed buffer0 every 500ms * (the v6.8 fix made last_redraw=el so the timer finally fired), plus * draw_statusbar()'s amixer popen on every redraw. An in-place full * rewrite of the buffer the LCDC is scanning produces visible tearing/ * pulsing on the app list, and the slow amixer popen stalls the loop. * Full-redraw in place is only correct right after a REAL change; the * steady-state list must not be touched. */ /* M2.16:只在真变化时标脏(精确到格子),绝无"动一下就全屏重绘"。 * 光标移动 → 旧/新两行;toast 起止 → toast 区;其余结构变化 → dirty=1(全帧)。 * Stage-One 状态栏收敛:音量渐变/时钟只经 paint_sb_right/draw_clock → sb_sdk_refresh() * 把权威状态喂进 SDK 全局状态栏 + 标脏顶条(0,0,xres,28),由 SDK present() 顶条重绘。 */ if (cursor != last_cursor) { /* M2.36 B2b:光标可能在应用行(0..napps-1)或「系统设置」行(napps,位于 Exit 行上方)。 * 分别标脏对应行 rect(设置行固定 y=EXIT_ROW-ROW_H,与 napps 无关)。 */ if (cursor == napps || last_cursor == napps) meow_dirty_add(8, EXIT_ROW - ROW_H - 4, fb_xres - 16, ROW_H - 8); if (state == 0 && napps > 0) { if (last_cursor >= 0 && last_cursor < napps) meow_dirty_add(8, ROW_TOP + last_cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8); if (cursor >= 0 && cursor < napps) meow_dirty_add(8, ROW_TOP + cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8); } last_cursor = cursor; need_menu_paint = 1; } if (toast_active != (toast_ms > 0)) { /* toast start/end */ toast_active = (toast_ms > 0); meow_dirty_add(20, TOAST_Y - 6, fb_xres - 40, 40); need_menu_paint = 1; } if (dirty) { /* 结构变化 → 全帧重画 + 全屏 blit */ meow_dirty_full(); need_menu_paint = 1; dirty = 0; } if (need_menu_paint) { draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */ need_menu_paint = 0; } /* A-1/A-2:呈现统一走 SDK meow_ui_flush()(内含帧率门控;无脏返回,门控未达期保留脏 * 待下一轮)。fb_norm_buf0 确保 LCDC 显示 buffer0(yoffset 非 0 才 pan,短路无开销)。 * 不再用 launcher 的 g_ndirty/g_dirty_full 做呈现门——SDK present 自会 no-op 于空脏。 */ fb_norm_buf0(); /* Bug 3:确保 LCDC 显示 buffer0 */ meow_ui_flush(); /* M2.16 无闪烁 + M2.16 帧率门控归口 SDK */ if (toast_ms > 0) { toast_ms -= POLL_MS; if (toast_ms < 0) toast_ms = 0; } /* Clock refresh ~1s: redraw ONLY the clock digits (and the right info * cell if battery/wifi changed), never the list. */ if (el - last_status_tick >= 1000) { last_status_tick = el; meow_sys_light(); /* 同步电量/WiFi/音量进 g_sys */ draw_clock(); /* Stage-One:喂 SDK 全局状态栏 + 标脏顶条;SDK present() 重绘 */ fb_norm_buf0(); meow_ui_flush(); /* blit 状态栏整条,不动列表 */ } usleep(POLL_MS * 1000); } if (touch_fd >= 0) close(touch_fd); /* v6.6: restore the player's frame + pan back BEFORE releasing fb (the * player is event-driven and won't redraw on its own — see §18.15). * M2.2:仅当存在存活播放器才恢复其帧;MeowOS 常驻主界面(无播放器)绝不 * 恢复死帧——屏幕保持自绘的应用列表(buffer0),不遗留播放器残帧。 */ if (had_player) fb_restore_player(); if (fbfd >= 0) { if (fb_fd == fbfd) fb_fd = -1; close(fbfd); } logmsg("menu close (exit=%d meow_mode=%d had_player=%d)", want_exit, meow_mode, had_player); } /* ================================================================== */ /* app launch */ /* ================================================================== */ static void on_sigterm(int s) { (void)s; sigterm_flag = 1; } /* Drain pending events from all input devices (non-blocking read to EAGAIN). * Called right before launching an app so residual events (the tap/key that * selected the row, the hotkey combo's release tail) are consumed before the * app opens its own fds. Note: events generated AFTER the drain still reach * the app (multicast); the app therefore also drains itself at startup and * only exits on a fresh press edge (see hello.c). */ void drain_input(void) { for (int i = 0; i < 4; i++) { char dev[32]; snprintf(dev, sizeof dev, "/dev/input/event%d", i); int fd = open(dev, O_RDONLY | O_NONBLOCK); if (fd < 0) continue; unsigned char ev[16]; while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } close(fd); } } /* ================================================================== */ /* main */ /* ================================================================== */ static void usage(void) { fprintf(stderr, "usage: meow_launcherd [--sim-hotkey N] [--sim-tap N] [--sim-exit N]\n" " [--sim-exit-tap N] [--sim-power-hold N]\n" " [--sim-power-pick N] [--sim-no-exec]\n" " [--dumpkeys N] [--scan] [--sysdump]\n" " [--pkg-install ] [--pkg-export ]\n" " [--ipc-only N] [--ipc-sock ] [--appinfo ]\n" " [--fontscan ]\n"); } int main(int argc, char **argv) { long sim_hotkey = 0, sim_tap = 0, sim_exit = 0, dumpkeys = 0; int do_scan = 0, do_sysdump = 0, sim_vol = 0; long ipc_only_ms = 0; const char *appinfo_path = NULL; const char *fontscan_root = NULL; /* --fontscan :宿主/qemu 测试字形扫描 */ const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-tap") && i + 1 < argc) sim_tap = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-exit") && i + 1 < argc) sim_exit = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-exit-tap") && i + 1 < argc) sim_exit_tap_ms = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-power-hold") && i + 1 < argc) sim_power_hold_ms = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-power-pick") && i + 1 < argc) sim_power_pick = atoi(argv[++i]); else if (!strcmp(argv[i], "--sim-no-exec")) sim_no_exec = 1; else if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]); else if (!strcmp(argv[i], "--scan")) do_scan = 1; else if (!strcmp(argv[i], "--sysdump")) do_sysdump = 1; else if (!strcmp(argv[i], "--sim-vol") && i + 1 < argc) sim_vol = atoi(argv[++i]); else if (!strcmp(argv[i], "--ipc-only") && i + 1 < argc) ipc_only_ms = atol(argv[++i]); else if (!strcmp(argv[i], "--ipc-sock") && i + 1 < argc) { snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]); } else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i]; else if (!strcmp(argv[i], "--fontscan") && i + 1 < argc) fontscan_root = argv[++i]; else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i]; else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; } else { usage(); return 1; } } /* unified log: TF card (or /tmp boot buffer when the card is absent) */ log_cleanup_old(); if (log_open_card() == 0) fprintf(stderr, "log file: %s\n", log_file); else fprintf(stderr, "log: no TF card, using /tmp boot buffer (%s)\n", BOOT_BUF_TMP); logmsg("=== meow_launcherd start (sim_hotkey=%ld sim_tap=%ld sim_exit=%ld dumpkeys=%ld scan=%d) ===", sim_hotkey, sim_tap, sim_exit, dumpkeys, do_scan); load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per * menu open so edits apply without a daemon restart) */ settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */ /* M2.6:启动扫描一次 TF 卡 MeowOS/fonts///,填充「字体文件」下拉 preset。 * 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */ meow_font_scan_presets(); if (fontscan_root) { /* M2.6 测试钩子:扫描指定卡根下的 MeowOS/fonts/// 并打印 preset(qemu/宿主可测)。 * 用于验证 font.json 解析(含 weight)/三件套/缺件跳过/sha 校验 + preset 动态化逻辑。 */ meow_font_scan_presets_root(fontscan_root); printf("fonts=%d\n", font_presets_n); for (int i = 0; i < font_presets_n; i++) printf(" %d %s -> %s (fam=%s wt=%s)\n", i, font_preset_labels[i], font_presets[i], font_preset_fams[i], font_preset_wts[i] ? font_preset_wts[i] : "regular"); return 0; } if (sim_vol > 0) { /* M2.14 sim:验证线性渐变——step=1/2/5 下设定音量按 1% 逐级爬升/回落, * 每 1% 经 meow_dac_vol_write 写一次真实 DAC,打点进 log(stderr)。 */ settings_volume = 60; /* 固定起点,便于对比 */ settings_volume_step = (sim_vol > 5) ? 5 : (sim_vol < 1 ? 1 : sim_vol); int base = settings_volume; /* UP */ settings_volume_adj(+settings_volume_step); for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } int up = settings_volume; /* DOWN */ settings_volume_adj(-settings_volume_step); for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); } int down = settings_volume; printf("sim-vol: step=%d base=%d up=%d down=%d (each 1%% logged 'DAC vol -> L/R=')\n", settings_volume_step, base, up, down); /* 长按:held=1 持续延段爬升 → 连续渐变到上限 clamp */ settings_volume = 60; settings_volume_adj(+settings_volume_step); for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(1); usleep(20000); } printf("sim-vol-held: step=%d long-press top=%d (expect clamp 100)\n", settings_volume_step, settings_volume); return 0; } if (pkg_install_mpk) { /* M2.12 .mpk 安装接口(CLI 钩子;UI 后期)。安全、离开主循环。 */ int r = meow_pkg_install(pkg_install_mpk); printf("pkg_install %s -> %d\n", pkg_install_mpk, r); return r; } if (pkg_export_name && pkg_export_out) { /* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */ int r = meow_pkg_export(pkg_export_name, pkg_export_out); printf("pkg_export %s -> %s (%d)\n", pkg_export_name, pkg_export_out, r); return r; } if (do_sysdump) { /* M2.11 测试钩子:安全打印系统状态快照(不碰 fb/输入设备,可在常驻 launcher * 旁运行验证 A/E 的 BT/WiFi/电量/音量/亮度抓取),然后退出。 */ meow_sys_refresh(); printf("batt=%d%% chg=%d mv=%d\n", g_sys.batt, g_sys.charging, g_sys.batt_mv); printf("wifi_on=%d ip=%s ssid=%s\n", g_sys.wifi_on, g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-"); printf("bt_seen=%d bt_on=%d\n", g_sys.bt_seen, g_sys.bt_on); printf("vol=%d brightness=%d\n", g_sys.vol, g_sys.brightness); printf("data_free_kb=%ld sd_mounted=%d sd_free=%ld sd_total=%ld\n", g_sys.data_free_kb, g_sys.sd_mounted, g_sys.sd_free_kb, g_sys.sd_total_kb); printf("mode=%d napps=%d ver=%s\n", g_sys.mode, g_sys.napps, g_sys.ver); printf("render: eff_rate=%d cfg=%d always_max=%d frames=%llu\n", meow_ui_effective_rate(), refresh_rate_cfg, refresh_always_max, meow_ui_frames_presented()); return 0; } if (do_scan) { struct app apps[MAX_APPS]; int n = scan_apps(apps); printf("state=%d napps=%d\n", n < 0 ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0), n < 0 ? 0 : n); for (int i = 0; i < n && n > 0; i++) printf(" %d %s -> %s ok=%d\n", i, apps[i].name, apps[i].exec, apps[i].ok); return 0; } if (appinfo_path) { /* 阶段 D · M1 debug:解析单个 app.json 并打印路由/框架/服务/图标(qemu 可测静态路由) */ FILE *f = fopen(appinfo_path, "r"); if (!f) { printf("appinfo: cannot open %s\n", appinfo_path); return 1; } struct app a; memset(&a, 0, sizeof a); meow_parse_app_json(f, &a); fclose(f); printf("name=%s\n", a.name[0] ? a.name : "(missing)"); printf("exec=%s\n", a.exec[0] ? a.exec : "(missing)"); printf("ok=%d\n", (a.exec[0] && access(a.exec, X_OK) == 0) ? 1 : 0); printf("framework=%d\n", a.framework ? 1 : 0); printf("icon=%s\n", a.icon[0] ? a.icon : "(none)"); printf("routes=%d\n", a.nroute); for (int i = 0; i < a.nroute; i++) printf(" route.%s -> %s (depth=%d keep_awake=%d)\n", a.routes[i].act, a.routes[i].chain, chain_depth(a.routes[i].chain), a.routes[i].keep_awake); printf("services=%d\n", a.nsvc); for (int i = 0; i < a.nsvc; i++) printf(" service.%s -> mode:%d restart:%d max:%d window_ms:%d\n", a.svcs[i].name, a.svcs[i].mode, a.svcs[i].restart, a.svcs[i].max, a.svcs[i].window_ms); return 0; } if (ipc_only_ms > 0) { /* 阶段 D · M0/M1 sim:仅跑 IPC 骨架(免 fb/输入设备),供 qemu 下用 meow_ipc * 验证 HELLO→ACK、ACT_QUERY→链路。无卡时 /tmp 缓冲承载日志。 */ fprintf(stderr, "ipc-only: socket=%s\n", ipc_sock_path); int rc = meow_ipc_init(); if (rc != 0) { fprintf(stderr, "ipc-only: IPC init failed, exiting\n"); return 1; } unsigned long long s0 = now_ms(); while (!sigterm_flag && now_ms() - s0 < (unsigned long long)ipc_only_ms) { meow_ipc_process(); meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ usleep(POLL_MS * 1000); } ipc_shutdown = 1; unlink(ipc_sock_path); logmsg("=== meow_launcherd ipc-only exit ==="); return 0; } /* pid file */ int pfd = open(PID_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (pfd >= 0) { char pbuf[16]; int l = snprintf(pbuf, sizeof pbuf, "%d\n", getpid()); write(pfd, pbuf, l); close(pfd); } signal(SIGTERM, on_sigterm); signal(SIGINT, on_sigterm); signal(SIGHUP, SIG_IGN); /* 阶段 D · M0:启动 IPC 线程(AF_UNIX /usr/data/meow_ipc.sock)。幂等;失败只 log, * 主循环仍跑(10ms 热键轮询不受影响)。线程创建晚于进程但有独立生命周期。 */ meow_ipc_init(); struct keypoll kp_power, kp_keys; /* retry opening the input devices (they may not exist yet at boot) */ for (int tries = 0; tries < 30; tries++) { if (kp_open(&kp_power, DEV_POWER) == 0 && kp_open(&kp_keys, DEV_KEYS) == 0) break; logmsg("input devices not ready, retry %d/30 in 1s", tries + 1); log_poll_card(); /* flush boot buffer even if input devices lag */ sleep(1); } if (!kp_power.ok || !kp_keys.ok) logmsg("WARNING: input devices unavailable, launcher blind"); /* ===== MeowOS 常驻主界面(M2.2 修复批次)===== * 播放器已死(find_hiby_pid()==0:bootmenu MeowOS 分支 exec、或热键切 MeowOS 后 exec, * 二者皆此语境)时,launcher == MeowOS 主界面常驻: * - 启动即扫描并自绘应用列目录(run_menu 画 buffer0 + FBIOPAN yoffset=0), * 绝不等到首次热键才画 —— 修复「切到 MeowOS 后屏幕停留在播放器最后一帧」; * - 无存活播放器 → run_menu 内不 SIGSTOP / 不备份 / 不 SIGCONT 死播放器、不恢复死帧; * - BACK TO PLAYER → 显示「需重启」提示并留在主界面(不渲染死帧); * - 仅 SIGTERM 退出(rcS/SSH 关停),退出时屏幕保持应用列表主界面(无残帧)。 * 若此时竟有存活播放器(不应用于 MeowOS,防御性),回落到下面的旧覆盖路径。 */ meow_mode = is_meow_mode(); /* M2.12:卡在则把 /usr/data/mnt/sd_0/MeowOS/opt/bin 前插 PATH(外围 CLI 已迁卡, * 按需可用;卡不在跳过,绝不崩)。SSH 会话另由 rootfs /etc/profile.d/meow_card_path.sh 处理。 */ if (ensure_card_mounted() || sd_mounted()) { setenv("PATH", "/usr/data/mnt/sd_0/MeowOS/opt/bin:/usr/bin:/bin:/usr/sbin:/sbin", 1); logmsg("PATH: card MeowOS/opt/bin prepended"); } else { logmsg("PATH: no card, MeowOS/opt/bin skipped"); } if (meow_mode) { /* M2.2 拔营:启动即尝试挂 TF 卡 + 联网(非阻塞);失败由 run_menu 后台周期重试。 */ ensure_card_mounted(); ensure_wifi(); struct app apps[MAX_APPS]; int n = scan_apps(apps); int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); int napps = n < 0 ? 0 : n; logmsg("MeowOS mode: hiby_player NOT running -> resident app-list home " "(state=%d napps=%d), drain-to-home immediately", state, napps); run_menu(apps, napps, state, sim_tap, sim_exit); logmsg("=== meow_launcherd exit (MeowOS mode) ==="); if (log_fd >= 0) { close(log_fd); log_fd = -1; } ipc_shutdown = 1; unlink(ipc_sock_path); unlink(PID_PATH); return 0; } /* hotkey state: power_voldown (default) + legacy seq */ int prev_pwr = 0, prev_prev = 0; int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_nxt = 0; int pwr_hold_armed = 0; /* power held, within trigger window */ unsigned long long pwr_down_at = 0; int seq_key_prev = 0; /* the configured seq key, edge-tracked */ int seq_hits = 0; unsigned long long seq_last_at = 0; struct vol_snap seq_vol0 = { -1, -1 }; long sim_hk_done = 0; unsigned long long t0 = now_ms(); int log_tick = 0; /* periodic card-log retry (~1s) while no card log */ if (hotkey_mode == HOTKEY_MODE_PWRVOL) logmsg("ready, hotkey = power_voldown (hold power, tap VOL_DOWN)"); else logmsg("ready, hotkey = %d taps of key code %d (gap %dms)", seq_len, seq_codes[0], seq_gap_ms); while (!sigterm_flag) { unsigned long long t = now_ms(); long el = (long)(t - t0); /* 阶段 D · M0:排空 IPC 队列(非阻塞,零延迟;热键轮询 10ms 不受 socket 阻塞) */ meow_ipc_process(); meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ /* sample current key states */ kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); int pwr = 0, prev = 0, vold = 0, volu = 0, play = 0, nxt = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) nxt = 1; } /* sim hotkey: emulate the configured hotkey. * power_voldown (default): HOLD power from N, VOL_DOWN tap at N+800 * (within the 60-2500ms window) — this drives pwr/vold so the * real detection below sees the same DOWN edges a physical press * would, and fires the menu at ~N+800. * seq: 3 quick VOL_UP taps at N, N+300, N+600. */ if (sim_hotkey > 0 && !sim_hk_done) { if (hotkey_mode == HOTKEY_MODE_PWRVOL) { if (el >= sim_hotkey && el < sim_hotkey + 3000) pwr = 1; if (el >= sim_hotkey + 800 && el < sim_hotkey + 950) vold = 1; } else { if (el >= sim_hotkey && el < sim_hotkey + 150) volu = 1; if (el >= sim_hotkey + 300 && el < sim_hotkey + 450) volu = 1; if (el >= sim_hotkey + 600 && el < sim_hotkey + 750) volu = 1; if (el >= sim_hotkey + 750) sim_hk_done = 1; } } if (vold != prev_vold) logmsg("key VOL_DOWN %s", vold ? "DOWN" : "UP"); if (volu != prev_volu) logmsg("key VOL_UP %s", volu ? "DOWN" : "UP"); if (play != prev_play) logmsg("key PLAY %s", play ? "DOWN" : "UP"); if (nxt != prev_nxt) logmsg("key NEXT %s", nxt ? "DOWN" : "UP"); if (pwr != prev_pwr) logmsg("key POWER %s", pwr ? "DOWN" : "UP"); if (prev != prev_prev) logmsg("key PREVIOUS %s", prev ? "DOWN" : "UP"); /* hotkey trigger (v6.5). * DEFAULT — power_voldown: the launcher arms on the power DOWN edge * and fires when a VOL_DOWN DOWN edge lands while power is still * held, 60-2500ms after the power press. The player sees the power * press (its screen-off flow starts — accepted side effect) and the * VOL_DOWN tap (volume -1 — accepted). The menu open runs the * verified unblank + bring-backlight-only-if-dark logic, so a * power-initiated screen-off is undone for the menu. Power release * or window expiry disarms; a VOL_DOWN before 60ms or after 2500ms * is ignored (a plain volume press while holding power). * LEGACY — seq: N quick taps of one key within seq_gap_ms opens the * menu (single/double presses stay ordinary volume keys); the taps * raise volume, snapshotted and restored on trigger. */ if (pwr && !prev_pwr && hotkey_mode == HOTKEY_MODE_PWRVOL) { pwr_hold_armed = 1; pwr_down_at = t; logmsg("hotkey: power DOWN, arm hold-window"); } if (!pwr && prev_pwr) pwr_hold_armed = 0; /* release: disarm */ int hotkey_fired = 0; if (hotkey_mode == HOTKEY_MODE_PWRVOL) { /* disarm once the hold window has passed (power still held) */ if (pwr_hold_armed && pwr && t - pwr_down_at > HK_PWRWIN_MAX) pwr_hold_armed = 0; if (pwr_hold_armed && pwr && vold && !prev_vold) { long since = (long)(t - pwr_down_at); if (since >= HK_PWRWIN_MIN && since <= HK_PWRWIN_MAX) { logmsg("HOTKEY: power held + VOL_DOWN tap (%ldms) -> menu", since); hotkey_fired = 1; } else { logmsg("hotkey: POWER+VOL_DOWN ignored (%ldms outside window)", since); } } } else { int seq_key = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_power.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; if (kp_keys.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1; } if (seq_key && !seq_key_prev) { /* DOWN edge */ if (seq_hits == 0) seq_vol0 = vol_snapshot(); else if (t - seq_last_at > (unsigned)seq_gap_ms) { seq_hits = 0; /* gap too long: restart */ seq_vol0 = vol_snapshot(); } seq_hits++; seq_last_at = t; logmsg("seq tap %d/%d", seq_hits, seq_len); if (seq_hits >= seq_len) { logmsg("SEQ HOTKEY: %d taps of key %d -> menu", seq_len, seq_codes[0]); vol_restore(seq_vol0); seq_hits = 0; hotkey_fired = 1; } } else if (!seq_key && seq_key_prev) { /* UP edge: nothing */ } seq_key_prev = seq_key; if (seq_hits > 0 && t - seq_last_at > (unsigned)seq_gap_ms * 2) seq_hits = 0; } if (hotkey_fired) { struct app apps[MAX_APPS]; int n = scan_apps(apps); int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0); int napps = n < 0 ? 0 : n; run_menu(apps, napps, state, sim_tap, sim_exit); /* restore the player's panel state FIRST (re-blank if the menu * opened while blanked), THEN let it resume — so the player never * sees a panel that contradicts its display state machine * (v6.2 hotfix) */ restore_backlight(); cont_player(); sim_hk_done = 1; pwr_hold_armed = 0; /* absorb current state as baseline for the menu keys */ kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); prev_vold = prev_volu = prev_play = prev_nxt = 0; prev_pwr = prev_prev = 0; seq_key_prev = 0; seq_hits = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) prev_vold = 1; if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) prev_volu = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) prev_play = 1; if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) prev_nxt = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) prev_pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev_prev = 1; } /* the menu just proved the card is present: flush */ log_poll_card(); continue; } prev_vold = vold; prev_volu = volu; prev_play = play; prev_nxt = nxt; prev_pwr = pwr; prev_prev = prev; if (dumpkeys > 0 && el >= dumpkeys) { logmsg("dumpkeys duration reached, exit"); break; } /* card may have mounted after S90: open the card log and move the * /tmp boot buffer into it (no menu needed to see boot logs) */ if (log_fd < 0 && (++log_tick % 100) == 0) log_poll_card(); /* NOTE: the Bug B backlight keeper was REMOVED (2026-08-28 hotfix). * It restored brightness + FBIOBLANK(0) after the player's bkl_time * screen timeout, which unblanked the panel behind the player's back * and froze its display state machine (UI gone, keys dead — device * log: keeper at 40.9s, player wedged). The player owns the panel * blank state; the launcher never touches fb0/backlight outside the * menu/app sessions anymore. The player's own "背光时间" timeout is * expected stock behaviour (set it to 常亮 in the player settings UI * if unwanted). */ usleep(POLL_MS * 1000); } /* If the player is still stopped we never made it through the menu path's * restore+resume (e.g. SIGKILL-free SIGTERM race): restore the panel state * before resuming. When the player is already resumed the menu path did it, * and re-running restore_backlight here would write brightness onto a panel * the player still believes is blanked — leave it alone. */ if (player_stopped) { fb_restore_player(); restore_backlight(); } cont_player(); logmsg("=== meow_launcherd exit ==="); if (log_fd >= 0) { close(log_fd); log_fd = -1; } ipc_shutdown = 1; unlink(ipc_sock_path); unlink(PID_PATH); return 0; }