Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/main_menu.c
T
MeowOS ade0a57353 A1: meow_audiod 独占音频服务(agent-244 骨架 + agent-246 真机收尾)
- meow_audiod:启动门控(/tmp/meow_boot_mode 非 meow exit 0)、IO/播放/路由三线程、
  ring ~200ms、ALSA UAPI 独占直开、切率 mute-重配-恢复、抢占(后开赢+PREEMPTED)、
  socket EOF 回收、xrun PREPARE 恢复;meow_route 轮询逻辑并入路由线程
- meow_audio CLI:status/tone/route/vol;services.d/audiod.conf(trigger=manual,on-fail)
- SDK meow_ui_audio.c 编进 meow_ui.a:懒连接/读线程分发 PREEMPTED 回调/EPIPE 标死/
  无 audiod 返回 MEOW_ERR_NOENT 不隐式直开(MEOW_AUDIO_DIRECT=1 仅调试)
- launcher:确认 MeowOS 模式后 meow_svcd start audiod(double-fork best-effort 不阻塞)
- meow_route 退役:S89 预置换 audiod.conf + 清理旧 meow_route.conf(防拉起已删二进制
  重启循环);fw_content.conf 删 meow_route、加 meow_audiod+meow_audio(md5 打包时填)
- 真机定位并修复 ALSA UAPI 两大坑(即 ncm 播放无声根因):
  ① hw_params 须全放行初始化(mask 全 1/interval [0,UINT_MAX]),memset 0 interval
     等价只能取 0 → refine 必空 → HW_PARAMS 恒 EINVAL
  ② access/format mask 先清零再单点(pmask 只 OR),全 1 被 choose 成 MMAP_INTERLEAVED
     → write() 恒 EINVAL
- 附带修复:dev_write fd<0 防日志风暴丢 PCM 限频日志;SDK state 解析 app_id 截空白;
  meow_ui.h 文档负错误码表述修正
- 验证:宿主 FILE sink 全 IPC 链+抢占+tone WAV 20/20,SDK 客户端 13/13;
  qemu-mipsel 同过;真机 tone 44.1k/96k 切率 written=played、underrun=0、日志完整
- meow_ui.a 53a3edd2 → ncm 重链 d2031829(已热更到卡);launcher a2f932a1
2026-08-31 22:40:08 +08:00

2177 lines
122 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 int menu_title_size(void); /* 前向声明:draw_header_content 用(定义在下方布局帮助块) */
static void draw_header_content(void)
{
draw_statusbar();
const meow_ui_theme_t *th = s_ui_theme();
int tsize = menu_title_size(); /* 标题字号接 font.size.title */
/* M2.6 标题真字体(字号接 font.size.title;回退点阵3x);MD3 on_surface 主文字色 */
if (mfont_ready) {
meow_font_drawtext((uint16_t *)fb, 24, 48, "MeowOS 应用", rgb565(th->on_surface), tsize);
fill_rect(0, 48 + meow_font_line_height(tsize) + 2, fb_xres, 2, th->outline);
} else {
draw_text(24, 28, "MEOWAPPS", 3, th->on_surface);
fill_rect(0, 56, fb_xres, 2, th->outline);
}
}
/* 每秒一次刷新状态栏时钟(顶部 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);
}
}
/* ================================================================== */
/* 主界面 MD3 双布局(卡片宫格 card / 列表 list)+ 应用图标 + 背景 */
/* 布局切换:设置键 ui.home_layout(card=0 默认 / list=1)。 */
/* 图标:读 app.json icon(相对应用目录/绝对),stb_image 解码一次进 */
/* 缓存(menu_icons_load);缺失/失败 → 主题色圆角块+首字母。 */
/* 背景:display.background(纯色名 或 图片路径 MeowAPPs/backgrounds/xx.png)*/
/* 图片 → cover 居中裁剪铺底 + 叠加层(深色叠黑/浅色叠白, */
/* 强度 display.bg_overlay_alpha 缺省 128)。 */
/* 观感:MD3 语义色全程(s_ui_theme()),无散落硬编码;标题字号接 */
/* font.size.title。状态栏由 SDK 全局绘制(不在此改动)。 */
/* ================================================================== */
/* 布局几何常量(480×720;app 区在标题栏之下、设置行之上) */
#define MENU_GRID_PAD 12 /* 宫格卡片外边距 */
#define MENU_GRID_GAP 10 /* 宫格卡片间隙 */
#define MENU_GRID_COLS 2
#define MENU_GRID_CARD_H 112 /* 宫格卡片高 */
#define MENU_GRID_RADIUS 16 /* 卡片圆角 */
#define MENU_GRID_ICON 44 /* 宫格图标 ink 高(方) */
#define MENU_LIST_ROW_H 60 /* 列表行高(大图标行) */
#define MENU_LIST_ICON 40 /* 列表图标 ink 高(方) */
#define MENU_LIST_ICON_X 24 /* 列表图标左缘 */
/* 应用图标缓存(进主界面加载一次,不每帧解码;index 对齐 apps[]) */
static meow_img *s_app_icons[MAX_APPS];
/* 当前按压命中的应用格(-1=无);触摸按下置位、抬起清除(按压态视觉) */
static int g_menu_pressed = -1;
/* 背景状态(display.background:纯色名 or 图片路径) */
static meow_img *s_bg_img = NULL; /* 解码后的背景图(缓存,NULL=纯色) */
static int s_bg_is_img = 0; /* 1=图片背景 */
static int s_bg_overlay = 128; /* 叠加层强度 0-255 */
static uint16_t *s_bg_cache = NULL; /* cover+overlay 后的全屏 RGB565 快照 */
static int s_bg_cache_w = 0, s_bg_cache_h = 0;
static unsigned s_bg_sig = 0; /* 背景设置签名(变化才重建缓存) */
static int s_bg_ready = 0; /* 背景是否已初始化(首次必然构建,防签名 0 碰撞) */
/* 当前主界面布局:0=card(默认) 1=list */
static int menu_layout(void)
{
int v = 0;
if (meow_settings_get_enum("ui.home_layout", &v) != 0) v = 0;
return v ? 1 : 0;
}
/* 标题字号(读 font.size.title,钳制到设置页取值域 16-40) */
static int menu_title_size(void)
{
int t = 28;
if (meow_settings_get_int("font.size.title", &t) != 0) t = 28;
if (t < 16) t = 16;
if (t > 40) t = 40;
return t;
}
/* 应用区顶缘:标题基线(y=48) + 标题行高 + 分隔线下方留白,且不小于 ROW_TOP。
* 大标题(字号>36)时下移,避免与首行卡片重叠。 */
static int menu_apps_top(void)
{
int t = ROW_TOP;
if (mfont_ready) {
int d = 48 + meow_font_line_height(menu_title_size()) + 2 + 10;
if (d > t) t = d;
}
return t;
}
/* 返回足够对比的前景色(YIQ 亮度阈值 140),用于主题色底上的文字/字母。 */
static uint32_t menu_contrast(uint32_t c)
{
int r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF;
long y = (r * 299 + g * 587 + b * 114) / 1000;
return y >= 140 ? 0x000000 : 0xFFFFFF;
}
/* 按 on_surface 亮度判定当前主题是否深色(浅色主题 on_surface 为深色小值)。
* 用于图片背景叠加层配色:深色叠黑 / 浅色叠白(用户定案,固定跟明暗主题)。 */
static int menu_is_dark(const meow_ui_theme_t *th)
{
return th ? (th->on_surface > 0x808080) : 1;
}
/* 背景设置签名:display.background 串 + 叠加层强度 + 明暗 + ui.style(§8.1.1:背景底随
* style×mode 换族)。变化才重建缓存——ui.style 切换必须入签名,否则切风格后主界面背景滞留旧族。 */
static unsigned menu_bg_sig(const meow_ui_theme_t *th, int ov)
{
char bg[MEOW_SET_MAX_LEN];
if (meow_settings_get_str("display.background", bg, sizeof bg) != 0) bg[0] = 0;
int style = 0; if (meow_settings_get_enum("ui.style", &style) != 0) style = 0;
unsigned h = 2166136261u;
for (const char *p = bg; *p; p++) { h ^= (unsigned char)*p; h *= 16777619u; }
h = h * 31u + (unsigned)ov;
h = h * 31u + (unsigned)menu_is_dark(th);
h = h * 31u + (unsigned)style;
return h;
}
/* 解码后背景图路径解析:绝对/相对卡根(MeowOS 约定图片放 MeowOS/opt 或卡根,
* 计划定案为 MeowAPPs/backgrounds/xx.png → 卡根相对)。 */
static meow_img *menu_bg_load_img(const char *bg)
{
char path[320];
if (bg[0] == '/') snprintf(path, sizeof path, "%s", bg);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", bg);
return meow_img_load_file(path);
}
/* 重建背景缓存:铺基色 → cover 居中裁剪图片 → 叠加层 → 快照 480×720 到 s_bg_cache。 */
static void menu_bg_cache_build(const meow_ui_theme_t *th)
{
int W = fb_xres, H = fb_yres;
fill_rect(0, 0, W, H, menu_bg);
if (!s_bg_is_img || !s_bg_img) { free(s_bg_cache); s_bg_cache = NULL; return; }
int iw = meow_img_w(s_bg_img), ih = meow_img_h(s_bg_img);
if (iw <= 0 || ih <= 0) { free(s_bg_cache); s_bg_cache = NULL; return; }
/* cover:整数定点求 scale=max(W/iw,H/ih),居中裁剪;dw/dh 向上取整保证铺满(不露边) */
long long sw = (long long)W * 65536 / iw;
long long sh = (long long)H * 65536 / ih;
long long s = sw > sh ? sw : sh;
int dw = (int)(((long long)iw * s + 65535) / 65536);
int dh = (int)(((long long)ih * s + 65535) / 65536);
int ox = (dw - W) / 2, oy = (dh - H) / 2;
if (dw > 0 && dh > 0) meow_ui_img_scaled(-ox, -oy, dw, dh, s_bg_img);
/* 叠加层(深色叠黑/浅色叠白,强度 s_bg_overlay) */
int ov = s_bg_overlay;
if (ov < 0) ov = 0; if (ov > 255) ov = 255;
if (ov > 0) meow_ui_fill_rect_alpha(0, 0, W, H, menu_is_dark(th) ? 0x000000 : 0xFFFFFF, ov);
/* 快照全屏 565 到缓存(仅支持 16bpp;32bpp 回退纯色) */
int pitch = meow_ui_back_buf_pitch();
if (meow_ui_back_buf_bpp() != 16) { free(s_bg_cache); s_bg_cache = NULL; return; }
if (s_bg_cache && (s_bg_cache_w != W || s_bg_cache_h != H)) { free(s_bg_cache); s_bg_cache = NULL; }
if (!s_bg_cache) {
s_bg_cache = (uint16_t *)malloc((size_t)W * H * 2);
if (!s_bg_cache) return;
s_bg_cache_w = W; s_bg_cache_h = H;
}
const unsigned char *bb = meow_ui_back_buf();
for (int y = 0; y < H; y++)
memcpy(s_bg_cache + (size_t)y * W, bb + (size_t)y * pitch, (size_t)W * 2);
}
/* 背景准备:读设置、检测变化、重载图片/重建缓存。draw_menu 每次调用(变化才重建)。 */
static void menu_bg_prepare(const meow_ui_theme_t *th)
{
int ov = 128;
if (meow_settings_get_int("display.bg_overlay_alpha", &ov) != 0) ov = 128;
if (ov < 0) ov = 0; if (ov > 255) ov = 255;
unsigned sig = menu_bg_sig(th, ov);
if (s_bg_ready && sig == s_bg_sig) { if (ov != s_bg_overlay) s_bg_overlay = ov; return; }
s_bg_sig = sig;
s_bg_overlay = ov;
s_bg_ready = 1;
char bg[MEOW_SET_MAX_LEN];
if (meow_settings_get_str("display.background", bg, sizeof bg) != 0) bg[0] = 0;
if (s_bg_img) { meow_img_free(s_bg_img); s_bg_img = NULL; }
s_bg_is_img = 0;
menu_bg = 0x000000;
if (strchr(bg, '/')) { /* 含路径分隔符 → 图片路径 */
/* 基色用主题 surface:图片透明区/加载失败时回退,避免生硬黑底 */
menu_bg = th->surface;
s_bg_img = menu_bg_load_img(bg);
if (s_bg_img) s_bg_is_img = 1;
} else { /* 纯色名(沿用既有 black/blue/green 语义;随 ui.style 换族) */
int mstyle = 0; if (meow_settings_get_enum("ui.style", &mstyle) != 0) mstyle = 0;
if (mstyle == MEOW_STYLE_MIUIX) { /* MIUIx 暖色族(与 SDK bg_surfaces 同色) */
if (!strcmp(bg, "blue")) menu_bg = 0x101A2E;
else if (!strcmp(bg, "green")) menu_bg = 0x14251A;
else menu_bg = 0x181310; /* black(默认)=MIUIx 深色面 */
} else { /* MD3(既有) */
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
else menu_bg = 0x000000; /* black(默认) */
}
}
menu_bg_cache_build(th);
}
/* 背景绘制:图片缓存 blit(快)或纯色填充。 */
static void menu_bg_paint(void)
{
int W = fb_xres, H = fb_yres;
if (s_bg_is_img && s_bg_img && s_bg_cache) {
int pitch = meow_ui_back_buf_pitch();
for (int y = 0; y < H; y++)
memcpy(fb + (size_t)y * pitch, s_bg_cache + (size_t)y * W, (size_t)W * 2);
} else {
fill_rect(0, 0, W, H, menu_bg);
}
}
/* 应用图标路径:app.json icon 相对应用目录(dirname(exec))/ 绝对;失败回退卡根相对。 */
static void menu_icon_path(const struct app *a, char *out, int olen)
{
if (a->icon[0] == '/') { snprintf(out, olen, "%s", a->icon); return; }
const char *sl = strrchr(a->exec, '/');
if (sl && a->icon[0]) {
int n = (int)(sl - a->exec);
char dir[180];
if (n >= (int)sizeof dir) n = (int)sizeof dir - 1;
memcpy(dir, a->exec, (size_t)n); dir[n] = 0;
snprintf(out, olen, "%s/%s", dir, a->icon);
} else {
snprintf(out, olen, "/usr/data/mnt/sd_0/%s", a->icon);
}
}
/* 加载单个应用图标(解码一次进缓存);失败返回 NULL(调用方回落首字母占位)。 */
static meow_img *menu_app_icon_load(const struct app *a)
{
if (!a->icon[0]) return NULL;
char p[220];
menu_icon_path(a, p, sizeof p);
meow_img *img = meow_img_load_file(p);
if (!img && a->icon[0] != '/') { /* 应用目录相对失败 → 回退卡根相对 */
snprintf(p, sizeof p, "/usr/data/mnt/sd_0/%s", a->icon);
img = meow_img_load_file(p);
}
logmsg("icon: %s -> %s %s", a->id[0] ? a->id : a->name, p, img ? "OK" : "FAIL"); /* M2.87 排查 */
return img;
}
/* 进主界面/热重载:批量加载应用图标(一次,不每帧解码;先释放旧图防泄漏)。 */
static void menu_icons_load(struct app *apps, int napps)
{
for (int i = 0; i < MAX_APPS; i++)
if (s_app_icons[i]) { meow_img_free(s_app_icons[i]); s_app_icons[i] = NULL; }
for (int i = 0; i < napps; i++) s_app_icons[i] = menu_app_icon_load(&apps[i]);
}
/* 主界面退出:释放图标缓存。 */
static void menu_icons_free(void)
{
for (int i = 0; i < MAX_APPS; i++)
if (s_app_icons[i]) { meow_img_free(s_app_icons[i]); s_app_icons[i] = NULL; }
g_menu_pressed = -1;
}
/* 首字符(UTF-8 整码,中文取第一字) */
static void menu_first_glyph(const char *nm, char out[4])
{
out[0] = out[1] = out[2] = out[3] = 0;
if (!nm || !nm[0]) { out[0] = '?'; return; }
char c0 = nm[0];
out[0] = c0;
if ((unsigned char)c0 >= 0x80 && nm[1]) {
out[1] = nm[1];
if ((unsigned char)c0 >= 0xE0 && nm[2]) out[2] = nm[2];
}
}
/* 名称截断(按可用像素宽;超出加 '…')。 */
static void menu_name_fit(const char *name, int maxw, int size, char *out, int olen)
{
char buf[64];
snprintf(buf, sizeof buf, "%s", name);
if (ui_width(buf, size) <= maxw) { snprintf(out, olen, "%s", buf); return; }
int n = (int)strlen(buf);
while (n > 0 && ui_width(buf, size) > maxw) {
/* 回退一个 UTF-8 字符边界 */
do { buf[--n] = 0; } while (n > 0 && ((unsigned char)buf[n] & 0xC0) == 0x80);
}
snprintf(out, olen, "%s...", buf);
}
/* 图标绘制:primary 圆角垫块始终垫底(MD3 图标 tile),首字母叠加其上,最后有图叠加
* meow_ui_img_cover(居中裁方)。
* 修复:原实现 `if (icon) { meow_ui_img_cover; return; }` 在图标为透明背景 logo(不透明像素
* 极少/无)时整区只剩卡面,且因 icon!=NULL 完全跳过占位 →「图标区+首字母占位都不见了」。
* 现让垫块+首字母先画:不透明/方图盖住它们(观感=纯图);透明图则露出垫块+首字母(占位)。
* 图标区任何情况下都非空,且首字母占位不再被图标句柄吞掉。 */
static void menu_draw_icon(const struct app *a, meow_img *icon, int x, int y, int sz,
const meow_ui_theme_t *th)
{
meow_gfx_fill_round(x, y, sz, sz, sz * 3 / 10, th->primary);
char g[4];
menu_first_glyph(a->name, g);
uint32_t fg = menu_contrast(th->primary);
int cw = ui_width(g, 20);
ui_draw(x + sz / 2 - cw / 2, y + sz / 2 - meow_font_line_height(20) / 2, g, fg, 20);
if (icon) { meow_ui_img_cover(x, y, sz, icon); }
}
/* 应用格几何(grid/list 统一入口;供 draw 与 touch 命中共用)。 */
static void menu_cell_rect(int i, int layout, int apps_top,
int *cx, int *cy, int *cw, int *ch)
{
if (layout == 0) { /* card grid */
int row = i / MENU_GRID_COLS, col = i % MENU_GRID_COLS;
*cw = (fb_xres - 2 * MENU_GRID_PAD - (MENU_GRID_COLS - 1) * MENU_GRID_GAP) / MENU_GRID_COLS;
*ch = MENU_GRID_CARD_H;
*cx = MENU_GRID_PAD + col * (*cw + MENU_GRID_GAP);
*cy = apps_top + row * (*ch + MENU_GRID_GAP);
} else { /* list */
*cw = fb_xres - 16;
*ch = MENU_LIST_ROW_H;
*cx = 8;
*cy = apps_top + i * MENU_LIST_ROW_H;
}
}
/* 触摸命中应用格;未命中返回 -1。 */
static int menu_hit_app(int x, int y, struct app *apps, int napps, int state,
int layout, int apps_top)
{
(void)apps; /* 命中判定只依赖几何;apps 保留作接口(0-warning) */
if (state != 0 || napps <= 0) return -1;
for (int i = 0; i < napps; i++) {
int cx, cy, cw, ch;
menu_cell_rect(i, layout, apps_top, &cx, &cy, &cw, &ch);
if (x >= cx && x < cx + cw && y >= cy && y < cy + ch) return i;
}
return -1;
}
/* 宫格卡片:surface_container 底 + 圆角 + 选中/按压态 tonal 高亮 + 图标 + 名称。 */
static void menu_draw_grid_cell(const struct app *a, meow_img *icon, int x, int y,
int w, int h, int sel, int pressed,
const meow_ui_theme_t *th)
{
meow_gfx_fill_round(x, y, w, h, MENU_GRID_RADIUS, th->surface_container);
if (sel || pressed) {
int alpha = pressed ? 72 : 40; /* MD3 状态层:按压更浓 */
meow_gfx_fill_round_alpha(x, y, w, h, MENU_GRID_RADIUS, th->primary, alpha);
}
int sz = (h > MENU_GRID_ICON + 40) ? MENU_GRID_ICON : h - 40;
int icx = x + (w - sz) / 2, icy = y + 12;
menu_draw_icon(a, icon, icx, icy, sz, th);
char nm[40];
menu_name_fit(a->name, w - 14, 18, nm, sizeof nm);
int name_y = y + 12 + sz + 10;
int nw = ui_width(nm, 18);
ui_draw(x + (w - nw) / 2, name_y, nm, a->ok ? th->on_surface : th->error, 18);
if (!a->ok) {
int ew = ui_width("[缺失]", 14);
ui_draw(x + (w - ew) / 2, name_y + meow_font_line_height(18) + 2, "[缺失]", th->error, 14);
}
}
/* 列表行:图标 + 名称 + 副标题(package/id)+ 选中/按压 tonal 高亮。 */
static void menu_draw_list_cell(const struct app *a, meow_img *icon, int x, int y,
int w, int h, int sel, int pressed,
const meow_ui_theme_t *th)
{
meow_gfx_fill_round(x, y, w, h, 12, th->surface_container);
if (sel || pressed) {
int alpha = pressed ? 72 : 40;
meow_gfx_fill_round_alpha(x, y, w, h, 12, th->primary, alpha);
}
int sz = MENU_LIST_ICON;
int icy = y + (h - sz) / 2;
menu_draw_icon(a, icon, x + MENU_LIST_ICON_X, icy, sz, th);
int tx = x + MENU_LIST_ICON_X + sz + 14;
char nm[40];
menu_name_fit(a->name, w - (tx - x) - 12, 20, nm, sizeof nm);
int name_y, sub_y;
if (mfont_ready) {
int lh20 = meow_font_line_height(20), lh14 = meow_font_line_height(14);
name_y = y + (h - (lh20 + 4 + lh14)) / 2;
sub_y = name_y + lh20 + 4;
} else {
name_y = y + 8;
sub_y = y + 30;
}
ui_draw(tx, name_y, nm, a->ok ? th->on_surface : th->error, 20);
/* 副标题:package(无则 id),on_surface_variant */
char sub[40];
snprintf(sub, sizeof sub, "%s", a->package[0] ? a->package : (a->id[0] ? a->id : ""));
if (sub[0]) {
char subf[44];
menu_name_fit(sub, w - (tx - x) - 12, 14, subf, sizeof subf);
ui_draw(tx, sub_y, subf, th->on_surface_variant, 14);
}
}
/* 应用格绘制入口:按布局分派。 */
static void menu_draw_app_cell(const struct app *a, meow_img *icon, int x, int y,
int w, int h, int sel, int pressed, int layout,
const meow_ui_theme_t *th)
{
if (layout == 0) menu_draw_grid_cell(a, icon, x, y, w, h, sel, pressed, th);
else menu_draw_list_cell(a, icon, x, y, w, h, sel, pressed, th);
}
static void draw_menu(struct app apps[MAX_APPS], int napps, int state,
int cursor, int toast_ms, const char *toast)
{
const meow_ui_theme_t *th = s_ui_theme();
int layout = menu_layout();
int apps_top = menu_apps_top();
int bsize = 20;
meow_settings_get_int("font.size.body", &bsize);
if (bsize < 12) bsize = 12;
if (bsize > 28) bsize = 28;
/* 背景(纯色 or 图片 cover+overlay;缓存变化才重建,不每帧解码) */
menu_bg_prepare(th);
menu_bg_paint();
draw_header_content();
if (state == 1) {
if (mfont_ready) {
ui_draw(30, 200, "没有存储卡", th->on_surface, bsize);
ui_draw(30, 234, "请插入TF卡后重开菜单", th->on_surface_variant, bsize);
} else {
draw_text(30, 200, "NO STORAGE CARD", 2, th->on_surface);
draw_text(30, 240, "INSERT TF CARD", 2, th->on_surface_variant);
draw_text(30, 272, "THEN RE-OPEN MENU", 2, th->on_surface_variant);
}
} else if (state == 2 || napps == 0) {
if (mfont_ready) {
ui_draw(30, 200, "没有找到应用", th->on_surface, bsize);
ui_draw(30, 234, "请放入应用到", th->on_surface_variant, bsize);
ui_draw(30, 268, "MeowAPPs/<id>/app.json", th->on_surface_variant, bsize);
} else {
draw_text(30, 200, "NO APPS FOUND", 2, th->on_surface);
draw_text(30, 240, "PUT APP IN", 2, th->on_surface_variant);
draw_text(30, 272, "MeowAPPs/<id>/app.json", 2, th->on_surface_variant);
}
} else {
for (int i = 0; i < napps; i++) {
int cx, cy, cw, ch;
menu_cell_rect(i, layout, apps_top, &cx, &cy, &cw, &ch);
menu_draw_app_cell(&apps[i], (i < MAX_APPS) ? s_app_icons[i] : NULL,
cx, cy, cw, ch, i == cursor, g_menu_pressed == i,
layout, th);
}
}
/* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前;
* 音量导航可把光标移到此处(cursor==napps),触摸点击该行进入设置页。 */
{
int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */
int selset = (cursor == napps); /* 光标已移到设置行 */
meow_gfx_fill_round(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container);
if (selset) meow_gfx_fill_round_alpha(8, sy - 4, fb_xres - 16, ROW_H - 8, 12, th->primary, 40);
if (mfont_ready) ui_draw(24, sy, "系统设置", th->on_surface, bsize);
else draw_text(24, sy, "SETTINGS", 2, th->on_surface);
s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, th->on_surface_variant);
}
/* exit row */
meow_gfx_fill_round(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 12, th->surface_container);
/* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */
if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", th->on_surface, bsize);
else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, th->on_surface);
/* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。字号 20 真实行高 meow_font_line_height(20)。
* 非 mfont 回退分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */
#define FOOTY_PAD 8
{
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(bsize) - FOOTY_PAD;
ui_draw(fb_xres - 8 - ui_width(ver, bsize), foot_y, ver, th->on_surface_variant, bsize);
}
if (toast_ms > 0 && toast && toast[0]) {
fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, th->surface_variant);
ui_draw(30, TOAST_Y, toast, th->error, bsize);
}
}
/* ================================================================== */
/* 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 */
/* M2.77 应用列表热重载:MeowAPPs 目录签名(自身 + 各应用目录 mtime 的 FNV 混合),
* run_menu 2s 轮询比对,变化即重扫——覆盖 SSH/面板推新包、删包、应用目录内容变动等场景
* (此前仅「无卡→有卡」过渡才重扫,常驻主界面下推包不刷新)。 */
static unsigned long long apps_dir_sig(void)
{
unsigned long long sig = 1469598103934665603ULL; /* FNV-1a offset */
struct stat st;
if (stat(MEOWAPPS, &st) != 0) return 0;
sig ^= (unsigned long long)st.st_mtime; sig *= 1099511628211ULL;
DIR *d = opendir(MEOWAPPS);
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (de->d_name[0] == '.') continue;
char p[300];
snprintf(p, sizeof p, "%s/%s", MEOWAPPS, de->d_name);
if (stat(p, &st) == 0 && S_ISDIR(st.st_mode)) {
sig ^= (unsigned long long)st.st_mtime * 256 + (unsigned char)de->d_name[0];
sig *= 1099511628211ULL;
}
}
closedir(d);
}
return sig;
}
/* M2.83 修补:字库目录签名(根+各 family 目录 mtime)。meow_font_scan_presets 内含逐套
* sha256 校验(gbk 套 6.8MB 级),绝不能每 2s 轮询全量扫——只在目录签名变化时扫。 */
static unsigned long long fonts_dir_sig(void)
{
char root[300];
snprintf(root, sizeof root, "/usr/data/mnt/sd_0/%s", MEOW_FONTS_CFG);
unsigned long long sig = 1469598103934665603ULL;
struct stat st;
if (stat(root, &st) != 0) return 0;
sig ^= (unsigned long long)st.st_mtime; sig *= 1099511628211ULL;
DIR *d = opendir(root);
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (de->d_name[0] == '.') continue;
char p[400];
snprintf(p, sizeof p, "%s/%s", root, de->d_name);
if (stat(p, &st) == 0 && S_ISDIR(st.st_mode)) {
sig ^= (unsigned long long)st.st_mtime * 256 + (unsigned char)de->d_name[0];
sig *= 1099511628211ULL;
}
}
closedir(d);
}
return sig;
}
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 后台拔营(卡/网)轮询时间戳 */
unsigned long long apps_sig = 0; /* M2.77:应用目录签名(0=未初始化,首圈建立基线) */
unsigned long long fonts_sig = 0; /* M2.83:字库目录签名(门控 scan_presets,防 2s 轮询 sha256 风暴) */
load_config(); /* meow.conf on the card: bg_color etc. */
menu_icons_load(apps, napps); /* MD3:应用图标解码一次进缓存(不每帧解码) */
/* 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();
/* AMSD_DESIGN §4.3:amsd `am start` → 消费 pending-start(仅菜单态/无前台 App 时拉起)。
* 镜像 sim-tap 启动路径:launch 后 menu_after_app_exit + dirty 重绘菜单。 */
{
char spkg[MEOW_APP_ID_MAX], sact[MEOW_ACT_MAX];
if (meow_ams_take_start(spkg, sizeof spkg, sact, sizeof sact)) {
if (state != 0) {
snprintf(toast, sizeof toast, "AMS START: app list not ready");
toast_ms = TOAST_MS;
} else {
int idx = -1;
for (int i = 0; i < napps; i++)
if (!strcmp(apps[i].id, spkg)) { idx = i; break; }
if (idx < 0) {
snprintf(toast, sizeof toast, "AMS START: NO APP %s", spkg);
toast_ms = TOAST_MS;
} else {
int ret;
if (sact[0]) {
char chain[MEOW_CHAIN_MAX]; int depth;
if (resolve_chain(spkg, sact, chain, &depth) == MEOW_OK)
ret = launch_activity(&apps[idx], chain);
else {
snprintf(toast, sizeof toast, "AMS: NO ROUTE %s/%s", spkg, sact);
toast_ms = TOAST_MS; ret = 0;
}
} else {
ret = launch_app(&apps[idx]);
}
if (ret != 0) {
snprintf(toast, sizeof toast, "CANNOT START: %s", apps[idx].name);
toast_ms = TOAST_MS;
}
menu_after_app_exit(ret == 0);
last_input_ms = now_ms(); /* M2.17:回主界面即视为输入 */
dirty = 1;
}
}
}
}
meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/
meow_media_tick(); /* 阶段D附录B:媒体源死亡(播放中无 META 超时)清槽回空态 */
/* 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();
/* M2.86 开机亮度竞态:launcher 被 sysui 在 S70 早拉起,/usr/data(UBIFS) 未就绪 →
* 首次 settings_load 读到纯默认值(display.brightness=101→clamp 100),apply_user_brightness
* 已挂起 meow_brightness_pending(不盲目写 100)。每 2s 重试 settings_load 直到读到
* settings.conf;settings_load 内置在「未读到→已读到」转换时补应用真实用户亮度。 */
if (meow_brightness_pending && !meow_settings_loaded())
settings_load();
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:卡挂载后重配多字体(此前无全局可配) */
/* M2.83 修补:字库重扫改为签名门控——扫描含逐套 sha256(gbk 套 6.8MB 级),
* 每 2s 全量扫会把 CPU 烧满(真机实测 100%)。只在字体目录签名变化时扫。 */
{
unsigned long long fsig = fonts_dir_sig();
if (fsig != fonts_sig) {
fonts_sig = fsig;
meow_font_scan_presets();
}
}
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;
menu_icons_load(apps, napps); /* MD3:图标缓存随重扫刷新 */
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();
/* M2.77 应用列表热重载:目录签名变化(推新包/删包/应用目录变动)→ 重扫刷新。
* 与上面「无卡→有卡」过渡重扫互补:常驻主界面下目录内容变化也覆盖。 */
{
unsigned long long asig = apps_dir_sig();
if (apps_sig && asig && asig != apps_sig) {
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) {
memcpy(apps, fresh, sizeof fresh);
napps = fn; state = 0;
if (cursor >= napps) cursor = napps - 1;
menu_icons_load(apps, napps); /* MD3:图标缓存随热重载刷新 */
dirty = 1;
logmsg("hot-reload: MeowAPPs changed -> %d apps", napps);
} else if (fstate != state) {
state = fstate; napps = (fn > 0) ? fn : 0;
dirty = 1;
logmsg("hot-reload: state -> %d", state);
}
}
apps_sig = asig;
}
} 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 >= (unsigned long long)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); meow_power_action(POWER_SHUTDOWN); }
m_prev_pwr = pwr; continue;
} else if (t - pwr_hold_at >= (unsigned long long)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 < (unsigned long long)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 */
/* 阶段D附录B:媒体键路由到最近活跃媒体会话(播放/暂停切换)。无源=打点。 */
const meow_media_session_t *ms = meow_media_current();
if (ms) {
uint32_t cmd = (ms->state == MEOW_MEDIA_PLAYING) ? 2u /* 暂停 */ : 1u /* 播放 */;
int rc = meow_media_playctl(cmd, 0);
logmsg("play key: media control -> cmd=%u rc=%d (%s)", cmd, rc, ms->app_id);
} else {
logmsg("play key: media-reserved (no media source)");
}
}
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;
/* MD3 按压态:锁定原点后立即判定命中的应用格(按下即显示高亮) */
g_menu_pressed = menu_hit_app(t_last_x, t_last_y, apps, napps, state,
menu_layout(), menu_apps_top());
if (g_menu_pressed >= 0) {
int cx, cy, cw, ch;
menu_cell_rect(g_menu_pressed, menu_layout(), menu_apps_top(),
&cx, &cy, &cw, &ch);
meow_dirty_add(cx, cy, cw, ch);
need_menu_paint = 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);
notif_click_t click;
memset(&click, 0, sizeof click);
click.app_idx = -1;
click.status = NOTIF_CLICK_STATUS_NONE;
run_notif_center(apps, napps, &click, 1, t_down_x, loy);
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
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;
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
if (click.status == NOTIF_CLICK_STATUS_OK &&
click.app_idx >= 0 && click.app_idx < napps) {
logmsg("menu: notif click -> launch idx=%d chain=\"%s\"",
click.app_idx, click.chain);
int ret = click.chain[0]
? launch_activity(&apps[click.app_idx], click.chain)
: launch_app(&apps[click.app_idx]);
menu_after_app_exit(ret == 0);
last_input_ms = now_ms();
dirty = 1;
if (ret != 0) {
snprintf(toast, sizeof toast, "CANNOT START: %s",
apps[click.app_idx].name);
toast_ms = TOAST_MS;
}
} else if (click.status == NOTIF_CLICK_STATUS_NOROUTE) {
logmsg("menu: notif click NO ROUTE");
snprintf(toast, sizeof toast, "NO ROUTE");
toast_ms = TOAST_MS;
dirty = 1;
} else if (click.status == NOTIF_CLICK_STATUS_NOSRC) {
logmsg("menu: notif click SOURCE NOT FOUND");
snprintf(toast, sizeof toast, "SOURCE NOT FOUND");
toast_ms = TOAST_MS;
dirty = 1;
}
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;
/* MD3 按压态:抬手清除按压高亮(无论 tap/滑动,先回退到常态) */
if (g_menu_pressed >= 0) {
int cx, cy, cw, ch;
menu_cell_rect(g_menu_pressed, menu_layout(), menu_apps_top(),
&cx, &cy, &cw, &ch);
meow_dirty_add(cx, cy, cw, ch);
g_menu_pressed = -1;
need_menu_paint = 1;
}
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);
notif_click_t click;
memset(&click, 0, sizeof click);
click.app_idx = -1;
click.status = NOTIF_CLICK_STATUS_NONE;
run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
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;
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
if (click.status == NOTIF_CLICK_STATUS_OK &&
click.app_idx >= 0 && click.app_idx < napps) {
logmsg("menu: notif click -> launch idx=%d chain=\"%s\"",
click.app_idx, click.chain);
int ret = click.chain[0]
? launch_activity(&apps[click.app_idx], click.chain)
: launch_app(&apps[click.app_idx]);
menu_after_app_exit(ret == 0);
last_input_ms = now_ms();
dirty = 1;
if (ret != 0) {
snprintf(toast, sizeof toast, "CANNOT START: %s",
apps[click.app_idx].name);
toast_ms = TOAST_MS;
}
} else if (click.status == NOTIF_CLICK_STATUS_NOROUTE) {
logmsg("menu: notif click NO ROUTE");
snprintf(toast, sizeof toast, "NO ROUTE");
toast_ms = TOAST_MS;
dirty = 1;
} else if (click.status == NOTIF_CLICK_STATUS_NOSRC) {
logmsg("menu: notif click SOURCE NOT FOUND");
snprintf(toast, sizeof toast, "SOURCE NOT FOUND");
toast_ms = TOAST_MS;
dirty = 1;
}
continue; /* 本圈不再按 tap/其余处理 */
}
/* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、
* 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。
* 阶段D附录B:主界面「右缘/中部向左横滑」→ 进入 Now Playing 大页。
* 手势分流(MEOW_APP_SDK §5.2):顶部下滑=通知中心(已在上方处理)、
* 左缘右滑=返回(左缘起点不至此),此处只接管「中部/右缘向左」横滑。 */
if (dx <= -MENU_SWIPE_THRESH && adx > ady &&
tx >= MEOW_EDGE_W && ty >= SB_BAR_H) {
logmsg("menu: left-swipe -> Now Playing (dx=%d,dy=%d)", dx, dy);
menu_arm_touch(TAP_ARM_DELAY_MS);
run_now_playing();
/* 重播按键防抖基线,防播放页内电源/音量边沿被 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;
continue;
}
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 走人)→ 概率乱跳。退出行优先,命中即不再
* 落到应用行。普通小列表两区间不重叠,行为不变。
* MD3 双布局:应用格用 menu_hit_app(按 ui.home_layout 几何命中)。 */
else {
int idx = menu_hit_app(tx, ty, apps, napps, state,
menu_layout(), menu_apps_top());
if (idx >= 0) {
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 行上方)。
* 应用格用 menu_cell_rect(按 ui.home_layout 几何)标脏;设置行固定 y=EXIT_ROW-ROW_H。 */
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) {
int layout = menu_layout(), apps_top = menu_apps_top();
if (last_cursor >= 0 && last_cursor < napps) {
int cx, cy, cw, ch;
menu_cell_rect(last_cursor, layout, apps_top, &cx, &cy, &cw, &ch);
meow_dirty_add(cx, cy - 2, cw, ch + 4);
}
if (cursor >= 0 && cursor < napps) {
int cx, cy, cw, ch;
menu_cell_rect(cursor, layout, apps_top, &cx, &cy, &cw, &ch);
meow_dirty_add(cx, cy - 2, cw, ch + 4);
}
}
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;
}
/* Bug2 残帧防护 (agent-x):App 退出/结构变化触发全帧重画时,用 fb_pan_buf0_force()
* 无条件把 LCDC pan 回 buffer0(菜单/所有 app 都画 buffer0 且从不自己 pan)。仅
* fb_norm_buf0() 会在缓存的 fb_active_yoffset==0 时短路——若 App 运行期把物理
* yoffset 留到了 buffer1(如其内部双缓冲翻页),缓存仍为 0 导致真正的 re-pan 从未
* 发出,重画后的菜单落在 buffer0 而 LCDC 仍显示 buffer1 的 App 残帧(=菜单区残帧)。
* 只在真重画(need_menu_paint)时 force-pan,不每圈 ioctl;重复 pan 到同一 yoffset
* 是硬件 no-op,无频闪(fb_pan_buf0_force 注释)。 */
if (need_menu_paint) {
draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */
fb_pan_buf0_force(); /* 真重画 → 无条件 pan 回 buffer0(防 App 残留 buffer1) */
need_menu_paint = 0;
} else {
fb_norm_buf0(); /* 无重画 → 短路;yoffset 非 0 才 pan */
}
/* 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 于空脏。 */
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);
}
menu_icons_free(); /* MD3:主界面退出释放应用图标缓存 */
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");
}
/* /tmp/meow_boot_mode:开机模式标记(bootmenu 在 S90 选定后写 "meow"/"hiby",tmpfs 开机清空)。
* M2.89 回归修复用,见 main() 内「开机选单期不抢屏」。 */
#define MEOW_BOOT_MODE_PATH "/tmp/meow_boot_mode"
/* 拉起独占音频服务(AUDIOD_DESIGN.md §1):launcher 确认 MeowOS 模式后 `meow_svcd start audiod`。
* 幂等(svcd cmd_start 在 pid>0 时 no-op)、best-effort(audiod 未装/失败静默)、
* 不阻塞主循环(double-fork + 仅 waitpid 立即退出的中间子进程,避免僵尸 + 不占用 fb)。
* 双重门控:audiod 自身读 /tmp/meow_boot_mode,非 meow 即 exit(0)——HibyOS 下物理不存在。 */
static void meow_audiod_kick(void)
{
if (access("/usr/bin/meow_audiod", X_OK) != 0) return; /* 固件未装 audiod → 静默 */
pid_t pid = fork();
if (pid < 0) return;
if (pid == 0) {
pid_t pid2 = fork(); /* double-fork:隔离执行者,防僵尸 */
if (pid2 != 0) _exit(0); /* 中间子进程立即退出,由父进程 waitpid 回收 */
{
/* 执行者:重定向 stdio 到 /dev/null,exec meow_svcd start audiod */
int devnull = open("/dev/null", O_RDWR);
if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); if (devnull > 2) close(devnull); }
execl("/usr/bin/meow_svcd", "meow_svcd", "start", "audiod", (char *)0);
_exit(127);
}
}
waitpid(pid, NULL, 0); /* 等中间子进程退出(即刻返回,不实质阻塞) */
logmsg("audiod: meow_svcd start audiod requested");
}
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,防御性),回落到下面的旧覆盖路径。 */
/* ===== M2.89 回归修复:开机选单期不抢屏 =====
* sysui 在 S70 即拉起 launcher(早于 S90 bootmenu 写 /tmp/meow_boot_mode)。
* 旧判定 is_meow_mode()=find_hiby_pid()<=0 在选单阶段恒真(播放器尚未启动),
* launcher 误入 MeowOS 主屏 → 与选单/HibyOS 播放器抢 fb0 与触摸输入
* (开机选 HibyOS 后黑屏/选单被主屏重绘覆盖的回归根因)。
* 修复:mode 文件不存在且 bootmenu 存活 → 怠速等它写(选单无超时,不设等待上限;
* bootmenu 消失且仍无文件 → 非开机语境如 SSH 手跑/S92 兜底,回退旧判定)。
* 读到 hiby → 怠速守护(不碰 fb):轮询 mode 文件,变 meow(HibyOS 热键切换)→ 落地主屏。 */
char boot_mode[16] = { 0 };
if (!sigterm_flag) {
int announced = 0;
while (access(MEOW_BOOT_MODE_PATH, F_OK) != 0 && !sigterm_flag) {
if (proc_by_comm("meow_bootmenu") <= 0) break; /* 非开机语境:回退旧判定 */
if (!announced) { logmsg("boot: mode undecided, bootmenu active -> idle wait (no fb grab)"); announced = 1; }
usleep(200 * 1000);
}
FILE *mf = fopen(MEOW_BOOT_MODE_PATH, "r");
if (mf) { if (fscanf(mf, "%15s", boot_mode) != 1) boot_mode[0] = 0; fclose(mf); }
while (!sigterm_flag && !strcmp(boot_mode, "hiby")) {
/* HibyOS 会话怠速:bootmenu supervisor 管热键/播放器;mode 变 meow(热键切换)即落地。 */
usleep(500 * 1000);
mf = fopen(MEOW_BOOT_MODE_PATH, "r");
if (mf) { if (fscanf(mf, "%15s", boot_mode) != 1) boot_mode[0] = 0; fclose(mf); }
}
if (boot_mode[0]) logmsg("boot mode = %s", boot_mode);
}
meow_mode = !strcmp(boot_mode, "meow") ? 1 :
!strcmp(boot_mode, "hiby") ? 0 : 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 (!sigterm_flag && meow_mode) { /* M2.89:怠速等待期间被 sysui stop(HibyOS 选定)→ 直走退出尾 */
/* M2.2 拔营:启动即尝试挂 TF 卡 + 联网(非阻塞);失败由 run_menu 后台周期重试。 */
ensure_card_mounted();
ensure_wifi();
meow_audiod_kick(); /* 确认 MeowOS 模式 → 拉起独占音频服务(幂等、best-effort、不阻塞主循环) */
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;
}