Files
MeowOS-For-R3P2/patch_v2/meow_watchdog/meow_watchdog.c
T
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
2026-11-04 20:02:28 +08:00

861 lines
41 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 不可读 -> 音量/媒体键仍由本进程兜底(不造成功能真空)。
* 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 用) */
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);
}
/* 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)),`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(wd_settings_path(), "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(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 */