#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; }