Files
MeowOS-For-R3P2/patch_v2/meow_watchdog/meow_watchdog.c
T
AskaEth f2551a28a2 fix(watchdog): M20 亮屏亮度不再回退固定 60(settings 读失败重试 + 未知快照语义拆分)— 重编出货二进制
真机 0.8.46(2026-11-05 21:09 日志)复现「开机/亮屏背光跳到固定值」:
  76646 [watchdog] power: panel ON (blank 0 + brightness 60)
而 /usr/data/meow/settings.conf 是 display.brightness = 1。根因链:
- 出货 /usr/bin/meow_watchdog(md5 553840cd…,与仓库内二进制逐字节相同)是
  2026-09-01 6a4d330 编出的旧版:不含 M6.23 的 settings 读取(strings 无
  display.brightness_min,日志仍是旧格式 "brightness %d")—— M6.23 只改了源码、
  没重编。旧版 wd_panel_on 无条件 write_brightness(wd_saved_bri>0?wd_saved_bri:60),
  wd_saved_bri 初值 60 且该 session 未熄屏过 → 亮屏写 60,覆盖 launcher 刚设的 1。
- 源码侧同样有 60 兜底:wd_settings_brightness() fopen 失败即 -1 → 回退快照 60。

改动(patch_v2/meow_watchdog/):
- wd_settings_brightness():fopen 失败重试 3×10ms。覆盖 settings.conf「写 tmp+rename」
  原子替换、开机早期 /usr/data 未就绪的瞬时窗口;只在失败时发生(成功路径 0 开销),
  最坏 20ms,远小于亮屏轮询粒度 REFRESH_MS=100ms。
- 新增 wd_bri_decide() 纯函数:settings(clamp 后)→ 本次运行期真实快照 → 不写
  (面板已亮,untouched)→ 安全低亮 10(面板黑且无任何值来源)。任何路径都不再写 60。
- wd_saved_bri 初值 60 → WD_BRI_UNKNOWN(0);wd_panel_off 只在读到正亮度时更新快照,
  读失败/被 launcher 抢先写 0 时保留上一次真实值(不再是 (br>0)?br:60)。
- panel ON/OFF 日志带 src=settings|snapshot|untouched|safe-low 与 user/saved/cur,
  真机一眼看出用的是哪一支。
- 背光路径加 MEOW_BACKLIGHT env 测试钩子(与 MEOW_SETTINGS_FILE 同款,真机无 env)。
- test_wd_fw.c 新增 21 条用例(决策 7 + settings 解析/重试 6 + panel OFF/ON 端到端 8);
  端到端用例真的读写背光文件,复现「settings=1 → 亮屏写 1」而不是 60。
- 重编 meow_watchdog:mipsel 静态 MIPS32r2 ELF,md5 ad8c1f2b…,0 warning。

验证:./build_watchdog.sh host → 38 pass / 0 fail;交叉编译(-Os -Wall -Wextra -static)
0 warning,产物静态 MIPS32r2。
生效条件:meow_watchdog 在 squashfs /usr/bin,必须刷固件(或等价的重打包流程)才生效。
后续(不在本单 scope):scripts/build/fw_content.conf 第 32 行仍 pin 旧 md5 553840cd…,
不更新会让 build_fw_apps.sh 的强校验失败。
2026-11-05 21:32:27 +08:00

927 lines
45 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_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 <app_pid> [--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) -> 音量收编 v2(FUTURE_AUDIO 事项三):首选发 audiod 权威
* MSG_AUDIO_VOLUME(audiod 写 DAC numid=1/2 + DSD_VOLUME + 持久化
* settings.conf,同源同键单写者);audiod 未起(HibyOS/开机早期)
* 才回退本地 amixer 直写兜底 + 本地持久化。
* 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿
* (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。
* 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。
* (8s 强关的「防误触 / pm_interacted」逻辑在 launcher 的 show_power_menu 内,此进程只做兜底。)
* - M2.86(226) SDK 框架 App 让位:启动时经 /proc/<pid>/exe 读同目录 app.json 的 framework
* (true|1|yes)。判定为 SDK 框架 App -> 音量/媒体键让位 SDK 输入层(meow_ui_input.c 已消化,
* 防同一次按键双步进);电源仍由本进程兜底(SDK 对电源是 no-op)。裸自绘 App(framework=0/
* 无 framework)或 app.json 不可读 -> 音量/媒体键仍由本进程兜底(不造成功能真空)。
* - M20 亮屏亮度:settings.conf 的 display.brightness(clamp 到 brightness_min..100)为唯一
* 权威;settings 瞬时不可读用**真实观测快照**;两者都未知时**不写背光**(不再回退固定 60,
* 那会覆盖 launcher 刚设好的用户值 → 开机/亮屏跳 60)。面板已黑且无任何值来源才写安全低亮。
* 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 <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <poll.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#include <stdint.h>
#include <time.h>
#include "meow_ipc.h" /* MSG_AUDIO_VOLUME/MSG_ACK/帧格式(音量收编 v2:首选 audiod 权威) */
/* 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 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_<bootts>.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.86(226):1=当前前台 App 为 SDK 框架 App(音量/媒体键让位 SDK;M2.87 起左缘返回
* 手势 SIGUSR1 也照样让位 SDK——框架 App 经 SDK on_swipe 处理返回)。在 handle_touch 前声明。 */
static int g_fw_app = 0;
/* ---- M2.22(A)+ M2.24 电源键接管:息屏/亮屏 + 长按菜单/强关 ---- */
static int wd_fbfd = -1; /* fb0 fd(main 里打开,供 FBIOBLANK/FBIOPAN 用) */
/* M20(远古 bug 回归:开机/亮屏背光跳到固定 60):0 是「未知」哨兵,不是亮度值
* (0 = 面板全黑/息屏,任何情况下都不是有效用户亮度)。旧代码让 60 同时表达
* 「未知快照」和「兜底值」,于是一旦没有可信来源就把 60 写进背光,覆盖 launcher
* 刚按 display.brightness 设好的用户值(真机 0.8.46:用户亮度 1,亮屏仍写 60)。 */
#define WD_BRI_UNKNOWN 0
/* 唯一保留的固定亮度:settings 读不到 + 快照未知 + **当前亮度确实是 0**(面板黑)时才用。
* 这条退路上不存在任何可保留的有效用户值,不写就是永久黑屏;取低亮(≈10% of
* max_brightness 101)而不是 60 —— 宁可暗到用户手动调亮,也不要夜里突然刺眼。
* 注意 60 是固件 S11jpeg_display_shell 画完开机 logo 后写死的值(/etc/init.d,不在本仓库),
* 本进程任何分支都不再写 60。 */
#define WD_BRI_SAFE_LOW 10
/* M20 短期修复 A:settings 读取重试。settings.conf 由 launcher/audiod 以
* 「写 .tmp + rename」原子替换,开机早期 /usr/data(UBIFS) 也可能尚未就绪,
* fopen 存在瞬时失败窗口。3 次 × 10ms 只覆盖该窗口:最坏总阻塞 (3-1)*10 = 20ms,
* 且只在失败时发生(成功路径 0 开销)。亮屏轮询粒度本身是 REFRESH_MS=100ms,
* 20ms 在「按键→亮屏」路径上不可感知(见 M20 报文)。 */
#define WD_SETTINGS_TRIES 3
#define WD_SETTINGS_GAP_MS 10
static int wd_saved_bri = WD_BRI_UNKNOWN; /* 熄屏前亮度快照;未知=WD_BRI_UNKNOWN(不再用 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 强关已发(防重复) */
/* 背光 sysfs 路径(缺省 BACKLIGHT;env MEOW_BACKLIGHT 覆盖,供宿主测试隔离——与
* MEOW_SETTINGS_FILE 同款测试钩子,真机无 env 走默认,行为不变)。 */
static const char *wd_backlight_path(void)
{
const char *e = getenv("MEOW_BACKLIGHT");
return (e && e[0]) ? e : BACKLIGHT;
}
static int read_brightness(void)
{
int fd = open(wd_backlight_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 void write_brightness(int val)
{
char buf[8];
int l = snprintf(buf, sizeof buf, "%d\n", val);
int fd = open(wd_backlight_path(), O_WRONLY);
if (fd < 0) return;
if (write(fd, buf, l) < 0) wd_log("backlight write %d: %s", val, strerror(errno));
close(fd);
}
/* settings.conf 路径(缺省 /usr/data/meow/settings.conf;env MEOW_SETTINGS_FILE 覆盖,
* 供宿主测试隔离——与 audiod MEOW_AUDIO_SETTINGS 同款测试钩子,真机无 env 走默认)。 */
static const char *wd_settings_path(void)
{
const char *e = getenv("MEOW_SETTINGS_FILE");
return (e && e[0]) ? e : MEOW_SETTINGS_FILE;
}
/* M6.23:亮屏恢复亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness),
* 而非 wd_saved_bri 快照——该快照可能被污染:panel OFF 前 read_brightness() 可能读到 launcher
* 先写好的 0(launcher meow_panel_off 已 write_brightness(0)),旧代码 `(br>0)?br:60` 便落到
* 默认 60;panel ON 恢复 60 与 launcher 按用户设置恢复 7 竞态 → 亮屏亮度跳变(真机「跳高到 60 /
* 又降回 7」,2026-09-03)。旧公式里的 `:60` 已在 M20 移除。
* 与 launcher meow_settings_brightness_clamped() 同源(同文件、同 clamp:下限 display.brightness_min
* 默认 1=极暗但可见、上限 100=防闪屏);本进程自身 wd_vol_load 早已读该文件,不引入 launcher 全量
* 依赖,保持 watchdog 独立性。返回 clamped 值;settings 文件不可读(S70 早启动 /usr/data 未就绪)
* M20 起:fopen 失败做 WD_SETTINGS_TRIES 次、间隔 WD_SETTINGS_GAP_MS 的短重试,
* 覆盖 settings.conf 原子替换 / 开机早期 /usr/data 未就绪的瞬时窗口;仍失败才返回 -1。
* 只重试 open:rename 是原子的,已打开的 fd 指向旧 inode、内容完整,读内容无需重试。
* 返回 -1 时调用方**不再**回退「默认 60」,取值语义统一由 wd_bri_decide() 决定。 */
static int wd_settings_brightness(void)
{
FILE *f = NULL;
for (int i = 0; i < WD_SETTINGS_TRIES; i++) {
f = fopen(wd_settings_path(), "r");
if (f) break;
if (i + 1 < WD_SETTINGS_TRIES) usleep(WD_SETTINGS_GAP_MS * 1000);
}
if (!f) return -1;
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
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, "display.brightness_min")) { if (iv >= 1 && iv <= 100) mn = iv; }
else if (!strcmp(k, "display.brightness")) { b = iv; }
}
fclose(f);
if (b < mn) b = mn;
if (b > 100) b = 100;
return b;
}
/* 强制 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);
}
/* 亮屏亮度决策(纯函数:宿主单测直接断言,见 test_wd_fw.c)。
* 返回 >0 = 要写入的背光值;返回 -1 = **不写背光**(保持当前值)。
* *src 输出取值来源,原样写进日志供真机排查:
* settings settings.conf 的 display.brightness(已 clamp)—— 唯一权威
* snapshot 本次运行期熄屏前读到的真实亮度(>0 才算数)—— settings 瞬时不可读时的过渡值
* untouched settings 与快照都未知、但面板已亮 → 不覆盖现场(M20 核心修复)
* safe-low settings/快照都未知且面板当前为 0(黑)→ 点亮,用低亮而不是 60
* 优先级:settings → 快照(真实近似值)→ 保持现状 → 安全低亮。
* 任何分支都不再写「固定 60」:60 只在固件 S11 画完开机 logo 后写一次(固件侧问题,非本进程)。 */
static int wd_bri_decide(int user, int saved, int cur, const char **src)
{
if (user > 0) { *src = "settings"; return user; } /* 用户设置优先 */
if (saved > 0) { *src = "snapshot"; return saved; } /* 真实观测到的近似值 */
if (cur > 0) { *src = "untouched"; return -1; } /* 已有可见亮度:绝不覆盖 */
*src = "safe-low"; return WD_BRI_SAFE_LOW; /* 面板黑 + 无任何值来源 */
}
static void wd_panel_off(void)
{
int br = read_brightness();
/* M20:快照只记「真实观测到的正亮度」。br<=0(读失败,或 launcher meow_panel_off 抢先
* 写成 0)时保留上一次已知真实值,绝不写死 60 —— 否则 60 会在下次亮屏被当成用户值回放
* (远古 bug 的燃料)。真的一次都没观测到正亮度时保持 WD_BRI_UNKNOWN。 */
if (br > 0) wd_saved_bri = br;
write_brightness(0);
if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 4); /* 面板下电 + 挂起 goodix 触摸 */
wd_log("power: panel OFF (was %d -> 0, blank 4, saved=%d)", br, wd_saved_bri);
}
static void wd_panel_on(void)
{
if (wd_fbfd >= 0) ioctl(wd_fbfd, FBIOBLANK, 0);
/* M6.23 + M20:用户设置为唯一权威;settings 瞬时不可读用真实快照;两者都未知时
* **不写背光**(旧代码在这里写死 60,直接覆盖 launcher 刚按 display.brightness 设好的
* 用户值 → 开机/亮屏跳 60,真机 0.8.46 实证)。取值分支见 wd_bri_decide()。 */
int user = wd_settings_brightness(); /* >0=用户设置;-1=不可读 */
int cur = read_brightness(); /* 当前系统背光:判定「不写」是否安全 + 日志 */
const char *src = "?";
int restore = wd_bri_decide(user, wd_saved_bri, cur, &src);
if (restore > 0) write_brightness(restore);
wd_pan_buf0_force(); /* 唤醒后强制回 buffer0 */
if (restore > 0)
wd_log("power: panel ON (blank 0 + brightness %d src=%s user=%d saved=%d cur=%d)",
restore, src, user, wd_saved_bri, cur);
else
wd_log("power: panel ON (blank 0 + brightness UNTOUCHED cur=%d src=%s user=%d saved=%d)",
cur, src, user, 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(wd_settings_path(), "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 同路径。
* ⚠️ 音量收编 v2 兜底函数:仅 audiod 未起(HibyOS/开机早期)时由 wd_vol_adj 回退调用;
* MeowOS 常态一律走 wd_audiod_set_volume(audiod 唯一权威,防多写者漂移)。
* 锥度为定案平方锥度(与 audiod dac_vol_set 同款,勿改语义:CS43198=衰减量,0 最响/255 静音)。 */
static void wd_vol_write(int pct)
{
int t = 100 - pct, dac;
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
if (dac > 255) dac = 255;
if (dac < 0) dac = 0; /* CS43198=衰减量(0最响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);
}
/* ---- 音量收编 v2(FUTURE_AUDIO 事项三):首选 audiod 权威 MSG_AUDIO_VOLUME ----
* watchdog 为 per-App 小进程,audiod sock 0666 无鉴权可直接连。一次性短连接(按键 rare,
* 无常驻连接/陈旧态负担):非阻塞 connect + poll 300ms,SO_SNDTIMEO/RCVTIMEO 1s,发 i32 pct
* 等 MSG_ACK。audiod 侧已异步化(末值去抖,ACK=已应用+已持久化),正常 <<1s 返回。
* 返回 0=audiod 已受理(DAC 写+持久化均由 audiod 完成,本进程不再写 settings 防双写);
* -1=audiod 缺席/失败(调用方回退 wd_vol_write 本地直写 + wd_vol_save 本地持久化)。 */
#define WD_AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock" /* 同 audiod AUD_SOCK_DEFAULT */
#define WD_AUD_CONN_TMO_MS 300
#define WD_AUD_IO_TMO_MS 1000
static const char *wd_aud_sock(void)
{
const char *e = getenv("MEOW_AUDIO_SOCK"); /* 与 SDK sock_path() 同款 env(宿主测覆盖) */
return (e && e[0]) ? e : WD_AUD_SOCK_DEFAULT;
}
static int wd_wr_all(int fd, const void *p, size_t n)
{
const uint8_t *b = p; size_t o = 0;
while (o < n) { ssize_t r = write(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } o += (size_t)r; }
return 0;
}
static int wd_rd_all(int fd, void *p, size_t n)
{
uint8_t *b = p; size_t o = 0;
while (o < n) { ssize_t r = read(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } if (r == 0) return -1; o += (size_t)r; }
return 0;
}
static int wd_audiod_set_volume(int pct)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", wd_aud_sock());
int r = connect(fd, (struct sockaddr *)&a, sizeof a);
if (r < 0 && errno == EINPROGRESS) {
struct pollfd p;
p.fd = fd; p.events = POLLOUT; p.revents = 0;
if (poll(&p, 1, WD_AUD_CONN_TMO_MS) > 0) {
int err = 0;
socklen_t el = sizeof err;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
} else {
r = -1; /* 超时/错误 */
}
}
if (r < 0) { close(fd); return -1; }
if (fl >= 0) fcntl(fd, F_SETFL, fl); /* 回阻塞模式;读写超时由 SO_*TIMEO 兜底 */
struct timeval tv;
tv.tv_sec = WD_AUD_IO_TMO_MS / 1000; tv.tv_usec = (WD_AUD_IO_TMO_MS % 1000) * 1000;
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
/* 发 MSG_AUDIO_VOLUME(i32 pct);req_id 任意(一次性连接单请求) */
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
uint32_t magic = MEOW_IPC_MAGIC, flen = sizeof(int32_t), freq = 1;
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
memcpy(frame + 0, &magic, 4);
memcpy(frame + 4, &ver, 2);
memcpy(frame + 6, &type, 2);
memcpy(frame + 8, &freq, 4);
memcpy(frame + 12, &flen, 4);
int32_t p32 = pct;
memcpy(frame + MEOW_IPC_HDR_LEN, &p32, sizeof p32);
if (wd_wr_all(fd, frame, sizeof frame) != 0) { close(fd); return -1; }
/* 等 MSG_ACK(有界:RCVTIMEO;audiod 异步执行点应用后回 ACK) */
uint8_t hdr[MEOW_IPC_HDR_LEN];
if (wd_rd_all(fd, hdr, sizeof hdr) != 0) { close(fd); return -1; }
uint32_t rmagic, rlen;
uint16_t rver, rtype;
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2); memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION || rtype != MSG_ACK || rlen > 64) { close(fd); return -1; }
uint8_t pl[64];
if (rlen && wd_rd_all(fd, pl, rlen) != 0) { close(fd); return -1; }
close(fd);
return 0;
}
/* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 ——
* 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。
* 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换),
* 与 SDK meow_ui_vol_set 同款语义。 */
static void wd_vol_save(void)
{
const char *path = wd_settings_path();
if (strcmp(path, MEOW_SETTINGS_FILE) == 0) /* 仅默认路径补目录(env 覆盖=宿主测,目录自备) */
system("mkdir -p /usr/data/meow 2>/dev/null");
char tmp[300];
snprintf(tmp, sizeof tmp, "%s.tmp", path);
FILE *in = fopen(path, "r");
FILE *out = fopen(tmp, "w");
if (!out) { if (in) fclose(in); wd_log("vol: save open tmp fail"); return; }
int found = 0;
char line[256];
while (in && fgets(line, sizeof line, in)) {
char *eq = strchr(line, '=');
char *k = line;
while (*k == ' ' || *k == '\t') k++;
int n = eq ? (int)(eq - k) : 0;
while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--;
if (eq && n == 12 && strncmp(k, "audio.volume", 12) == 0) {
if (!found) fprintf(out, "audio.volume = %d\n", wd_vol); /* 命中:只写一次(去重) */
found = 1;
} else {
fputs(line, out);
}
}
if (in) fclose(in);
if (!found) fprintf(out, "audio.volume = %d\n", wd_vol);
fclose(out);
if (rename(tmp, path) != 0) { remove(tmp); wd_log("vol: save rename fail"); return; }
wd_log("vol: saved volume=%d(max=%d,step=%d)", wd_vol, wd_vol_max, wd_vol_step);
}
/* ---- 插孔消抖(2026-09-01,与 launcher audio.c 同款,真机日志实证)----
* 4.4/3.5 耳机插拔的电气瞬态在音量键 ADC(jz_adc_aux,event2)上产生幽灵按下
* (launcher 侧实证:VOL_UP 假按住 → ramp 24%→上限 50,紧随 audiod「jack changed」)。
* /sys/class/switch/{headset,balance}/state 任一变化后 WD_JACK_DEBOUNCE_MS 内忽略音量键
* 边沿(插拔耳机时没人按音量,误伤可忽略)。读文件走节流缓存,不每圈读。 */
#define WD_JACK_HS_STATE "/sys/class/switch/headset/state"
#define WD_JACK_BAL_STATE "/sys/class/switch/balance/state"
#define WD_JACK_POLL_MS 500 /* 节流读取间隔(主循环 100ms,5 圈一读) */
#define WD_JACK_DEBOUNCE_MS 800 /* 插孔变化后忽略音量键边沿的窗口 */
static int wd_jack_combo = -1; /* hs*2+bal;-1=未初始化/节点缺失 */
static unsigned long long wd_jack_poll_at = 0;
static unsigned long long wd_jack_changed_at = 0; /* 0=未检测到变化 */
static int wd_read_int_file(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 void wd_jack_tick(void)
{
unsigned long long now = now_ms();
if (now - wd_jack_poll_at < WD_JACK_POLL_MS) return;
wd_jack_poll_at = now;
int hs = wd_read_int_file(WD_JACK_HS_STATE);
int bal = wd_read_int_file(WD_JACK_BAL_STATE);
if (hs < 0 && bal < 0) return; /* 节点缺失:不更新缓存,不误判变化 */
int c = (hs > 0 ? 2 : 0) | (bal > 0 ? 1 : 0);
if (wd_jack_combo >= 0 && c != wd_jack_combo) {
wd_jack_changed_at = now;
wd_log("jack state changed (%d -> %d), vol-key debounce %dms",
wd_jack_combo, c, WD_JACK_DEBOUNCE_MS);
}
wd_jack_combo = c;
}
static int wd_jack_debounce(void)
{
return wd_jack_changed_at &&
(now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS);
}
/* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。
* 音量收编 v2:首选 audiod 权威(IPC;DAC 写+持久化 audiod 完成);audiod 缺席才本地
* amixer 直写 + 本地持久化兜底(wd_vol 本地缓存仅作步进基数,两路都维护)。 */
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;
if (wd_audiod_set_volume(wd_vol) == 0) {
wd_log("volume %s -> %d (via audiod)", dir > 0 ? "UP" : "DOWN", wd_vol);
return;
}
wd_vol_write(wd_vol); /* 兜底:仅 audiod 未起(HibyOS/开机早期) */
wd_vol_save();
wd_log("volume %s -> %d (local amixer fallback, audiod absent)", 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。
* M2.87:SIGUSR1 只发给「裸沉浸 App」(framework=0/无 framework 的自绘 App,
* 其自己处理 SIGUSR1 当返回事件)。SDK 框架 App(g_fw_app=1,如 hello/files/ncm)
* 自己经 SDK on_swipe 处理左缘返回/顶部下拉(meow_ui_input.c),若 watchdog 也发
* SIGUSR1 → 默认动作杀死 App(ncm `signal=16` 崩溃根因)→ 本进程不发、避免双处理/误杀。
* 裸沉浸判定 = 非框架 且 /tmp/meowapp_state=1(app.json immersive 声明,launcher 写)。 */
if (down_x < EDGE_W && (last_x - down_x) > SWIPE_DX) {
if (!g_fw_app && immersive) {
wd_log("swipe -> SIGUSR1 (bare immersive, framework=%d)", g_fw_app);
kill(app_pid, SIGUSR1);
} else {
wd_log("swipe ignored (framework=%d immersive=%d) -> SDK owns back", g_fw_app, immersive);
}
}
}
/* ---- M2.86(226) SDK 框架 App 判定:app.json framework=1 -> 音量/媒体键让位 SDK ----
* 背景:App 前台时 SDK 输入层(meow_ui_input.c)已消化音量/媒体键;若 watchdog 也消化,
* 同一次物理按键会步进两次(冗余双处理)。判定「SDK 框架 App 在跑」用 app.json 的
* framework=true|1|yes —— 这是同类候选中更稳的一个:launcher 启动应用前已读同一 app.json
* 并成功 exec(app.json 必然可读、结果确定);SDK 侧无稳定「框架注册」标记
* (/tmp/meow_app_activity 交互节流、/tmp/meowapp_sbar 仅状态栏模式变化时写),不宜作判定源。
* 判定为框架 App -> watchdog 对音量/媒体键让位(交给 SDK),电源仍由本进程兜底(SDK 对电源
* 是 no-op,见 meow_ui_input.c);裸自绘 App(framework=0/无 framework)或 app.json 不可读 ->
* watchdog 保持兜底,绝不造成功能真空。 */
/* 极简 JSON/扁平值提取:与 launcher util.c json_str 同语义(键前缀须独立 + ':'/'=' + 可选引号)。 */
static int wd_json_str(const char *buf, const char *key, char *out, int olen)
{
out[0] = 0;
if (!buf || !key || olen <= 0) return 0;
const char *p = strstr(buf, key);
if (!p) return 0;
if (p > buf) { /* 键前缀须独立:防 "x_framework":1 被误命中为 framework */
unsigned char pc = (unsigned char)p[-1];
if ((pc >= 'a' && pc <= 'z') || (pc >= 'A' && pc <= 'Z') ||
(pc >= '0' && pc <= '9') || pc == '_' || pc == '.')
return 0;
}
p += strlen(key);
if (*p == '"') p++; /* JSON 键右引号 */
while (*p == ' ' || *p == '\t') p++; /* 键与 ':'/'=' 间空白(宽松容错) */
while (*p == ':' || *p == '=') p++;
while (*p == ' ' || *p == '\t') p++;
if (*p == '"') p++; /* 值左引号 */
int i = 0;
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' &&
*p != '}' && *p != ']' && *p != ':' && *p != ' ' && *p != '\t' && i < olen - 1)
out[i++] = *p++;
out[i] = 0;
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
return i;
}
/* 读 <dir>/app.json 的 framework 字段(true|1|yes = 1)。返回 1=SDK 框架 App。 */
static int wd_app_is_framework(const char *dir)
{
char path[576];
int n = snprintf(path, sizeof path, "%s/app.json", dir);
if (n < 0 || n >= (int)sizeof path) return 0;
FILE *f = fopen(path, "r");
if (!f) return 0;
int fw = 0;
char buf[256];
while (fgets(buf, sizeof buf, f)) {
if (!strstr(buf, "framework")) continue;
char v[16] = "";
if (wd_json_str(buf, "framework", v, sizeof v) > 0 &&
(!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes")))
fw = 1;
break; /* framework 每 app.json 至多一次,读到即定 */
}
fclose(f);
return fw;
}
/* 经 /proc/<pid>/exe 反查应用安装目录(exec 与 app.json 同目录),再判 framework。 */
static int wd_framework_from_pid(int pid)
{
char exe[576], link[64];
snprintf(link, sizeof link, "/proc/%d/exe", pid);
ssize_t r = readlink(link, exe, sizeof exe - 1);
if (r < 0 || r >= (ssize_t)sizeof exe) return 0;
exe[r] = 0;
char *slash = strrchr(exe, '/');
if (slash) *slash = 0; /* 目录 = exec 所在目录(app.json 同目录) */
if (!exe[0]) return 0;
return wd_app_is_framework(exe);
}
#ifndef WD_NO_MAIN
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "usage: %s <app_pid> [--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 */
/* M2.86(226):SDK 框架 App 判定(app.json framework=1)-> 音量/媒体键让位 SDK。 */
g_fw_app = wd_framework_from_pid(app_pid);
wd_log("app_pid=%d sim_tap=%d sim_swipe=%d framework=%d%s",
app_pid, sim_tap, sim_swipe, g_fw_app,
g_fw_app ? " -> yield vol/media to SDK" : " -> watchdog owns vol/media");
/* 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) {
/* M2.86(226):SDK 框架 App 前台 -> 音量键让位 SDK(防双步进)。
* 插孔消抖:插拔瞬态的 ADC 幽灵按下直接忽略。 */
if (val == 1 && !g_fw_app) {
if (wd_jack_debounce()) wd_log("volume UP ignored (jack debounce)");
else wd_vol_adj(+1); /* M2.24:音量+ = 系统音量 */
}
} else if (type == EV_KEY && code == KEY_VOLUMEDOWN) {
if (val == 1 && !g_fw_app) {
if (wd_jack_debounce()) wd_log("volume DOWN ignored (jack debounce)");
else wd_vol_adj(-1); /* M2.24:音量- = 系统音量 */
}
} else if (type == EV_KEY && (code == KEY_PLAYPAUSE || code == KEY_PREVIOUSSONG ||
code == KEY_NEXTSONG || code == KEY_PREVIOUS)) {
/* M2.86(226):框架 App 前台 -> 媒体键让位 SDK(接媒体会话路由)。 */
if (val == 1 && !g_fw_app)
wd_log("media key captured (code %d) -> consumed, no route", code);
}
}
}
}
wd_power_tick(); /* M2.24:电源长按检测(3s 菜单 / 8s 强关 / 菜单期让位) */
wd_jack_tick(); /* 插孔状态节流轮询(消抖音量键幽灵按下) */
elapsed += 100;
}
return 0;
}
#endif /* !WD_NO_MAIN */