feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)

脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
AskaEth committed 2026-11-04 20:02:28 +08:00
1 parent e87ccdf061
commit 7c7d891570
59 files changed
+5305 -590

No files matched your search

+85 -14
View File
@@ -8,9 +8,10 @@
* 核心设计(用户定案,勿推翻):
* - 独占 + 抢占(后开流赢,旧流收 MSG_AUDIO_PREEMPTED 并被置 draining,PCM 丢弃);
* 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。
* - 单进程双线程(+路由线程):IO 线程(accept/读帧/解析/ring 写)、播放线程
* - 单进程四线程:IO 线程(accept/读帧/解析/ring 写)、播放线程
* (独占 ALSA 直开,从 ring 取数写 DAC,xrun PREPARE 恢复)、路由线程
* (轮询 headset/balance 插孔 switch 状态 → amixer cset numid=9)。
* (轮询 headset/balance 插孔 switch 状态 → amixer cset numid=9)、
* 音量执行线程(MSG_AUDIO_VOLUME 异步落 amixer + 持久化,末值去抖,见事项三)。
* - 格式归一 = 协商:grant_rate/ch/fmt 回客户端应产出的格式(SDK 负责 mono→stereo
* 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。
* - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。
@@ -130,6 +131,7 @@ static void session_clean_locked(void);
static void _audio_mark_reconfig_needed(void);
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cond;
static pthread_cond_t g_vol_cond; /* 音量异步执行线程等待(CLOCK_MONOTONIC,main 中初始化) */
struct client {
int used;
@@ -137,10 +139,23 @@ struct client {
char app_id[MEOW_APP_ID_MAX];
uint32_t sid;
int state; /* CLS_* */
uint32_t gen; /* 分配代际(音量异步 ACK 防 fd/槽复用竞态) */
int vol_ack; /* 音量请求已执行,待 IO 线程回 ACK(vol 线程写/IO 线程清) */
uint32_t vol_ack_req; /* 对应 req_id */
uint32_t vol_ack_status; /* ACK status(应用后 g_vol_pct) */
uint8_t rbuf[READ_BUF_MAX];
size_t rlen;
};
static struct client g_cli[CLI_MAX];
static uint32_t g_cli_gen = 0; /* 客户端分配代际计数(cli_alloc 递增) */
/* ---- 音量异步化(事项三):IO 线程只入队末值,专用 vol 线程落 amixer+持久化后
* 经 IO 线程回 ACK。驱动 wedge 时 3×popen≈4.5s 不再卡持锁的 dispatch/IO 循环。 ---- */
static int g_vol_pending = 0; /* 有待执行音量请求(g_mu 保护) */
static int g_vol_pending_pct = 0; /* 末值(0-100;连续请求只保留最新=去抖) */
static struct client *g_vol_pending_cli = NULL; /* 末次请求方(ACK 用;可能已回收,靠 gen 校验) */
static uint32_t g_vol_pending_req = 0; /* 末次请求 req_id */
static uint32_t g_vol_pending_gen = 0; /* 末次请求方 gen(防槽复用误 ACK) */
/* 当前授权流(喂养 DAC 的唯一流) */
struct stream_info {
@@ -308,15 +323,16 @@ static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payloa
}
/* cond 有界等待(g_cond 为 CLOCK_MONOTONIC;调用方已持 g_mu)。 */
static void cond_wait_tmo(int ms)
static void cond_wait_tmo_cv(pthread_cond_t *cv, int ms)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
ts.tv_sec += ms / 1000;
ts.tv_nsec += (long)(ms % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
pthread_cond_timedwait(&g_cond, &g_mu, &ts);
pthread_cond_timedwait(cv, &g_mu, &ts);
}
static void cond_wait_tmo(int ms) { cond_wait_tmo_cv(&g_cond, ms); }
/* ---- frame 大小/位深 ---- */
static int fmt_bps(int fmt)
@@ -1135,12 +1151,14 @@ static void bt_vol_full(void)
}
/* 音量应用(含 BT 隔离):BT → 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、不持久化
* audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。 */
static void vol_apply(int pct)
* audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。
* target 为调用方在持锁下快照的 g_target;本函数不持 g_mu 执行(g_vol_pct 为 plain int
* 松散写,同 route_thread 的 g_route_cur 语义,MIPS32 int 写天然原子)。 */
static void vol_apply(int pct, int target)
{
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
if (g_target == AUDIO_OUT_BT) {
if (target == AUDIO_OUT_BT) {
logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct);
return;
}
@@ -1551,6 +1569,7 @@ static struct client *cli_alloc(int fd)
freec->fd = fd;
freec->state = CLS_OPEN;
freec->sid = 0;
freec->gen = ++g_cli_gen; /* 代际递增:音量异步 ACK 校验用,防槽复用误发 */
return freec;
}
static void cli_free(struct client *c)
@@ -1771,13 +1790,20 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
int32_t pct = -1;
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
if (pct >= 0) {
/* 音量应用含 BT 隔离:target=BT 时绝不下发 DAC 反转(vol_apply 内判断) */
vol_apply((int)pct);
/* 异步化(事项三):只入队末值,**不在持 g_mu 下 popen amixer**——驱动 wedge 时
* 一次 set = 3×popen ≈ 4.5s,会把 OPEN/QUERY/喂流一起卡死。连续请求合并为末值
* (去抖),由 vol 线程应用+持久化后经 IO 线程回 ACK。 */
g_vol_pending = 1;
g_vol_pending_pct = (pct > 100) ? 100 : (int)pct;
g_vol_pending_cli = c;
g_vol_pending_req = req_id;
g_vol_pending_gen = c->gen;
pthread_cond_signal(&g_vol_cond);
} else {
g_vol_pct = settings_get_volume();
g_vol_pct = settings_get_volume(); /* 读当前:文件读非 popen,可同步 */
m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_vol_pct;
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
}
m_ack_t a; a.code = MEOW_OK; a.status = (uint32_t)g_vol_pct;
send_frame(c->fd, MSG_ACK, req_id, &a, sizeof a);
break;
}
case MSG_AUDIO_OUTPUT: {
@@ -1911,6 +1937,14 @@ static void *io_thread(void *arg)
if (snaps[i].revents & (POLLHUP | POLLERR | POLLNVAL)) { cli_free(c); continue; }
if (snaps[i].revents & POLLIN) cx_read_frames(c);
}
/* 音量 ACK 回写(事项三):vol 线程执行完只置标记,由 IO 线程统一发送——客户端 fd
* 生命周期归 IO 线程独占,跨线程直接 send_frame 有 fd 复用竞态。 */
for (int i = 0; i < CLI_MAX; i++) {
if (!g_cli[i].used || !g_cli[i].vol_ack) continue;
m_ack_t va; va.code = MEOW_OK; va.status = g_cli[i].vol_ack_status;
send_frame(g_cli[i].fd, MSG_ACK, g_cli[i].vol_ack_req, &va, sizeof va);
g_cli[i].vol_ack = 0;
}
pthread_mutex_unlock(&g_mu);
/* 路由线程与播放线程独立;此处不再做 */
}
@@ -1921,6 +1955,40 @@ static void *io_thread(void *arg)
return NULL;
}
/* ---- 音量执行线程(事项三):异步落 amixer + 持久化,不在 IO 线程持锁 popen ----
* 等待 g_vol_cond 取末值(连续请求合并=去抖),放锁后执行 dac_vol_set(3×popen,有界)
* + settings_set_volume(文件 IO),完成后只置 ACK 标记,由 IO 线程统一发送(防 fd 复用竞态)。
* BT 目标只记日志不动 DAC/不持久化(AVRCP 隔离,g_vol_pct 保持不变)。 */
static void *vol_thread(void *arg)
{
(void)arg;
for (;;) {
pthread_mutex_lock(&g_mu);
while (!g_vol_pending && !g_stop)
cond_wait_tmo_cv(&g_vol_cond, COND_WAIT_MS);
if (g_stop) { pthread_mutex_unlock(&g_mu); break; }
int pct = g_vol_pending_pct;
int target = g_target; /* 持锁快照:vol_apply 不读全局 */
struct client *c = g_vol_pending_cli;
uint32_t req = g_vol_pending_req;
uint32_t cgen = g_vol_pending_gen;
g_vol_pending = 0;
g_vol_pending_cli = NULL;
pthread_mutex_unlock(&g_mu);
vol_apply(pct, target);
pthread_mutex_lock(&g_mu);
if (c && c->used && c->gen == cgen) { /* 槽复用校验:只 ACK 原请求方 */
c->vol_ack = 1;
c->vol_ack_req = req;
c->vol_ack_status = (uint32_t)g_vol_pct;
}
pthread_mutex_unlock(&g_mu);
}
return NULL;
}
/* ---- 路由线程主体 ---- */
static void *route_thread(void *arg)
{
@@ -2149,19 +2217,21 @@ int main(int argc, char **argv)
logmsg("volume init = %d%% target=%s codec=%s volctl=%s",
g_vol_pct, target_name(), g_bt_codec, volctl_name());
/* g_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
/* g_cond/g_vol_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
{
pthread_condattr_t ca;
pthread_condattr_init(&ca);
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
pthread_cond_init(&g_cond, &ca);
pthread_cond_init(&g_vol_cond, &ca);
pthread_condattr_destroy(&ca);
}
pthread_t ti, tp, tr;
pthread_t ti, tp, tr, tv;
pthread_create(&ti, NULL, io_thread, NULL);
pthread_create(&tp, NULL, playback_thread, NULL);
pthread_create(&tr, NULL, route_thread, NULL);
pthread_create(&tv, NULL, vol_thread, NULL);
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
@@ -2191,6 +2261,7 @@ int main(int argc, char **argv)
pthread_join(ti, NULL);
pthread_join(tp, NULL);
pthread_join(tr, NULL);
pthread_join(tv, NULL);
unlink(g_sock);
if (g_logfd >= 0) close(g_logfd);
return 0;
+74
View File
@@ -187,6 +187,24 @@ static int start_audiod_trace(const char *sink, const char *settings_path, const
return pid;
}
/* 带假 amixer 的启动(事项三异步化验证:假 amixer 每次 cset 睡 1s,模拟慢 popen)。
* 用于断言「MSG_AUDIO_VOLUME 在 IO 线程只入队、不放锁 popen」——慢 amixer 期间 QUERY 仍即时。 */
static int start_audiod_fake_amixer(const char *sink, const char *settings_path, const char *amixer_script)
{
pid_t pid = fork();
if (pid == 0) {
setenv("MEOW_AUDIO_SINK", sink, 1);
setenv("MEOW_AUDIO_OUT", TWAV, 1);
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
setenv("MEOW_AUDIO_FORCE", "1", 1);
if (settings_path) setenv("MEOW_AUDIO_SETTINGS", settings_path, 1);
if (amixer_script) setenv("MEOW_AUDIO_AMIXER", amixer_script, 1);
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
_exit(127);
}
return pid;
}
/* 直接客户端 open(带 flags:AUDIO_FLAG_DSD 用于 DoP) */
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, m_audio_open_ack_t *ack)
{
@@ -1080,6 +1098,62 @@ int main(void)
}
stop_audiod();
/* ---- T17:MSG_AUDIO_VOLUME 异步化(事项三)。假 amixer 每次 cset 睡 1s(一次 set=3×cset≈3s)。
* 断言:慢 amixer 期间 QUERY 即时返回(dispatch 不放锁 popen);应用后回 ACK + 双组写 + 持久化。 */
printf("[T17 volume async (dispatch not blocked by slow amixer)]\n");
unlink(TSOCK); unlink(TWAV);
const char *tconf17 = "/tmp/meow_test_settings17.conf";
FILE *tf17 = fopen(tconf17, "w");
if (tf17) { fprintf(tf17, "# meow-settings 1\naudio.volume = 60\n"); fclose(tf17); }
const char *fake17 = "/tmp/meow_fake_amixer17.sh";
const char *trace17 = "/tmp/meow_fake_amixer17_trace.txt";
unlink(trace17);
{
FILE *fs = fopen(fake17, "w");
if (fs) {
fprintf(fs, "#!/bin/sh\necho \"cset $4 $5\" >> %s\nsleep 1\nexit 0\n", trace17);
fclose(fs);
chmod(fake17, 0755);
}
}
g_audiod_pid = start_audiod_fake_amixer("file", tconf17, fake17);
usleep(400000);
{
struct cli v17;
check(cli_connect(&v17) == 0, "connect V17");
int32_t pct = 40;
check(sf(v17.fd, MSG_AUDIO_VOLUME, 17, &pct, sizeof pct) == 0, "send async volume 40");
/* 慢 amixer 执行期间 QUERY 必须即时(<600ms),证明 IO 线程没被 popen 卡住 */
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
char q17[1024];
int qok = do_query(&v17, q17, sizeof q17);
clock_gettime(CLOCK_MONOTONIC, &t1);
long qms = (t1.tv_sec - t0.tv_sec) * 1000L + (t1.tv_nsec - t0.tv_nsec) / 1000000L;
check(qok == 0 && qms < 600, "QUERY immediate during slow amixer (async dispatch)");
if (qok == 0 && qms >= 600) printf(" (query took %ldms)\n", qms);
/* 应用后回 ACK(末值 40) */
uint8_t ab[64]; uint16_t at;
int rlen = rf_tmo(v17.fd, 4000, &at, ab, sizeof ab);
check(rlen >= 0 && at == MSG_ACK, "async volume ACK returned after apply");
usleep(100000);
{
FILE *chk = fopen(tconf17, "r"); int has40 = 0; char l[128];
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
check(has40, "async volume persisted = 40");
}
{
char trb[2048]; int trl = 0;
int fd = open(trace17, O_RDONLY);
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
check(trl > 0 && strstr(trb, "numid=1 ") && strstr(trb, "numid=2 ") && strstr(trb, "numid=11 "),
"async dual-group write (numid=1/2/11) via fake amixer");
}
close(v17.fd);
}
stop_audiod();
unlink(fake17); unlink(trace17);
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}