- 14 模块 .c + meow_launcher_internal.h(642): util/font_theme/fb_compositor/settings_page/power_menu/notif_center/app_launcher/main_menu/audio/backlight/net/keys/about/statusbar; meow_launcherd.c 已删 - 按行逐字搬移(0-diff), 118函数提升非static+47组extern(内部头), #if0退役区不编入(源码留.pre_split.bak) - 关于本机小改: 作者 AskaEth/17 + info/item[13]防越界 + s_dd_modal 超高滚动(clamp/触摸滚动/音量键跟随/裁剪, 仅超高启用) - build_launcher.sh 编译列表→模块; 文档/注释引用 meow_launcherd.c→模块/目录 - 编译0warning/-Wall -Wextra告警集与原文一致(77条无新增)/qemu-mipsel冒烟一致(--scan/--sysdump); launcher=c0799357(505KB; md5因多文件重编译与原文异,非回归依据) - 备份 meow_launcherd.c.pre_split.bak/meow_launcherd.pre_split.bak; M2.69(0.7.64)已推卡并用户验证OK
1481 lines
86 KiB
C
1481 lines
86 KiB
C
/* 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/<id>/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/<id>/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<SB_BAR_H) + 下拉达 NCP_DRAG_ENTER_SLOP →
|
||
* 立即切入 run_notif_center(此时手指仍按下拖动),由面板内拖动路径把 progress
|
||
* 映射为手指位移(跟手 1:1),松手由面板内锚定判定补到展开/收回(半程可回)。
|
||
* 原「抬手边沿 mnp_top_swipe_down」打开改为「拖动中即开」(该抬手边沿保留为防御)。
|
||
* 先在本次 ABS 内触发一次(menu_live_open_pending latch);
|
||
* 进入前 t_down=0 使返回后同次手势的剩余事件被忽略(防 double-open/残留)。 */
|
||
if (t_down && t_down_valid && !menu_live_open_pending &&
|
||
t_down_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<MEOW_EDGE_W)起点交给 meow_edge_back 优先消费;
|
||
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
|
||
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
|
||
logmsg("menu: top-edge swipe-down -> notification center (release edge, fallback)");
|
||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||
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<last_input 无符号回绕误判「空闲30s」。 */
|
||
if (idle_ms_since(t, last_input_ms) >= (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.mpk>] [--pkg-export <pkg> <out.mpk>]\n"
|
||
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
|
||
" [--fontscan <card_root>]\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 <root>:宿主/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/<family>/<weight>/,填充「字体文件」下拉 preset。
|
||
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */
|
||
meow_font_scan_presets();
|
||
|
||
if (fontscan_root) {
|
||
/* M2.6 测试钩子:扫描指定卡根下的 MeowOS/fonts/<family>/<weight>/ 并打印 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;
|
||
}
|
||
|