/* meow_watchdog.c — virtual back button + gesture watchdog for the * R3 Pro II app launcher. * * Spawned by the launcher (hiby_player cave code) alongside an app: * meow_watchdog [--sim-tap N] [--sim-swipe N] * * Behavior (M2.21+): * - mmap /dev/fb0 (RGB565 auto-detect); no virtual back button (removed). * - read /dev/input/event*; a swipe starting in the left 24px edge * (rightward movement > 50px) sends SIGUSR1 to the app (app decides). * - M2.22(A)key: read KEY_POWER (event0) and own the panel during app * run — short press (<3s release) = toggle screen off/on (brightness 0 + * FBIOBLANK(4) / FBIOBLANK(0)+brightness restore, with a force re-pan to * buffer0 on wake). * - M2.24 全面按键接管(KEY_CONTROL_DESIGN):应用运行期所有物理键由本进程统一消费, * 应用绝不碰物理键。 * 电源单击(<2s) -> 息屏/亮屏(沿用 M2.22); * 电源长按 ≥2s -> 写 /tmp/meow_power_menu,由 launcher SIGSTOP app 后弹统一电源菜单 * (launcher 在 waitpid 轮询中消费;菜单期该进程靠 /tmp/meow_power_menu_active * 让位,绝不与 launcher 抢屏/抢键);launcher 画菜单,本进程不自绘。 * 电源长按 ≥8s -> 强制关机兜底(仅当菜单未生效/launcher 未接管)。 * 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。 * 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。 * (8s 强关的「防误触 / pm_interacted」逻辑在 launcher 的 show_power_menu 内,此进程只做兜底。) * Exits when the app process disappears (kill(pid,0) -> ESRCH) or on * SIGTERM from the parent. * * Test modes (no touch device needed): * --sim-tap N : after N seconds, simulate a button hit * --sim-swipe N : after N seconds, simulate an immersive swipe * * Static MIPS32r2 musl build: * ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -static meow_watchdog.c -o meow_watchdog */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */ #define FBIOGET_VSCREENINFO 0x4600 #define FBIOGET_FSCREENINFO 0x4602 #define BTN_TOUCH 0x14a #define ABS_X 0x00 #define ABS_Y 0x01 #define ABS_MT_POSITION_X 0x35 #define ABS_MT_POSITION_Y 0x36 #define EV_SYN 0 #define EV_KEY 1 #define EV_ABS 3 #define EDGE_W 24 /* left-edge swipe start zone(返回键已删,左滑返回仍用) */ #define SWIPE_DX 50 /* rightward movement threshold */ #define REFRESH_MS 100 /* M2.22(A):watchdog 在应用运行时接管电源键 = 息屏/亮屏(仅系统组件可做)。 * launcher 应用运行循环只处理「自动息屏/唤醒」,电源键单击(短按松开)在应用内 * 是孤儿 → 无法息屏。本进程既已读 event0(md-gpio-keys)+ mmap fb0,故在此复刻 * launcher 的 meow_panel_off/on 轻量版:背光 brightness>0 亮、0 灭;FBIOBLANK(4/0) * 面板下电/上电(4=挂起 goodix 触摸,防误触)。本轮只做息屏/亮屏,不做电源菜单/ * 长按关机(那是后续,且属于 launcher/系统层)。 */ #define FBIOBLANK 0x4611 #define FBIOPAN_DISPLAY 0x4606 #define KEY_POWER 116 #define KEY_VOLUMEUP 115 #define KEY_VOLUMEDOWN 114 #define KEY_PLAYPAUSE 164 #define KEY_PREVIOUSSONG 165 #define KEY_NEXTSONG 163 #define KEY_PREVIOUS 19 #define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness" /* M2.24 全面按键接管:电源单击(<2s)=息屏/亮屏;长按≥2s=通知 launcher 弹统一电源菜单; * ≥8s=强制关机兜底(仅当 launcher 未接管菜单时)。音量± = 写真实 DAC;播放/上下曲 = 抓消费。 */ #define PWR_TOGGLE_MS 2000 /* 短按上限:<2s 松开=单击(镜像 launcher 单单击语义) */ #define PWR_MENU_MS 2000 /* 长按≥2s -> 写 /tmp/meow_power_menu 请 launcher 弹菜单 */ #define PWR_FORCE_MS 8000 /* 长按≥8s -> 强制关机(launcher 未接管时的兜底) */ #define PWR_MENU_MARKER "/tmp/meow_power_menu" /* watchdog->launcher 菜单请求 */ #define PWR_MENU_ACTIVE "/tmp/meow_power_menu_active" /* launcher 画菜单中,watchdog 让位 */ #define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 音量持久化(与 launcher settings.conf 同源) */ #define LINUX_REBOOT_MAGIC1 0xfee1dead #define LINUX_REBOOT_MAGIC2 672274793 /* 0x28121969 */ #define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc /* Debounce for the launch-tap remnant (2026-08-28 fix). * On a TOUCH launch the app is killed a second or two later (log: 1.2–2.25s * after the launch tap) even though the user pressed nothing in between and * the launcher logged no further tap — a ghost/spurious touch the goodix * emits as the app starts. The old toggle state machine read whatever it * saw with the default coords 0,0 (exactly the top-left button region) and * killed the freshly-launched app, so the app could never stay open. * * Guards, layered from most-specific to most-general: * 1. last_x/last_y start at -1 and a press with no coordinate seen is * dropped — the default (0,0) can never be latched as a corner press. * 2. BTN_TOUCH edges are matched on the actual value: a stray/out-of-sync * UP never starts a press; only a real DOWN then UP dispatches. * 3. A "click" must be SHORT (SHORT_PRESS_MS); a held press is ignored. * 4. The input queues are drained once at start, so the launch-tap tail * is not replayed as a fresh press. * 5. Every press within COOLDOWN_MS of app start is ignored. This is set * to cover the measured ghost window (1.2–2.25s) — the "~800ms 残影" * assumption was too short. */ #define COOLDOWN_MS 2500 /* cover the measured ghost touch window + margin */ #define SHORT_PRESS_MS 600 /* a "click"; any longer and it is not a tap */ static unsigned char *fb; static int xres, yres, line_len, bpp; static inline void put_px(int x, int y, uint32_t color) { if (x < 0 || x >= xres || y < 0 || y >= yres) return; unsigned char *p = fb + y * line_len + x * (bpp / 8); if (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; } } /* 5x7 "<" arrow + border */ 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); } static int read_state_file(void) { /* returns 1 if immersive */ int fd = open("/tmp/meowapp_state", O_RDONLY); if (fd < 0) return 0; char buf[8]; int n = read(fd, buf, sizeof buf - 1); close(fd); if (n <= 0) return 0; buf[n] = 0; return buf[0] == '1'; } /* unified log: write to the boot log file passed by the launcher via * MEOW_LOG_FILE (TF card MeowOS/logs/meow_.log); fall back to * stderr when unset (e.g. no TF card at launch time). */ static int wd_log_fd = -1; 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; } static void wd_log_init(void) { const char *lf = getenv("MEOW_LOG_FILE"); if (lf && lf[0]) wd_log_fd = open(lf, O_WRONLY | O_APPEND | O_CREAT, 0644); } static void wd_log(const char *fmt, ...) { char buf[256]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof buf, fmt, ap); va_end(ap); char line[288]; int l = snprintf(line, sizeof line, "%llu [watchdog] %s\n", now_ms(), buf); if (wd_log_fd >= 0) { if (write(wd_log_fd, line, l) < 0) wd_log_fd = -1; return; } fprintf(stderr, "%llu [watchdog] %s\n", now_ms(), buf); } /* touch tracking state. * last_x/last_y start at -1 (never seen a coordinate) so a press that * arrives before any ABS coordinate is not latched at the default 0,0 — * which is exactly the corner the old code used to mis-fire on. */ static int last_x = -1, last_y = -1; static int down_valid = 0; /* a press is currently being tracked */ static int down_x = 0, down_y = 0; static unsigned long long down_ms = 0; /* ---- M2.22(A)+ M2.24 电源键接管:息屏/亮屏 + 长按菜单/强关 ---- */ static int wd_fbfd = -1; /* fb0 fd(main 里打开,供 FBIOBLANK/FBIOPAN 用) */ static int wd_saved_bri = 60; /* 熄屏前亮度快照 */ static int pwr_down = 0; /* 电源当前按住 */ static unsigned long long pwr_down_ms = 0; static int pwr_woke = 0; /* 按下时屏已黑 → 本次只作唤醒,松开不再 toggle */ static int pwr_menu_notified = 0; /* 已写 /tmp/meow_power_menu 请 launcher 弹菜单 */ static int pwr_force_done = 0; /* 8s 强关已发(防重复) */ 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_brightness(int val) { char buf[8]; int l = snprintf(buf, sizeof buf, "%d\n", val); int fd = open(BACKLIGHT, O_WRONLY); if (fd < 0) return; if (write(fd, buf, l) < 0) wd_log("backlight write %d: %s", val, strerror(errno)); close(fd); } /* 强制 re-pan 到 buffer0:FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位到非 buffer0/失效 * 位置(launcher M2.8 Bug-3-residual 根因),而应用始终画 buffer0,必须无条件把显示翻回 * buffer0,否则唤醒后黑屏/旧帧。 */ static void wd_pan_buf0_force(void) { if (wd_fbfd < 0) return; unsigned char v[160]; memset(v, 0, sizeof v); if (ioctl(wd_fbfd, FBIOGET_VSCREENINFO, v) < 0) return; *(uint32_t *)(v + 16) = 0; /* xoffset */ *(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */ ioctl(wd_fbfd, FBIOPAN_DISPLAY, v); } static void wd_panel_off(void) { int br = read_brightness(); wd_saved_bri = (br > 0) ? br : 60; write_brightness(0); if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 4); /* 面板下电 + 挂起 goodix 触摸 */ wd_log("power: panel OFF (was %d -> 0, blank 4)", br); } static void wd_panel_on(void) { if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 0); write_brightness(wd_saved_bri > 0 ? wd_saved_bri : 60); wd_pan_buf0_force(); /* 唤醒后强制回 buffer0 */ wd_log("power: panel ON (blank 0 + brightness %d)", wd_saved_bri); } /* launcher 正在画电源菜单(/tmp/meow_power_menu_active 存在)→ watchdog 完全让位。 */ static int wd_menu_active(void) { return access(PWR_MENU_ACTIVE, F_OK) == 0; } /* 强制关机兜底:与 launcher sys_do_reboot 同路径(真机关机,正常不返回)。 */ static void wd_do_poweroff(void) { sync(); wd_log("sys_reboot POWER_OFF"); syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_CMD_POWER_OFF, NULL); wd_log("sys_reboot returned: %s", strerror(errno)); /* 正常不会返回 */ for (;;) usleep(1000000); } /* 电源键(KEY_POWER)边沿驱动:单击(短按松开 <3s)= 息屏/亮屏;按下时屏已黑 = 只唤醒。 * 长按(≥3s)由 wd_power_tick() 在轮询中检测 → 写 /tmp/meow_power_menu 请 launcher 弹菜单; * 松开边沿不再 toggle(菜单/长按语义归 launcher 菜单,防「长按后仍 toggle」)。 */ static void handle_power(int val) { if (wd_menu_active()) return; /* 电源菜单期:launcher 独占物理键语义 */ if (val) { /* 按下边沿 */ pwr_down = 1; pwr_down_ms = now_ms(); pwr_menu_notified = 0; pwr_force_done = 0; int br = read_brightness(); pwr_woke = (br <= 0) ? 1 : 0; /* 屏已黑:本次电源=唤醒,松开不 toggle */ if (pwr_woke) wd_panel_on(); /* 立即亮屏(launcher 自动息屏唤醒路径同步醒,双保险幂等) */ wd_log("power down (bri=%d woke=%d)", br, pwr_woke); return; } /* 松开边沿 */ if (!pwr_down) return; /* 未跟踪到按下(启动前已按住/残留):忽略,防误 toggle */ pwr_down = 0; unsigned long long held = now_ms() - pwr_down_ms; if (pwr_woke) { pwr_woke = 0; wd_log("power release after wake (%llums) -> no toggle", held); return; } if (pwr_menu_notified) { /* 本次已请求电源菜单(≥3s):菜单是否画出由 launcher 负责,这里绝不 toggle。 */ wd_log("power release after menu req (%llums) -> menu owns toggle", held); pwr_menu_notified = 0; return; } if (held >= PWR_TOGGLE_MS) { wd_log("power hold %llums -> no-op (menu/force-off in tick)", held); return; } int br = read_brightness(); if (br > 0) wd_panel_off(); else wd_panel_on(); /* 屏已黑仍短按:亮屏 */ wd_log("power short-press %llums -> toggle panel (bri was %d)", held, br); } /* 电源长按检测(每轮询 tick 调):≥3s 写菜单请求;≥8s 强制关机兜底(仅当菜单未生效时)。 */ static void wd_power_tick(void) { /* launcher 已接管电源菜单:本进程完全让位,清空电源状态,下一手势从零开始。 */ if (wd_menu_active()) { if (pwr_down) wd_log("power: launcher menu active -> watchdog inert"); pwr_down = 0; pwr_menu_notified = 0; pwr_force_done = 0; pwr_woke = 0; return; } if (!pwr_down) return; unsigned long long held = now_ms() - pwr_down_ms; if (held >= PWR_MENU_MS && !pwr_menu_notified) { int fd = open(PWR_MENU_MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd >= 0) { write(fd, "1", 1); close(fd); } pwr_menu_notified = 1; wd_log("power hold %llums -> %s (launcher draws menu)", held, PWR_MENU_MARKER); } if (held >= PWR_FORCE_MS && !pwr_force_done && !pwr_menu_notified) { /* 长按 8s 仍无菜单接管(launcher 未响应/未消费)→ 强制关机兜底。 */ pwr_force_done = 1; wd_log("power hold %llums -> FORCE POWER OFF", held); wd_do_poweroff(); } } /* ---- M2.24 音量接管(watchdog 自实现,与 launcher 同源路径:写 DAC + settings.conf)---- * launcher 菜单期由 launcher 调 settings_volume_adj;应用运行期 launcher 卡在 waitpid、 * 不调音量,故由本进程接管。音量值持久化到与 launcher 相同的 settings.conf,launcher 在 * 应用返回(launch_app 末尾)re-load,保证菜单音量与 DAC 一致。 */ static int wd_vol = 60; /* 当前音量 0-100(= 写入 DAC 的映射值) */ static int wd_vol_max = 100; /* 上限 */ static int wd_vol_step = 1; /* 跳步 */ static void wd_vol_load(void) { wd_vol = 60; wd_vol_max = 100; wd_vol_step = 1; FILE *f = fopen(MEOW_SETTINGS_FILE, "r"); if (!f) { wd_log("vol: no settings, default %d", wd_vol); return; } char buf[128]; while (fgets(buf, sizeof buf, f)) { char *eq = strchr(buf, '='); if (!eq) continue; *eq = 0; char *k = buf; int kl = (int)strlen(k); while (kl > 0 && (k[kl-1] == ' ' || k[kl-1] == '\t')) k[--kl] = 0; char *v = eq + 1; while (*v == ' ' || *v == '\t') v++; int iv = atoi(v); if (!strcmp(k, "audio.volume")) { if (iv >= 0 && iv <= 100) wd_vol = iv; } else if (!strcmp(k, "audio.volume_max")) { if (iv >= 0 && iv <= 100) wd_vol_max = iv; } else if (!strcmp(k, "audio.volume_step")){ if (iv >= 1 && iv <= 5) wd_vol_step = iv; } } fclose(f); wd_log("vol: loaded volume=%d max=%d step=%d", wd_vol, wd_vol_max, wd_vol_step); } /* 写真实 DAC(CS43198, numid 1/2, 0-255),与 launcher meow_dac_vol_write 同路径。 */ static void wd_vol_write(int pct) { int dac = pct * 255 / 100; if (dac < 0) dac = 0; if (dac > 255) dac = 255; char cmd[160]; snprintf(cmd, sizeof cmd, "amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1", dac, dac); system(cmd); wd_log("DAC vol -> L/R=%d (pct %d)", dac, pct); } static void wd_vol_save(void) { system("mkdir -p /usr/data/meow 2>/dev/null"); int fd = open(MEOW_SETTINGS_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { wd_log("vol: open %s fail: %s", MEOW_SETTINGS_FILE, strerror(errno)); return; } char buf[128]; int l = snprintf(buf, sizeof buf, "# meow-settings 1\naudio.volume = %d\naudio.volume_max = %d\naudio.volume_step = %d\n", wd_vol, wd_vol_max, wd_vol_step); if (write(fd, buf, l) < 0) wd_log("vol: write fail: %s", strerror(errno)); close(fd); wd_log("vol: saved volume=%d(max=%d,step=%d)", wd_vol, wd_vol_max, wd_vol_step); } /* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。 */ static void wd_vol_adj(int dir) { int target = wd_vol + dir * wd_vol_step; if (target < 0) target = 0; if (target > wd_vol_max) target = wd_vol_max; if (target == wd_vol) { wd_log("volume %s at limit (%d)", dir > 0 ? "UP" : "DOWN", wd_vol); return; } wd_vol = target; wd_vol_write(wd_vol); wd_vol_save(); wd_log("volume %s -> %d", dir > 0 ? "UP" : "DOWN", wd_vol); } static void handle_touch(int immersive, int app_pid, unsigned long long start_ms, int btn_val) { if (btn_val) { /* press edge (BTN_TOUCH value 1). * 1) Cooldown: any press within COOLDOWN_MS of app start is the * remnant of the menu launch tap, not a deliberate back gesture. * 2) No coordinate seen yet -> cannot be a corner press, ignore. */ if (now_ms() - start_ms < COOLDOWN_MS) return; if (last_x < 0 || last_y < 0) return; down_x = last_x; down_y = last_y; down_ms = now_ms(); down_valid = 1; return; } /* release edge (BTN_TOUCH value 0). A value-driven edge (not a toggle) * means a stray/out-of-sync UP never latches coords; only a real press * that was actually tracked (down_valid) can reach the dispatch below. */ if (!down_valid) return; /* stray UP: nothing tracked */ down_valid = 0; if (now_ms() - down_ms > SHORT_PRESS_MS) return; /* held too long */ /* M2.21:返回键彻底移除。watchdog 不再有「按钮命中->SIGTERM」路径(那是 * 虚拟返回键的年代)。返回手势统一为「左缘起点 + 向右滑 SWIPE_DX」→ SIGUSR1, * 不论 immersive 与否(返回键没了,所有 app 都靠左滑返回)。app 卡死仍可由 * 外层强杀序列(音量减x3)兜底。 */ if (down_x < EDGE_W && (last_x - down_x) > SWIPE_DX) { wd_log("swipe -> SIGUSR1"); kill(app_pid, SIGUSR1); } } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s [--sim-tap N] [--sim-swipe N]\n", argv[0]); return 1; } int app_pid = atoi(argv[1]); int sim_tap = 0, sim_swipe = 0; for (int i = 2; i < argc; i++) { if (!strcmp(argv[i], "--sim-tap") && i + 1 < argc) sim_tap = atoi(argv[++i]); if (!strcmp(argv[i], "--sim-swipe") && i + 1 < argc) sim_swipe = atoi(argv[++i]); } setvbuf(stderr, NULL, _IONBF, 0); wd_log_init(); wd_vol_load(); /* M2.24:音量初始值读自与 launcher 同源 settings.conf */ wd_log("app_pid=%d sim_tap=%d sim_swipe=%d", app_pid, sim_tap, sim_swipe); /* cooldown anchor: ~the moment this watchdog started (== app launch). */ unsigned long long start_ms = now_ms(); /* fb;fd 存全局 wd_fbfd 供电源键面板 ioctl(FBIOBLANK/FBIOPAN)复用 */ wd_fbfd = open("/dev/fb0", O_RDWR); if (wd_fbfd >= 0) { unsigned char vinfo[160], finfo[68]; memset(vinfo, 0, sizeof vinfo); memset(finfo, 0, sizeof finfo); if (ioctl(wd_fbfd, FBIOGET_VSCREENINFO, vinfo) == 0 && ioctl(wd_fbfd, FBIOGET_FSCREENINFO, finfo) == 0) { xres = *(uint32_t *)(vinfo + 0); yres = *(uint32_t *)(vinfo + 4); bpp = *(uint32_t *)(vinfo + 24); line_len = *(uint32_t *)(finfo + 44); if (!line_len) line_len = xres * (bpp / 8); fb = mmap(NULL, (size_t)line_len * yres, PROT_READ | PROT_WRITE, MAP_SHARED, wd_fbfd, 0); if (fb == MAP_FAILED) fb = NULL; } else { fb = NULL; } } /* input devices */ struct pollfd fds[8]; int nfd = 0; char dev[32]; for (int i = 0; i < 8; i++) { snprintf(dev, sizeof dev, "/dev/input/event%d", i); int e = open(dev, O_RDONLY | O_NONBLOCK); if (e >= 0) { fds[nfd].fd = e; fds[nfd].events = POLLIN; nfd++; } } /* drain once: anything already buffered (the launch tap's tail, which the * launcher may not have fully consumed) is discarded so this process never * sees a half-gesture as a clean press/release pair. */ for (int i = 0; i < nfd; i++) { unsigned char ev[16]; while (read(fds[i].fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } } long elapsed = 0; for (;;) { /* app gone? */ if (kill(app_pid, 0) < 0 && errno == ESRCH) { wd_log("app gone, exit"); break; } int immersive = read_state_file(); /* M2.21:返回键彻底移除(用户要求)。不再画左上角 48x48 虚拟返回键, * 也不再在非沉浸分支处理「按钮命中->SIGTERM」。返回手势统一由 SDK/ * 应用侧处理;watchdog 仅保留兜底(app 卡死强杀、存活检测)。 */ (void)immersive; if (sim_tap && elapsed >= (long)sim_tap * 1000) { wd_log("SIM TAP"); kill(app_pid, SIGTERM); usleep(1000000); kill(app_pid, SIGKILL); break; } if (sim_swipe && elapsed >= (long)sim_swipe * 1000) { wd_log("SIM SWIPE"); kill(app_pid, SIGUSR1); sim_swipe = 0; } int r = poll(fds, nfd, 100); if (r > 0) { for (int i = 0; i < nfd; i++) { if (!(fds[i].revents & POLLIN)) continue; unsigned char ev[16]; while (read(fds[i].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) last_x = val; else if (code == ABS_Y || code == ABS_MT_POSITION_Y) last_y = val; } else if (type == EV_KEY && code == BTN_TOUCH) { handle_touch(immersive, app_pid, start_ms, val); } else if (type == EV_KEY && code == KEY_POWER) { handle_power(val); /* M2.22(A)/M2.24:电源单击=息屏/亮屏,长按 tick 里判菜单 */ } else if (type == EV_KEY && code == KEY_VOLUMEUP) { if (val == 1) wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */ } else if (type == EV_KEY && code == KEY_VOLUMEDOWN) { if (val == 1) wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */ } else if (type == EV_KEY && (code == KEY_PLAYPAUSE || code == KEY_PREVIOUSSONG || code == KEY_NEXTSONG || code == KEY_PREVIOUS)) { if (val == 1) wd_log("media key captured (code %d) -> consumed, no route", code); } } } } wd_power_tick(); /* M2.24:电源长按检测(3s 菜单 / 8s 强关 / 菜单期让位) */ elapsed += 100; } return 0; }