Files
MeowOS-For-R3P2/patch_v2/bootmenu/meow_bootmenu.c
T

1635 lines
75 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.
/* 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_<bootts>.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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/poll.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/syscall.h> /* M2.29:supervisor 接管关机/升级,直连 reboot(2) syscall */
#include <linux/input.h>
#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/<pid>/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_<ts>,直接复读会拼成 meow_boot_meow_…);
* 每次自行按 /proc/uptime 推开机时间戳,同一 boot 内文件名恒定。
* - 写 /usr/data/mnt/sd_0/MeowOS/logs/meow_boot_<bootts>.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/<pid>/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/<pid>/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;
}