/* meow_bootmenu.c — R3 Pro II MeowOS/HibyOS 开机引导选单(阶段0 · M0 二进制自检版) * * M2.14 热键一次触发修复(2026-08-29,MEOW_BOOTMENU_VER 0.1.0-m2.14): * 开机武装延迟(ARM_DELAY_MS) + 播放器忙判门(proc_is_busy) 不再「静默丢弃」热键—— * 命中即置 hotkey_pending,待条件满足(延迟结束/播放器不忙)后自动弹确认框 * (用户只按一次,不需第二下)。新增 --sim-hotkey-strict 让 sim 走该门(测 pending)。 * * M2.26 触摸确认迁移(2026-08-29,MEOW_BOOTMENU_VER 0.1.0-m2.26):选单触摸 + 热键 * 确认框触摸从各自 t_last_x/y + t_down_x/y 解析迁到 meow_touch 统一接口(mt_reset/ * mt_abs/mt_down/mt_up;goodix 时序免疫:BTN_TOUCH=1 不锁原点,等按下后首个有效 ABS * 才锁,位移>=65 判滑动不命中)——修「触摸确认要点两下」(电源键一下即可确认)。 * 绘制/选单逻辑/超时倒计时/fork 播放器/supervisor/热键触发核心路径不动。 * * M2 诊断 · 信号记录 hook(仅记录,不改行为;排查「HibyOS 播放器 UI 触发关机/升级 → * 卡最后一帧但系统没真关机/SSH 能连」第一步):supervisor 加 bm_log() 写 TF 卡 * MeowOS/logs/meow_boot_.log(卡不可用降级 stderr),记录 supervisor * **收到**的信号(SIGTERM/SIGINT/SIGQUIT/SIGHUP——handler 只置标志,主循环 log 后 * signal(s,SIG_DFL)+raise(s) 恢复默认重抛,与未加 hook 一致终止)、**发出**给播放器的 * 信号(SIGSTOP/SIGCONT/SIGTERM/SIGKILL)、以及**播放器退出原因**(WTERMSIG/WEXITSTATUS)。 * 不转发信号、不善后、不 SHOW 关机画面、不截断。 * * M2.29 supervisor 接管关机/升级(2026-08-29,MEOW_BOOTMENU_VER 0.1.0-m2.29-takeover): * 基于 M2 诊断 hook 抓到的两条真机信号,让 supervisor 把「播放器 UI 触发关机/升级 → * 卡最后一帧但系统没真关机(SSH 能连)」收束掉: * - 播放器**自然退出态 0**(UI 关机,非热键 SIGTERM 杀):waitpid 识别 WIFEXITED&&0 * → supervisor 执行真关机(sync + reboot(2) POWER_OFF),不卡最后一帧。 * 区分:热键切 MeowOS 的 SIGTERM 杀仍在 confirm 分支内 waitpid,不进自然退出检测。 * - mtd5(ota 标记分区) 变 ota:kernel2(播放器 UI 升级 → nandwrite 写 mtd5):supervisor * 主循环每 500ms 读一次 /dev/mtd5 头字节(nandwrite 在 offset 0 写 256B 定长标记, * 只读 ~15B,IO 极低),命中即判升级 → reboot(2) RESTART → uboot 读 mtd5=ota:kernel2 * 引导 kernel2(升级模式,自动刷 TF 卡 r3proii.upt)。不卡最后一帧。 * 说明:/proc/mtd 只反映分区表不变读,无法观察内容变化,故用轻量读 /dev/mtd5 轮询。 * 两根都写 bm_log(supervisor 接管 shutdown/upgrade)。 * * ============================================================================ * WHY (设计见 BOOT_SELECTOR_DESIGN.md §3) * ============================================================================ * - 开机最先显示选单(HibyOS / MeowOS),选择后 fork 播放器(HibyOS)或 * exec MeowOS 主屏(MeowOS);HibyOS 模式热键(电源+音量减)杀播放器接管。 * - 本二进制 = 选单本体(M0)。M0 只做二进制自检:SSH 推 /data 独立跑, * **不进启动链**(不碰 S90/S92/播放器/SSH 顺序)。 * * M0 自检目标(BOOT_SELECTOR_DESIGN.md §9 M0): * --dumpkeys N / --sim-select H|M / --boot-only / --version 验证 * fb0 初始化、绘制、选择/超时决策、CLI 钩子。**默认不 fork 播放器**—— * M0 严禁触碰到正在运行的 hiby_player(设备上它正由 init 拉起)。 * * ⚠️ M0 记号(BOOT_SELECTOR_DESIGN.md §2.3 播放器 fork 契约已按 §3.6 写好, * 但本阶段默认关闭):fork_hiby_player() 实现了「env -i 复刻 11 变量 + * chdir("/") + stdio 重定向 + execve /usr/bin/hiby_player」的完整契约, * 仅在传 --fork 时才会真正 exec。M2 接管 HibyOS 时由 S90 传 --fork 打开。 * * ⚠️ 字体限制:本设备的 5x7 点阵字体(与 meow_launcherd/hello 相同)只含 * ASCII(0-9 A-Z a-z 空格 . ! - _),**无 CJK 字形**。设计稿中的中文标签 * (如「[HIBYOS 官方播放器]」)无法用该字体渲染,故本版用 ASCII 标签 * ([HIBYOS PLAYER] / [MEOWOS HOME]),CJK 渲染留待后续引入 TTF 渲染器。 * * fb0 要点(ANALYSIS.md §18.18,勿写反): * FBIOGET_VSCREENINFO=0x4600 / FBIOGET_FSCREENINFO=0x4602 / FBIOPAN_DISPLAY=0x4606 * 本机 xres=480 yres=720 bpp=16 line=960 yres_virtual=1440(双缓冲)ypanstep=1 * mmap 整个虚拟帧缓冲(含两帧);画前 FBIOPAN_DISPLAY pan 到 yoffset=0(buffer0)。 * 显示哪个缓冲由 fb_var.yoffset 决定——播放器电源/息屏重绘可能把 yoffset 留在 * buffer1,选单画在 buffer0 而面板显示 buffer1 会「选单不可见/屏幕冻结」(v6.5 bug)。 * * 静态 MIPS32r2 musl(soft-float)编译: * ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -static meow_bootmenu.c -o meow_bootmenu */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* M2.29:supervisor 接管关机/升级,直连 reboot(2) syscall */ #include #include "meow_touch.h" /* ---- 本设备 fb ioctl 号(实测,勿写反)---- */ #define FBIOGET_VSCREENINFO 0x4600 #define FBIOGET_FSCREENINFO 0x4602 #define FBIOPAN_DISPLAY 0x4606 /* ---- 路径 ---- */ #define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER(116) */ #define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL(114/115) PLAY(164) */ #define DEV_TOUCH "/dev/input/event1" /* goodix-ts(未 grab,可共享读) */ #define FB_DEV "/dev/fb0" #define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs" #define CONF_FILE MEOWAPPS "/meow.conf" /* TF 卡配置:boot_timeout_ms/boot_menu/bg_color;boot_default 权威在板载 /data/meow_boot_default */ #define MODE_FILE "/tmp/meow_boot_mode" /* 开机模式标记(tmpfs,开机清空) */ #define PLAYER_BIN "/usr/bin/hiby_player" #define PLAYER_LOG "/usr/data/meow_player.log" #define LAUNCHER_BIN "/usr/bin/meow_launcherd" /* MeowOS 基础主屏(M2 接管) */ /* ---- 面板/背光(与 meow_launcherd 相同路径,§18.21/§红线)---- */ #ifndef FBIOBLANK #define FBIOBLANK 0x4611 #endif #define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness" #define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power" /* ---- 热键切换(HibyOS→MeowOS):按住电源 + 窗口内单击音量减 ---- */ #define HK_PWRWIN_MIN 60 /* 电源按下后 60ms 起 */ #define HK_PWRWIN_MAX 2500 /* 至 2500ms 止 */ #define KILL_GRACE_MS 3000 /* SIGTERM 后等待退出,超时 SIGKILL */ #define ARM_DELAY_MS 20000 /* 开机武装延迟(§18.23):播放器拉起后 N ms 内热键不触发,避媒体扫描期冻结 */ #define BUSY_STATE_CHAR 'D' /* 播放器忙判门:/proc//stat 态为 D(不可中断 IO=扫描)则暂不热键 */ #define SCAN_KEEP_MS 90000 /* Bug 4:HibyOS 开机媒体扫描窗(90s)内亮度保底,防黑屏几分钟(只写亮度不碰 blank) */ /* ---- key codes ---- */ #ifndef KEY_POWER #define KEY_POWER 116 #endif #ifndef KEY_VOLUMEDOWN #define KEY_VOLUMEDOWN 114 #endif #ifndef KEY_VOLUMEUP #define KEY_VOLUMEUP 115 #endif #ifndef KEY_PLAYPAUSE #define KEY_PLAYPAUSE 164 #endif /* ---- 事件号 / ioctl ---- */ #define BTN_TOUCH 0x14a #define ABS_X 0x00 #define ABS_Y 0x01 #define ABS_MT_POSITION_X 0x35 /* goodix 走 MT 协议 B,仅 ABS_X/Y 收不到坐标(M2.1 bug) */ #define ABS_MT_POSITION_Y 0x36 #ifndef EV_KEY #define EV_KEY 1 #endif #ifndef EV_ABS #define EV_ABS 3 #endif #ifndef EVIOCGKEY #define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len) #endif #define KEYBUF_WORDS 32 /* 1024 bits, 覆盖 KEY_MAX 767 */ /* ---- 编译默认值(可被 /usr/data/mnt/sd_0/MeowAPPs/meow.conf 覆写)---- */ #define DEF_BOOT_DEFAULT 1 /* 1=HibyOS 0=MeowOS */ #define DEF_BOOT_TIMEOUT_MS 5000 #define DEF_BOOT_MENU 1 #define DEF_BG 0x000000 #define POLL_MS 10 #define SIM_SELECT_DELAY_MS 2000 /* --sim-select 延迟后模拟选 */ #define GHOST_COOLDOWN_MS 800 /* 入口 drain + 短暂冷却防幽灵触摸(M2.1:缩短,不吞首次有效输入) */ #define MEOW_BOOTMENU_VER "0.1.0-m2.29-takeover" /* M2.29:supervisor 接管关机(poweroff)/升级(reboot) */ /* ---- 版式 ---- */ #define STATUS_H 40 #define ROW_H 52 #define ENTRY0_Y 176 #define ENTRY1_Y 228 #define FOOTER_Y 700 #define BOOTMENU_OS_VER_FILE "/etc/meowos_version" /* 版本号单点:与 launcher 同源读 /etc/meowos_version */ #define BOOTMENU_OS_VER "dev" /* 兜底:文件缺失/读失败时用(与 launcher MEOWOS_VERSION=dev 对齐) */ /* 右下角显示的 MeowOS 版本:启动时读 /etc/meowos_version(与 launcher 同一数据源), * 不再硬编码 0.7.x。读到的值存 g_boot_ver;缺失/读失败保留兜底默认。 */ static char g_boot_ver[32] = BOOTMENU_OS_VER; static void load_boot_ver(void) { FILE *f = fopen(BOOTMENU_OS_VER_FILE, "r"); if (!f) return; /* 缺失:保留兜底 default */ char buf[32]; if (fgets(buf, sizeof buf, f)) { char *e = buf + strlen(buf); while (e > buf && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ')) e--; *e = 0; if (buf[0]) snprintf(g_boot_ver, sizeof g_boot_ver, "%s", buf); } fclose(f); } /* ================================================================== */ /* fb0 状态 */ /* ================================================================== */ static unsigned char *fb = NULL; static int fb_fd = -1; static int fb_xres = 480, fb_yres = 720, fb_line = 960, fb_bpp = 16; static unsigned int fb_yres_virtual = 0; static unsigned int fb_active_yoffset = 0; static unsigned char *fb_snapshot = NULL; /* 进入选单前快照(恢复播放器帧用) */ static size_t fb_snapshot_len = 0; static int fb_orig_yoffset = -1; /* 触摸状态(选单/确认框/--dumpkeys 共用;M2.26 迁到 meow_touch 统一接口: * goodix 时序免疫——BTN_TOUCH=1 不锁原点,等按下后首个有效 ABS 才锁,位移>=65 判滑动)。 */ static meow_touch mt_touch; static int touch_fd = -1; static inline void put_px(int x, int y, uint32_t color) { if (x < 0 || x >= fb_xres || y < 0 || y >= fb_yres) return; unsigned char *p = fb + (size_t)y * fb_line + x * (fb_bpp / 8); if (fb_bpp == 16) { uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) | ((color >> 3) & 0x001F); *(uint16_t *)p = c; } else { *(uint32_t *)p = (color << 8) | 0xFF; } } static void fill_rect(int x0, int y0, int w, int h, uint32_t color) { for (int y = y0; y < y0 + h; y++) for (int x = x0; x < x0 + w; x++) put_px(x, y, color); } /* 5x7 点阵(与 meow_launcherd/hello 相同;仅 ASCII) */ static const unsigned char font[][8] = { {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00}, {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00}, {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00}, {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00}, {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00}, {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00}, {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00}, {0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00}, {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00}, {0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00}, {0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00}, {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00}, {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00}, {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00}, {0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00}, {0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00}, {0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00}, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00}, {0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00}, {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, }; static int glyph_index(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'Z') return 10 + (c - 'A'); if (c >= 'a' && c <= 'z') return 10 + (c - 'a'); if (c == ' ') return 36; if (c == '.') return 37; if (c == '!') return 38; if (c == '-' || c == '_') return 39; return 36; } static void draw_glyph(int x, int y, char c, int scale, uint32_t color) { const unsigned char *g = font[glyph_index(c)]; for (int row = 0; row < 7; row++) { unsigned char bits = g[row]; for (int col = 0; col < 5; col++) { if (bits & (0x10 >> col)) { for (int dy = 0; dy < scale; dy++) for (int dx = 0; dx < scale; dx++) put_px(x + col * scale + dx, y + row * scale + dy, color); } } } } static void draw_text(int x, int y, const char *s, int scale, uint32_t color) { while (*s) { draw_glyph(x, y, *s, scale, color); x += 6 * scale; s++; } } /* ================================================================== */ /* fb0 初始化 / 双缓冲 pan(§18.18) */ /* ================================================================== */ static int fb_open(void) { int fd = open(FB_DEV, O_RDWR); if (fd < 0) { fprintf(stderr, "fb open failed: %s\n", strerror(errno)); return -1; } unsigned char vinfo[160], finfo[68]; memset(vinfo, 0, sizeof vinfo); memset(finfo, 0, sizeof finfo); if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 || ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) { fprintf(stderr, "fb ioctl failed\n"); close(fd); return -1; } fb_xres = *(uint32_t *)(vinfo + 0); fb_yres = *(uint32_t *)(vinfo + 4); fb_yres_virtual = *(uint32_t *)(vinfo + 12); fb_active_yoffset = *(uint32_t *)(vinfo + 20); fb_bpp = *(uint32_t *)(vinfo + 24); fb_line = *(uint32_t *)(finfo + 44); if (!fb_line) fb_line = fb_xres * (fb_bpp / 8); if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* 单缓冲兜底 */ fb = mmap(NULL, (size_t)fb_line * fb_yres_virtual, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (fb == MAP_FAILED) { fprintf(stderr, "fb mmap failed: %s\n", strerror(errno)); close(fd); return -1; } fb_fd = fd; fprintf(stderr, "fb: %dx%d bpp=%d line=%d vres=%u yoffset=%u\n", fb_xres, fb_yres, fb_bpp, fb_line, fb_yres_virtual, fb_active_yoffset); return 0; } /* 选单进入前快照「当前显示缓冲」一帧(播放器事件驱动不主动重绘,须恢复) */ static void fb_snapshot_player(void) { if (fb_yres_virtual <= fb_yres) { fb_orig_yoffset = -1; return; } if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0; fb_orig_yoffset = (int)fb_active_yoffset; fb_snapshot_len = (size_t)fb_line * fb_yres; fb_snapshot = malloc(fb_snapshot_len); if (!fb_snapshot) { fb_orig_yoffset = -1; fb_snapshot_len = 0; return; } memcpy(fb_snapshot, fb + (size_t)fb_active_yoffset * fb_line, fb_snapshot_len); } /* 画前 FBIOPAN_DISPLAY pan 到 yoffset=0(buffer0,选单/app 共同绘制目标) */ static void fb_pan_buf0(void) { if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; } if (fb_active_yoffset == 0) return; /* 已显示 buffer0 */ unsigned char v[160]; memset(v, 0, sizeof v); if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return; *(uint32_t *)(v + 16) = 0; /* xoffset */ *(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */ ioctl(fb_fd, FBIOPAN_DISPLAY, v); fb_active_yoffset = 0; } /* 选单退出:把快照拷回原缓冲并 pan 回原 yoffset(播放器帧保持不动) */ static void fb_restore_player_frame(void) { int restore_y = fb_orig_yoffset; if (fb_fd >= 0 && restore_y >= 0 && fb_snapshot) { memcpy(fb + (size_t)restore_y * fb_line, fb_snapshot, fb_snapshot_len); unsigned char v[160]; memset(v, 0, sizeof v); if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) == 0) { *(uint32_t *)(v + 16) = 0; *(uint32_t *)(v + 20) = (uint32_t)restore_y; ioctl(fb_fd, FBIOPAN_DISPLAY, v); } fprintf(stderr, "bootmenu: fb restore -> yoffset=%d\n", restore_y); } if (fb_snapshot) { free(fb_snapshot); fb_snapshot = NULL; } fb_orig_yoffset = -1; fb_snapshot_len = 0; } static void fb_close(void) { if (fb && fb != MAP_FAILED) munmap(fb, (size_t)fb_line * fb_yres_virtual); if (fb_fd >= 0) close(fb_fd); fb = NULL; fb_fd = -1; } /* ================================================================== */ /* 状态栏数据源(与 meow_launcherd 相同:电量/WIFI/BT/音量) */ /* ================================================================== */ static int read_file_int(const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; char buf[16]; int n = read(fd, buf, sizeof buf - 1); close(fd); if (n <= 0) return -1; buf[n] = 0; return atoi(buf); } static int bluetooth_present(void) { return access("/sys/class/bluetooth/hci0", F_OK) == 0; } /* 音量:CS43198 ALSA control(实测死镜像,可能恒 0%,见 §18.13) */ static int amixer_vol(int numid) { char cmd[96]; snprintf(cmd, sizeof cmd, "amixer -c 0 cget numid=%d 2>/dev/null", numid); FILE *p = popen(cmd, "r"); if (!p) return -1; char buf[128]; int v = -1; while (fgets(buf, sizeof buf, p)) { char *s = strstr(buf, ": values="); if (s) { v = atoi(s + 9); break; } } pclose(p); return v; } /* 28px 状态栏:行1=时钟(左) 电量% WIFI BT(右),行2=VOL nn%(左),y=28 分隔线(40→28,用户定) */ static void draw_statusbar(uint32_t bg) { char timebuf[16], right[48]; time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); right[0] = 0; char batt[8] = ""; int b = read_file_int("/sys/class/power_supply/battery/capacity"); if (b >= 0) snprintf(batt, sizeof batt, "%d%%", b); char wifi[8] = ""; int wf = open("/sys/class/net/wlan0/operstate", O_RDONLY); if (wf >= 0) { char x[8]; int n = read(wf, x, sizeof x - 1); close(wf); if (n > 0) { x[n] = 0; if (!strncmp(x, "up", 2)) snprintf(wifi, sizeof wifi, "WIFI"); } } char vol[16] = ""; int v = amixer_vol(1); if (v >= 0) snprintf(vol, sizeof vol, "VOL %d%%", v * 100 / 255); char bt[8] = ""; if (bluetooth_present()) snprintf(bt, sizeof bt, "BT"); draw_text(8, 2, timebuf, 1, 0x9FB8D9); if (batt[0]) strcat(right, batt); if (wifi[0]) { if (right[0]) strcat(right, " "); strcat(right, wifi); } if (bt[0]) { if (right[0]) strcat(right, " "); strcat(right, bt); } if (right[0]) { int rx = fb_xres - 8 - (int)strlen(right) * 6; draw_text(rx, 2, right, 1, 0x9FB8D9); } if (vol[0]) draw_text(8, 14, vol, 1, 0x9FB8D9); fill_rect(0, 28, fb_xres, 1, 0x333355); /* 分隔线(移至 y=28) */ } /* 只刷时钟数字(每秒一次,不弹 amixer/不碰菜单体) */ static void draw_clock(uint32_t bg) { char timebuf[16]; time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); int w = (int)strlen(timebuf) * 6; fill_rect(4, 0, 8 + w, 12, bg); draw_text(8, 2, timebuf, 1, 0x9FB8D9); } /* ================================================================== */ /* 配置:boot_default / boot_timeout_ms / boot_menu / bg_color */ /* boot_default:权威 = 板载 /data/meow_boot_default(O_TRUNC 单值、不写 TF 卡); * meow.conf 的 boot_default= 仅作无板载标记时的兜底(不新增写入,见 set_boot_default_meow)。 */ /* ================================================================== */ static int cfg_boot_default = DEF_BOOT_DEFAULT; static long cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS; static int cfg_boot_menu = DEF_BOOT_MENU; static uint32_t cfg_bg = DEF_BG; /* 读板载 /data/meow_boot_default(开机默认权威标记)。返回 0/1;-1=不存在/非法。 */ static int read_boot_default_onboard(void) { int fd = open("/data/meow_boot_default", O_RDONLY); if (fd < 0) return -1; char buf[8]; int n = read(fd, buf, sizeof buf - 1); close(fd); if (n <= 0) return -1; buf[n] = 0; int v = atoi(buf); return (v == 0 || v == 1) ? v : -1; } static void load_config(void) { cfg_boot_default = DEF_BOOT_DEFAULT; cfg_boot_timeout_ms = DEF_BOOT_TIMEOUT_MS; cfg_boot_menu = DEF_BOOT_MENU; cfg_bg = DEF_BG; int has_conf = 1; FILE *f = fopen(CONF_FILE, "r"); if (!f) { fprintf(stderr, "bootmenu: no %s, using defaults\n", CONF_FILE); has_conf = 0; } if (has_conf) { char buf[128]; while (fgets(buf, sizeof buf, f)) { if (!strncmp(buf, "boot_default=", 13)) { /* 板载 /data/meow_boot_default 为权威;此处仅作无板载标记时的兜底(不再新增写入)。 */ cfg_boot_default = atoi(buf + 13); if (cfg_boot_default != 0 && cfg_boot_default != 1) cfg_boot_default = DEF_BOOT_DEFAULT; } else if (!strncmp(buf, "boot_timeout_ms=", 16)) { long t = atol(buf + 16); if (t >= 500 && t <= 60000) cfg_boot_timeout_ms = t; } else if (!strncmp(buf, "boot_menu=", 10)) { int m = atoi(buf + 10); if (m == 0 || m == 1) cfg_boot_menu = m; } else if (!strncmp(buf, "bg_color=", 9)) { unsigned int v; if (sscanf(buf + 9, "%x", &v) == 1) cfg_bg = v & 0xFFFFFF; } } fclose(f); } /* 板载标记优先(权威):覆盖 meow.conf 兜底/编译默认。 */ int ob = read_boot_default_onboard(); if (ob == 0 || ob == 1) { if (ob != cfg_boot_default) fprintf(stderr, "bootmenu: boot_default onboard=%d (override %d)\n", ob, cfg_boot_default); cfg_boot_default = ob; } fprintf(stderr, "bootmenu: cfg boot_default=%d timeout_ms=%ld boot_menu=%d bg=%06x\n", cfg_boot_default, cfg_boot_timeout_ms, cfg_boot_menu, cfg_bg); } /* ================================================================== */ /* 时钟(单调,强收敛) */ /* ================================================================== */ static unsigned long long now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } /* ================================================================== */ /* TF 卡日志(bm_log)——诊断留存:supervisor 收到/发出信号、播放器退出原因。 * - 借鉴 launcher log_open_card/logmsg 思路,但**不共享 /tmp/meow_bootts** * (launcher 缓存的是完整基名 meow_,直接复读会拼成 meow_boot_meow_…); * 每次自行按 /proc/uptime 推开机时间戳,同一 boot 内文件名恒定。 * - 写 /usr/data/mnt/sd_0/MeowOS/logs/meow_boot_.log(O_APPEND|O_CREAT 0644)。 * - 卡不可用(未挂载/不可写)→ 降级 stderr。**只记录,不改任何行为。** * ================================================================== */ #define BM_LOG_DIR "/usr/data/mnt/sd_0/MeowOS/logs" static int bm_log_fd = -1; static char bm_log_file[256] = ""; static void bm_boot_ts(char *out, size_t n) { time_t now = time(NULL), up = 0; int fd = open("/proc/uptime", O_RDONLY); if (fd >= 0) { char buf[64]; int r = read(fd, buf, sizeof buf - 1); close(fd); if (r > 0) { buf[r] = 0; char *p = strchr(buf, ' '); if (p) *p = 0; up = (time_t)atof(buf); } } time_t boot = (time_t)((double)now - (double)up + 0.5); struct tm tm; localtime_r(&boot, &tm); strftime(out, n, "%Y%m%d_%H%M%S", &tm); } static void bm_log_open(void) { if (bm_log_fd >= 0) return; mkdir(BM_LOG_DIR, 0755); char ts[64]; bm_boot_ts(ts, sizeof ts); snprintf(bm_log_file, sizeof bm_log_file, "%s/meow_boot_%s.log", BM_LOG_DIR, ts); bm_log_fd = open(bm_log_file, O_WRONLY | O_APPEND | O_CREAT, 0644); if (bm_log_fd < 0) bm_log_file[0] = 0; /* 卡不可用 → 降级 stderr */ } /* 带时间戳写 TF 卡日志;卡不可用则降级 stderr。仅记录,不改行为。 */ static void bm_log(const char *fmt, ...) { char buf[512]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof buf, fmt, ap); va_end(ap); char line[576]; int l = snprintf(line, sizeof line, "%llu %s\n", now_ms(), buf); if (bm_log_fd < 0) bm_log_open(); if (bm_log_fd >= 0) { if (write(bm_log_fd, line, l) < 0) bm_log_fd = -1; /* 卡拔出 → 回退 stderr */ } fprintf(stderr, "%llu %s\n", now_ms(), buf); } /* ---- supervisor 信号记录(只记录,不改行为)---- * 注册 SIGTERM/SIGINT/SIGQUIT/SIGHUP,handler 只置标志;主循环顶部先 bm_log * 「收到信号 N」,再 signal(s, SIG_DFL) + raise(s) 重抛——**恢复默认动作**, * 使 supervisor 仍按今日方式终止(不转发、不善后、不截断,与未加 hook 一致)。 */ static volatile sig_atomic_t bm_sig_flag[64]; static const char *bm_sig_name(int sig) { switch (sig) { case SIGTERM: return "SIGTERM"; case SIGKILL: return "SIGKILL"; case SIGSTOP: return "SIGSTOP"; case SIGCONT: return "SIGCONT"; case SIGINT: return "SIGINT"; case SIGQUIT: return "SIGQUIT"; case SIGHUP: return "SIGHUP"; default: return "?"; } } static void bm_sig_catch(int sig) { if (sig > 0 && sig < 64) bm_sig_flag[sig] = 1; } static void bm_sig_install(void) { signal(SIGTERM, bm_sig_catch); signal(SIGINT, bm_sig_catch); signal(SIGQUIT, bm_sig_catch); signal(SIGHUP, bm_sig_catch); } /* 主循环顶部调用:记录已收到的信号,并恢复默认动作重抛(保行为不变)。 */ static void bm_sig_poll(void) { for (int s = 1; s < 64; s++) { if (bm_sig_flag[s]) { bm_sig_flag[s] = 0; bm_log("supervisor: received signal %d (%s)", s, bm_sig_name(s)); signal(s, SIG_DFL); /* 恢复默认动作(== 未加 hook 时) */ raise(s); /* 重抛:已登记即按原方式终止 */ break; } } } /* ---- M2.29 supervisor 接管:直连 reboot(2) syscall + mtd5(ota) 侦测 ---- * 借鉴 meow_launcherd sys_do_reboot(BusyBox reboot 非 PID1 不完成握手,故直接 syscall)。 * sync() 落盘后调用 reboot(2),内核立即重启/关机;正常不返回,防御性死循环。 */ #define LINUX_REBOOT_MAGIC1 0xfee1dead #define LINUX_REBOOT_MAGIC2 672274793 /* 0x28121969 */ #define LINUX_REBOOT_CMD_RESTART 0x01234567 #define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc static void bm_do_reboot(int cmd) { sync(); bm_log("supervisor: bm_do_reboot cmd=0x%x", cmd); syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, NULL); bm_log("supervisor: bm_do_reboot returned: %s", strerror(errno)); for (;;) usleep(1000); /* 防御:永不返回 */ } /* mtd5 是 ota 标记分区(8M)。bootmode.sh Recovery 用 nandwrite 在 offset 0 写 256B * 定长标记。这里每 500ms 读一次 mtd5 头 ~15B(单页一小读,IO 极低),命中 * "ota:kernel2" 即判「播放器触发升级」。open/close 每次都关,避免长期占 mtd 设备。 */ #define MTD_OTA "/dev/mtd5" static int mtd_ota_is_upgrade(void) { int fd = open(MTD_OTA, O_RDONLY); if (fd < 0) return 0; /* 读不到 → 无升级迹象 */ char buf[16]; memset(buf, 0, sizeof buf); ssize_t n = read(fd, buf, sizeof buf - 1); close(fd); if (n <= 0) return 0; return strstr(buf, "ota:kernel2") != NULL; } /* ================================================================== */ /* 按键状态轮询(EVIOCGKEY) */ /* ================================================================== */ struct keypoll { int fd; unsigned long cur[KEYBUF_WORDS]; unsigned long prev[KEYBUF_WORDS]; int ok; }; static int kp_open(struct keypoll *kp, const char *dev) { memset(kp, 0, sizeof *kp); kp->fd = open(dev, O_RDONLY | O_NONBLOCK); if (kp->fd < 0) { fprintf(stderr, "open %s: %s\n", dev, strerror(errno)); return -1; } if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) { fprintf(stderr, "EVIOCGKEY %s: %s\n", dev, strerror(errno)); close(kp->fd); return -1; } memcpy(kp->prev, kp->cur, sizeof kp->cur); kp->ok = 1; return 0; } /* 轮询设备键状态;对每个「按下边沿/抬起边沿」调 cb(code, down, ctx)。 * 返回是否有边沿。 */ static int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx), void *ctx) { if (!kp->ok) return 0; if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) return 0; int changed = 0; for (int w = 0; w < KEYBUF_WORDS; w++) { if (kp->cur[w] == kp->prev[w]) continue; for (int b = 0; b < 32; b++) { int c = (kp->cur[w] >> b) & 1; int p = (kp->prev[w] >> b) & 1; if (c != p) { int code = w * 32 + b; changed = 1; if (cb) cb(code, c, ctx); } } kp->prev[w] = kp->cur[w]; } return changed; } static int kp_is_down(const struct keypoll *k, int code) { return (k->cur[code / 32] >> (code % 32)) & 1; } /* ================================================================== */ /* 选单绘制 */ /* ================================================================== */ static void draw_countdown(uint32_t bg, long remaining_ms) { /* 右侧倒计时 'AUTO N S'(scale2) */ int sec = (int)((remaining_ms + 999) / 1000); char s[16]; snprintf(s, sizeof s, "AUTO %dS", sec); int w = (int)strlen(s) * 12; /* scale2 = 12px/字 */ int x = fb_xres - 8 - w; /* 清一下原区域(含右边距)再画 */ fill_rect(x - 4, 92, w + 12, 16, bg); draw_text(x, 94, s, 2, 0xFFE066); } static void draw_entry(int y, const char *label, int selected) { if (selected) { fill_rect(8, y - 4, fb_xres - 16, ROW_H - 8, 0x2A3A6A); draw_text(24, y, label, 2, 0xFFFFFF); } else { draw_text(24, y, label, 2, 0xC8D0E8); } } static void draw_menu(int cursor, long remaining_ms) { fill_rect(0, 0, fb_xres, fb_yres, cfg_bg); /* 状态栏 + 标题 + 分隔线 */ draw_statusbar(cfg_bg); draw_text(20, 64, "MEOWOS", 3, 0xFFE066); draw_text(22, 96, "BOOT SELECTOR", 2, 0x9FB8D9); fill_rect(0, 122, fb_xres, 2, 0x3A3A5C); /* 两个入口 */ draw_entry(ENTRY0_Y, "[HIBYOS PLAYER]", cursor == 0); draw_entry(ENTRY1_Y, "[MEOWOS HOME]", cursor == 1); /* 右侧倒计时 */ draw_countdown(cfg_bg, remaining_ms); /* 底部按键提示 */ draw_text(20, FOOTER_Y, "VOLUP UP VOLDN DOWN PLAY OK POWER OK", 1, 0x666688); /* 右下角开发版本 */ { char ver[40]; snprintf(ver, sizeof ver, "MeowOS v%s boot", g_boot_ver); draw_text(fb_xres - 8 - (int)strlen(ver) * 6, FOOTER_Y, ver, 1, 0x666688); } } /* ================================================================== */ /* 面板/背光 helper(复用 meow_launcherd 语义,§18.21.5 / §红线) */ /* ================================================================== */ static int read_brightness(void) { int fd = open(BACKLIGHT, O_RDONLY); if (fd < 0) return -1; char buf[16]; int n = read(fd, buf, sizeof buf - 1); close(fd); if (n <= 0) return -1; buf[n] = 0; return atoi(buf); } static void write_file_str(const char *path, const char *val) { int fd = open(path, O_WRONLY); if (fd < 0) return; if (write(fd, val, strlen(val)) < 0) fprintf(stderr, "bootmenu: write %s <- %s: %s\n", path, val, strerror(errno)); close(fd); } static void write_brightness(int val) { char buf[8]; int l = snprintf(buf, sizeof buf, "%d\n", val); int fd = open(BACKLIGHT, O_WRONLY); if (fd < 0) { fprintf(stderr, "bootmenu: backlight open fail: %s\n", strerror(errno)); return; } if (write(fd, buf, l) < 0) fprintf(stderr, "bootmenu: backlight write %d: %s\n", val, strerror(errno)); close(fd); } /* 面板 unblank(FBIOBLANK 0)。仅用于杀播放器后把屏点亮,以便 MeowOS 接管; * 播放器已死,故不涉及「播放器 state machine」一致性(§18.11 关注点)。 * bootmenu 选单/自检阶段绝不调用本函数(§18.24 纪律:不碰 panel blank)。 */ static void unblank_panel(void) { int fbfd = open(FB_DEV, O_RDWR); if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } fprintf(stderr, "bootmenu: panel unblanked\n"); } /* 读取 /proc//stat 状态字符('S'/'T'/'D'...),失败返回 '\0'。 */ static char proc_state(int pid) { char path[64], line[512]; snprintf(path, sizeof path, "/proc/%d/stat", pid); int fd = open(path, O_RDONLY); if (fd < 0) return '\0'; ssize_t n = read(fd, line, sizeof line - 1); close(fd); if (n <= 0) return '\0'; line[n] = 0; char *p = strrchr(line, ')'); if (!p || p + 2 >= line + n) return '\0'; return p[2]; } /* 读取 /proc//stat 状态是否为 T(stopped)。T 态 SIGTERM 不投递, * 须先 SIGCONT(§18.21.3)。 */ static int proc_is_stopped(int pid) { return proc_state(pid) == 'T'; } /* 播放器忙判门(§18.23):D(不可中断 IO = 开机媒体库扫描)视为忙。 * 忙期热键暂不触发,避免扫描期 SIGSTOP 冻结拉长卡顿。 */ static int proc_is_busy(int pid) { return proc_state(pid) == BUSY_STATE_CHAR; } /* ================================================================== */ /* 播放器 fork 契约(顺序:先 munmap+close fb0,再 fork) */ /* ⚠️ M0 记号:完整契约已按 BOOT_SELECTOR_DESIGN.md §3.6 实现,但仅在 */ /* 显式 --fork 时执行。M0 自检不传 --fork,故绝不 fork 正在运行的 */ /* hiby_player。M2 接管 HibyOS 时由 S90meow_bootmenu 传 --fork 开启。 */ /* ================================================================== */ extern char **environ; static void write_mode(int choice); /* 定义在下方;supervisor 使用 */ static void bootmenu_supervisor(int player_pid); /* 定义在下方;fork 函数调用 */ /* ---- 真机 verif sim 钩子(M2):--sim-hotkey N / --sim-confirm Y|N ---- */ static long sim_hotkey_ms = 0; /* supervisor 启动后 N ms 模拟热键(电源按住+音量减) */ static int sim_confirm = 0; /* confirm 框打开后 2s 自动回答:'Y'=是 'N'=否 */ static int sim_confirm_done = 0; static int sim_hk_done = 0; static int sim_hotkey_strict = 0; /* M2.14:--sim-hotkey-strict = 让 sim 热键走武装延迟/忙判门(测 pending) */ static void fork_hiby_player(void) { pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "fork player failed: %s\n", strerror(errno)); return; } if (pid == 0) { /* child:完全复刻 init 拉起时的环境(§18.21.5 最小启动契约) */ chdir("/"); /* 必须:播放器有相对路径读(充电态等) */ clearenv(); setenv("USER", "root", 1); setenv("SHLVL", "3", 1); setenv("HOME", "/", 1); setenv("PS1", "#", 1); setenv("TERM", "vt102", 1); setenv("PATH", "/bin:/sbin:/usr/bin:/usr/sbin", 1); setenv("env_adb_device_name_prefix", "ingenic", 1); setenv("env_adb_device_use_diffrent_name", "y", 1); setenv("SHELL", "/bin/sh", 1); setenv("PWD", "/", 1); setenv("EDITOR", "/bin/vi", 1); /* 无 LD_LIBRARY_PATH / 无 TZ(与开机一致) */ int dn = open("/dev/null", O_RDONLY); if (dn >= 0) { dup2(dn, 0); close(dn); } int lf = open(PLAYER_LOG, O_WRONLY | O_CREAT | O_APPEND, 0644); if (lf >= 0) { dup2(lf, 1); dup2(lf, 2); close(lf); } execve(PLAYER_BIN, (char *[]){ "hiby_player", NULL }, environ); _exit(127); } /* parent(M2):退居监督者/热键守护——poll(10ms) 检测热键(按住电源 + * 窗口 60-2500ms 内单击音量减)→ SIGTERM 播放器 → 3s 未退 SIGKILL * (T 态先 SIGCONT)→ waitpid 收割 → unblank+亮度恢复 → 写 * /tmp/meow_boot_mode=meow → exec meow_launcherd(MeowOS 基础主屏)。 * 播放器异常退出(waitpid WNOHANG)→ 记日志,阶段 0 不自动重启(HGL)。 * 本函数在父进程永不返回(阻塞 rcS 直到整机重启或热键切换)。 */ bootmenu_supervisor(pid); /* fork 失败已在上面 return;此处不可达(supervisor 永不返回), * 仅防御:若 supervisor 异常返回则让 rcS 推进(S92 兜底)。 */ return; } /* ---- 热键确认框(M2 用户定案:热键后先确认,是才杀播放器)---- */ #define CONF_DEFAULT_CURSOR 1 /* 默认 NO(更安全:不误杀) */ #define CONF_COOLDOWN_MS 800 /* 防 goodix 幽灵触摸(§18.16);M2.1 缩短,不吞首次有效输入 */ #define CONF_ROW0_Y 280 #define CONF_ROW1_Y 330 #define CONF_ROW_H 44 #define CONF_ROW_LEFT 60 #define CONF_ROW_RIGHT (fb_xres - 60) /* 简单绘制:压暗底 + 外框 + 标题 + [YES][NO] + 按键提示。 * 字体仅 ASCII(无 CJK),故用 ENTER MEOWOS? / STOP HIBYOS PLAYER。 */ static void draw_confirm_box(int cursor) { fill_rect(0, 0, fb_xres, fb_yres, 0x0D0D14); /* 压暗背景 */ fill_rect(36, 180, fb_xres - 72, 230, 0x3A3A5C); /* 外框 */ fill_rect(40, 184, fb_xres - 80, 222, 0x1A1A2A); /* 内底 */ draw_text(56, 200, "STOP HIBYOS PLAYER", 2, 0xFFE066); draw_text(56, 232, "AND ENTER MEOWOS?", 2, 0x9FB8D9); /* YES / NO 两行 */ if (cursor == 0) { fill_rect(CONF_ROW_LEFT, CONF_ROW0_Y, CONF_ROW_RIGHT - CONF_ROW_LEFT, CONF_ROW_H, 0x2A3A6A); draw_text(CONF_ROW_LEFT + 30, CONF_ROW0_Y + 12, "YES", 2, 0xFFFFFF); } else { draw_text(CONF_ROW_LEFT + 30, CONF_ROW0_Y + 12, "YES", 2, 0x667088); } if (cursor == 1) { fill_rect(CONF_ROW_LEFT, CONF_ROW1_Y, CONF_ROW_RIGHT - CONF_ROW_LEFT, CONF_ROW_H, 0x2A3A6A); draw_text(CONF_ROW_LEFT + 30, CONF_ROW1_Y + 12, "NO", 2, 0xFFFFFF); } else { draw_text(CONF_ROW_LEFT + 30, CONF_ROW1_Y + 12, "NO", 2, 0x667088); } draw_text(56, CONF_ROW1_Y + CONF_ROW_H + 20, "VOL^V NAV PLAY OK POWER EXIT", 1, 0x666688); } /* 弹确认框并等待选择。返回 0=否(留下) 1=是(杀播放器) -1=无法绘制/异常。 * 调用方负责:先 SIGSTOP 播放器(防其重绘覆盖)+ unblank/亮度点亮面板; * 返回后调用方按结果处理面板/播放器。本函数只负责 fb0 叠框 + 输入。 */ static int confirm_switch(void) { if (fb_open() != 0) { fprintf(stderr, "confirm: fb open failed -> default NO\n"); return -1; } fb_snapshot_player(); fb_pan_buf0(); /* touch drain + 短暂冷却(防 goodix 幽灵触摸;M2.1 缩短到 800ms 不吞首次有效输入) */ int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } } unsigned long long cooldown_until = now_ms() + CONF_COOLDOWN_MS; mt_reset(&mt_touch); /* 清触摸手势态(down/valid/origin/last),避免上次残留误派发 */ struct keypoll kp_keys, kp_power; if (kp_open(&kp_keys, DEV_KEYS) != 0) fprintf(stderr, "confirm: WARN no keys dev\n"); if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "confirm: WARN no power dev\n"); int cursor = CONF_DEFAULT_CURSOR; int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0; /* 键基线:把「当前已按住的键」写进 prev,避免热键残留的电源按住被误判为 * 新的下沿而立即取消(电源仍按住,kp.poll 的 prev 已同步,但本地 prev_pwr * 未同步——launcher run_menu 同款处理,§18.17)。 */ if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL); if (kp_is_down(&kp_keys, KEY_VOLUMEDOWN)) prev_vold = 1; if (kp_is_down(&kp_keys, KEY_VOLUMEUP)) prev_volu = 1; if (kp_is_down(&kp_keys, KEY_PLAYPAUSE)) prev_play = 1; } if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); if (kp_is_down(&kp_power, KEY_POWER)) prev_pwr = 1; } draw_confirm_box(cursor); fprintf(stderr, "confirm: box shown (default cursor %d)\n", cursor); int result = -1; unsigned long long box_t0 = now_ms(); for (;;) { /* --sim-confirm Y|N:框打开 2s 后自动回答 */ if (sim_confirm && !sim_confirm_done && now_ms() - box_t0 >= 2000) { sim_confirm_done = 1; result = (sim_confirm == 'Y') ? 1 : 0; fprintf(stderr, "confirm: SIM answer %c -> %s\n", sim_confirm, result == 1 ? "YES" : "NO"); break; } int vold = 0, volu = 0, play = 0, pwr = 0; if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL); vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); } if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); } if (vold && !prev_vold) { cursor = (cursor + 1) % 2; draw_confirm_box(cursor); } if (volu && !prev_volu) { cursor = (cursor - 1 + 2) % 2; draw_confirm_box(cursor); } if (play && !prev_play) { result = (cursor == 0) ? 1 : 0; break; } if (pwr && !prev_pwr) { result = 0; break; } /* 电源=取消=否 */ prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr; if (tfd >= 0) { struct pollfd pfd = { tfd, POLLIN, 0 }; if (poll(&pfd, 1, 0) > 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) { 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) mt_abs(&mt_touch, val, mt_touch.last_y); else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt_touch, mt_touch.last_x, val); } else if (type == EV_KEY && code == BTN_TOUCH) { if (val) { /* 按压边沿:接受但**不锁原点**——等按下后首个有效 ABS 才锁(goodix 时序免疫) */ if (now_ms() < cooldown_until) mt_reset(&mt_touch); /* 幽灵/冷却:丢弃本次 */ else mt_down(&mt_touch); } else { /* 抬起边沿:按按下原点派发;滑动/未锁到坐标 → mt_up 返回 0,不命中、不崩溃 */ int dx = 0, dy = 0; if (mt_up(&mt_touch, &dx, &dy)) { int ty = mt_touch.down_y; if (ty >= CONF_ROW0_Y && ty < CONF_ROW0_Y + CONF_ROW_H) { result = 1; break; } else if (ty >= CONF_ROW1_Y && ty < CONF_ROW1_Y + CONF_ROW_H) { result = 0; break; } } } } } if (result >= 0) break; } } usleep(POLL_MS * 1000); } if (tfd >= 0) close(tfd); fb_restore_player_frame(); fb_close(); fprintf(stderr, "confirm: result %s\n", result == 1 ? "YES (kill player)" : result == 0 ? "NO (stay HibyOS)" : "unknown"); return result; } /* ---- HibyOS 分支:父进程监督者/热键守护(M2)---- */ static void bootmenu_supervisor(int player_pid) { /* 记录开机后的正常亮度,供「杀播放器后亮度==0 则恢复 saved/60」。 * 此刻播放器刚拉起,可能已写入其背光值;过早读到 0 则回退默认 60。 */ int saved_brightness = read_brightness(); /* M2.9 Bug 3:开机初期播放器启动画面会把背光设为瞬时高值(本机 max=101), * 若直接以其为「保底亮度」,会在扫描窗首次写回时跳一次最高亮度(开机瞬间闪一下)。 * 只信任稳定读数:读到 0 或异常高(>稳定上限 60,适中非最大)一律回退稳定值 60, * 确保扫描期写回平滑(只写目标一次、不跳 max)。 */ if (saved_brightness <= 0 || saved_brightness > 60) saved_brightness = 60; fprintf(stderr, "supervisor: player pid=%d, saved brightness=%d, monitoring...\n", player_pid, saved_brightness); /* 信号记录 hook(M2 诊断):先装 handler、start 一行写入 TF 卡日志。 * 只记录,不改行为(handler 仅置标志,主循环 log 后恢复默认重抛)。 */ bm_sig_install(); bm_log("supervisor: start pid=%d saved_brightness=%d mode=hiby ver=%s", player_pid, saved_brightness, MEOW_BOOTMENU_VER); /* Bug 4:HibyOS 开机媒体扫描窗内做亮度保底。播放器忙(媒体扫描)时其背光 * 待机会把亮度降到 0、面板 blank → 黑屏几分钟。supervisor 在扫描窗内若读到 * 亮度 0,写回保底亮度(saved 或 60)。**只写 brightness、绝不碰 FBIOBLANK** * (不抢播放器的面板归属语义,参考 §18.30 面板归属)。扫描窗结束即放手。 */ unsigned long long scan_keep_until = now_ms() + SCAN_KEEP_MS; unsigned long long last_bri_floor_at = 0; /* 亮度保底节流:≥400ms 才再写一次 */ struct keypoll kp_power, kp_keys; if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "supervisor: WARN no power dev\n"); if (kp_open(&kp_keys, DEV_KEYS) != 0) fprintf(stderr, "supervisor: WARN no keys dev\n"); int prev_pwr = 0, prev_vold = 0; int pwr_hold_armed = 0; unsigned long long pwr_down_at = 0; int player_alive = 1; int hotkey_pending = 0; /* M2.14:热键在武装延迟/播放器忙时置 pending,条件满足后自动弹框 */ unsigned long long sim_t0 = now_ms(); /* --sim-hotkey: supervisor 启动计时 */ unsigned long long last_mtd5_check = 0; /* T2 升级接管:mtd5 轮询节流 */ for (;;) { unsigned long long t = now_ms(); /* 0) 记录 supervisor 收到的信号(SIGTERM/SIGINT/SIGQUIT/SIGHUP)。 * 只记录后恢复默认重抛——不改行为(仍按今日方式终止)。 */ bm_sig_poll(); /* 0b) T2 升级接管:每 500ms 读一次 mtd5 头字节,命中 ota:kernel2 即判 * 「播放器 UI 触发升级」(nandwrite 已写标记但 system 呆在最后一帧没 reboot) * → supervisor 执行 reboot → uboot 引导 kernel2(升级模式,自动刷固件)。 * 只读 ~15B/500ms,IO 极低;/proc/mtd 只反映分区表不变读,观察不了内容。 */ if (now_ms() - last_mtd5_check >= 500) { last_mtd5_check = now_ms(); if (mtd_ota_is_upgrade()) { bm_log("supervisor 接管 upgrade: mtd5=ota:kernel2 -> reboot"); bm_do_reboot(LINUX_REBOOT_CMD_RESTART); } } /* 1) waitpid(WNOHANG):播放器退出 → 记日志;自然退出态 0(UI 关机)→ 真关机 * (M2.29 接管,避免「卡最后一帧但系统活着/SSH 能连」);其余不自动重启(HGL)。 */ if (player_alive) { int st = 0; pid_t r = waitpid(player_pid, &st, WNOHANG); if (r == player_pid) { player_alive = 0; if (WIFSIGNALED(st)) { bm_log("player exited by signal %d (%s)", WTERMSIG(st), bm_sig_name(WTERMSIG(st))); } else if (WIFEXITED(st) && WEXITSTATUS(st) == 0) { /* T2 关机接管:播放器 **自然** exit(0)(非热键 SIGTERM 杀)—— * 热键杀仍在 confirm 分支内 waitpid,不会走到这里。 */ bm_log("supervisor 接管 shutdown: player exit 0 -> poweroff"); bm_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); } else { bm_log("player exited status=%d", WEXITSTATUS(st)); /* 阶段 0:不自动重启(HGL 不可二次获取,§18.21.4)。 */ } } else if (r < 0) { player_alive = 0; /* 已被收割/错误 */ } } /* 2a) Bug 4 亮度保底:扫描窗内、播放器存活且亮度被降到 0 → 写回保底亮度。 * (播放器扫描忙时无法响应唤醒,我们只把背光拉回,画面仍为播放器最后帧, * 不再黑屏数分钟;不碰 blank。) */ if (player_alive && now_ms() < scan_keep_until) { int bri = read_brightness(); if (bri <= 0 && now_ms() - last_bri_floor_at >= 400) { int floor = saved_brightness > 0 ? saved_brightness : 60; write_brightness(floor); last_bri_floor_at = now_ms(); fprintf(stderr, "supervisor: scan-window brightness floor -> %d\n", floor); } } /* 2) EVIOCGKEY 采样(event0 电源 / event2 音量) */ int pwr = 0, vold = 0; if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); } if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL); vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); } /* --sim-hotkey N:supervisor 启动后 N ms 模拟「电源按住 3s + N+800 音量减点按」, * 驱动同一套热键判定(与 launcher --sim-hotkey 同构,§18.17 注入不可投递)。 */ if (sim_hotkey_ms > 0 && !sim_hk_done) { long se = (long)(now_ms() - sim_t0); if (se >= sim_hotkey_ms && se < sim_hotkey_ms + 3000) pwr = 1; if (se >= sim_hotkey_ms + 800 && se < sim_hotkey_ms + 950) vold = 1; } /* 3) 热键:电源按下边沿 arming,窗口内音量减按下边沿触发 */ if (pwr && !prev_pwr) { pwr_hold_armed = 1; pwr_down_at = t; } if (!pwr && prev_pwr) pwr_hold_armed = 0; /* 松开:disarm */ if (pwr_hold_armed && pwr && (t - pwr_down_at) > HK_PWRWIN_MAX) pwr_hold_armed = 0; /* 超窗:disarm */ int hotkey_fired = 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) { fprintf(stderr, "supervisor: HOTKEY power held + VOL_DOWN tap (%ldms) -> switch to MeowOS\n", since); hotkey_fired = 1; } else { fprintf(stderr, "supervisor: POWER+VOL_DOWN ignored (%ldms outside window)\n", since); } } prev_pwr = pwr; prev_vold = vold; /* 4) 热键触发 → 弹确认框 → 是:杀播放器进 MeowOS;否:留在 HibyOS。 * 「是/否」分支严格按用户定案:否不移交给 MeowOS, * 恢复播放器息屏副作用并 SIGCONT;是才 kill。 * M2.14:开机武装延迟 + 播放器忙判门不再「静默丢弃」——热键一旦命中即置 * hotkey_pending,待条件满足(延迟结束 / 播放器不忙)后**自动**弹确认框,用户只按一次。 */ int in_armed_delay = (sim_hotkey_ms <= 0 || sim_hotkey_strict) && (now_ms() - sim_t0) < ARM_DELAY_MS; int player_busy = (sim_hotkey_ms <= 0 || sim_hotkey_strict) && player_alive && proc_is_busy(player_pid); if (hotkey_pending && !in_armed_delay && !player_busy) { hotkey_fired = 1; hotkey_pending = 0; fprintf(stderr, "supervisor: HOTKEY pending cleared -> fire confirm box\n"); } if (hotkey_fired) { sim_hk_done = 1; /* --sim-hotkey 触发一次后不再模拟 */ /* §18.23:开机武装延迟 / 播放器忙判门——不静默丢弃,置 pending 待条件满足后自动弹框 * (仅约束真实热键;--sim-hotkey 为显式测试钩子,直通)。 */ if (in_armed_delay || player_busy) { hotkey_pending = 1; fprintf(stderr, "supervisor: hotkey queued (armed delay=%d busy=%c) -> pending\n", (int)in_armed_delay, player_busy ? proc_state(player_pid) : '-'); pwr_hold_armed = 0; kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); prev_pwr = kp_is_down(&kp_power, KEY_POWER); prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); continue; } /* 刷新 saved 亮度:若播放器当前亮度仍为正(如「背光常亮」设置), * 以它为准;若电源按下已触发灭屏(亮度 0),保留此前 saved(≥60)。 */ int now_bri = read_brightness(); if (now_bri > 0) saved_brightness = now_bri; /* 先冻结播放器(SIGSTOP→T),防止其在确认框上重绘覆盖; * 同时捕获面板是否已因电源按下而灭屏(was_blank),供较后恢复。 */ if (player_alive && !proc_is_stopped(player_pid)) { bm_log("supervisor -> player %d SIGSTOP (freeze for confirm)", player_pid); kill(player_pid, SIGSTOP); for (int i = 0; i < 100 && !proc_is_stopped(player_pid); i++) usleep(10000); } int raw_brightness = read_brightness(); int was_blank = (raw_brightness <= 0); /* 点亮面板以便看清确认框(播放器已冻结,不会 re-blank)。 */ unblank_panel(); if (was_blank) write_brightness(saved_brightness); if (read_file_int(BACKLIGHT_POWER) > 0) write_file_str(BACKLIGHT_POWER, "0\n"); int ans = confirm_switch(); if (ans == 1) { /* ===== 是:杀播放器 → 进 MeowOS ===== */ if (player_alive) { /* 帧冻结是 T 态:先 SIGCONT 让 SIGTERM 可投递(§18.21.3) */ if (proc_is_stopped(player_pid)) { fprintf(stderr, "supervisor: player is T, SIGCONT first\n"); bm_log("supervisor -> player %d SIGCONT (resume from T)", player_pid); kill(player_pid, SIGCONT); } fprintf(stderr, "supervisor: SIGTERM %d\n", player_pid); bm_log("supervisor -> player %d SIGTERM (hotkey -> MeowOS)", player_pid); kill(player_pid, SIGTERM); unsigned long long kstart = now_ms(); int exited = 0; while (now_ms() - kstart < KILL_GRACE_MS) { int st = 0; if (waitpid(player_pid, &st, WNOHANG) == player_pid) { exited = 1; break; } usleep(POLL_MS * 1000); } if (!exited) { fprintf(stderr, "supervisor: player not exiting in %dms, SIGKILL\n", KILL_GRACE_MS); bm_log("supervisor -> player %d SIGKILL (grace %dms expired)", player_pid, KILL_GRACE_MS); kill(player_pid, SIGKILL); int st = 0; waitpid(player_pid, &st, 0); } player_alive = 0; } /* 播放器已死:面板已在确认框点亮(brightness=saved)。写 mode=meow, * 然后 exec MeowOS 基础主屏(接管 fb0 画 buffer0+pan0)。 */ write_mode(0); fprintf(stderr, "supervisor: exec /usr/bin/meow_launcherd\n"); fflush(NULL); execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); fprintf(stderr, "supervisor: exec launcher failed: %s\n", strerror(errno)); _exit(127); } else { /* ===== 否 / -1:留在 HibyOS ===== * 恢复播放器息屏副作用:进框前已灭屏则 re-blank(0+FBIOBLANK 4) * 使物理面板匹配播放器 state machine;否则仅当读回 0 才写回 saved。 */ if (was_blank) { write_brightness(0); int f = open(FB_DEV, O_RDWR); if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); } } else if (read_brightness() <= 0) { write_brightness(saved_brightness); } fprintf(stderr, "supervisor: hotkey cancelled (stay HibyOS)\n"); if (player_alive && proc_is_stopped(player_pid)) { bm_log("supervisor -> player %d SIGCONT (hotkey cancelled)", player_pid); kill(player_pid, SIGCONT); } /* 消化键边沿基线:把 prev 对齐到当前实际按下态(含电源仍按住), * 并 disarm 热键,避免残留边沿立即重触发新热键(误弹确认框)。 */ pwr_hold_armed = 0; kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); prev_pwr = kp_is_down(&kp_power, KEY_POWER); prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); continue; } } usleep(POLL_MS * 1000); } } /* ================================================================== */ /* 模式标记 + 决策 */ /* ================================================================== */ static void write_mode(int choice) { /* choice: 1=HibyOS 0=MeowOS */ FILE *f = fopen(MODE_FILE, "w"); if (f) { fprintf(f, "%s\n", choice == 1 ? "hiby" : "meow"); fclose(f); } fprintf(stdout, "bootmenu: DECISION -> %s (mode=%s)\n", choice == 1 ? "HIBYOS 官方播放器" : "MEOWOS MeowOS 主屏", choice == 1 ? "hiby" : "meow"); } /* Bug 1:选 MeowOS → 把它持久为开机默认(写 spec 标记 + meow.conf boot_default=0),与 launcher * «回到HibyOS»(set_boot_default_hiby 写 boot_default=1)对称。效果:本次选中即进 MeowOS 主屏,且 * 下一次开机选单默认 MeowOS(不再「选 MeowOS 仍落回 HibyOS」)。 * 权威标记 = 板载 /data/meow_boot_default(O_TRUNC 单值);meow.conf 不再写/承载 boot_default。 */ static void set_boot_default_meow(void) { int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd >= 0) { write(fd, "0\n", 2); close(fd); fprintf(stderr, "bootmenu: /data/meow_boot_default=0\n"); } } /* ================================================================== */ /* --dumpkeys N:轮询 N ms,打印按键边沿(真机抓 key code 用) */ /* ================================================================== */ static int dump_cb(int code, int down, void *ctx) { unsigned long long start = *(unsigned long long *)ctx; fprintf(stdout, "[%llums] key %d %s\n", now_ms() - start, code, down ? "DOWN" : "UP"); return 0; } static int run_dumpkeys(long ms) { struct keypoll kp_power, kp_keys; int p_ok = kp_open(&kp_power, DEV_POWER) == 0; int k_ok = kp_open(&kp_keys, DEV_KEYS) == 0; touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (touch_fd < 0) fprintf(stderr, "dumpkeys: open %s: %s\n", DEV_TOUCH, strerror(errno)); fprintf(stderr, "dumpkeys: polling %ld ms (power=%d keys=%d touch=%d)\n", ms, p_ok, k_ok, touch_fd >= 0); unsigned long long start = now_ms(); while ((long)(now_ms() - start) < ms) { if (p_ok) kp_poll(&kp_power, dump_cb, &start); if (k_ok) kp_poll(&kp_keys, dump_cb, &start); if (touch_fd >= 0) { struct pollfd pfd = { touch_fd, POLLIN, 0 }; if (poll(&pfd, 1, 0) > 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { 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) mt_abs(&mt_touch, val, mt_touch.last_y); else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt_touch, mt_touch.last_x, val); } else if (type == EV_KEY && code == BTN_TOUCH) { fprintf(stdout, "[%llums] touch BTN_TOUCH %d @ (%d,%d)\n", now_ms() - start, val, mt_touch.last_x, mt_touch.last_y); } } } } usleep(POLL_MS * 1000); } return 0; } /* ================================================================== */ /* 选单守护模式主循环 */ /* ================================================================== */ static int run_menu(int sim_sel, int do_fork) { struct keypoll kp_power, kp_keys; if (kp_open(&kp_power, DEV_POWER) != 0) fprintf(stderr, "bootmenu: WARN no power dev\n"); if (kp_open(&kp_keys, DEV_KEYS) != 0) fprintf(stderr, "bootmenu: WARN no keys dev\n"); touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (touch_fd < 0) fprintf(stderr, "bootmenu: WARN no touch dev\n"); /* 入口 drain + 短暂冷却(防 goodix 幽灵触摸,§18.16;M2.1 缩短到 800ms 不吞首次有效输入) */ if (touch_fd >= 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } } unsigned long long cooldown_until = now_ms() + GHOST_COOLDOWN_MS; mt_reset(&mt_touch); /* 清触摸手势态,避免上次残留误派发(goodix 时序免疫:last 归 -1 起步) */ int cursor = (cfg_boot_default == 1) ? 0 : 1; /* 高亮随默认 */ unsigned long long start = now_ms(); unsigned long long deadline = start + (unsigned long long)cfg_boot_timeout_ms; /* 用户操作→重计(倒计时交互) */ int dirty = 1, last_cursor = -1; long last_clock_tick = -10000, last_cd_tick = -10000; int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0; int sim_done = 0; int choice = -1; for (;;) { unsigned long long t = now_ms(); long el = (long)(t - start); long remaining = (long)(deadline - t); /* 距 deadline:空闲才倒数,有输入则重计 */ if (remaining <= 0) { /* 空闲超时 → 单调强收敛到默认 */ choice = cfg_boot_default; fprintf(stderr, "bootmenu: TIMEOUT -> default %s\n", choice == 1 ? "hiby" : "meow"); break; } /* --sim-select H|M:在 SIM_SELECT_DELAY_MS 后模拟确认 */ if (sim_sel && !sim_done && el >= SIM_SELECT_DELAY_MS) { sim_done = 1; if (sim_sel == 'H') { cursor = 0; choice = 1; } else { cursor = 1; choice = 0; } fprintf(stderr, "bootmenu: SIM SELECT %c -> %s\n", sim_sel, choice == 1 ? "hiby" : "meow"); break; } /* 按键边沿 */ int vold = 0, volu = 0, play = 0, pwr = 0; if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); } if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL); vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); } if (vold && !prev_vold) { /* 音量下 = 下移 */ cursor = (cursor + 1) % 2; deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; /* 用户操作 → 倒计时重计 */ fprintf(stderr, "bootmenu: key VOL_DOWN -> cursor %d (cd reset)\n", cursor); } if (volu && !prev_volu) { /* 音量上 = 上移(2 项循环:-1 取模) */ cursor = (cursor - 1 + 2) % 2; deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; /* 用户操作 → 倒计时重计 */ fprintf(stderr, "bootmenu: key VOL_UP -> cursor %d (cd reset)\n", cursor); } if (play && !prev_play) { /* 播放 = 确认 */ choice = (cursor == 0) ? 1 : 0; fprintf(stderr, "bootmenu: key PLAY -> select row %d (%s)\n", cursor, choice == 1 ? "hiby" : "meow"); break; } if (pwr && !prev_pwr) { /* 电源 = 确认当前光标项(M2.9 Bug 1:不再固定取默认) */ choice = (cursor == 0) ? 1 : 0; fprintf(stderr, "bootmenu: key POWER -> confirm row %d (%s)\n", cursor, choice == 1 ? "hiby" : "meow"); break; } prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr; /* 触摸(event1):按压→抬起 tap(同 launcher,读 ABS_X/Y 与 ABS_MT_*,§18.20) */ if (touch_fd >= 0) { struct pollfd pfd = { touch_fd, POLLIN, 0 }; if (poll(&pfd, 1, 0) > 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { 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) mt_abs(&mt_touch, val, mt_touch.last_y); else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt_touch, mt_touch.last_x, val); } else if (type == EV_KEY && code == BTN_TOUCH) { if (val) { /* 按压边沿:接受但**不锁原点**——等按下后首个有效 ABS 才锁(goodix 时序免疫,修“点两下”) */ if (now_ms() < cooldown_until) mt_reset(&mt_touch); /* 幽灵/冷却:丢弃本次 */ else mt_down(&mt_touch); } else { /* 抬起边沿:按按下原点派发;滑动/未锁到坐标 → mt_up 返回 0,不命中、不崩溃 */ int dx = 0, dy = 0; if (mt_up(&mt_touch, &dx, &dy)) { int ty = mt_touch.down_y; if (ty >= ENTRY0_Y - 4 && ty < ENTRY0_Y + ROW_H - 8) { cursor = 0; choice = 1; deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; fprintf(stderr, "bootmenu: touch row 0 -> hiby\n"); } else if (ty >= ENTRY1_Y - 4 && ty < ENTRY1_Y + ROW_H - 8) { cursor = 1; choice = 0; deadline = now_ms() + (unsigned long long)cfg_boot_timeout_ms; fprintf(stderr, "bootmenu: touch row 1 -> meow\n"); } } } } } if (choice >= 0) break; } } /* 光标变化 → 整屏重绘 */ if (cursor != last_cursor) dirty = 1; if (dirty) { draw_menu(cursor, remaining); last_cursor = cursor; dirty = 0; } /* 每秒刷时钟 */ if (el - last_clock_tick >= 1000) { last_clock_tick = el; draw_clock(cfg_bg); } /* 每秒刷倒计时(秒值变化才刷) */ long cd = (remaining + 999) / 1000; if (el - last_cd_tick >= 1000) { last_cd_tick = el; draw_countdown(cfg_bg, remaining); } usleep(POLL_MS * 1000); } write_mode(choice >= 0 ? choice : cfg_boot_default); if (touch_fd >= 0) close(touch_fd); /* 顺序(§2.3):先恢复 fb(选单不再占屏),再 munmap+close fb0,才 fork */ fb_restore_player_frame(); fb_close(); /* M2(--fork):HibyOS→fork 播放器(父进程转监督者,阻塞 rcS); * MeowOS→exec meow_launcherd(MeowOS 基础主屏,接管 fb0)。 */ if (do_fork) { if (choice == 1) { fork_hiby_player(); /* 父进程陷入 supervisor,永不返回 */ } else { set_boot_default_meow(); /* Bug 1:选 MeowOS → 持久为开机默认 */ execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); fprintf(stderr, "bootmenu: exec launcher failed: %s\n", strerror(errno)); _exit(127); } } return 0; /* M0/M1(无 --fork):选单退出,rcS 推进 */ } /* ================================================================== */ /* main */ /* ================================================================== */ static void usage(void) { fprintf(stderr, "usage: meow_bootmenu [--dumpkeys N] [--sim-select H|M] [--boot-only]\n" " [--timeout-ms N] [--fork] [--version]\n" " (no args) 守护选单模式(显示菜单,等待选择/超时)\n" " --dumpkeys N 轮询 N ms 打印按键边沿(抓 key code)\n" " --sim-select H|M 显示菜单,%d ms 后模拟选 H=HibyOS / M=MeowOS\n" " --boot-only 跳过选单,直接取默认(boot_default)\n" " --timeout-ms N 覆写超时毫秒(测试钩子)\n" " --fork M2 开启:选 HibyOS 时 fork 播放器并转监督者;\n" " 选 MeowOS 时 exec /usr/bin/meow_launcherd(默认关)\n" " --sim-hotkey N 监督者启动 N ms 后模拟热键(M2 真机 verif)\n" " --sim-confirm Y|N 确认框打开 2s 后自动答 Y=是 / N=否\n" " --version 打印版本并退出\n", SIM_SELECT_DELAY_MS); } int main(int argc, char **argv) { long dumpkeys = 0, timeout_override = 0; int sim_sel = 0; int boot_only = 0; int do_fork = 0; int do_version = 0; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-select") && i + 1 < argc) { char c = argv[++i][0]; if (c == 'h' || c == 'H') sim_sel = 'H'; else if (c == 'm' || c == 'M') sim_sel = 'M'; else { usage(); return 1; } } else if (!strcmp(argv[i], "--boot-only")) boot_only = 1; else if (!strcmp(argv[i], "--timeout-ms") && i + 1 < argc) timeout_override = atol(argv[++i]); else if (!strcmp(argv[i], "--fork")) do_fork = 1; else if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey_ms = atol(argv[++i]); else if (!strcmp(argv[i], "--sim-hotkey-strict") && i + 1 < argc) { sim_hotkey_ms = atol(argv[++i]); sim_hotkey_strict = 1; } else if (!strcmp(argv[i], "--sim-confirm") && i + 1 < argc) { char c = argv[++i][0]; if (c == 'y' || c == 'Y') sim_confirm = 'Y'; else if (c == 'n' || c == 'N') sim_confirm = 'N'; else { usage(); return 1; } } else if (!strcmp(argv[i], "--version")) do_version = 1; else { usage(); return 1; } } load_boot_ver(); /* 读 /etc/meowos_version(版本单点);--version 与选单右下角共用 */ if (do_version) { printf("meow_bootmenu %s (mipsel static musl)\n", MEOW_BOOTMENU_VER); printf("MeowOS v%s\n", g_boot_ver); return 0; } load_config(); if (timeout_override > 0) cfg_boot_timeout_ms = timeout_override; if (dumpkeys > 0) return run_dumpkeys(dumpkeys); /* 无菜单直进(boot_menu=0 或 --boot-only):§3「直进默认锁 HGL」 */ if (boot_only || !cfg_boot_menu) { int choice = cfg_boot_default; fprintf(stderr, "bootmenu: boot-only -> default %s\n", choice == 1 ? "hiby" : "meow"); write_mode(choice); if (do_fork) { if (choice == 1) fork_hiby_player(); else { set_boot_default_meow(); /* Bug 1:选 MeowOS → 持久为开机默认 */ execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); fprintf(stderr, "bootmenu: exec launcher failed: %s\n", strerror(errno)); _exit(127); } } return 0; } if (fb_open() != 0) return 1; fb_snapshot_player(); /* 快照当前显示缓冲(恢复播放器帧用) */ fb_pan_buf0(); /* 画前 pan 到 yoffset=0(buffer0) */ draw_menu(cfg_boot_default == 1 ? 0 : 1, cfg_boot_timeout_ms); /* 预画一帧 */ run_menu(sim_sel, do_fork); return 0; }