753 lines
35 KiB
C
753 lines
35 KiB
C
/* 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) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。
|
||
* 插孔消抖:/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 不可读 -> 音量/媒体键仍由本进程兜底(不造成功能真空)。
|
||
* 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 <stdint.h>
|
||
#include <time.h>
|
||
|
||
/* 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 用) */
|
||
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);
|
||
}
|
||
|
||
/* 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)),`wd_saved_bri=(br>0)?br:60` 便落到
|
||
* 默认 60;panel ON 恢复 60 与 launcher 按用户设置恢复 7 竞态 → 亮屏亮度跳变(真机「跳高到 60 /
|
||
* 又降回 7」,2026-09-03)。
|
||
* 与 launcher meow_settings_brightness_clamped() 同源(同文件、同 clamp:下限 display.brightness_min
|
||
* 默认 1=极暗但可见、上限 100=防闪屏);本进程自身 wd_vol_load 早已读该文件,不引入 launcher 全量
|
||
* 依赖,保持 watchdog 独立性。返回 clamped 值;settings 文件不可读(S70 早启动 /usr/data 未就绪)
|
||
* → 返回 -1,调用方回退 wd_saved_bri(保持旧行为,防开机竞态)。 */
|
||
static int wd_settings_brightness(void)
|
||
{
|
||
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
|
||
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);
|
||
}
|
||
|
||
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);
|
||
/* M6.23:恢复亮度以**用户设置**为准(wd_settings_brightness),settings 不可用时才回退
|
||
* wd_saved_bri 快照(再退默认 60)——与 launcher meow_panel_on 同语义,杜绝「亮屏恢复 60
|
||
* 与 launcher 恢复 7 竞态 → 跳变」。 */
|
||
int b = wd_settings_brightness();
|
||
int restore = (b > 0) ? b : (wd_saved_bri > 0 ? wd_saved_bri : 60);
|
||
write_brightness(restore);
|
||
wd_pan_buf0_force(); /* 唤醒后强制回 buffer0 */
|
||
wd_log("power: panel ON (blank 0 + brightness %d, user=%d saved=%d)", restore, b, 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 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);
|
||
}
|
||
|
||
/* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 ——
|
||
* 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。
|
||
* 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换),
|
||
* 与 SDK meow_ui_vol_set 同款语义。 */
|
||
static void wd_vol_save(void)
|
||
{
|
||
system("mkdir -p /usr/data/meow 2>/dev/null");
|
||
const char *path = MEOW_SETTINGS_FILE;
|
||
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 到极大/最小)。 */
|
||
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。
|
||
* 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 */
|