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验证通过)
This commit is contained in:
MeowOS committed 2026-08-31 15:32:13 +08:00
1 parent e8deed88bd
commit 249b6224ab
5 files changed
+322

No files matched your search

+28
View File
@@ -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
BIN
View File
Binary file not shown.
+278
View File
@@ -0,0 +1,278 @@
#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;
}
Binary file not shown.
@@ -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 <port>`(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=