diff --git a/patch_v2/meow_route/build_route.sh b/patch_v2/meow_route/build_route.sh new file mode 100755 index 0000000..0c3916a --- /dev/null +++ b/patch_v2/meow_route/build_route.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# build_route.sh — meow_route 交叉编译(mipsel 静态)+ 宿主编译可选。 +# +# 目标:R3 Pro II / MeowOS(mipsel-linux-muslsf,静态)。仅 POSIX + Linux 基接口, +# 无 launcher/IPC 依赖(不链 meow_ipc.h),单文件、体积小。 +# +# 用法: +# ./build_route.sh # 交叉编译 → patch_v2/meow_route/meow_route(mipsel 静态) +# ./build_route.sh host # 宿主编译 → patch_v2/meow_route/meow_route_host +set -e +cd "$(dirname "$0")" +ROOT="$HOME/Project/R3ProII" +CC="$ROOT/.tools/zig-mipsel-cc" + +if [ "$1" = "host" ]; then + HOST_CC="${CC_HOST:-cc}" + "$HOST_CC" -Os -Wall -Wextra meow_route.c -o meow_route_host + echo "== host build ==" + ls -la meow_route_host + file meow_route_host + exit 0 +fi + +"$CC" -Os -Wall -Wextra -static meow_route.c -o meow_route +echo "== meow_route (mipsel static) ==" +ls -la meow_route +md5sum meow_route +file meow_route diff --git a/patch_v2/meow_route/meow_route b/patch_v2/meow_route/meow_route new file mode 100755 index 0000000..c26ebac Binary files /dev/null and b/patch_v2/meow_route/meow_route differ diff --git a/patch_v2/meow_route/meow_route.c b/patch_v2/meow_route/meow_route.c new file mode 100644 index 0000000..2300622 --- /dev/null +++ b/patch_v2/meow_route/meow_route.c @@ -0,0 +1,278 @@ +#define _GNU_SOURCE + +/* meow_route.c — MeowOS 音频输出路由自动跟随插孔专项服务。 + * + * 背景:R3 Pro II 有 3.5mm 单端 + 4.4mm 平衡口。输出路由由 ALSA + * `amixer cset numid=9 ` 控制:0=空闲 1=lineout 2=3.5 headset + * 3=4.4 balance 4=Type-C SPDIF(0/5 危险勿写)。插入检测: + * /sys/class/switch/headset/state(3.5)、/sys/class/switch/balance/state(4.4)。 + * MeowOS 若不设路由 → App 播放无声(真机踩过:插 4.4 但路由在 idle=0)。 + * + * 职责:周期(默认 1s,轻量;`--ms` 可覆盖供测试)读两个 switch state; + * 插入变化时自动路由:4.4 → port=3,3.5 → port=2(4.4 优先若同插), + * 都不插 → 不动、保持上次路由。写后 cget 读回校验,失败记日志不重试风暴。 + * + * 双形态: + * - svcd 以**空参**拉起(conf `args=` 空)→ 常驻轮询守护(不 exit,崩溃由 svcd on-fail 收)。 + * - 测试/自检 CLI:`meow_route [--ms M] [--iters N]` 跑 N 轮后退出(N=0 常驻)。 + * + * 测试挂钩(宿主模拟用,默认取真机路径): + * MEOW_AMIXER amixer 可执行(默认 /usr/bin/amixer) + * MEOW_HEADSET_SW 3.5 插孔 switch state 路径 + * MEOW_BALANCE_SW 4.4 平衡 switch state 路径 + * + * 编译(mipsel 静态,仅 POSIX+Linux,无 launcher/IPC 依赖):build_route.sh。 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ROUTE_POLL_MS_DEF 1000 /* 周期轮询,秒级即可(检测 3.5/4.4 插拔) */ +#define AMIXER_TMO_MS 1500 /* amixer 子进程超时,防挂 */ +#define PIPE_MAX 4096 + +/* ALSA Output Port Switch(numid=9)端口值:0=空闲 1=lineout 2=3.5 3=4.4 4=Type-C SPDIF */ +#define CTL_NUMID 9 +#define PORT_BALANCE 3 +#define PORT_HEADSET 2 +#define PORT_NONE (-1) /* 都不插 → 不动、保持上次 */ + +/* 路径通过 env 可覆盖(宿主/模拟测试),默认真机路径 */ +static const char *g_amixer = "/usr/bin/amixer"; +static const char *g_headset = "/sys/class/switch/headset/state"; +static const char *g_balance = "/sys/class/switch/balance/state"; + +static volatile sig_atomic_t g_shutdown = 0; + +/* ---- 日志(走 stderr,supervisor/svcd 捕获;与 meow_svc_perf/power 同纪律) ---- */ +static void svc_log(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "[meow_route] "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); +} + +static void on_sig(int sig) +{ + (void)sig; + g_shutdown = 1; +} + +/* ---- 读单个 switch 文件(0/1=插孔状态,-1=读失败) ---- */ +static int read_switch(const char *path) +{ + char buf[32]; + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + ssize_t n = read(fd, buf, sizeof buf - 1); + close(fd); + if (n <= 0) return -1; + buf[n] = 0; + return atoi(buf); +} + +/* ---- popen 限时读(fork+exec+非阻塞 poll,杀子进程防挂) ---- */ +static char *popen_read_tmo(int tmo_ms, const char *cmd) +{ + int p[2]; + if (pipe(p) < 0) return NULL; + pid_t pid = fork(); + if (pid < 0) { close(p[0]); close(p[1]); return NULL; } + if (pid == 0) { dup2(p[1], 1); close(p[0]); close(p[1]); execl("/bin/sh", "sh", "-c", cmd, (char *)0); _exit(127); } + close(p[1]); + fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK); + char *buf = malloc(PIPE_MAX); + if (!buf) { close(p[0]); waitpid(pid, NULL, 0); return NULL; } + size_t off = 0; + int remain = tmo_ms; + while (remain > 0 && off < PIPE_MAX - 1) { + struct pollfd pf = { p[0], POLLIN, 0 }; + int r = poll(&pf, 1, remain); + if (r < 0) { if (errno == EINTR) { if (g_shutdown) break; continue; } break; } + if (r == 0) break; + ssize_t rd = read(p[0], buf + off, PIPE_MAX - 1 - off); + if (rd == 0) break; + if (rd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; break; } + off += (size_t)rd; + } + close(p[0]); + waitpid(pid, NULL, 0); + buf[off] = 0; + return buf; +} + +/* 从 `amixer cget` 输出解析当前 port 值。cget 有两处 "values=": + * 首行元数据 `values=1`(数量),末行 `: values=<当前值>`——取末行。 */ +static int parse_amixer_value(const char *out) +{ + if (!out) return -1; + const char *ln = out; + while (ln && *ln) { + const char *nl = strchr(ln, '\n'); + const char *p = ln; + while (*p == ' ' || *p == '\t') p++; + if (p[0] == ':' && strncmp(p, ": values=", 9) == 0) { + return atoi(p + 9); + } + if (!nl) break; + ln = nl + 1; + } + return -1; +} + +/* 读当前输出端口(numid=9);读失败/解析失败返回 -1 */ +static int amixer_get_port(void) +{ + char cmd[160]; + snprintf(cmd, sizeof cmd, "%s cget numid=%d 2>/dev/null", g_amixer, CTL_NUMID); + char *out = popen_read_tmo(AMIXER_TMO_MS, cmd); + if (!out) return -1; + int v = parse_amixer_value(out); + free(out); + return v; +} + +/* 发出 cset 写(输出仅参考,权威以 cget 读回为准);执行失败返回 -1 */ +static int amixer_set_port(int port) +{ + char cmd[160]; + snprintf(cmd, sizeof cmd, "%s cset numid=%d %d 2>/dev/null", g_amixer, CTL_NUMID, port); + char *out = popen_read_tmo(AMIXER_TMO_MS, cmd); + if (!out) return -1; + free(out); + return 0; +} + +static const char *port_name(int port) +{ + switch (port) { + case PORT_BALANCE: return "4.4 balance"; + case PORT_HEADSET: return "3.5 headset"; + default: return "idle"; + } +} + +/* 一次评估:读两 switch,若(有插入且状态变化或与上次处理目标不同)则写路由并读回校验。 + * last_hb/last_bal 为上次观测记忆(evaluate 内更新,仅成功读才更新;初始 -1, + * 首次强制评估 → 覆盖"开机即已插"场景)。 + * last_target = 上次对当前插孔态**决策过**的目标端口(成功/已接/失败都记), + * 状态不变时不重复尝试 → 满足"写失败不重试风暴"。 */ +static void evaluate(int *last_hb, int *last_bal, int *last_target) +{ + int hb = read_switch(g_headset); + int bal = read_switch(g_balance); + + /* 读失败:未知状态,不动端口,不更新记忆,待下轮重试(防误路由) */ + if (hb < 0 || bal < 0) { + svc_log("switch read err: headset=%d balance=%d (skip this cycle)", hb, bal); + return; + } + int prev_hb = *last_hb, prev_bal = *last_bal; + *last_hb = hb; + *last_bal = bal; + + int target; + if (bal == 1) target = PORT_BALANCE; /* 4.4 优先(同插) */ + else if (hb == 1) target = PORT_HEADSET; + else target = PORT_NONE; + + if (target == PORT_NONE) { + /* 都不插 → 不动、保持上次。日志仅在状态翻转时给一次,避免每秒刷屏 */ + if (prev_hb != hb || prev_bal != bal) + svc_log("no jack (headset=%d balance=%d), keep last port", hb, bal); + *last_target = PORT_NONE; + return; + } + + int changed = (prev_hb != hb || prev_bal != bal); + if (!changed && *last_target == target) { + /* 状态未变且上次已决策过该目标(成功/已接/失败)→ 不再写,防重试/cset 风暴 */ + return; + } + + int cur = amixer_get_port(); + if (cur == target) { + /* 路由已是目标(如上次设置成功/开机偶合),无需写 */ + svc_log("%s already routed (port=%d)", port_name(target), cur); + *last_target = target; + return; + } + + svc_log("jack changed -> routing to %s (wanted %d, was %d)", + port_name(target), target, cur); + if (amixer_set_port(target) < 0) { + svc_log("amixer cset exec failed (wanted %d)", target); + *last_target = target; /* 状态不变不再重试 */ + return; + } + int rb = amixer_get_port(); + if (rb == target) { + svc_log("routed ok: numid=9 -> %d (%s)", rb, port_name(target)); + } else { + /* 读回不符:记日志,不重试(下次同状态不再写;状态翻身后才再评估) */ + svc_log("route verify failed: wanted %d got %d (no retry)", target, rb); + } + *last_target = target; +} + +int main(int argc, char **argv) +{ + /* 测试挂钩:env 覆盖路径 */ + const char *e; + if ((e = getenv("MEOW_AMIXER")) && e[0]) g_amixer = e; + if ((e = getenv("MEOW_HEADSET_SW")) && e[0]) g_headset = e; + if ((e = getenv("MEOW_BALANCE_SW")) && e[0]) g_balance = e; + + int poll_ms = ROUTE_POLL_MS_DEF; + long iters = 0; /* 0=常驻 */ + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--ms") && i + 1 < argc) { poll_ms = atoi(argv[++i]); if (poll_ms < 50) poll_ms = 50; } + else if (!strcmp(argv[i], "--iters") && i + 1 < argc) { iters = atol(argv[++i]); if (iters < 0) iters = 0; } + else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + printf("meow_route [--ms MS] [--iters N] (默认常驻轮询; N>0 跑 N 轮后退出)\n"); + return 0; + } + } + + signal(SIGPIPE, SIG_IGN); + signal(SIGTERM, on_sig); + signal(SIGINT, on_sig); + + int last_hb = -1, last_bal = -1, last_target = PORT_NONE; + + svc_log("start: numid=%d poll_ms=%d amixer=%s", CTL_NUMID, poll_ms, g_amixer); + + long cycles = 0; + for (;;) { + evaluate(&last_hb, &last_bal, &last_target); + + if (iters > 0 && ++cycles >= iters) break; + if (g_shutdown) break; + + /* 分段睡眠以即时响应 SIGTERM */ + int naps = poll_ms / 200; + if (naps < 1) naps = 1; + for (int k = 0; k < naps; k++) { + if (g_shutdown) break; + struct timespec ts = { 0, 200 * 1000000L }; + nanosleep(&ts, NULL); + } + if (g_shutdown) break; + } + + svc_log("exit (iters=%ld)", iters); + return 0; +} diff --git a/patch_v2/meow_route/meow_route_host b/patch_v2/meow_route/meow_route_host new file mode 100755 index 0000000..8de2213 Binary files /dev/null and b/patch_v2/meow_route/meow_route_host differ diff --git a/patch_v2/meow_route/services.d/meow_route.conf b/patch_v2/meow_route/services.d/meow_route.conf new file mode 100644 index 0000000..d8b30b3 --- /dev/null +++ b/patch_v2/meow_route/services.d/meow_route.conf @@ -0,0 +1,16 @@ +# meow_route.conf — 音频输出路由自动跟随插孔(svcd 托管;本阶段走卡上,不刷机)。 +# 部署:TF 卡 `/usr/data/mnt/sd_0/MeowOS/services.d/meow_route.conf` + 二进制 +# `MeowOS/opt/bin/meow_route`。svcd 双源扫(板载 /usr/data/meow/services.d + TF +# MeowOS/services.d,同名板载优先);无卡时 svcd 跳过本服务,系统核心不依赖它。 +# 职责:周期读 /sys/class/switch/{headset,balance}/state,插入变化时自动 +# `amixer cset numid=9 `(4.4→3 优先于 3.5→2;都不插→不动保持上次)。 +# 写后 cget 读回校验,失败记日志不重试风暴(状态翻身后才再评估)。 +# 触发即启动(trigger=boot);子进程崩则 on-fail 拉起,防常驻守护静默丢失。 +exec=/usr/data/mnt/sd_0/MeowOS/opt/bin/meow_route +args= +enabled=1 +restart=on-fail +backoff_ms=2000 +max_restart=5 +trigger=boot +cap=