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:
1 parent
e8deed88bd
commit
249b6224ab
5 files changed
+322
No files matched your search
Executable
+28
@@ -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
|
||||
Executable
BIN
Binary file not shown.
@@ -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;
|
||||
}
|
||||
Executable
BIN
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=
|
||||
Reference in new issue
Block a user