Files
MeowOS-For-R3P2/patch_v2/meow_route/meow_route.c
T
MeowOS 249b6224ab feat(route): meow_route 音频输出路由自动跟随插孔 (agent-220) — svcd托管,1s轮询switch state,4.4优先; 只写变化+读回校验+不重试风暴; 真机已部署运行(svcd tf conf)
meow_route fa2b80be; 主机模拟状态机全过+真机cset/cget往返验证; 物理插拔端到端待用户插耳机实测
注: 223 拼音IME设计稿已出(devdocs/plan/IME_PINYIN_DESIGN.md, 静态表方案~272KB, qemu验证通过)
2026-08-31 15:32:13 +08:00

279 lines
9.8 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.
#define _GNU_SOURCE
/* meow_route.c — MeowOS 音频输出路由自动跟随插孔专项服务。
*
* 背景:R3 Pro II 有 3.5mm 单端 + 4.4mm 平衡口。输出路由由 ALSA
* `amixer cset numid=9 <port>` 控制: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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <poll.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>
#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;
}