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 丢弃); * - 独占 + 抢占(后开流赢,旧流收 MSG_AUDIO_PREEMPTED 并被置 draining,PCM 丢弃);
* 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。 * 软混音已否决(MIPS32 软 SRC 伤音质),留混音器接口位(v1 不实现)。
* - 单进程双线程(+路由线程):IO 线程(accept/读帧/解析/ring 写)、播放线程 * - 单进程四线程:IO 线程(accept/读帧/解析/ring 写)、播放线程
* (独占 ALSA 直开,从 ring 取数写 DAC,xrun PREPARE 恢复)、路由线程 * (独占 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 * - 格式归一 = 协商:grant_rate/ch/fmt 回客户端应产出的格式(SDK 负责 mono→stereo
* 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。 * 复制 / S16↔S32 定值升降位);audiod 只做「硬件最优」格式的字节管道。
* - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。 * - DAC 原生率跟随(无 SRC);切率 mute-重配-恢复。
@@ -130,6 +131,7 @@ static void session_clean_locked(void);
static void _audio_mark_reconfig_needed(void); static void _audio_mark_reconfig_needed(void);
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cond; static pthread_cond_t g_cond;
static pthread_cond_t g_vol_cond; /* 音量异步执行线程等待(CLOCK_MONOTONIC,main 中初始化) */
struct client { struct client {
int used; int used;
@@ -137,10 +139,23 @@ struct client {
char app_id[MEOW_APP_ID_MAX]; char app_id[MEOW_APP_ID_MAX];
uint32_t sid; uint32_t sid;
int state; /* CLS_* */ 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]; uint8_t rbuf[READ_BUF_MAX];
size_t rlen; size_t rlen;
}; };
static struct client g_cli[CLI_MAX]; 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 的唯一流) */ /* 当前授权流(喂养 DAC 的唯一流) */
struct stream_info { 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)。 */ /* 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; struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts); clock_gettime(CLOCK_MONOTONIC, &ts);
ts.tv_sec += ms / 1000; ts.tv_sec += ms / 1000;
ts.tv_nsec += (long)(ms % 1000) * 1000000L; ts.tv_nsec += (long)(ms % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } 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 大小/位深 ---- */ /* ---- frame 大小/位深 ---- */
static int fmt_bps(int fmt) 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、不持久化 /* 音量应用(含 BT 隔离):BT → 只通知 AVRCP(best-effort),绝不动 DAC numid=1/2、不持久化
* audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。 */ * audio.volume(那是 DAC 音量键);DAC → 原感知锥度反转 + 持久化。
static void vol_apply(int pct) * 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 < 0) pct = 0;
if (pct > 100) pct = 100; 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); logmsg("vol -> %d%% (BT/AVRCP, DAC numid=1/2 NOT written)", pct);
return; return;
} }
@@ -1551,6 +1569,7 @@ static struct client *cli_alloc(int fd)
freec->fd = fd; freec->fd = fd;
freec->state = CLS_OPEN; freec->state = CLS_OPEN;
freec->sid = 0; freec->sid = 0;
freec->gen = ++g_cli_gen; /* 代际递增:音量异步 ACK 校验用,防槽复用误发 */
return freec; return freec;
} }
static void cli_free(struct client *c) 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; int32_t pct = -1;
if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct); if (len >= sizeof(int32_t)) memcpy(&pct, pl, sizeof pct);
if (pct >= 0) { if (pct >= 0) {
/* 音量应用含 BT 隔离:target=BT 时绝不下发 DAC 反转(vol_apply 内判断) */ /* 异步化(事项三):只入队末值,**不在持 g_mu 下 popen amixer**——驱动 wedge 时
vol_apply((int)pct); * 一次 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 { } 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; break;
} }
case MSG_AUDIO_OUTPUT: { 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 & (POLLHUP | POLLERR | POLLNVAL)) { cli_free(c); continue; }
if (snaps[i].revents & POLLIN) cx_read_frames(c); 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); pthread_mutex_unlock(&g_mu);
/* 路由线程与播放线程独立;此处不再做 */ /* 路由线程与播放线程独立;此处不再做 */
} }
@@ -1921,6 +1955,40 @@ static void *io_thread(void *arg)
return NULL; 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) 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", logmsg("volume init = %d%% target=%s codec=%s volctl=%s",
g_vol_pct, target_name(), g_bt_codec, volctl_name()); 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_t ca;
pthread_condattr_init(&ca); pthread_condattr_init(&ca);
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC); pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
pthread_cond_init(&g_cond, &ca); pthread_cond_init(&g_cond, &ca);
pthread_cond_init(&g_vol_cond, &ca);
pthread_condattr_destroy(&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(&ti, NULL, io_thread, NULL);
pthread_create(&tp, NULL, playback_thread, NULL); pthread_create(&tp, NULL, playback_thread, NULL);
pthread_create(&tr, NULL, route_thread, NULL); pthread_create(&tr, NULL, route_thread, NULL);
pthread_create(&tv, NULL, vol_thread, NULL);
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀—— /* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */ * 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
@@ -2191,6 +2261,7 @@ int main(int argc, char **argv)
pthread_join(ti, NULL); pthread_join(ti, NULL);
pthread_join(tp, NULL); pthread_join(tp, NULL);
pthread_join(tr, NULL); pthread_join(tr, NULL);
pthread_join(tv, NULL);
unlink(g_sock); unlink(g_sock);
if (g_logfd >= 0) close(g_logfd); if (g_logfd >= 0) close(g_logfd);
return 0; return 0;
+74
View File
@@ -187,6 +187,24 @@ static int start_audiod_trace(const char *sink, const char *settings_path, const
return pid; 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) */ /* 直接客户端 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) 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(); 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); printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0; return g_fail ? 1 : 0;
} }
+1 -1
View File
@@ -26,7 +26,7 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \ main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \
notif_center.c app_launcher.c audio.c backlight.c net.c about.c \ notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.c \ subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.c \
font_theme.c keys.c util.c meow_dep.c \ font_theme.c keys.c util.c meow_dep.c key_router.c \
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \ meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
meow_bgmgr.c \ meow_bgmgr.c \
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \ meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
+142
View File
@@ -0,0 +1,142 @@
/* key_router.c — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
*
* 本模块持有一个进程内唯一的音量键防抖/基线:
* kp_keys / kp_power(事件设备 poll 态,key_router_init 单开)
* kd_vold / kd_volu(40ms 防抖态,KEY_DEBOUNCE_MS)
* m_prev_vold / m_prev_volu(音量边沿基线)
* 各 launcher-UI 页每圈调 key_router_pump() —— 它只产「音量边沿 + 原始 held + ramp 状态」,
* 产出的边沿由「谁在跑循环」天然归属当前页;页据此决定 settings_volume_adj(±step) /
* 光标导航 / ramp 推进。这样 run_menu 的 meow_mode 分支(HibyOS overlay 音量=导航)不被破坏,
* 其余页(设置/通知中心/Now Playing/子页)用默认「音量=调音量」。
*
* 边界(§3 P1 / §4):
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸;launcher-UI
* 下 sink 恒空,此闸是「不改变行为」的保险)。
* - 播放键/电源键 P1 不路由(各页现状不动;播放键严禁 cmd=6)。
* - 锁屏(EVIOCGRAB)/App 会话层不调用本模块 —— 边界由「调用栈里当前循环是不是 launcher-UI 页」
* 自动强加。
*/
#include "meow_launcher_internal.h"
static int g_inited = 0;
/* 进程唯一事件设备 poll 态(key_router_init 单开;各页另自开 kp_* 仅供播放/电源键用)。 */
static struct keypoll kp_power, kp_keys;
/* 进程唯一音量键防抖态 + 边沿基线。 */
static struct kd_state kd_vold, kd_volu;
static int m_prev_vold = 0, m_prev_volu = 0;
/* 当前活动页哨兵(key_router_page_enter/exit 登记;与 sdk meow_ui_activity 交叉校验用)。 */
static int g_page_sentinel = 0;
void key_router_init(void)
{
if (g_inited) return;
g_inited = 1;
memset(&kp_power, 0, sizeof kp_power);
memset(&kp_keys, 0, sizeof kp_keys);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
m_prev_vold = 0; m_prev_volu = 0;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
/* 基线对齐当前 raw:进入时已按住的音量键不产假边沿。 */
key_router_rebase();
}
void key_router_rebase(void)
{
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
if (!g_inited) return;
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
}
key_evt_t key_router_pump(void)
{
key_evt_t ev;
memset(&ev, 0, sizeof ev);
if (!g_inited) key_router_init();
/* 设备晚就绪兜底:init 时 open 失败则此处每圈重试(open 失败快速返回,无阻塞)。 */
if (!kp_keys.ok) kp_open(&kp_keys, DEV_KEYS);
if (!kp_power.ok) kp_open(&kp_power, DEV_POWER);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
}
/* 原始(未防抖)值:长按 ramp held 用(松开立即停止;同 run_menu/now_playing 现状)。 */
ev.vol_held_dn = vold;
ev.vol_held_up = volu;
(void)kd_tick(&kd_vold, vold);
(void)kd_tick(&kd_volu, volu);
vold = kd_vold.stable; volu = kd_volu.stable;
ev.vol_st_dn = vold; ev.vol_st_up = volu;
if (meow_volpage_armed()) {
/* 防御闸:App 前台 volume_page 武装时(launcher-UI 下恒空)不放行音量边沿。
* 音量键在那态下由 app_launcher 的 meow_volpage_key 消费(翻页),不落系统调音。 */
ev.vol_dn_edge = 0;
ev.vol_up_edge = 0;
} else {
ev.vol_dn_edge = (vold && !m_prev_vold);
ev.vol_up_edge = (volu && !m_prev_volu);
}
ev.ramping = (vol_ramp_dir != 0);
m_prev_vold = vold;
m_prev_volu = volu;
return ev;
}
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input)
{
if (ev->vol_dn_edge) {
settings_volume_adj(-settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("key_router: volume DOWN <- step=%d", settings_volume_step);
}
if (ev->vol_up_edge) {
settings_volume_adj(+settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("key_router: volume UP <- step=%d", settings_volume_step);
}
/* 在 settings_volume_adj 之后重查 vol_ramp_dir:同圈「起 ramp」即立刻迈第一步(同现状
* 各页「settings_volume_adj 后 if(vol_ramp_dir) vol_ramp_tick」),且长按续爬随每圈推进。 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? ev->vol_held_up : ev->vol_held_dn;
(void)vol_ramp_tick(held);
}
}
void key_router_page_enter(int sentinel)
{
g_page_sentinel = sentinel;
/* 与 sdk 活动栈交叉校验:哨兵非 0 且栈顶不一致 → 记录(不中止,防宿主/真机栈深度差异)。 */
if (sentinel) {
int act = meow_ui_activity_active();
if (act && act != sentinel)
logmsg("key_router: page_enter sentinel=%#x but active=%#x", sentinel, act);
}
}
void key_router_page_exit(int sentinel)
{
if (g_page_sentinel == sentinel) g_page_sentinel = 0;
/* 页退出即重播基线(§6 嵌套返回风险项:返回瞬间不产假边沿)。 */
key_router_rebase();
}
+65
View File
@@ -0,0 +1,65 @@
/* key_router.h — launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY,P1:音量键)。
*
* 设计(devdocs/plan/KEY_ROUTER_UNIFY.md §2.5 方案 C / §3 P1):
* - 进程内**唯一**的 kd_*(防抖态)+ m_prev_*(边沿基线)+ kp_*(事件设备 poll 态),
* 把「读边沿 + 防抖 + 长按 held + ramp 推进」这层从 5 个会话(run_menu /
* run_settings_page / run_notif_center / run_now_playing / meow_subpage_run)
* 收敛到一处。
* - 每个 launcher-UI 页在其 for(;;) 每圈调一次 key_router_pump(),得到「防抖后的
* 音量边沿 + 原始 held + ramp 状态」;**真正的动作**(音量边沿 →
* settings_volume_adj(±step),还是光标导航)由调用页决定 —— 这样 run_menu 的
* meow_mode 分支(HibyOS overlay:音量=导航;MeowOS:音量=调音量)得以保留。
* - 非导航页可经 key_router_default_volume()(内含 settings_volume_adj + vol_ramp_tick +
* last_input 复位)直接用;run_menu 走自己的 meow_mode 分支。
*
* 范围与边界(§3 P1,勿越界):
* - 本模块只收敛**音量键**(KEY_VOLUMEDOWN/KEY_VOLUMEUP)。播放键/电源键 P1 不路由,
* 维持各页现状(播放键严禁改成 cmd=6;电源键单击/菜单/8s 状态机不动)。
* - App 前台 volume_page 武装时(meow_volpage_armed())音量边沿被抑制(防御闸)。
* - 锁屏层(EVIOCGRAB)/App 会话层从不调用 key_router_pump,天然退出本路由。
*/
#ifndef MEOW_KEY_ROUTER_H
#define MEOW_KEY_ROUTER_H
/* 一框音量边沿/held/ramp 输出。P1 只填音量;播放/电源字段后续期(P2/P3)再扩展。
* 字段语义:
* vol_dn_edge / vol_up_edge —— 防抖后**稳定按下边沿**(= stable ∧ ¬上一 stable;
* 1=本圈新按下)。这是「音量边沿」的唯一权威判定。
* vol_held_dn / vol_held_up —— **原始(未防抖)**按住态,ramp 长按 held 用
* (松开立即停止,同 run_menu/now_playing 现状)。
* vol_st_dn / vol_st_up —— **防抖稳定**按住态(wake_suppress「全键抬起」判定用)。
* ramping —— vol_ramp_dir != 0(线性渐变是否在跑;repaint 用)。
*/
typedef struct {
int vol_dn_edge;
int vol_up_edge;
int vol_held_dn;
int vol_held_up;
int vol_st_dn;
int vol_st_up;
int ramping;
} key_evt_t;
/* 进程级单开 kp_*(事件设备)+ 初始化防抖态/基线(幂等,可安全重复调用)。 */
void key_router_init(void);
/* 每页 for(;;) 每圈调用一次:poll 两设备 → 防抖音量键 → 产边沿/held/ramp。
* 不做 settings_volume_adj、不驱动 vol_ramp_tick、不复位 last_input —— 皆由调用页决定
* (见 key_router_default_volume 与各页自己的分支)。 */
key_evt_t key_router_pump(void);
/* 模态/菜单/嵌套会话返回/唤醒后重播基线:kd_* 清零 + m_prev_* 对齐当前 raw
* (对齐现状各页「memset(kd_*) + kp_poll + m_prev_*=当前 raw」的语义,防假边沿)。
* 各页在原本做该重播处调用。 */
void key_router_rebase(void);
/* 页进入/退出登记(记录当前哨兵;与 sdk meow_ui_activity_active 交叉校验)。 */
void key_router_page_enter(int sentinel);
void key_router_page_exit(int sentinel);
/* 默认「音量键=调音量」页回调(非导航页直接用):音量边沿 → settings_volume_adj(±step);
* 若有 ramp 在爬 → vol_ramp_tick(held);边沿 → 复位 *last_input。
* run_menu 不走本函数(自行 meow_mode 分支)。 */
void key_router_default_volume(const key_evt_t *ev, unsigned long long *last_input);
#endif /* MEOW_KEY_ROUTER_H */
+36 -57
View File
@@ -1214,10 +1214,11 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
struct keypoll kp_power, kp_keys; struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS); kp_open(&kp_keys, DEV_KEYS);
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */ /* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动。
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; * 音量键防抖/基线已上收 key_router(进程唯一);本页只保留播放/电源键自家防抖。 */
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); struct kd_state kd_play, kd_pwr;
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
key_router_rebase(); /* 进入主菜单:音量基线对齐当前 raw(防残留边沿) */
logmsg("menu open: napps=%d state=%d cursor=%d", logmsg("menu open: napps=%d state=%d cursor=%d",
napps, state, cursor); napps, state, cursor);
@@ -1226,10 +1227,8 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* when the menu opens (e.g. the hotkey release tail) do not fire * when the menu opens (e.g. the hotkey release tail) do not fire
* edges; kp.prev cannot be used for this because kp_poll() syncs it * edges; kp.prev cannot be used for this because kp_poll() syncs it
* to cur before we compare */ * to cur before we compare */
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; int m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
} }
@@ -1433,26 +1432,20 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* does not affect it) — the launcher must NOT touch brightness/panel * does not affect it) — the launcher must NOT touch brightness/panel
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch * or exit the menu on it. Exit is only via the BACK TO PLAYER touch
* row (below) or sim-exit. */ * row (below) or sim-exit. */
/* 音量键走 key_router(进程唯一防抖/基线,产边沿/held/ramp);播放/电源键仍本页自理。 */
key_evt_t kev = key_router_pump();
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0; int play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
} }
if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */ if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */
/* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */
int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */
(void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */
/* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。 /* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。
* 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */ * 稳定态 play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
(void)kd_tick(&kd_vold, vold);
(void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_play, play);
(void)kd_tick(&kd_pwr, pwr); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable; play = kd_play.stable; pwr = kd_pwr.stable;
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河): /* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
* 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */ * 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */
@@ -1497,15 +1490,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw * 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw * 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */ * 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
continue; continue;
} }
@@ -1530,15 +1521,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
if (lck_wake_should_lock()) { if (lck_wake_should_lock()) {
int lr = run_lock_screen(); int lr = run_lock_screen();
/* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */ /* 重播按键防抖基线(阻塞期间按键可能已变化,防误判新边沿) */
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
if (lr == 1) if (lr == 1)
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */ meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
@@ -1567,11 +1556,11 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp), * M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp),
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */ * 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
if (meow_mode) { if (meow_mode) {
if (vold && !m_prev_vold) { if (kev.vol_dn_edge) {
any_input = 1; any_input = 1;
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */ settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
logmsg("volume DOWN <- ramp step=%d", settings_volume_step); logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
} else if (volu && !m_prev_volu) { } else if (kev.vol_up_edge) {
any_input = 1; any_input = 1;
settings_volume_adj(+settings_volume_step); settings_volume_adj(+settings_volume_step);
logmsg("volume UP <- ramp step=%d", settings_volume_step); logmsg("volume UP <- ramp step=%d", settings_volume_step);
@@ -1579,22 +1568,23 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
} else { } else {
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。 /* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */ * 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */
if (vold && !m_prev_vold && napps > 0 && state == 0) { if (kev.vol_dn_edge && napps > 0 && state == 0) {
any_input = 1; any_input = 1;
cursor++; cursor++;
if (cursor > napps) cursor = 0; /* napps=设置行 */ if (cursor > napps) cursor = 0; /* napps=设置行 */
logmsg("menu cursor -> %d", cursor); logmsg("menu cursor -> %d", cursor);
} }
if (volu && !m_prev_volu && napps > 0 && state == 0) { if (kev.vol_up_edge && napps > 0 && state == 0) {
any_input = 1; any_input = 1;
cursor--; cursor--;
if (cursor < 0) cursor = napps; /* 回绕到设置行 */ if (cursor < 0) cursor = napps; /* 回绕到设置行 */
logmsg("menu cursor -> %d", cursor); logmsg("menu cursor -> %d", cursor);
} }
} }
/* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */ /* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值)。
* 长按 raw held 取自 key_router(kev.vol_held_*,同现状 rvolu/rvold),松开立即停止。 */
if (meow_mode && vol_ramp_dir != 0) { if (meow_mode && vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */ int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
int going = vol_ramp_tick(held); int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get(); g_sys.vol = settings_volume_get();
paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */ paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */
@@ -1618,7 +1608,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
logmsg("play key: media-reserved (no media source)"); logmsg("play key: media-reserved (no media source)");
} }
} }
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_play = play;
/* touch (event reads) */ /* touch (event reads) */
if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */ if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */
@@ -1683,15 +1673,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
click.status = NOTIF_CLICK_STATUS_NONE; click.status = NOTIF_CLICK_STATUS_NONE;
run_notif_center(apps, napps, &click, 1, t_down_x, loy); run_notif_center(apps, napps, &click, 1, t_down_x, loy);
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */ /* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms(); last_input_ms = now_ms();
menu_arm_touch(800); menu_arm_touch(800);
@@ -1783,15 +1771,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
click.status = NOTIF_CLICK_STATUS_NONE; click.status = NOTIF_CLICK_STATUS_NONE;
run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */ run_notif_center(apps, napps, &click, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */ /* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms(); last_input_ms = now_ms();
menu_arm_touch(800); menu_arm_touch(800);
@@ -1835,15 +1821,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
menu_arm_touch(TAP_ARM_DELAY_MS); menu_arm_touch(TAP_ARM_DELAY_MS);
run_now_playing(apps, napps); run_now_playing(apps, napps);
/* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */ /* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms(); last_input_ms = now_ms();
menu_arm_touch(800); menu_arm_touch(800);
@@ -1858,15 +1842,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
logmsg("menu: touch topbar bg-manager button"); logmsg("menu: touch topbar bg-manager button");
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */ menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
run_bg_page(apps, napps); run_bg_page(apps, napps);
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms(); last_input_ms = now_ms();
menu_arm_touch(800); menu_arm_touch(800);
@@ -1898,15 +1880,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
logmsg("menu: touch settings row -> settings page"); logmsg("menu: touch settings row -> settings page");
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */ menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */
run_settings_page(); run_settings_page();
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms(); /* 回主界面即视为输入 */ last_input_ms = now_ms(); /* 回主界面即视为输入 */
menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */ menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */
@@ -1975,15 +1955,13 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
/* M2.x 锁屏:自动息屏唤醒后若需锁屏 → 弹阻塞式锁屏页;解锁才重绘,息屏留 dirty */ /* M2.x 锁屏:自动息屏唤醒后若需锁屏 → 弹阻塞式锁屏页;解锁才重绘,息屏留 dirty */
if (lck_wake_should_lock()) { if (lck_wake_should_lock()) {
int lr = run_lock_screen(); int lr = run_lock_screen();
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
if (lr == 1) if (lr == 1)
meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */ meow_repaint_surface(apps, napps, state, cursor); /* 已解锁重绘主屏 */
@@ -2219,6 +2197,7 @@ int main(int argc, char **argv)
load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per
* menu open so edits apply without a daemon restart) */ * menu open so edits apply without a daemon restart) */
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */ settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
key_router_init(); /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1)单开 kp_*/
/* M2.6:启动扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,填充「字体文件」下拉 preset。 /* M2.6:启动扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,填充「字体文件」下拉 preset。
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次(verify=0)。 */ * 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次(verify=0)。 */
@@ -28,6 +28,7 @@ extern "C" {
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */ #include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
#include <pthread.h> /* M0:IPC 线程 */ #include <pthread.h> /* M0:IPC 线程 */
#include "key_router.h" /* M6.x:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1) */
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */ #include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
#include <sys/socket.h> /* M0:AF_UNIX IPC */ #include <sys/socket.h> /* M0:AF_UNIX IPC */
#include <sys/un.h> /* M0:sockaddr_un */ #include <sys/un.h> /* M0:sockaddr_un */
@@ -847,6 +848,9 @@ int nm_wifi_saved(meow_net_t *out, int max);
int nm_wifi_scan(meow_net_t *out, int max); int nm_wifi_scan(meow_net_t *out, int max);
void nm_wifi_scan_trigger(void); void nm_wifi_scan_trigger(void);
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */ int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /* MEOW_OK=成功 */
/* WiFi 设置页 bug2(2026-09-15):hidden=1 走 netmgrd connecth(scan_ssid=1 + 更长 COMPLETED 等待),
* 回退直连同步对齐;err 回填 netmgrd/直连的诚实结果文本(成功含 ip / 失败含原因)。 */
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen);
int nm_wifi_disconnect(char *err, int elen); int nm_wifi_disconnect(char *err, int elen);
int nm_wifi_toggle(int on); int nm_wifi_toggle(int on);
+134 -38
View File
@@ -15,6 +15,14 @@
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock" #define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */ /* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
/* IPC 读超时(ms)。netmgrd 单线程 poll,connect/connecth 会阻塞 ~10s/~15s 有界等待
* wpa_state=COMPLETED(bug1),scan 内含 1.5s 触发等待(P3)——客户端 per-command 超时需
* 覆盖这些同步等待,其余读级子命令维持 1.5s 不被卡死。 */
#define NM_TMO_DFL 1500
#define NM_TMO_SCAN 5000
#define NM_TMO_CONNECT 12000
#define NM_TMO_CONNECTH 17000
/* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */ /* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */ static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
static time_t g_nm_ok_ts = 0; static time_t g_nm_ok_ts = 0;
@@ -50,7 +58,7 @@ static int nm_r_all(int fd, uint8_t *b, size_t n)
return 0; return 0;
} }
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len, static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype) uint8_t *rbuf, uint32_t rcap, uint16_t *rtype, int tmo_ms)
{ {
uint8_t hdr[MEOW_IPC_HDR_LEN]; uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rlen = len; uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
@@ -60,7 +68,7 @@ static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1; if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1; if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
struct pollfd pf = { fd, POLLIN, 0 }; struct pollfd pf = { fd, POLLIN, 0 };
int pr = poll(&pf, 1, 1500); int pr = poll(&pf, 1, tmo_ms);
if (pr <= 0) return -1; if (pr <= 0) return -1;
uint8_t rhdr[MEOW_IPC_HDR_LEN]; uint8_t rhdr[MEOW_IPC_HDR_LEN];
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1; if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
@@ -81,15 +89,16 @@ static int nm_hello(int fd)
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher"); snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
h.version = 1; h.version = 1;
uint8_t rbuf[256]; uint16_t rtype = 0; uint8_t rbuf[256]; uint16_t rtype = 0;
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype) < 0 || rtype != MSG_ACK) if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype, NM_TMO_DFL) < 0 || rtype != MSG_ACK)
return -1; return -1;
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak); m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
return (ak.code == MEOW_OK) ? 0 : -1; return (ak.code == MEOW_OK) ? 0 : -1;
} }
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。 */ /* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。
* tmo_ms:读超时(connect/connecth/scan 需覆盖 netmgrd 同步等待,见 NM_TMO_*)。 */
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg, static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
char *text, int tlen) char *text, int tlen, int tmo_ms)
{ {
m_net_cmd_t q; m_net_cmd_t q;
memset(&q, 0, sizeof q); memset(&q, 0, sizeof q);
@@ -100,7 +109,7 @@ static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const c
if (nm_hello(fd) < 0) { close(fd); nm_cache(0); return NETMGR_UNREACH; } if (nm_hello(fd) < 0) { close(fd); nm_cache(0); return NETMGR_UNREACH; }
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0; uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
memset(rbuf, 0, sizeof rbuf); memset(rbuf, 0, sizeof rbuf);
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype) < 0 || rtype != res_type) { if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype, tmo_ms) < 0 || rtype != res_type) {
close(fd); nm_cache(0); return NETMGR_UNREACH; close(fd); nm_cache(0); return NETMGR_UNREACH;
} }
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res); m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
@@ -112,11 +121,14 @@ static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const c
return (int)res.code; return (int)res.code;
} }
int netmgr_wifi_tmo(const char *cmd, const char *arg, char *text, int tlen, int tmo_ms)
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, tlen, tmo_ms); }
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen) int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen)
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, tlen); } { return netmgr_wifi_tmo(cmd, arg, text, tlen, NM_TMO_DFL); }
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen) int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen)
{ return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen); } { return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen, NM_TMO_DFL); }
int netmgr_ok(void) int netmgr_ok(void)
{ {
@@ -361,7 +373,7 @@ int nm_wifi_saved(meow_net_t *out, int max)
int nm_wifi_scan(meow_net_t *out, int max) int nm_wifi_scan(meow_net_t *out, int max)
{ {
char text[MEOW_IPC_MAX - 32]; char text[MEOW_IPC_MAX - 32];
int rc = netmgr_wifi("scan", NULL, text, sizeof text); /* netmgrd 内含触发+有界等待+读结果 */ int rc = netmgr_wifi_tmo("scan", NULL, text, sizeof text, NM_TMO_SCAN); /* netmgrd 内含触发+有界等待+读结果 */
if (rc == NETMGR_UNREACH) return net_wifi_scan(out, max); if (rc == NETMGR_UNREACH) return net_wifi_scan(out, max);
if (rc != MEOW_OK) return 0; if (rc != MEOW_OK) return 0;
return nm_parse_scan(text, out, max); return nm_parse_scan(text, out, max);
@@ -374,49 +386,133 @@ void nm_wifi_scan_trigger(void)
if (!netmgr_ok()) net_wifi_scan_trigger(); if (!netmgr_ok()) net_wifi_scan_trigger();
} }
/* —— 直连回退:wpa_cli add/set/enable/select 新网络(复刻 netmgrd wifi_connect 逻辑)—— */ /* —— 直连回退:与 netmgrd wifi_connect/connecth 对齐(bug1/bug2/bug4)。
static int nm_direct_connect(const char *ssid, const char *psk, char *err, int elen) * 匹配既有(\xNN 还原同表示)→ 新建或(命中且带新 psk)更新凭据 → hidden 补 scan_ssid=1 →
* enable+select → 有界等待 wpa_state=COMPLETED → 成功 enable_network all + save_config(校验重试);
* 失败时本次新建的网络回收,报文明示原因。仅在 netmgrd 不可达时走此路径。 */
static const char NM_WPA_PREFIX[] = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
static int nm_direct_wpa(char *out, int cap, const char *args, int tmo)
{ {
const char *wpa = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0"; char cmd[384];
char cmd[256]; snprintf(cmd, sizeof cmd, "%s %s 2>/dev/null", NM_WPA_PREFIX, args);
snprintf(cmd, sizeof cmd, "%s add_network 2>/dev/null", wpa); char *o = popen_read_tmo(tmo, cmd);
char *o = popen_read_tmo(1200, cmd); if (!o) { out[0] = 0; return -1; }
int id = o ? atoi(o) : -1; int l = (int)strlen(o);
if (o) free(o); if (l >= cap) l = cap - 1;
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; } memcpy(out, o, (size_t)l); out[l] = 0;
{ free(o);
char *r; return l;
snprintf(cmd, sizeof cmd, "%s set_network %d ssid '\"%s\"' 2>/dev/null", wpa, id, ssid); }
if ((r = popen_read_tmo(1200, cmd))) free(r);
if (psk && psk[0]) { static int nm_direct_save_config(void)
snprintf(cmd, sizeof cmd, "%s set_network %d psk '\"%s\"' 2>/dev/null", wpa, id, psk); {
if ((r = popen_read_tmo(1200, cmd))) free(r); char out[128];
} else { if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
snprintf(cmd, sizeof cmd, "%s set_network %d key_mgmt NONE 2>/dev/null", wpa, id); nm_direct_wpa(out, sizeof out, "set update_config 1", 1500);
if ((r = popen_read_tmo(1200, cmd))) free(r); if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
return 0;
}
static int nm_direct_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
{
char out[1024]; char args[256];
int id = -1;
if (nm_direct_wpa(out, sizeof out, "list_networks", 1500) > 0) {
char *ln = strtok(out, "\n");
while (ln) {
char *tab0 = strchr(ln, '\t');
if (tab0) {
int this_id = atoi(ln);
char *ss = tab0 + 1;
char *tab1 = strchr(ss, '\t');
if (tab1) *tab1 = 0;
char clean[128], unesc[160], norm[128];
size_t cl = strlen(ss);
if (cl >= 2 && ss[0] == '"' && ss[cl - 1] == '"') {
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
} else {
snprintf(clean, sizeof clean, "%s", ss);
}
nm_decode_ssid(clean, unesc, sizeof unesc);
char *p = unesc; while (*p == ' ' || *p == '\t') p++;
snprintf(norm, sizeof norm, "%s", p);
size_t nl = strlen(norm);
while (nl > 0 && (norm[nl - 1] == ' ' || norm[nl - 1] == '\t')) norm[--nl] = 0;
if (strcmp(norm, ssid) == 0) { id = this_id; break; }
}
ln = strtok(NULL, "\n");
} }
snprintf(cmd, sizeof cmd, "%s enable_network %d 2>/dev/null", wpa, id);
if ((r = popen_read_tmo(1200, cmd))) free(r);
snprintf(cmd, sizeof cmd, "%s select_network %d 2>/dev/null", wpa, id);
if ((r = popen_read_tmo(1200, cmd))) free(r);
} }
if (err) snprintf(err, elen, "connect: ssid=%s network_id=%d", ssid, id); int created = 0;
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s id=%d", ssid, id); if (id < 0) {
if (nm_direct_wpa(out, sizeof out, "add_network", 1500) <= 0) {
if (err) snprintf(err, elen, "add_network 无 id");
return MEOW_ERR_INVAL;
}
id = atoi(out);
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
created = 1;
snprintf(args, sizeof args, "set_network %d ssid '\"%s\"'", id, ssid);
nm_direct_wpa(out, sizeof out, args, 1500);
if (psk && psk[0]) { snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk); nm_direct_wpa(out, sizeof out, args, 1500); }
else { snprintf(args, sizeof args, "set_network %d key_mgmt NONE", id); nm_direct_wpa(out, sizeof out, args, 1500); }
} else if (psk && psk[0]) {
snprintf(args, sizeof args, "set_network %d psk '\"%s\"'", id, psk);
nm_direct_wpa(out, sizeof out, args, 1500);
}
if (hidden) { snprintf(args, sizeof args, "set_network %d scan_ssid 1", id); nm_direct_wpa(out, sizeof out, args, 1500); }
snprintf(args, sizeof args, "enable_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
snprintf(args, sizeof args, "select_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
int wait_ms = hidden ? 15000 : 10000;
int waited = 0, ok = 0; char ip[40] = "";
for (;;) {
char st[512];
if (nm_direct_wpa(st, sizeof st, "status", 1500) > 0) {
char *ws = strstr(st, "wpa_state=");
if (ws && !strncmp(ws + 10, "COMPLETED", 9)) {
ok = 1;
char *ib = strstr(st, "ip_address=");
if (ib) { ib += 11; char *ie = ib; while (*ie && *ie != '\n' && *ie != '\r') ie++;
int il = (int)(ie - ib); if (il >= (int)sizeof ip) il = (int)sizeof ip - 1;
memcpy(ip, ib, (size_t)il); ip[il] = 0; }
break;
}
}
if (waited >= wait_ms) break;
usleep(200000);
waited += 200;
}
if (!ok) {
if (created) { snprintf(args, sizeof args, "remove_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500); nm_direct_save_config(); }
if (err) snprintf(err, elen, "连接失败:密码错误或信号不佳(超时未关联)");
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d timeout", ssid, hidden, id);
return MEOW_ERR_NODEV;
}
nm_direct_wpa(out, sizeof out, "enable_network all", 1500);
int persisted = nm_direct_save_config();
if (err) snprintf(err, elen, "已连接:%s ip=%s%s", ssid, ip[0] ? ip : "-", persisted ? "" : "(记录未持久化)");
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d ip=%s", ssid, hidden, id, ip[0] ? ip : "-");
return MEOW_OK; return MEOW_OK;
} }
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen) int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
{ {
if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; } if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; }
char arg[132]; char arg[132];
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* connect 带空 psk → netmgrd 复用既有网络 */ snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* 带空 psk → netmgrd 复用既有网络 */
char text[192]; char text[192];
int rc = netmgr_wifi("connect", arg, text, sizeof text); int rc = netmgr_wifi_tmo(hidden ? "connecth" : "connect", arg, text, sizeof text,
if (rc == NETMGR_UNREACH) return nm_direct_connect(ssid, psk, err, elen); hidden ? NM_TMO_CONNECTH : NM_TMO_CONNECT);
if (rc == NETMGR_UNREACH) return nm_direct_connect_ex(ssid, psk, hidden, err, elen);
if (err && elen > 0) snprintf(err, elen, "%s", text); if (err && elen > 0) snprintf(err, elen, "%s", text);
return rc; return rc;
} }
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
{ return nm_wifi_connect_ex(ssid, psk, 0, err, elen); }
int nm_wifi_disconnect(char *err, int elen) int nm_wifi_disconnect(char *err, int elen)
{ {
char text[128]; char text[128];
+19 -27
View File
@@ -1168,16 +1168,15 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
struct keypoll kp_power, kp_keys; struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; /* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); struct kd_state kd_play, kd_pwr;
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; int m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
} }
key_router_rebase(); /* 进入通知中心:音量基线对齐当前 raw(防残留边沿) */
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300; unsigned long long touch_cooldown_until = now_ms() + 300;
@@ -1304,12 +1303,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
} }
if (wak) { if (wak) {
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms(); meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); /* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1; td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
@@ -1331,23 +1329,20 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
} }
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */ /* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0; int play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
} }
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable; play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) { if (wake_suppress) {
if (!pwr && !vold && !volu && !play) wake_suppress = 0; if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
} else { } else {
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (play && !m_prev_play) { if (play && !m_prev_play) {
logmsg("notif-center: play key caught (media-reserved, no exit)"); logmsg("notif-center: play key caught (media-reserved, no exit)");
last_act = now; last_act = now;
@@ -1366,12 +1361,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
show_power_menu(); show_power_menu();
last_act = now_ms(); last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1; pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); /* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; need_paint = 1; after_modal = 1; need_paint = 1;
goto ncp_keys_seeded; goto ncp_keys_seeded;
@@ -1390,14 +1384,14 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
} }
} }
if (vol_ramp_dir != 0) { if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold; int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
int going = vol_ramp_tick(held); int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get(); g_sys.vol = settings_volume_get();
paint_sb_right(); paint_sb_right();
need_paint = 1; need_paint = 1;
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol); if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
} }
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; m_prev_play = play; m_prev_pwr = pwr;
ncp_keys_seeded: ncp_keys_seeded:
; ;
@@ -1562,15 +1556,13 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */ after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配, /* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */ * 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
/* 菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空, /* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */ * 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
+128 -17
View File
@@ -198,6 +198,54 @@ static void np_center_text(int y, const char *s, int size, uint32_t color, int m
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size); ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
} }
/* 左对齐 + 超宽截断(UTF-8 安全,近似按字节比例切;歌词页顶部元信息用)。 */
static void np_left_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
if (ui_width(s, size) <= maxw) {
ui_draw(x, y, s, color, size);
return;
}
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw(x, y, buf, color, size);
}
/* 歌词页顶部元信息(两行居左,复刻 ncm 歌词页):第一行歌名(大字白)、第二行歌手(小字灰)。
* 多歌手已由源侧拼串(ncm art_name 即「A / B」),此处原样显示 + 超宽截断。
* 左边距与歌词正文一致(LYR_PAD=24);离状态栏留边距(SB_BAR_H + 20);右缘同距截断。 */
static void np_draw_lyric_title(const meow_media_session_t *s)
{
if (!s) return;
if (!s->title[0] && !s->artist[0]) return;
int pad = 24;
int y = SB_BAR_H + 20;
int maxw = fb_xres - 2 * pad;
if (s->title[0])
np_left_text(pad, y, s->title, 28, 0xFFFFFF, maxw);
if (s->artist[0])
np_left_text(pad, y + meow_font_line_height(28) + 6, s->artist, 20, 0xBBBBBB, maxw);
}
/* 歌词页强制 ncm 配色:活动行恒纯白 + 上下逐级灰阶(对齐 ncm lyr_gray_tbl
* {FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}——gray[0]=0 按 cur=0xFFFFFF 自动递减即为该表)。
* 背景恒为 np_draw_lyric_page 铺的黑色半透明叠加层,故关掉 lyric_view 自带 scrim
* (scrim_alpha=0)避免双重压暗;当前行/灰阶不随主题/封面取色(保黑底对比度,夜间可读)。 */
static void np_set_lyric_ncm_colors(void)
{
if (!g_np_lyric) return;
meow_lyric_view_colors c;
memset(&c, 0, sizeof c);
c.scrim = 0x000000;
c.cur = 0xFFFFFF;
c.scrim_alpha = 0; /* 不画 scrim:由 launcher 黑底铺(单一叠加层) */
meow_lyric_view_set_colors(g_np_lyric, &c);
}
/* ================================================================== /* ==================================================================
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件): * M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
* - 进度条 → meow_progress(线性,SDK 主题 track/fill); * - 进度条 → meow_progress(线性,SDK 主题 track/fill);
@@ -346,28 +394,49 @@ static void np_draw_home_bar(void)
NP_HOME_BAR_H / 2, th->on_surface_variant); NP_HOME_BAR_H / 2, th->on_surface_variant);
} }
/* 歌词页整页绘制:背景统一黑色半透明叠加层(用户 2026-09-04,弃封面取色/封面图半透明底, /* 网易云式歌词页封面背景(用户 2026-09-05):有封面 → 等比居中放大铺满内容区(cover-fit,
* 保证高亮封面下歌词对比度)→ lyric_view(自绘 scrim + 歌词/空态)。 */ * 超出部分由画布裁剪,镜像 ncm plr_draw_lyrics 的 meow_ui_img_scaled 算法);无封面 → 纯黑底
* (现状回退)。本函数只铺底;压暗由调用方随后叠黑色半透明叠加层(alpha176)完成。 */
static void np_draw_lyric_cover_bg(int W, int H)
{
int x = 0, y = SB_BAR_H, cw = W, ch = H - SB_BAR_H; /* 只盖内容区(状态栏下),与播放页一致 */
if (!g_np_cover || meow_img_w(g_np_cover) <= 0 || meow_img_h(g_np_cover) <= 0) {
fill_rect(x, y, cw, ch, 0x000000); /* 无封面 → 纯黑底(回退现状) */
return;
}
int iw = meow_img_w(g_np_cover), ih = meow_img_h(g_np_cover);
long dw = cw, dh = (long)ih * cw / iw; /* cover-fit:铺满宽度,高按等比 */
if (dh < ch) { dh = ch; dw = (long)iw * ch / ih; } /* 宽度不足再铺满高度(超出裁剪) */
meow_ui_img_scaled(x + (cw - (int)dw) / 2, y + (ch - (int)dh) / 2, (int)dw, (int)dh, g_np_cover);
}
/* 歌词页整页绘制:网易云式封面背景(封面图当底 + 黑色半透明叠加层压暗,保证歌词对比度)→
* lyric_view(自绘 scrim 关闭,仅歌词/空态)→ 顶部元信息(歌名/歌手)。 */
static void np_draw_lyric_page(int redraw_sb) static void np_draw_lyric_page(int redraw_sb)
{ {
const meow_ui_theme_t *th = s_ui_theme(); const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres; int W = fb_xres, H = fb_yres;
if (!g_np_init) np_widgets_create(); if (!g_np_init) np_widgets_create();
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface); if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
/* 批5:封面取色背景(主→次渐变)或主题面;并注入歌词页 set_colors。 /* 批5:np_cover_theme_update 取色(仅为播放页文字/按钮主题;歌词页背景不走取色,恒 ncm 白/灰)。
* 批6:有封面时以封面作整页背景(衬在 lyric_view 半透明 scrim 之下),取色只做附加; * 批6:有封面时以封面作整页背景(网易云式 cover-fit 铺满,见 np_draw_lyric_cover_bg),
* 无封面回退渐变/主题面。 * 无封面回退纯黑底;压暗由下方黑色半透明叠加层(alpha176)统一完成,lyric_view 关内置 scrim。
* M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反, * M6.33:先 np_cover_load(artwork 空→复原/换源时重载封面)再取色——顺序不能反,
* 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */ * 取色读 g_np_cover,须在封面载入后(否则歌词页在 artwork 复原后仍无封面)。 */
np_cover_load(meow_media_current()); np_cover_load(meow_media_current());
np_cover_theme_update(); np_cover_theme_update();
np_apply_lyric_colors(); /* 网易云式封面背景(用户 2026-09-05):先铺封面底(cover-fit 原图铺满),无封面回退纯黑底;
/* 用户 2026-09-04:歌词页背景统一黑色半透明叠加层(弃封面取色/封面图半透明底)—— * 随后统一叠黑色半透明叠加层(alpha176)压暗,保证歌词对比度。歌词配色仍恒 ncm 白/灰
* 部分高亮度封面对歌词显示不友好,黑底保证对比度。歌词行颜色/布局/滚动不变。 */ * (不随封面取色——取色仅服务播放页文字/按钮)。歌词行布局/滚动逻辑不变,只换背景。 */
np_draw_lyric_cover_bg(W, H);
np_set_lyric_ncm_colors();
/* 用户 2026-09-04:黑色半透明叠加层(alpha176)——压暗封面底,保证歌词可读;无封面时叠于
* 纯黑底之上(仍纯黑,即现状回退)。 */
meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176); meow_ui_fill_rect_alpha(0, SB_BAR_H, W, H - SB_BAR_H, 0x000000, 176);
np_lyric_refresh(meow_media_current()); np_lyric_refresh(meow_media_current());
if (g_np_lyric) if (g_np_lyric)
meow_widget_draw(meow_lyric_view_widget(g_np_lyric)); meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
np_draw_lyric_title(meow_media_current()); /* 顶部元信息(歌名/歌手,居左,避状态栏) */
np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */ np_draw_home_bar(); /* 批②:歌词页也示 home 指示(底部上滑可回桌面) */
if (redraw_sb) draw_statusbar(); if (redraw_sb) draw_statusbar();
} }
@@ -486,6 +555,31 @@ static int64_t np_progress_seek(int tx, int ty)
return 0; return 0;
} }
/* 播放页按键动作应用(音量 + 播放切换)。语义与主界面/子页一致:
* - 音量键 = 系统音量(settings_volume_adj 内含插孔消抖 + 线性渐变);
* - 播放键 = 媒体播放/暂停切换(路由最近活跃媒体会话,与主界面同判);
* 边沿由调用方按防抖后稳定值计算(vold_edge = 稳定按下 ∧ 上一稳定为抬起)。
* 抽成独立小函数供宿主单测(np_apply_key_actions 仅依赖 settings_volume_adj/
* meow_media_playctl/now_ms/logmsg,可 -ffunction-sections 链接隔离)。 */
static void np_apply_key_actions(int vold_edge, int volu_edge, int play_edge,
const meow_media_session_t *s,
unsigned long long *last_input)
{
if (vold_edge) {
settings_volume_adj(-settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("np: volume DOWN <- ramp step=%d", settings_volume_step);
} else if (volu_edge) {
settings_volume_adj(+settings_volume_step);
if (last_input) *last_input = now_ms();
logmsg("np: volume UP <- ramp step=%d", settings_volume_step);
}
if (play_edge) {
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
if (last_input) *last_input = now_ms();
}
}
void run_now_playing(struct app *apps, int napps) void run_now_playing(struct app *apps, int napps)
{ {
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
@@ -495,15 +589,17 @@ void run_now_playing(struct app *apps, int napps)
kp_open(&kp_keys, DEV_KEYS); kp_open(&kp_keys, DEV_KEYS);
struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */ struct keypoll kp_power; /* M2.92:NP 页接管电源键(修「页内电源键无反应」) */
kp_open(&kp_power, DEV_POWER); kp_open(&kp_power, DEV_POWER);
struct kd_state kd_play; struct kd_state kd_play; /* 音量键防抖/基线上收 key_router(进程唯一) */
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
/* 读播放键按住态(用于边沿) */ /* 读播放键按住态(用于边沿) */
int m_prev_play = 0; int m_prev_play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
}
int np_prev_power = 0; /* 电源键边沿跟踪 */ int np_prev_power = 0; /* 电源键边沿跟踪 */
for (int w = 0; w < KEYBUF_WORDS; w++) for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) np_prev_power = 1;
key_router_rebase(); /* 进入 Now Playing:音量基线对齐当前 raw(防残留边沿) */
unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */ unsigned long long np_last_input = now_ms(); /* 自动息屏计时(M2.92:NP 页也遵守 display.screen_off_timeout) */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
@@ -521,6 +617,7 @@ void run_now_playing(struct app *apps, int napps)
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的 * 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
* owner==active 闸门——漏 push 则 active()!=0x51, * owner==active 闸门——漏 push 则 active()!=0x51,
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */ * 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
key_router_page_enter(NP_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */ g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */ g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */ g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
@@ -631,17 +728,30 @@ void run_now_playing(struct app *apps, int napps)
} }
} }
/* 播放键边沿 → 播放/暂停切换 */ /* 播放键/音量键边沿 → 媒体播放/暂停切换 + 系统音量(与主界面/子页一致)。
* 修复:run_now_playing 原先只接播放/电源键,未接音量键 → 播放页/歌词页(同一循环,
* 与页无关)音量键无反应。 */
key_evt_t kev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int play = 0; int play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (play && !m_prev_play) {
const meow_media_session_t *s = meow_media_current();
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
np_last_input = now_ms();
} }
(void)kd_tick(&kd_play, play);
play = kd_play.stable;
/* 音量键/播放键边沿 → 动作(与主界面/子页同语义;见 np_apply_key_actions) */
np_apply_key_actions(kev.vol_dn_edge, kev.vol_up_edge,
play && !m_prev_play, meow_media_current(), &np_last_input);
m_prev_play = play; m_prev_play = play;
/* 音量线性渐变(按住长爬;与主界面/子页同语义,状态栏即时刷新)。
* Now Playing 用 raw held(kev.vol_held_*,同现状 rvold/rvolu)。 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? kev.vol_held_up : kev.vol_held_dn;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
if (!going) logmsg("np: volume ramp end -> %d", g_sys.vol);
}
/* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致; /* M2.92:NP 页电源键接管——单击边沿 = 息屏/亮屏(与主界面语义一致;
* 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费, * 修「NP 页电源键无反应」。3s 电源菜单仍由 watchdog→/tmp 标记→主循环消费,
@@ -714,6 +824,7 @@ void run_now_playing(struct app *apps, int napps)
usleep(10000); usleep(10000);
} }
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */ meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
key_router_page_exit(NP_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
if (tfd >= 0) close(tfd); if (tfd >= 0) close(tfd);
kp_close(&kp_keys); kp_close(&kp_keys);
kp_close(&kp_power); kp_close(&kp_power);
+24 -38
View File
@@ -1315,16 +1315,15 @@ void run_settings_page(void)
struct keypoll kp_power, kp_keys; struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; /* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); struct kd_state kd_play, kd_pwr;
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; int m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
} }
key_router_rebase(); /* 进入设置页:音量基线对齐当前 raw(防残留边沿) */
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300; unsigned long long touch_cooldown_until = now_ms() + 300;
@@ -1358,6 +1357,7 @@ void run_settings_page(void)
logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets); logmsg("settings entry: net toggle-sync+kick %llums", now_ms() - t_sets);
if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */ if (!g_set_init) set_widgets_create(); /* M7.2:lazy 建 SDK 控件集(一次,跨次进入复用) */
meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */ meow_ui_activity_push(SET_SENTINEL_ACT); /* 控件 owner 闸门打开(hit/按压态生效) */
key_router_page_enter(SET_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
set_paint_page(sel); set_paint_page(sel);
meow_dirty_full(); meow_ui_flush(); meow_dirty_full(); meow_ui_flush();
logmsg("settings entry: page shown in %llums", now_ms() - t_sets); logmsg("settings entry: page shown in %llums", now_ms() - t_sets);
@@ -1396,13 +1396,12 @@ void run_settings_page(void)
meow_auto_slept = 0; meow_auto_slept = 0;
wake_suppress = 1; wake_suppress = 1;
last_act = now_ms(); last_act = now_ms();
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */ /* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸。
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); * 音量防抖/基线上收 key_router:唤醒时重播音量基线;播放/电源仍本页自理。 */
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1; td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
@@ -1423,28 +1422,25 @@ void run_settings_page(void)
} }
/* ---- 按键 ---- */ /* ---- 按键 ---- */
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0; int play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
} }
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable; play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) { if (wake_suppress) {
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作, /* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */ * 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
if (!pwr && !vold && !volu && !play) wake_suppress = 0; if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
} else { } else {
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束: /* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。 * 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */ * 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」: /* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。 * 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */ * 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
@@ -1480,14 +1476,11 @@ void run_settings_page(void)
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */ pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配, /* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */ * 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
memset(&kd_pwr, 0, sizeof kd_pwr); /* 重播音量基线(key_router)+ 本页播放/电源基线 */
memset(&kd_vold, 0, sizeof kd_vold); key_router_rebase(); /* 音量:kd_vold/volu 清零 + m_prev_vold/volu=当前 raw */
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */ after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
need_paint = 1; need_paint = 1;
@@ -1514,14 +1507,14 @@ void run_settings_page(void)
} }
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */ /* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
if (vol_ramp_dir != 0) { if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold; int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
int going = vol_ramp_tick(held); int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get(); g_sys.vol = settings_volume_get();
paint_sb_right(); paint_sb_right();
need_paint = 1; need_paint = 1;
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol); if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
} }
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; m_prev_play = play; m_prev_pwr = pwr;
settings_keys_seeded: settings_keys_seeded:
; ;
@@ -1658,14 +1651,10 @@ void run_settings_page(void)
scroll_y0 = 0; scroll_y0 = 0;
tdx = tdy = tlx = tly = -1; tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
memset(&kd_pwr, 0, sizeof kd_pwr); key_router_rebase(); /* 音量基线重播 */
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1; need_paint = 1;
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) { } else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
@@ -1703,14 +1692,10 @@ void run_settings_page(void)
tdx = tdy = tlx = tly = -1; tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
/* 重置设置页按键防抖基线,防残边 */ /* 重置设置页按键防抖基线,防残边 */
memset(&kd_pwr, 0, sizeof kd_pwr); key_router_rebase(); /* 音量基线重播 */
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1; need_paint = 1;
} else if (r->ctrl == SC_SLIDER && tlx >= 0) { } else if (r->ctrl == SC_SLIDER && tlx >= 0) {
@@ -1768,6 +1753,7 @@ void run_settings_page(void)
done: done:
meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */ meow_input_touch_set(0, 0, 0, 0); /* M7.2 防残留按压态 */
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */ meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
key_router_page_exit(SET_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
if (tfd >= 0) close(tfd); if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys); kp_close(&kp_power); kp_close(&kp_keys);
logmsg("settings page closed"); logmsg("settings page closed");
+27 -39
View File
@@ -247,12 +247,11 @@ int meow_subpage_run(sub_page_t *p)
meow_auto_slept = 0; meow_auto_slept = 0;
wake_suppress = 1; wake_suppress = 1;
last_act = now; last_act = now;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); /* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1; td = 0; td_ok = 0; scroll_drag = 0; tlx = tly = -1;
@@ -302,14 +301,12 @@ int meow_subpage_run(sub_page_t *p)
last_act = now_ms(); last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1; pwr_suppress_click = 1;
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; after_modal = 1;
need_paint = 1; need_paint = 1;
@@ -456,14 +453,12 @@ int meow_subpage_run(sub_page_t *p)
scroll_base = 0; scroll_y0 = 0; scroll_base = 0; scroll_y0 = 0;
tdx = tdy = tlx = tly = -1; tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; after_modal = 1;
need_paint = 1; need_paint = 1;
@@ -746,16 +741,15 @@ int meow_subpage_run(sub_page_t *p)
{ {
struct keypoll kp_power, kp_keys; struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS); kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr; /* 音量键防抖/基线已上收 key_router(进程唯一)——本页只保留播放/电源键自家防抖。 */
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); struct kd_state kd_play, kd_pwr;
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0; int m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
} }
key_router_rebase(); /* 进入子页:音量基线对齐当前 raw(防残留边沿) */
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} } if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300; unsigned long long touch_cooldown_until = now_ms() + 300;
@@ -783,6 +777,7 @@ int meow_subpage_run(sub_page_t *p)
sp_configure_page(p, top_sw_en, top_on); sp_configure_page(p, top_sw_en, top_on);
meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */ meow_ui_activity_push(SUB_SENTINEL_ACT); /* 控件 owner 闸门打开(hit 生效) */
key_router_page_enter(SUB_SENTINEL_ACT); /* 路由登记活动页(与 activity 栈交叉校验) */
sp_paint_page(); sp_paint_page();
meow_dirty_full(); meow_ui_flush(); meow_dirty_full(); meow_ui_flush();
@@ -817,12 +812,11 @@ int meow_subpage_run(sub_page_t *p)
meow_auto_slept = 0; meow_auto_slept = 0;
wake_suppress = 1; wake_suppress = 1;
last_act = now; last_act = now;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); /* 唤醒:音量基线重播(key_router)+ 本页播放/电源基线 */
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play); key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1; td = 0; td_ok = 0; scroll_drag = 0; drag_last_y = 0; tlx = tly = -1;
@@ -835,23 +829,20 @@ int meow_subpage_run(sub_page_t *p)
} }
/* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */ /* ---- 按键(与旧版逐字一致:音量=系统音量/播放=媒体预留/电源=系统级语义)---- */
key_evt_t ev = key_router_pump(); /* 音量边沿/held/ramp(进程唯一防抖基线) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0; int play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) { for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1; if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1; if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
} }
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr); (void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable; play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) { if (wake_suppress) {
if (!pwr && !vold && !volu && !play) wake_suppress = 0; if (!pwr && !ev.vol_st_dn && !ev.vol_st_up && !play) wake_suppress = 0;
} else { } else {
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; } if (ev.vol_dn_edge) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; } if (ev.vol_up_edge) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; } if (play && !m_prev_play) { logmsg("subpage: play key caught (media-reserved)"); last_act = now; }
if (pwr && !m_prev_pwr) { if (pwr && !m_prev_pwr) {
pwr_hold_at = now; pwr_menu_state = 0; pwr_hold_at = now; pwr_menu_state = 0;
@@ -870,14 +861,12 @@ int meow_subpage_run(sub_page_t *p)
last_act = now_ms(); last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1; pwr_suppress_click = 1;
/* 电源菜单返回:音量基线重播(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; after_modal = 1;
need_paint = 1; need_paint = 1;
@@ -902,13 +891,13 @@ int meow_subpage_run(sub_page_t *p)
} }
} }
if (vol_ramp_dir != 0) { if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold; int held = (vol_ramp_dir > 0) ? ev.vol_held_up : ev.vol_held_dn;
(void)vol_ramp_tick(held); (void)vol_ramp_tick(held);
g_sys.vol = settings_volume_get(); g_sys.vol = settings_volume_get();
paint_sb_right(); paint_sb_right();
need_paint = 1; need_paint = 1;
} }
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr; m_prev_play = play; m_prev_pwr = pwr;
sub_keys_seeded: sub_keys_seeded:
; ;
@@ -1024,14 +1013,12 @@ int meow_subpage_run(sub_page_t *p)
drag_last_y = 0; drag_last_y = 0;
tdx = tdy = tlx = tly = -1; tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300; touch_cooldown_until = now_ms() + 300;
/* 返回重播:音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL); kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; after_modal = 1;
need_paint = 1; need_paint = 1;
@@ -1090,6 +1077,7 @@ int meow_subpage_run(sub_page_t *p)
subdone: subdone:
meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */ meow_input_touch_set(0, 0, 0, 0); /* 防残留按压态 */
meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */ meow_ui_activity_pop(); /* 关 owner 闸门(控件 owner 仍钉哨兵,复用) */
key_router_page_exit(SUB_SENTINEL_ACT); /* 路由:退出登记 + 重播音量基线 */
if (tfd >= 0) close(tfd); if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys); kp_close(&kp_power); kp_close(&kp_keys);
logmsg("subpage '%s' closed", p->title ? p->title : ""); logmsg("subpage '%s' closed", p->title ? p->title : "");
@@ -0,0 +1,229 @@
/* test_key_router_vol.c — 宿主单测:launcher-UI 页物理键统一路由(KEY_ROUTER_UNIFY P1)。
*
* 测「key_router 自身的音量编排」——它只产「音量边沿 + 原始 held + ramp 状态」,
* 由调用页决定 settings_volume_adj(±step) / 光标导航 / ramp 推进。本测用内存桩:
* - kp_poll/kp_open/kp_is_down → 注入 raw 键位矩阵到 key_router 的静态 kp_keys.cur[];
* - kd_tick → 用 keys.c 的真实现(40ms 防抖,V4 依赖);keys.c 内 kp_open/kp_poll/
* kp_is_down/meow_volpage_armed 用宏重命名避开真实 ioctl 与桩冲突;
* - settings_volume_adj/settings_volume_step/vol_ramp_dir/vol_ramp_tick → 内存桩;
* - now_ms → 假时钟(手动推进,40ms 防抖可瞬时测);
* - meow_ui_activity_* → 内存桩(V5 嵌套:真语义=纯静态数组,桩复刻)。
*
* 断言:
* V1 音量减边沿 → settings_volume_adj(-step) 恰一次 + last_input 复位;
* V2 同会话重复(同 raw 仍按住)→ 不再触发 settings_volume_adj;
* V3 长按 held → vol_ramp_tick 被驱动(vol_ramp_dir!=0,held=正确方向 raw);松开 → dir=0;
* V4 40ms 防抖:raw 在 40ms 内抖动 → 不产边沿;稳定按下 ≥40ms → 才产边沿;
* V5 嵌套返回:page_enter(0x53)→page_exit(0x53)→page_enter(0x55),基线重播后 held 键不产假边沿;
* V6 meow_volpage_armed() → 音量边沿被抑制(不下发,不调 settings_volume_adj)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -I../sdk -I. -ffunction-sections -fdata-sections \
* -Wl,--gc-sections test_key_router_vol.c -o test_key_router_vol
* ./test_key_router_vol
*/
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
/* ---- 假时钟 ---- */
static unsigned long long g_now = 100000;
unsigned long long now_ms(void) { return g_now; }
static void advance_ms(unsigned long long d) { g_now += d; }
/* ---- 音量机外部符号桩(key_router 依赖)---- */
int settings_volume_step = 5;
static int g_adj_count = 0, g_adj_delta = 0;
int vol_ramp_dir = 0; /* 桩全局:模拟 audio.c 的 ramp 状态 */
static int g_tick_count = 0, g_tick_held = -1;
void settings_volume_adj(int delta)
{
g_adj_count++; g_adj_delta = delta;
if (delta > 0) vol_ramp_dir = 1; else if (delta < 0) vol_ramp_dir = -1;
}
int vol_ramp_tick(int key_held)
{
g_tick_count++; g_tick_held = key_held;
if (!key_held) vol_ramp_dir = 0;
return vol_ramp_dir != 0;
}
static int g_volpage = 0; /* 桩:meow_volpage_armed */
/* ---- keys.c 内 kp_open/kp_poll/kp_is_down/meow_volpage_armed 用宏重命名,避开真实 ioctl ----
* (宏在 #include keys.c 前生效,仅重命名 keys.c 体内的调用/定义) ---- */
#define kp_open kp_open_rt_
#define kp_poll kp_poll_rt_
#define kp_is_down kp_is_down_rt_
#define meow_volpage_armed meow_volpage_armed_rt_
#include "keys.c" /* 真 kd_tick(40ms 防抖素材) */
#undef kp_open
#undef kp_poll
#undef kp_is_down
#undef meow_volpage_armed
/* host 桩声明(key_router.c 需要;恢复到正常名,与 keys.c 被更名后的定义不冲突) */
struct keypoll;
int kp_open(struct keypoll *kp, const char *dev);
int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx), void *ctx);
int kp_is_down(const struct keypoll *k, int code);
int meow_volpage_armed(void);
#include "key_router.c" /* 同 TU #include:拿到静态 kp_keys/kp_power/kd_vold/kd_volu + 全部函数 */
#include "key_router.h"
/* ---- meow_ui_activity_* 桩(复刻 sdk/meow_ui_activity.c:纯静态数组,真语义)---- */
static int g_act[32]; static int g_act_n = 0;
void meow_ui_activity_push(int id) { if (g_act_n < 32) g_act[g_act_n++] = id; }
void meow_ui_activity_pop(void) { if (g_act_n > 0) g_act_n--; }
int meow_ui_activity_active(void) { return g_act_n > 0 ? g_act[g_act_n - 1] : 0; }
/* ---- 注入 raw 到 key_router 静态 kp_keys ---- */
static void set_vol(int vold, int volu)
{
for (int w = 0; w < KEYBUF_WORDS; w++) kp_keys.cur[w] = 0;
if (vold) kp_keys.cur[KEY_VOLUMEDOWN / 32] |= (1UL << (KEY_VOLUMEDOWN % 32));
if (volu) kp_keys.cur[KEY_VOLUMEUP / 32] |= (1UL << (KEY_VOLUMEUP % 32));
}
/* ---- host 桩:kp_*(prev=cur 模拟真实 poll 的键位更新)+ meow_volpage_armed ---- */
int kp_open(struct keypoll *kp, const char *dev)
{ (void)dev; memset(kp, 0, sizeof *kp); kp->ok = 1; return 0; }
int kp_poll(struct keypoll *kp, int (*cb)(int, int, void *), void *ctx)
{
(void)cb; (void)ctx;
for (int w = 0; w < KEYBUF_WORDS; w++) kp->prev[w] = kp->cur[w];
return 0;
}
int kp_is_down(const struct keypoll *k, int code)
{ return (k->cur[code / 32] >> (code % 32)) & 1; }
int meow_volpage_armed(void) { return g_volpage; }
/* 其它全局桩(keys.c 内被更名函数引用;本测 GC 后不保留,仅满足声明一致性) */
uint32_t menu_bg = 0;
int screen_off_timeout = 0, screen_off_mode = 0, power_longpress_menu_ms = 0;
int power_forceoff_ms = 0, refresh_rate_cfg = 60, refresh_always_max = 0;
void logmsg(const char *fmt, ...) { (void)fmt; }
static int g_fails = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fails++; } } while (0)
static void reset_router(void)
{
set_vol(0, 0);
key_router_rebase();
g_adj_count = 0; g_adj_delta = 0; g_tick_count = 0; g_tick_held = -1;
vol_ramp_dir = 0; g_volpage = 0;
g_act_n = 0;
}
/* 按下并稳定:register(pump) → hold ≥40ms → pump(stable=1, 边沿)。 */
static key_evt_t press_settle(int vold, int volu)
{
set_vol(vold, volu);
key_router_pump(); /* 登记 raw 跳变(防抖窗口起点) */
advance_ms(50); /* ≥KEY_DEBOUNCE_MS(40) */
return key_router_pump(); /* stable→1,边沿 */
}
/* 抬起并稳定:register(pump) → → pump(stable=0)。 */
static key_evt_t release_settle(void)
{
set_vol(0, 0);
key_router_pump(); /* 登记 release */
advance_ms(50);
return key_router_pump(); /* stable→0 */
}
int main(void)
{
unsigned long long last_input = 0;
/* ---- V1 音量减边沿 → settings_volume_adj(-step) 恰一次 + last_input 复位 ---- */
reset_router();
set_vol(0, 0); advance_ms(10); key_router_pump(); /* 全抬起基线 */
last_input = 0;
key_evt_t ev = press_settle(1, 0);
CHECK(ev.vol_dn_edge == 1, "V1 vol-down edge fires");
key_router_default_volume(&ev, &last_input);
CHECK(g_adj_count == 1 && g_adj_delta == -settings_volume_step,
"V1 vol-down -> settings_volume_adj(-step) exactly once");
CHECK(last_input == g_now, "V1 last_input reset on edge");
/* ---- V2 同会话重复(同 raw 仍按住)→ 不再触发 ---- */
reset_router();
set_vol(0, 0); advance_ms(10); key_router_pump();
ev = press_settle(1, 0);
CHECK(ev.vol_dn_edge == 1, "V2a press edge fires");
ev = key_router_pump(); /* 同 raw 仍按住,无新边沿 */
CHECK(ev.vol_dn_edge == 0, "V2 held: no re-trigger edge");
key_router_default_volume(&ev, &last_input);
CHECK(g_adj_count == 0, "V2 held: no double settings_volume_adj");
/* ---- V3 长按 held → vol_ramp_tick 被驱动(held=正确方向 raw);松开 → dir=0 ---- */
reset_router();
set_vol(0, 0); advance_ms(10); key_router_pump(); last_input = 0;
ev = press_settle(1, 0); /* 按下(vol down)并转稳定 */
key_router_default_volume(&ev, &last_input);
CHECK(g_tick_count == 1 && g_tick_held == ev.vol_held_dn && vol_ramp_dir != 0,
"V3 hold vol-down -> ramp driven with held=raw down");
advance_ms(50);
ev = key_router_pump(); /* 继续长按(raw=1 仍按住) */
key_router_default_volume(&ev, &last_input);
CHECK(g_tick_count == 2 && g_tick_held == 1, "V3 long-press continues ramp");
ev = release_settle(); /* 松开:raw held 立即为 0 */
key_router_default_volume(&ev, &last_input);
CHECK(vol_ramp_dir == 0, "V3 release -> ramp dir 0 (stops)");
/* ---- V4 40ms 防抖:raw 在 40ms 内抖动不产边沿;稳定 ≥40ms 才产 ---- */
reset_router();
set_vol(0, 0); advance_ms(10); key_router_pump();
set_vol(1, 0); advance_ms(20); key_router_pump(); /* 按下未满 40ms */
set_vol(0, 0); advance_ms(20); key_router_pump(); /* 40ms 内又抬起 */
ev = key_router_pump();
CHECK(ev.vol_dn_edge == 0, "V4 <40ms jitter -> no edge");
ev = press_settle(1, 0); /* 稳定按下 ≥40ms */
CHECK(ev.vol_dn_edge == 1, "V4 stable >=40ms press -> edge");
ev = release_settle(); /* 抬起稳定 */
CHECK(ev.vol_dn_edge == 0 && ev.vol_up_edge == 0, "V4 released -> no residual edge");
ev = press_settle(0, 1); /* 稳定按音量加 */
CHECK(ev.vol_up_edge == 1, "V4 vol-up stable press -> up edge");
/* ---- V5 嵌套返回:page_enter/exit 重播基线,held 键不产假边沿 ---- */
reset_router();
meow_ui_activity_push(0x55); key_router_page_enter(0x55); /* 主菜单 */
set_vol(0, 0); advance_ms(10); key_router_pump();
ev = press_settle(1, 0);
CHECK(ev.vol_dn_edge == 1, "V5a press edge in menu");
/* 嵌套进设置页:压栈 + 登记 */
meow_ui_activity_push(0x53); key_router_page_enter(0x53);
CHECK(meow_ui_activity_active() == 0x53, "V5b settings active sentinel");
ev = release_settle(); /* 设置页内把键抬起 */
CHECK(ev.vol_dn_edge == 0, "V5c settings-up: no fresh edge (consumer)");
/* 设置页返回(模拟 run_settings_page 结束):弹栈 + 重播基线 → m_prev=当前 raw(0) */
key_router_page_exit(0x53); meow_ui_activity_pop();
key_router_page_enter(0x55);
CHECK(meow_ui_activity_active() == 0x55, "V5d menu active again");
/* 主菜单基线已重播;再次按下应为新边沿,而非残留/双发 */
ev = press_settle(1, 0);
CHECK(ev.vol_dn_edge == 1, "V5e fresh press after nest return -> single edge");
/* ---- V6 meow_volpage_armed() → 音量边沿被抑制(不下发)---- */
reset_router();
set_vol(0, 0); advance_ms(10); key_router_pump();
g_volpage = 1;
ev = press_settle(1, 0);
CHECK(ev.vol_dn_edge == 0, "V6 volpage armed -> edge suppressed");
key_router_default_volume(&ev, &last_input);
CHECK(g_adj_count == 0, "V6 volpage armed -> settings_volume_adj NOT called");
g_volpage = 0;
printf("\nRESULT: %s (%d fails)\n", g_fails ? "FAIL" : "ALL PASS", g_fails);
return g_fails ? 1 : 0;
}
+114
View File
@@ -0,0 +1,114 @@
/* test_np_keys.c — 宿主单测:Now Playing 播放页/歌词页物理键分发(Task C)。
*
* 背景(真机现象):桌面播放页与歌词页上,音量键 + 播放键「完全无反应」。
* 根因(now_playing.c):run_now_playing() 原先只接播放键(164)与电源键,未接音量键
* (114/115),导致播放页/歌词页(同一循环,与页无关)调不了音量。
*
* 本测复现 run_now_playing() 的按键动作应用(np_apply_key_actions):音量键走
* settings_volume_adj(±step)、播放键走 meow_media_playctl(按会话当前态 1=播放 2=暂停)。
* 断言:
* K1 音量减边沿 → settings_volume_adj(-step);
* K2 音量加边沿 → settings_volume_adj(+step);
* K3 播放键边沿 + 会话正在播放 → meow_media_playctl(2,0)(暂停);
* K4 播放键边沿 + 会话已暂停 → meow_media_playctl(1,0)(播放);
* K5 播放键边沿 + 无会话 → 不下发(空态不误报);
* K6 无任何边沿 → 两个回调都不触发;
* K7 音量/播放边沿都会更新 last_input(自动息屏计时复位)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -I../sdk -I. -ffunction-sections -fdata-sections \
* -Wl,--gc-sections -DMEOW_NP_KEYS_TEST \
* test_np_keys.c -o test_np_keys
* ./test_np_keys
* 说明:now_playing.c 用 #include 同 TU 平移(惯例同 test_vol_ramp_jack);-ffunction-sections
* + --gc-sections 只保留 np_apply_key_actions 及其依赖,其余(渲染/控件/绘制)被回收,
* 故仅需桩 settings_volume_adj/settings_volume_step/meow_media_playctl/now_ms/logmsg。
*/
#include "now_playing.c" /* 同 TU #include:拿到 static np_apply_key_actions */
#include <stdio.h>
#include <string.h>
static int g_fails = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fails++; } } while (0)
/* ---- 桩:np_apply_key_actions 依赖的 launcher 外部符号 ---- */
int settings_volume_step = 5;
static int g_adj_count = 0, g_adj_delta = 0;
void settings_volume_adj(int delta) { g_adj_count++; g_adj_delta = delta; }
static int g_playctl_count = 0;
static uint32_t g_playctl_cmd = 0;
static int64_t g_playctl_seek = 0;
int meow_media_playctl(uint32_t cmd, int64_t seek_ms)
{ g_playctl_count++; g_playctl_cmd = cmd; g_playctl_seek = seek_ms; return 0; }
static unsigned long long g_now = 100000;
unsigned long long now_ms(void) { return g_now; }
void logmsg(const char *fmt, ...) { (void)fmt; }
/* 会话(meow_media_session_t 来自 meow_media.h,含 state 字段) */
static meow_media_session_t g_s;
static void reset_callbacks(void)
{
g_adj_count = 0; g_adj_delta = 0;
g_playctl_count = 0; g_playctl_cmd = 0; g_playctl_seek = 0;
}
int main(void)
{
memset(&g_s, 0, sizeof g_s);
unsigned long long last_input = 0;
/* K1 音量减边沿 → settings_volume_adj(-step) */
reset_callbacks(); last_input = 0; g_now = 100000;
np_apply_key_actions(1, 0, 0, &g_s, &last_input);
CHECK(g_adj_count == 1 && g_adj_delta == -settings_volume_step,
"K1 vol-down edge -> settings_volume_adj(-step)");
CHECK(last_input == g_now, "K7a vol-down updates last_input");
/* K2 音量加边沿 → settings_volume_adj(+step) */
reset_callbacks(); last_input = 0; g_now = 100050;
np_apply_key_actions(0, 1, 0, &g_s, &last_input);
CHECK(g_adj_count == 1 && g_adj_delta == +settings_volume_step,
"K2 vol-up edge -> settings_volume_adj(+step)");
CHECK(last_input == g_now, "K7b vol-up updates last_input");
/* K3 播放键边沿 + 正在播放 → MSG_PLAYCTL(2,0) 暂停 */
g_s.state = MEOW_MEDIA_PLAYING;
reset_callbacks();
np_apply_key_actions(0, 0, 1, &g_s, &last_input);
CHECK(g_playctl_count == 1 && g_playctl_cmd == 2 && g_playctl_seek == 0,
"K3 play edge (playing) -> playctl pause(2)");
/* K4 播放键边沿 + 已暂停 → MSG_PLAYCTL(1,0) 播放 */
g_s.state = MEOW_MEDIA_PAUSED;
reset_callbacks();
np_apply_key_actions(0, 0, 1, &g_s, &last_input);
CHECK(g_playctl_count == 1 && g_playctl_cmd == 1 && g_playctl_seek == 0,
"K4 play edge (paused) -> playctl play(1)");
/* K5 播放键边沿 + 无会话 → 不下发(并仍复位 last_input) */
reset_callbacks(); last_input = 0; g_now = 100100;
np_apply_key_actions(0, 0, 1, NULL, &last_input);
CHECK(g_playctl_count == 0, "K5 play edge (no session) -> no playctl");
CHECK(last_input == g_now, "K7c play edge updates last_input (no session)");
/* K6 无任何边沿 → 两个回调都不触发 */
reset_callbacks(); last_input = 0;
np_apply_key_actions(0, 0, 0, &g_s, &last_input);
CHECK(g_adj_count == 0 && g_playctl_count == 0, "K6 no edge -> no callbacks");
/* K7 音量递减时播放键不同时触发(音量边沿优先,播放单独判) */
g_s.state = MEOW_MEDIA_PLAYING;
reset_callbacks();
np_apply_key_actions(1, 0, 1, &g_s, &last_input);
CHECK(g_adj_count == 1 && g_playctl_count == 1,
"K8 vol-down + play_edge both fire (independent)");
printf("\nRESULT: %s (%d fails)\n", g_fails ? "FAIL" : "ALL PASS", g_fails);
return g_fails ? 1 : 0;
}
+30 -24
View File
@@ -186,23 +186,29 @@ static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface; uint32_t textcol = sel ? mt->onPrimaryContainer : mt->onSurface;
uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant; uint32_t subcol = sel ? mt->onPrimaryContainer : mt->onSurfaceVariant;
/* MD3 容器化(P0):行=surface 上 surfaceContainer 圆角卡;段头=surface 上的 primary 段标签(无卡
* 无分隔线);与 settings_page.c set_lv_draw 同构造。 */
int cx = x + SET_CARD_X;
int cw = w - 2 * SET_CARD_X;
int cy = y + 8 / 2;
int ch = SET_ITEM_H - 8;
if (r->ctrl == SC_HEADER) { if (r->ctrl == SC_HEADER) {
meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->surfaceContainerHigh);
int ty = y + (SET_ITEM_H - thh) / 2; int ty = y + (SET_ITEM_H - thh) / 2;
if (ty >= 0) meow_ui_text(x + 16, ty, r->label, mt->font_body, mt->primary); if (ty >= 0) meow_ui_text(cx + 16, ty, r->label, mt->font_body, mt->primary);
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant);
return; return;
} }
if (sel) meow_ui_fill_rect(x, y, w, SET_ITEM_H, mt->primaryContainer); meow_gfx_fill_round(cx, cy, cw, ch, 12, mt->surfaceContainer);
else if (press) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->primary, 26); if (sel) meow_gfx_fill_round(cx, cy, cw, ch, 12, mt->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, ch, 12, mt->primary, 26);
int ly = y + (SET_ITEM_H - thh) / 2; int ly = y + (SET_ITEM_H - thh) / 2;
int rgt = cx + cw - 16;
switch (r->ctrl) { switch (r->ctrl) {
case SC_SWITCH: { case SC_SWITCH: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
meow_widget *sw = set_sw_take(); meow_widget *sw = set_sw_take();
if (sw) { if (sw) {
int on = 0; meow_settings_get_switch(r->key, &on); int on = 0; meow_settings_get_switch(r->key, &on);
meow_widget_set_rect(sw, x + w - 16 - 56, y + (SET_ITEM_H - 32) / 2, 56, 32); meow_widget_set_rect(sw, rgt - 56, y + (SET_ITEM_H - 32) / 2, 56, 32);
meow_switch_set(sw, on); meow_switch_set(sw, on);
meow_widget_draw(sw); meow_widget_draw(sw);
} }
@@ -211,26 +217,25 @@ static void lv_draw(int idx, int x, int y, int w, int h, int state, void *ctx)
case SC_SLIDER: { case SC_SLIDER: {
/* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由 /* 宿主测试不引入 launcher s_draw_slider/s_ui_theme:只验证行文字渲染 + 命中,track 视觉由
* 真机 s_draw_slider 呈现(settings_page.c set_lv_draw 已复用)。 */ * 真机 s_draw_slider 呈现(settings_page.c set_lv_draw 已复用)。 */
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
char vb[32]; s_fmt_value(r, vb, sizeof vb); char vb[32]; s_fmt_value(r, vb, sizeof vb);
meow_ui_text(x + w - 16 - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol); meow_ui_text(rgt - meow_ui_text_width(vb, mt->font_body), ly, vb, mt->font_body, subcol);
break; break;
} }
case SC_LIST: { case SC_LIST: {
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
meow_ui_text(x + w - 16 - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol); meow_ui_text(rgt - meow_ui_text_width("已连接", mt->font_body), ly, "已连接", mt->font_body, subcol);
break; break;
} }
default: { /* SC_ENUM / SC_PRESET */ default: { /* SC_ENUM / SC_PRESET */
meow_ui_text(x + 16, ly, r->label, mt->font_body, textcol); meow_ui_text(cx + 16, ly, r->label, mt->font_body, textcol);
char vb[40]; s_display_value(r, vb, sizeof vb); char vb[40]; s_display_value(r, vb, sizeof vb);
int vx = x + w - 16 - meow_ui_text_width(vb, mt->font_body); int vx = rgt - meow_ui_text_width(vb, mt->font_body);
meow_ui_text(vx, ly, vb, mt->font_body, subcol); meow_ui_text(vx, ly, vb, mt->font_body, subcol);
break; break;
} }
} }
meow_ui_fill_rect(x, y + SET_ITEM_H - 1, w, 1, mt->outlineVariant); if (state & MEOW_LISTVIEW_ST_DISABLED) meow_gfx_fill_round_alpha(cx, cy, cw, ch, 12, mt->onSurface, 90);
if (state & MEOW_LISTVIEW_ST_DISABLED) meow_ui_fill_rect_alpha(x, y, w, SET_ITEM_H, mt->onSurface, 90);
} }
static void mk_widgets(void) static void mk_widgets(void)
@@ -247,7 +252,7 @@ static void draw_all(void)
{ {
const meow_theme *th = meow_theme_get(); const meow_theme *th = meow_theme_get();
meow_ui_fill_rect(0, 0, XRES, YRES, th->surface); meow_ui_fill_rect(0, 0, XRES, YRES, th->surface);
meow_ui_fill_rect(0, SET_LIST_Y, XRES, LV_H, th->surfaceVariant); meow_ui_fill_rect(0, SET_LIST_Y, XRES, LV_H, th->surface); /* P0:内容底 surface(卡在其上抬升) */
if (lv) { g_set_sw_use = 0; meow_widget_draw(lv); } if (lv) { g_set_sw_use = 0; meow_widget_draw(lv); }
} }
@@ -305,16 +310,17 @@ int main(void)
meow_ui_activity_push(SET_SENTINEL); meow_ui_activity_push(SET_SENTINEL);
const meow_theme *th = meow_theme_get(); const meow_theme *th = meow_theme_get();
draw_all(); draw_all();
/* 段头行整体填 surfaceContainerHigh(覆盖列表底 surfaceVariant)→ 对列表底背景即非背景; /* MD3 容器化(P0):段头=surface 上的 primary 段标签(无满幅 band/卡,宿主文本为 no-op →
* 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「段头底 vs 列表底」判渲染而非文字像素。 */ * 段头区应回 surface);普通行=surfaceContainer 圆角卡(region_nonbg 应见大量非 surface 像素)。
int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant); * 注意宿主无 mfont 时 CJK 标题回退点阵不显形,故以「卡/段头 vs 列表底」判渲染而非文字像素。 */
int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surfaceVariant); int hdrcnt = region_nonbg(2, SET_LIST_Y + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
CHECK(hdrcnt > 0, "section header band renders (hdr=%d)", hdrcnt); int rowcnt = region_nonbg(2, SET_LIST_Y + SET_ITEM_H + 4, XRES - 4, SET_ITEM_H - 8, th->surface);
CHECK(rowcnt > 40, "row band renders (title/value/switch) cnt=%d", rowcnt); CHECK(hdrcnt == 0, "section header no-band(label-on-surface) nb=%d", hdrcnt);
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景 */ CHECK(rowcnt > 40, "row card renders (title/value/switch) cnt=%d", rowcnt);
/* P1b:开关行(ROWS[2]=最高刷新率 right switch)中心右缘区非背景(卡内 switch) */
{ {
int swy = SET_LIST_Y + 2 * SET_ITEM_H + SET_ITEM_H / 2; int swy = SET_LIST_Y + 2 * SET_ITEM_H + SET_ITEM_H / 2;
int swc = region_nonbg(XRES - 16 - 56, swy - 4, 56, 8, th->surfaceVariant); int swc = region_nonbg(XRES - 16 - 56, swy - 4, 56, 8, th->surface);
CHECK(swc > 0, "switch row right tail (meow_switch) renders swc=%d", swc); CHECK(swc > 0, "switch row right tail (meow_switch) renders swc=%d", swc);
} }
@@ -125,6 +125,7 @@ static void mk_widgets(void)
meow_listview_set_vsource(lv, sp_lv_count, sp_lv_bind, NULL); meow_listview_set_vsource(lv, sp_lv_count, sp_lv_bind, NULL);
meow_listview_set_item_renderer(lv, 2, 0, 1); meow_listview_set_item_renderer(lv, 2, 0, 1);
meow_listview_set_item_h(lv, SUB_ITEM_H); meow_listview_set_item_h(lv, SUB_ITEM_H);
meow_listview_set_container(lv, 1); /* MD3 容器模式(M7.1a P0):圆角卡+空隙+去分隔线 */
sw = meow_switch_create(XRES - 16 - 56, SUB_TB_Y + (SUB_TB_H - 32) / 2, 1); sw = meow_switch_create(XRES - 16 - 56, SUB_TB_Y + (SUB_TB_H - 32) / 2, 1);
meow_ui_activity_pop(); meow_ui_activity_pop();
} }
@@ -153,12 +154,14 @@ int main(void)
meow_ui_activity_push(SUB_SENTINEL); meow_ui_activity_push(SUB_SENTINEL);
const meow_theme *th = meow_theme_get(); const meow_theme *th = meow_theme_get();
draw_all(); draw_all();
/* MD3 容器模式:段头=surface 上的 primary 段标签(无满幅 band/卡,宿主文本为 no-op →
* 段头区应回 surface);普通行=surfaceContainer 圆角卡(region_nonbg 应见大量非 surface 像素)。 */
int hdrcnt = region_nonbg(2, SUB_CONT_TOP + 4, XRES - 4, SUB_ITEM_H - 8, th->surface); int hdrcnt = region_nonbg(2, SUB_CONT_TOP + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
int rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface); int rowcnt = region_nonbg(2, SUB_CONT_TOP + SUB_ITEM_H + 4, XRES - 4, SUB_ITEM_H - 8, th->surface);
char nm[80]; char nm[80];
snprintf(nm, sizeof nm, "section header renders (band=%d)", hdrcnt); snprintf(nm, sizeof nm, "section header no-band(label-on-surface) nb=%d", hdrcnt);
CHECK(hdrcnt > 0, "%s", nm); CHECK(hdrcnt == 0, "%s", nm);
snprintf(nm, sizeof nm, "row renders (title hdr/switch/arrow) cnt=%d", rowcnt); snprintf(nm, sizeof nm, "row renders (card) cnt=%d", rowcnt);
CHECK(rowcnt > 40, "%s", nm); CHECK(rowcnt > 40, "%s", nm);
/* ============ P2 listview 命中(行号 + right 操作区) ============ */ /* ============ P2 listview 命中(行号 + right 操作区) ============ */
@@ -190,6 +193,7 @@ int main(void)
meow_listview_set_vsource(scroll_lv, big_count, big_bind, NULL); meow_listview_set_vsource(scroll_lv, big_count, big_bind, NULL);
meow_listview_set_item_renderer(scroll_lv, 2, 0, 1); meow_listview_set_item_renderer(scroll_lv, 2, 0, 1);
meow_listview_set_item_h(scroll_lv, SUB_ITEM_H); meow_listview_set_item_h(scroll_lv, SUB_ITEM_H);
meow_listview_set_container(scroll_lv, 1); /* 与子页一致:MD3 容器模式 */
meow_scroll *s = meow_scroll_wrap(scroll_lv, 1); /* 1=W_LISTVIEW */ meow_scroll *s = meow_scroll_wrap(scroll_lv, 1); /* 1=W_LISTVIEW */
CHECK(s != NULL, "meow_scroll_wrap(listview)"); CHECK(s != NULL, "meow_scroll_wrap(listview)");
int maxs = meow_listview_max_scroll(scroll_lv); int maxs = meow_listview_max_scroll(scroll_lv);
+64 -15
View File
@@ -19,6 +19,7 @@
#define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */ #define WF_ACT_CURR (-1) /* 当前连接(SUB_ROW,detail) */
#define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */ #define WF_ACT_SAVED 0 /* 已保存网络 idx(SUB_ACT,connect) */
#define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */ #define WF_ACT_SCAN_BASE 1000 /* 附近网络 1000+idx(SUB_ROW,连接) */
#define WF_ACT_MANUAL 2000 /* 「手动添加隐藏网络」动作行(SUB_ACT,connecth) */
typedef struct { typedef struct {
meow_net_t saved[S_NET_MAX]; meow_net_t saved[S_NET_MAX];
@@ -122,13 +123,22 @@ static int wifi_rows(void *ctx, sub_row_t *rbuf, int max)
rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i; rbuf[n].on = 0; rbuf[n].act = WF_ACT_SCAN_BASE + i;
n++; n++;
} }
/* bug2:附近网络末尾的「手动添加隐藏网络」入口(SUB_ACT → wifi_act)。 */
if (n < max) {
rbuf[n].kind = SUB_ACT;
rbuf[n].label = "手动添加隐藏网络";
rbuf[n].sub = "";
rbuf[n].on = 0;
rbuf[n].act = WF_ACT_MANUAL;
n++;
}
return n; return n;
} }
/* SDK IME 输密码(PW 模式=ASCII 软键盘输入,现有输入法复用;回车提交/ESC或点背景取消)。 /* SDK IME 通用文本输入(MEOW_IME_ASCII:QWERTY,顶条「中」可切拼音输中文 SSID)。
* 返回 1=已输入;0=取消。 */ * 返回 1=已输入;0=取消。 */
static int wifi_ask_psk(char *psk, int maxlen) static int wifi_ask_text(char *out, int maxlen)
{ {
meow_ime_ctx ic; meow_ime_ctx ic;
memset(&ic, 0, sizeof ic); memset(&ic, 0, sizeof ic);
@@ -136,36 +146,72 @@ static int wifi_ask_psk(char *psk, int maxlen)
ic.maxlen = maxlen; ic.maxlen = maxlen;
ic.buf[0] = 0; ic.buf[0] = 0;
int res = meow_ui_text_input(&ic); /* 阻塞:提交后 ctx.buf 回填最终文本 */ int res = meow_ui_text_input(&ic); /* 阻塞:提交后 ctx.buf 回填最终文本 */
if (res && ic.buf[0]) { snprintf(psk, maxlen, "%s", ic.buf); return 1; } if (res && ic.buf[0]) { snprintf(out, maxlen, "%s", ic.buf); return 1; }
return 0; return 0;
} }
/* bug1:connect 阻塞前画非模态「正在连接…」横幅。子页单线程,阻塞期间画面停在此横幅。 */
static void wifi_connecting_banner(const char *ssid)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = meow_ui_width();
int y = 108, h = 44;
char line[160];
snprintf(line, sizeof line, "正在连接 %s ...", ssid && ssid[0] ? ssid : "网络");
meow_ui_fill_rect(0, y, W, h, th->surface_container);
meow_ui_text_center(W / 2, y + h / 2 - meow_ui_text_height(20) / 2, line, 20, th->on_surface);
meow_dirty_full();
meow_ui_flush();
}
/* bug2:手动添加隐藏网络(附近网络占位行点击 / 「手动添加隐藏网络」动作行共用)。 */
static void wifi_manual_add(wifi_ctx_t *c)
{
char ssid[96] = "";
if (!wifi_ask_text(ssid, sizeof ssid)) return;
const char *items[2];
items[0] = "加密网络(WPA/WPA2/WPA3)";
items[1] = "开放网络";
int pick = s_dd_modal("网络加密方式", items, 2, -1);
if (pick < 0) return;
int open = (pick == 1);
char psk[96] = "";
if (!open && !wifi_ask_text(psk, sizeof psk)) return;
char err[160]; err[0] = 0;
wifi_connecting_banner(ssid);
int rc = nm_wifi_connect_ex(ssid, open ? NULL : psk, 1, err, sizeof err);
char line[192]; const char *txt[1]; txt[0] = line;
snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
logmsg("wifi_page: manual connect ssid=%s hidden=1 open=%d rc=%d err=%s", ssid, open, rc, err);
s_dd_modal(ssid, txt, 1, -1);
nm_wifi_scan_trigger();
c->last_scan_ms = now_ms();
}
/* 附近网络点选 →【WiFi 新网络录入】连接流程。返回 1=重绘。 */ /* 附近网络点选 →【WiFi 新网络录入】连接流程。返回 1=重绘。 */
static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx) static int wifi_connect_scan(wifi_ctx_t *c, int scan_idx)
{ {
if (scan_idx < 0 || scan_idx >= c->scan_n) return 0; if (scan_idx < 0 || scan_idx >= c->scan_n) return 0;
meow_net_t *s = &c->scan[scan_idx]; meow_net_t *s = &c->scan[scan_idx];
if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) { if (!s->name[0] || !strcmp(s->name, "(隐藏网络)")) {
const char *txt[1]; txt[0] = "隐藏网络无法直接连接"; wifi_manual_add(c); /* bug2:占位隐藏网络 → 手动录入(SSID 待输) */
s_dd_modal("附近网络", txt, 1, -1);
return 1; return 1;
} }
int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open")); int open = (s->sec[0] == 0 || !strcmp(s->sec, "Open"));
char psk[96] = ""; char psk[96] = "";
if (!open) { if (!open) {
if (!wifi_ask_psk(psk, sizeof psk)) return 1; /* 用户取消输密码 */ if (!wifi_ask_text(psk, sizeof psk)) return 1; /* 用户取消输密码 */
} }
char err[160]; err[0] = 0; char err[160]; err[0] = 0;
wifi_connecting_banner(s->name); /* bug1:阻塞前横幅,避免「点完无反馈」 */
int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err); int rc = nm_wifi_connect(s->name, open ? NULL : psk, err, sizeof err);
char line[192]; char line[192];
const char *txt[1]; txt[0] = line; const char *txt[1]; txt[0] = line;
if (rc == MEOW_OK) { /* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
snprintf(line, sizeof line, "连接成功:%s%s", s->name, snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
err[0] ? "" : "(已启用)");
} else {
snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误");
}
logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err); logmsg("wifi_page: scan connect ssid=%s open=%d rc=%d err=%s", s->name, open, rc, err);
/* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */ /* 连接结果反馈(成功后进已保存网络列表由 rows() 重扫刷新) */
s_dd_modal(s->name, txt, 1, -1); s_dd_modal(s->name, txt, 1, -1);
@@ -208,20 +254,23 @@ static int wifi_detail(void *ctx, int idx, int act)
} }
/* 动作行(SUB_ACT)点击:已保存网络 → netmgr connect(ssid;复用既有网络) */ /* 动作行(SUB_ACT)点击:已保存网络 → netmgr connect(ssid;复用既有网络)。
* 另承接「手动添加隐藏网络」入口(bug2)。 */
static int wifi_act(void *ctx, int idx, int act) static int wifi_act(void *ctx, int idx, int act)
{ {
(void)idx; (void)idx;
wifi_ctx_t *c = ctx; wifi_ctx_t *c = ctx;
if (act == WF_ACT_MANUAL) { wifi_manual_add(c); return 1; }
if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 SUB_ROW(detail),不走这里 */ if (act >= WF_ACT_SCAN_BASE) return 0; /* 附近是 SUB_ROW(detail),不走这里 */
int sidx = act - WF_ACT_SAVED; int sidx = act - WF_ACT_SAVED;
if (sidx < 0 || sidx >= c->saved_n) return 0; if (sidx < 0 || sidx >= c->saved_n) return 0;
if (c->saved[sidx].id < 0) return 0; if (c->saved[sidx].id < 0) return 0;
char err[160]; err[0] = 0; char err[160]; err[0] = 0;
wifi_connecting_banner(c->saved[sidx].name); /* bug1:阻塞前横幅 */
int rc = nm_wifi_connect(c->saved[sidx].name, NULL, err, sizeof err); int rc = nm_wifi_connect(c->saved[sidx].name, NULL, err, sizeof err);
char line[192]; const char *txt[1]; txt[0] = line; char line[192]; const char *txt[1]; txt[0] = line;
if (rc == MEOW_OK) snprintf(line, sizeof line, "已连接:%s", c->saved[sidx].name); /* bug1:结果文案直接采用 netmgrd/直连回填的诚实文本(成功含 ip / 失败含原因)。 */
else snprintf(line, sizeof line, "连接失败:%s", err[0] ? err : "netmgr 返回错误"); snprintf(line, sizeof line, "%s", err[0] ? err : (rc == MEOW_OK ? "已连接" : "连接失败"));
logmsg("wifi_page: saved connect ssid=%s rc=%d err=%s", c->saved[sidx].name, rc, err); logmsg("wifi_page: saved connect ssid=%s rc=%d err=%s", c->saved[sidx].name, rc, err);
s_dd_modal(c->saved[sidx].name, txt, 1, -1); s_dd_modal(c->saved[sidx].name, txt, 1, -1);
nm_wifi_scan_trigger(); nm_wifi_scan_trigger();
+156 -52
View File
@@ -85,6 +85,10 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET
#define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */ #define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */
#define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */ #define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */
/* WiFi 设置页 bug1(2026-09-15):connect 有界等待 wpa_state=COMPLETED 的上限(ms)。
* 普通 ~10s;隐藏网络(connecth,主动探测+关联更慢)~15s。env 可调(宿主桩测试缩短)。 */
#define WIFI_CONNECT_WAIT_MS 10000 /* MEOW_WIFI_CONNECT_WAIT_MS */
#define WIFI_CONNECTH_WAIT_MS 15000 /* MEOW_WIFI_CONNECTH_WAIT_MS */
#define IFACE_MAX 16 #define IFACE_MAX 16
#define RFKILL_MAX 8 #define RFKILL_MAX 8
#define DNS_MAX 8 #define DNS_MAX 8
@@ -112,6 +116,8 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET
#define DNS_SELF_IP "127.0.0.1" #define DNS_SELF_IP "127.0.0.1"
static int g_poll_sec = NET_POLL_SEC_DEF; static int g_poll_sec = NET_POLL_SEC_DEF;
static int g_connect_wait_ms = WIFI_CONNECT_WAIT_MS; /* MEOW_WIFI_CONNECT_WAIT_MS */
static int g_connecth_wait_ms = WIFI_CONNECTH_WAIT_MS; /* MEOW_WIFI_CONNECTH_WAIT_MS */
static int g_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */ static int g_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */
static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */ static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */
@@ -2650,13 +2656,82 @@ static const char *str_capsafe(const char *s, char *tmp, size_t cap)
return tmp; return tmp;
} }
/* wpa_cli 单命令封装:拼 PATH 前缀 + 有界收割(RUNCMD_TMO_MS)。返回 run_cmd 读到的字节数。 */
static int wpa_cmd(char *out, int cap, const char *args)
{
char cmd[512];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null", g_wpa_cli, args);
return run_cmd(out, cap, RUNCMD_TMO_MS, cmd);
}
/* WiFi 设置页 bug4-②(2026-09-15):list_networks/scan_results 对非 ASCII 字节输出 \xNN 转义
* (中文 SSID)——还原为原始字节串再与输入 SSID 做同表示比较;不还原则中文 SSID 永不命中
* 既有网络 → 新建同名 open 记录顶影原记录。net.c wpa_decode_ssid / netmgr_ipc.c nm_decode_ssid
* 已有两份同功能实现(后续可收拢,本次先在 netmgrd 补齐)。 */
static int wpa_hexval(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static void wpa_unescape_ssid(const char *src, char *dst, size_t dcap)
{
size_t o = 0;
while (*src && o + 1 < dcap) {
if (src[0] == '\\' && src[1] == 'x' && wpa_hexval(src[2]) >= 0 && wpa_hexval(src[3]) >= 0) {
dst[o++] = (char)((wpa_hexval(src[2]) << 4) | wpa_hexval(src[3]));
src += 4;
} else {
dst[o++] = *src++;
}
}
dst[o] = 0;
}
/* WiFi 设置页 bug4-③(2026-09-15):save_config 结果校验。wpa_supplicant 仅在 conf 含
* update_config=1 时允许写盘;固件模板 /etc/wpa_supplicant.conf 无此项 → 首次 FAIL 时先
* `set update_config 1`(运行期开关,wpa_supplicant 标准用法;成功落盘后文件自带
* update_config=1)再重试一次。返回 1=已持久化;0=仍失败(调用方须在报文/日志明示
* 「记录未持久化」)。仅 MeowOS 模式调用(写盘红线不变,NETMGR_DESIGN §14)。 */
static int wifi_save_config(void)
{
char out[160];
if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) return 1;
wpa_cmd(out, sizeof out, "set update_config 1");
if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) {
net_log("info", "write", "save_config 初次 FAIL → set update_config 1 重试成功(此后 conf 自带该位)");
return 1;
}
net_log("warn", "write", "save_config 失败(含 set update_config 1 重试):记录未持久化");
return 0;
}
/* WiFi 设置页 bug1(2026-09-15):连接状态机——有界等待 wpa_state=COMPLETED(每 200ms 查
* status,上限 timeout_ms,防卡死)。返回 1=已关联(out 带回 ssid/ip 供诚实报文);0=超时。 */
static int wifi_wait_completed(int timeout_ms, wifi_t *out)
{
int waited = 0;
for (;;) {
wifi_t w;
wifi_status(&w);
if (w.connected) { if (out) *out = w; return 1; }
if (waited >= timeout_ms) return 0;
struct timespec ts = { 0, 200 * 1000000L };
nanosleep(&ts, NULL);
waited += 200;
}
}
static void wifi_scan(ctl_result_t *r) static void wifi_scan(ctl_result_t *r)
{ {
char cmd[160]; char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan 2>/dev/null", g_wpa_cli); snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan 2>/dev/null", g_wpa_cli);
char buf[128]; char buf[128];
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd); run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd);
struct timespec ts = { 0, 300 * 1000000L }; /* 触发后稍候再读结果(有界)。 */ /* P3(WiFi 设置页附带,2026-09-15):触发后 300ms→1500ms 再读结果——真机扫描常 1-2s,
* 300ms 读到的多是上一轮陈旧结果。IPC 客户端 per-cmd 超时相应放宽(netmgr_ipc scan=3.5s)。 */
struct timespec ts = { 1, 500 * 1000000L };
nanosleep(&ts, NULL); nanosleep(&ts, NULL);
char bufout[MEOW_IPC_MAX - 16]; char bufout[MEOW_IPC_MAX - 16];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan_results 2>/dev/null", g_wpa_cli); snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan_results 2>/dev/null", g_wpa_cli);
@@ -2675,7 +2750,13 @@ static void wifi_networks(ctl_result_t *r)
ctl_sets(r, MEOW_OK, buf); ctl_sets(r, MEOW_OK, buf);
} }
static void wifi_connect(const char *arg, ctl_result_t *r) /* WiFi 连接(bug1/bug2/bug4 修复,2026-09-15)。hidden=1 走隐藏网络流程(connecth):
* 对所用 id(新建/命中皆然)追加 scan_ssid=1,并把 COMPLETED 等待上限放宽(主动探测+关联更慢)。
* 流程:匹配既有(\xNN 还原同表示比较)→ 新建或(命中且带新凭据时)更新 psk →
* enable+select → 有界等待 wpa_state=COMPLETED(不再无条件报成功)→
* 成功:enable_network all(消除 select 的 disabled 副作用)+ save_config(校验+重试);
* 失败:本次新建的网络 remove 回收(防坏记录堆积),报文明示失败原因。 */
static void wifi_connect_ex(const char *arg, ctl_result_t *r, int hidden)
{ {
char ssid[96] = "", psk[96] = ""; char ssid[96] = "", psk[96] = "";
if (arg && arg[0]) { if (arg && arg[0]) {
@@ -2694,11 +2775,10 @@ static void wifi_connect(const char *arg, ctl_result_t *r)
str_capsafe(psk, tmp_psk, sizeof tmp_psk); str_capsafe(psk, tmp_psk, sizeof tmp_psk);
if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; } if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; }
/* 找既有网络:list_networks → 匹配 ssid(剥引号比较);命中则复用其 id。 */ /* 找既有网络:list_networks → 匹配 ssid(剥引号 + \xNN 还原后同表示比较);命中则复用其 id。 */
char cmd[200]; char cmd[256];
char list[1024]; char list[1024];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list_networks 2>/dev/null", g_wpa_cli); int n = wpa_cmd(list, sizeof list, "list_networks");
int n = run_cmd(list, sizeof list, RUNCMD_TMO_MS, cmd);
int id = -1; int id = -1;
if (n > 0) { if (n > 0) {
char *ln = strtok(list, "\n"); char *ln = strtok(list, "\n");
@@ -2709,61 +2789,81 @@ static void wifi_connect(const char *arg, ctl_result_t *r)
char *ss = tab0 + 1; char *ss = tab0 + 1;
char *tab1 = strchr(ss, '\t'); char *tab1 = strchr(ss, '\t');
if (tab1) *tab1 = 0; if (tab1) *tab1 = 0;
char clean[128], norm_clean[128]; char clean[128], unesc[160], norm_clean[128];
size_t cl = strlen(ss); size_t cl = strlen(ss);
if (cl >= 2 && ss[0] == '"' && ss[cl-1] == '"') { if (cl >= 2 && ss[0] == '"' && ss[cl-1] == '"') {
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0; memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
} else { } else {
snprintf(clean, sizeof clean, "%s", ss); snprintf(clean, sizeof clean, "%s", ss);
} }
str_capsafe(clean, norm_clean, sizeof norm_clean); wpa_unescape_ssid(clean, unesc, sizeof unesc); /* bug4-②:中文 SSID 转义还原 */
str_capsafe(unesc, norm_clean, sizeof norm_clean);
if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id; if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id;
} }
ln = strtok(NULL, "\n"); ln = strtok(NULL, "\n");
} }
} }
char out[128]; char out[128];
int created = 0;
if (id < 0) { if (id < 0) {
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s add_network 2>/dev/null", g_wpa_cli); if (wpa_cmd(out, sizeof out, "add_network") <= 0) {
if (run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd) <= 0) {
ctl_sets(r, MEOW_ERR_NODEV, "connect: add_network 失败 (wpa_cli 不可用?)"); ctl_sets(r, MEOW_ERR_NODEV, "connect: add_network 失败 (wpa_cli 不可用?)");
return; return;
} }
id = atoi(out); id = atoi(out);
if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; } if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; }
snprintf(cmd, sizeof cmd, created = 1;
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d ssid '\"%s\"' 2>/dev/null", snprintf(cmd, sizeof cmd, "set_network %d ssid '\"%s\"'", id, tmp_ssid);
g_wpa_cli, id, tmp_ssid); wpa_cmd(out, sizeof out, cmd);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd); if (tmp_psk[0]) snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
if (tmp_psk[0]) { else snprintf(cmd, sizeof cmd, "set_network %d key_mgmt NONE", id);
snprintf(cmd, sizeof cmd, wpa_cmd(out, sizeof out, cmd);
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d psk '\"%s\"' 2>/dev/null", } else if (tmp_psk[0]) {
g_wpa_cli, id, tmp_psk); /* bug4-①/bug1-④:命中既有网络且调用方带了新 psk → 先更新凭据再连
} else { * (旧记录密码错误可经扫描路径重输修复;psk 为空则不动既有凭据)。 */
snprintf(cmd, sizeof cmd, snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d key_mgmt NONE 2>/dev/null", wpa_cmd(out, sizeof out, cmd);
g_wpa_cli, id); }
} if (hidden) {
run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd); /* bug2:隐藏 AP 必须 scan_ssid=1,supplicant 才会主动发 Probe Request(命中既有也要补)。 */
snprintf(cmd, sizeof cmd, "set_network %d scan_ssid 1", id);
wpa_cmd(out, sizeof out, cmd);
} }
{ /* enable + select 该网络(复用/新建皆用)。 */ { /* enable + select 该网络(复用/新建皆用)。 */
char en[256], sel[256]; snprintf(cmd, sizeof cmd, "enable_network %d", id);
snprintf(en, sizeof en, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s enable_network %d 2>/dev/null", wpa_cmd(out, sizeof out, cmd);
g_wpa_cli, id); snprintf(cmd, sizeof cmd, "select_network %d", id);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, en); wpa_cmd(out, sizeof out, cmd);
snprintf(sel, sizeof sel, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s select_network %d 2>/dev/null",
g_wpa_cli, id);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, sel);
/* 期3:MeowOS 模式 connect 后持久化 wpa_supplicant.conf(HibyOS 只读由服务端门控,这里仅 MeowOS 生效)。 */
if (g_mode_meow) {
char sc[160];
snprintf(sc, sizeof sc, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, sc);
}
} }
char msg[140]; /* bug1:有界等待 wpa_state=COMPLETED——密码错误/AP 不在范围/握手失败不再误报成功。 */
snprintf(msg, sizeof msg, "connect: ssid=%s network_id=%d (已 enable+select%s)", tmp_ssid, id, wifi_t w;
g_mode_meow ? "+save_config" : ""); int wait_ms = hidden ? g_connecth_wait_ms : g_connect_wait_ms;
if (!wifi_wait_completed(wait_ms, &w)) {
if (created) { /* 本次新建且失败 → 回收防坏记录堆积(bug4 联动) */
snprintf(cmd, sizeof cmd, "remove_network %d", id);
wpa_cmd(out, sizeof out, cmd);
if (g_mode_meow) wifi_save_config();
}
net_log("info", "write", "wifi connect ssid=%s id=%d 超时未关联(%dms)", tmp_ssid, id, wait_ms);
char msg[128];
snprintf(msg, sizeof msg, "连接失败:密码错误或信号不佳(超时未关联)");
ctl_sets(r, MEOW_ERR_NODEV, msg);
return;
}
/* bug4-④:验证成功后 enable_network all 复位其它网络 disabled 态(select_network 副作用),
* 再 save_config 落盘(bug4-③:结果校验,FAIL 时 set update_config 1 重试)。
* 写盘仍受 MeowOS 模式门控(红线不变,NETMGR_DESIGN §14)。 */
int persisted = 0;
if (g_mode_meow) {
wpa_cmd(out, sizeof out, "enable_network all");
persisted = wifi_save_config();
}
net_log("info", "write", "wifi connect ssid=%s id=%d 已连接 ip=%s persisted=%d",
tmp_ssid, id, w.ip[0] ? w.ip : "-", persisted);
char msg[160];
snprintf(msg, sizeof msg, "已连接:%s ip=%s%s",
w.ssid[0] ? w.ssid : tmp_ssid, w.ip[0] ? w.ip : "-",
(g_mode_meow && !persisted) ? "(记录未持久化)" : "");
ctl_sets(r, MEOW_OK, msg); ctl_sets(r, MEOW_OK, msg);
} }
@@ -2840,7 +2940,9 @@ static void wifi_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
} else if (!strcmp(cmd, "networks")) { } else if (!strcmp(cmd, "networks")) {
wifi_networks(r); wifi_networks(r);
} else if (!strcmp(cmd, "connect")) { } else if (!strcmp(cmd, "connect")) {
wifi_connect(arg, r); wifi_connect_ex(arg, r, 0);
} else if (!strcmp(cmd, "connecth")) {
wifi_connect_ex(arg, r, 1); /* bug2:隐藏网络(scan_ssid=1 + 更长 COMPLETED 等待) */
} else if (!strcmp(cmd, "disconnect")) { } else if (!strcmp(cmd, "disconnect")) {
char cmd2[160]; char cmd2[160];
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s disconnect 2>/dev/null", g_wpa_cli); snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s disconnect 2>/dev/null", g_wpa_cli);
@@ -2857,13 +2959,13 @@ static void wifi_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
g_wpa_cli, nid); g_wpa_cli, nid);
char buf[128]; char buf[128];
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
/* 期4:移除后持久化(MeowOS 模式;HibyOS 只读由服务端门控)。 */ /* 期4:移除后持久化(MeowOS 模式;HibyOS 只读由服务端门控)。
if (g_mode_meow) { * bug4-③:save_config 结果校验(FAIL 时 set update_config 1 重试),报文不再谎称已落盘。 */
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli); int persisted = 0;
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); if (g_mode_meow) persisted = wifi_save_config();
} char msg[96];
char msg[64]; snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid,
snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid, g_mode_meow ? "+save_config" : ""); g_mode_meow ? (persisted ? "+save_config" : "(记录未持久化)") : "");
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg); ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg);
} else if (!strcmp(cmd, "rfkill")) { } else if (!strcmp(cmd, "rfkill")) {
rfkill_ctl(arg, r); rfkill_ctl(arg, r);
@@ -3704,7 +3806,7 @@ static int cli_policy(int op, const char *entity, const char *arg)
static const char *usage(void) static const char *usage(void)
{ {
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|disconnect|on|off" return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|connecth <ssid>[,psk]|disconnect|on|off"
" | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair <mac>" " | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair <mac>"
" | rfkill [<type> <on|off>] | udhcpc renew|release [if]" " | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
" | apps | conns [entity] | top | dns status|stats|flush" " | apps | conns [entity] | top | dns status|stats|flush"
@@ -3757,9 +3859,9 @@ static int cli_run(int n, char **v)
if (!strcmp(cmd, "wifi")) { if (!strcmp(cmd, "wifi")) {
if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; } if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; }
if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL); if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL);
if (!strcmp(arg, "connect")) { if (!strcmp(arg, "connect") || !strcmp(arg, "connecth")) {
if (!arg2) { fprintf(stderr, "connect 需 <ssid>[,psk]\n"); return 1; } if (!arg2) { fprintf(stderr, "%s 需 <ssid>[,psk]\n", arg); return 1; }
return cli_cmd_result(MSG_NET_WIFI_CTL, "connect", arg2); return cli_cmd_result(MSG_NET_WIFI_CTL, arg, arg2);
} }
if (!strcmp(arg, "forget")) { if (!strcmp(arg, "forget")) {
if (!arg2) { fprintf(stderr, "forget 需 <network_id>\n"); return 1; } if (!arg2) { fprintf(stderr, "forget 需 <network_id>\n"); return 1; }
@@ -3821,6 +3923,8 @@ int main(int argc, char **argv)
if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e; if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e;
if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e; if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e;
if ((e = getenv("MEOW_NET_POLL_SEC")) && atoi(e) > 0) g_poll_sec = atoi(e); if ((e = getenv("MEOW_NET_POLL_SEC")) && atoi(e) > 0) g_poll_sec = atoi(e);
if ((e = getenv("MEOW_WIFI_CONNECT_WAIT_MS")) && atoi(e) >= 200) g_connect_wait_ms = atoi(e);
if ((e = getenv("MEOW_WIFI_CONNECTH_WAIT_MS")) && atoi(e) >= 200) g_connecth_wait_ms = atoi(e);
if ((e = getenv("MEOW_TCP")) && e[0]) snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", e); if ((e = getenv("MEOW_TCP")) && e[0]) snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", e);
if ((e = getenv("MEOW_UDP")) && e[0]) snprintf(g_proc_udp, sizeof g_proc_udp, "%s", e); if ((e = getenv("MEOW_UDP")) && e[0]) snprintf(g_proc_udp, sizeof g_proc_udp, "%s", e);
if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) snprintf(g_ams_sock, sizeof g_ams_sock, "%s", e); if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) snprintf(g_ams_sock, sizeof g_ams_sock, "%s", e);
+19
View File
@@ -423,6 +423,25 @@ kill $D2PID 2>/dev/null; wait $D2PID 2>/dev/null
# 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认) # 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认)
grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; } grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; }
# ---- T19: WiFi connect 状态机/持久化/隐藏网络/中文 SSID(宿主桩直驱 wifi_connect_ex)----
# daemon IPC 写级需系统身份(SO_PEERCRED /proc/exe),宿主 CLI=CALL_OTHER 必拒;故用 #include
# meow_netmgrd.c 的独立宿主二进制直驱 static wifi_connect_ex,桩 wpa_cli 由二进制自建。
if [ -z "$Q" ]; then
WCON="$T/test_wifi_connect"
if cc -Os -Wall -Wextra -Wno-format-truncation -I../meow_launcherd \
test_wifi_connect.c -o "$WCON" >/dev/null 2>&1; then
if "$WCON" >"$T/wcon.out" 2>/dev/null && grep -q "FAIL=0" "$T/wcon.out"; then
PASS=$((PASS+1)); echo "PASS T19 wifi-connect host (12 asserts, FAIL=0)"
else
FAIL=$((FAIL+1)); echo "FAIL T19 wifi-connect host"; cat "$T/wcon.out"
fi
else
FAIL=$((FAIL+1)); echo "FAIL T19 wifi-connect host build"
fi
else
echo "SKIP T19 wifi-connect(qemu 下 fork/exec 宿主 /bin/sh 不便,宿主已覆盖)"
fi
echo "==============================" echo "=============================="
echo "PASS=$PASS FAIL=$FAIL" echo "PASS=$PASS FAIL=$FAIL"
[ $FAIL -eq 0 ] [ $FAIL -eq 0 ]
+168
View File
@@ -0,0 +1,168 @@
/* test_wifi_connect.c — WiFi 设置页 bug1/bug2/bug4 宿主桩测试(2026-09-15)。
*
* 通过 #include meow_netmgrd.c 直接驱动 static 的 wifi_connect_ex(),绕过 daemon 的
* SO_PEERCRED 系统身份鉴权(宿主 CLI=CALL_OTHER 写级必拒,无法经 IPC 测连接路径)。
* 桩 wpa_cli 由本程序写入临时目录;控制文件(status_mode/list/save_fail_once)决定
* status 序列 / list_networks 原文 / save_config 是否首败,调用日志写入 <dir>/log 供断言。
*
* 编译:cc -Wall -Wextra -I../meow_launcherd test_wifi_connect.c -o test_wifi_connect
*/
#define _GNU_SOURCE
#define main meow_netmgrd_main
#include "meow_netmgrd.c"
#undef main
#include <sys/stat.h>
static char g_dir[128];
static int g_pass = 0, g_fail = 0;
static void ck(const char *name, int cond, const char *detail)
{
if (cond) { g_pass++; printf("PASS %s\n", name); }
else { g_fail++; printf("FAIL %s: %s\n", name, detail ? detail : ""); }
fflush(stdout);
}
static void putf(const char *name, const char *fmt, ...)
{
char path[192], body[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(body, sizeof body, fmt, ap);
va_end(ap);
snprintf(path, sizeof path, "%s/%s", g_dir, name);
FILE *f = fopen(path, "w");
if (f) { fputs(body, f); fclose(f); }
}
static int log_has(const char *needle)
{
char path[192], buf[8192];
snprintf(path, sizeof path, "%s/log", g_dir);
FILE *f = fopen(path, "r");
if (!f) return 0;
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = 0;
return strstr(buf, needle) != NULL;
}
static void reset_log(void)
{
char path[192];
snprintf(path, sizeof path, "%s/log", g_dir);
FILE *f = fopen(path, "w");
if (f) fclose(f);
}
static void write_stub(void)
{
char path[192];
snprintf(path, sizeof path, "%s/wpa_cli", g_dir);
FILE *f = fopen(path, "w");
if (!f) { fprintf(stderr, "stub open fail\n"); exit(2); }
fputs(
"#!/bin/sh\n"
"D=\"${MN_WIFI_DIR:-/tmp}\"\n"
"echo \"$@\" >> \"$D/log\"\n"
"case \"$1\" in\n"
" status)\n"
" m=$(cat \"$D/status_mode\" 2>/dev/null || echo completed)\n"
" if [ \"$m\" = \"associating\" ]; then\n"
" echo \"wpa_state=ASSOCIATING\"; echo \"ssid=MyWiFi\"\n"
" else\n"
" echo \"wpa_state=COMPLETED\"; echo \"ssid=MyWiFi\"\n"
" echo \"ip_address=10.78.193.50\"; echo \"signal=-45\"\n"
" fi\n"
" ;;\n"
" list_networks)\n"
" if [ -f \"$D/list\" ]; then cat \"$D/list\"\n"
" else printf 'network id / ssid / bssid / flags\\n0\\tMyWiFi\\tany\\t[CURRENT]\\n'\n"
" fi\n"
" ;;\n"
" add_network) echo 1 ;;\n"
" set_network) echo OK ;;\n"
" enable_network) echo OK ;;\n"
" select_network) echo OK ;;\n"
" disconnect) echo OK ;;\n"
" save_config)\n"
" if [ -f \"$D/save_fail_once\" ]; then rm -f \"$D/save_fail_once\"; echo FAIL; else echo OK; fi\n"
" ;;\n"
" set) echo OK ;;\n"
" remove_network) echo OK ;;\n"
" *) echo OK ;;\n"
"esac\n"
"exit 0\n",
f);
fclose(f);
chmod(path, 0755);
}
int main(void)
{
snprintf(g_dir, sizeof g_dir, "/tmp/mnwifi.XXXXXX");
if (!mkdtemp(g_dir)) { fprintf(stderr, "mkdtemp fail\n"); return 2; }
write_stub();
setenv("MN_WIFI_DIR", g_dir, 1);
g_mode_meow = 1;
g_connect_wait_ms = 400;
g_connecth_wait_ms = 400;
{
char p[192];
snprintf(p, sizeof p, "%s/wpa_cli", g_dir);
g_wpa_cli = p;
}
ctl_result_t r;
/* ① 成功:命中既有 id=0 + 带新 psk → 更新凭据,等待 COMPLETED,成功报 ip。 */
putf("status_mode", "completed");
ctl_reset(&r, MEOW_OK);
reset_log();
wifi_connect_ex("MyWiFi,secret", &r, 0);
ck("connect-success code", r.code == MEOW_OK, r.text);
ck("connect-success text ip", strstr(r.text, "已连接") && strstr(r.text, "ip=10.78.193.50"), r.text);
ck("connect-success psk update", log_has("set_network 0 psk"), "");
ck("connect-success enable all", log_has("enable_network all"), "");
/* ② 错误密码/不在范围:status 恒 ASSOCIATING → 超时失败,新建 id 被回收。 */
putf("status_mode", "associating");
reset_log();
ctl_reset(&r, MEOW_OK);
wifi_connect_ex("OtherWiFi", &r, 0);
ck("connect-timeout code", r.code == MEOW_ERR_NODEV, r.text);
ck("connect-timeout text", strstr(r.text, "连接失败") != NULL, r.text);
ck("connect-timeout remove new", log_has("remove_network 1"), "");
/* ③ 中文 SSID \xNN 还原匹配:list_networks 原文转义,输入 UTF-8 名 → 命中 id=0 不新建。 */
putf("status_mode", "completed");
putf("list", "network id / ssid / bssid / flags\n0\t\\xe5\\xa4\\xa9\\xe5\\xa4\\xa9\tany\t[CURRENT]\n");
reset_log();
ctl_reset(&r, MEOW_OK);
wifi_connect_ex("天天", &r, 0);
ck("connect-escape match", r.code == MEOW_OK, r.text);
ck("connect-escape no add", !log_has("add_network"), "");
/* ④ connecth:隐藏网络命中既有 id → 追加 scan_ssid 1。 */
putf("status_mode", "completed");
putf("list", "network id / ssid / bssid / flags\n0\tMyWiFi\tany\t[CURRENT]\n");
reset_log();
ctl_reset(&r, MEOW_OK);
wifi_connect_ex("MyWiFi", &r, 1);
ck("connecth scan_ssid", r.code == MEOW_OK && log_has("set_network 0 scan_ssid 1"), r.text);
/* ⑤ save_config 首败 → set update_config 1 重试 → 持久化成功。 */
putf("status_mode", "completed");
putf("list", "network id / ssid / bssid / flags\n0\tMyWiFi\tany\t[CURRENT]\n");
putf("save_fail_once", "1");
reset_log();
ctl_reset(&r, MEOW_OK);
wifi_connect_ex("MyWiFi", &r, 0);
ck("save-config retry", r.code == MEOW_OK && !strstr(r.text, "记录未持久化"), r.text);
ck("save-config set update_config", log_has("set update_config 1"), "");
printf("==============================\nPASS=%d FAIL=%d\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
}
+4 -3
View File
@@ -8,16 +8,17 @@ set -e
cd "$(dirname "$0")" cd "$(dirname "$0")"
ROOT=../.. ROOT=../..
CC="$ROOT/.tools/zig-mipsel-cc" CC="$ROOT/.tools/zig-mipsel-cc"
INC="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(音量收编 v2:MSG_AUDIO_VOLUME/MSG_ACK)
if [ "$1" = "host" ]; then if [ "$1" = "host" ]; then
gcc -Wall -Wextra -Os -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \ gcc -Wall -Wextra -Os -I"$INC" -DWd_NO_MAIN -Wno-unused-function -Wno-unused-parameter \
test_wd_fw.c -o test_wd_fw -lm -Wno-unused-variable test_wd_fw.c -o test_wd_fw -lm
./test_wd_fw ./test_wd_fw
exit $? exit $?
fi fi
echo "== meow_watchdog (mipsel static) ==" echo "== meow_watchdog (mipsel static) =="
"$CC" -Os -Wall -Wextra -static meow_watchdog.c -o meow_watchdog "$CC" -Os -Wall -Wextra -static -I"$INC" meow_watchdog.c -o meow_watchdog
ls -la meow_watchdog ls -la meow_watchdog
md5sum meow_watchdog md5sum meow_watchdog
file meow_watchdog file meow_watchdog
+118 -10
View File
@@ -19,7 +19,10 @@
* (launcher 在 waitpid 轮询中消费;菜单期该进程靠 /tmp/meow_power_menu_active * (launcher 在 waitpid 轮询中消费;菜单期该进程靠 /tmp/meow_power_menu_active
* 让位,绝不与 launcher 抢屏/抢键);launcher 画菜单,本进程不自绘。 * 让位,绝不与 launcher 抢屏/抢键);launcher 画菜单,本进程不自绘。
* 电源长按 ≥8s -> 强制关机兜底(仅当菜单未生效/launcher 未接管)。 * 电源长按 ≥8s -> 强制关机兜底(仅当菜单未生效/launcher 未接管)。
* 音量±(115/114) -> 写真实 DAC(CS43198 amixer numid=1/2)+ 持久化 settings.conf(同 launcher 同源)。 * 音量±(115/114) -> 音量收编 v2(FUTURE_AUDIO 事项三):首选发 audiod 权威
* MSG_AUDIO_VOLUME(audiod 写 DAC numid=1/2 + DSD_VOLUME + 持久化
* settings.conf,同源同键单写者);audiod 未起(HibyOS/开机早期)
* 才回退本地 amixer 直写兜底 + 本地持久化。
* 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿 * 插孔消抖:/sys/class/switch/{headset,balance} 变化后 800ms 内忽略音量键边沿
* (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。 * (插拔电气瞬态会在 ADC 键上产生幽灵按下,真机实证 2026-09-01)。
* 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。 * 播放/上下曲(164/165/163/19) -> 捕获 + 日志 + 消费,不路由(媒体会话落地后转活跃会话)。
@@ -50,8 +53,12 @@
#include <sys/mman.h> #include <sys/mman.h>
#include <poll.h> #include <poll.h>
#include <sys/syscall.h> #include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#include <stdint.h> #include <stdint.h>
#include <time.h> #include <time.h>
#include "meow_ipc.h" /* MSG_AUDIO_VOLUME/MSG_ACK/帧格式(音量收编 v2:首选 audiod 权威) */
/* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */ /* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */
#define FBIOGET_VSCREENINFO 0x4600 #define FBIOGET_VSCREENINFO 0x4600
@@ -214,6 +221,14 @@ static void write_brightness(int val)
close(fd); close(fd);
} }
/* settings.conf 路径(缺省 /usr/data/meow/settings.conf;env MEOW_SETTINGS_FILE 覆盖,
* 供宿主测试隔离——与 audiod MEOW_AUDIO_SETTINGS 同款测试钩子,真机无 env 走默认)。 */
static const char *wd_settings_path(void)
{
const char *e = getenv("MEOW_SETTINGS_FILE");
return (e && e[0]) ? e : MEOW_SETTINGS_FILE;
}
/* M6.23:亮屏恢复亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness), /* M6.23:亮屏恢复亮度以**用户设置**为准(/usr/data/meow/settings.conf 的 display.brightness),
* 而非 wd_saved_bri 快照——该快照可能被污染:panel OFF 前 read_brightness() 可能读到 launcher * 而非 wd_saved_bri 快照——该快照可能被污染:panel OFF 前 read_brightness() 可能读到 launcher
* 先写好的 0(launcher meow_panel_off 已 write_brightness(0)),`wd_saved_bri=(br>0)?br:60` 便落到 * 先写好的 0(launcher meow_panel_off 已 write_brightness(0)),`wd_saved_bri=(br>0)?br:60` 便落到
@@ -225,7 +240,7 @@ static void write_brightness(int val)
* → 返回 -1,调用方回退 wd_saved_bri(保持旧行为,防开机竞态)。 */ * → 返回 -1,调用方回退 wd_saved_bri(保持旧行为,防开机竞态)。 */
static int wd_settings_brightness(void) static int wd_settings_brightness(void)
{ {
FILE *f = fopen(MEOW_SETTINGS_FILE, "r"); FILE *f = fopen(wd_settings_path(), "r");
if (!f) return -1; if (!f) return -1;
int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */ int b = 101; /* display.brightness 默认(与 launcher 设置表同值,键缺失时 clamp→100) */
int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */ int mn = 1; /* display.brightness_min 默认(极暗但可见;键缺失时保底) */
@@ -372,7 +387,7 @@ static int wd_vol_step = 1; /* 跳步 */
static void wd_vol_load(void) static void wd_vol_load(void)
{ {
wd_vol = 60; wd_vol_max = 100; wd_vol_step = 1; wd_vol = 60; wd_vol_max = 100; wd_vol_step = 1;
FILE *f = fopen(MEOW_SETTINGS_FILE, "r"); FILE *f = fopen(wd_settings_path(), "r");
if (!f) { wd_log("vol: no settings, default %d", wd_vol); return; } if (!f) { wd_log("vol: no settings, default %d", wd_vol); return; }
char buf[128]; char buf[128];
while (fgets(buf, sizeof buf, f)) { while (fgets(buf, sizeof buf, f)) {
@@ -391,13 +406,17 @@ static void wd_vol_load(void)
wd_log("vol: loaded volume=%d max=%d step=%d", wd_vol, wd_vol_max, wd_vol_step); wd_log("vol: loaded volume=%d max=%d step=%d", wd_vol, wd_vol_max, wd_vol_step);
} }
/* 写真实 DAC(CS43198, numid 1/2, 0-255),与 launcher meow_dac_vol_write 同路径。 */ /* 写真实 DAC(CS43198, numid 1/2, 0-255),与 launcher meow_dac_vol_write 同路径。
* ⚠️ 音量收编 v2 兜底函数:仅 audiod 未起(HibyOS/开机早期)时由 wd_vol_adj 回退调用;
* MeowOS 常态一律走 wd_audiod_set_volume(audiod 唯一权威,防多写者漂移)。
* 锥度为定案平方锥度(与 audiod dac_vol_set 同款,勿改语义:CS43198=衰减量,0 最响/255 静音)。 */
static void wd_vol_write(int pct) static void wd_vol_write(int pct)
{ {
int t = 100 - pct, dac; int t = 100 - pct, dac;
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */ if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */ else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
if (dac > 255) dac = 255; if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */ if (dac > 255) dac = 255;
if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
char cmd[160]; char cmd[160];
snprintf(cmd, sizeof cmd, snprintf(cmd, sizeof cmd,
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1", "amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
@@ -406,14 +425,97 @@ static void wd_vol_write(int pct)
wd_log("DAC vol -> L/R=%d (pct %d)", dac, pct); wd_log("DAC vol -> L/R=%d (pct %d)", dac, pct);
} }
/* ---- 音量收编 v2(FUTURE_AUDIO 事项三):首选 audiod 权威 MSG_AUDIO_VOLUME ----
* watchdog 为 per-App 小进程,audiod sock 0666 无鉴权可直接连。一次性短连接(按键 rare,
* 无常驻连接/陈旧态负担):非阻塞 connect + poll 300ms,SO_SNDTIMEO/RCVTIMEO 1s,发 i32 pct
* 等 MSG_ACK。audiod 侧已异步化(末值去抖,ACK=已应用+已持久化),正常 <<1s 返回。
* 返回 0=audiod 已受理(DAC 写+持久化均由 audiod 完成,本进程不再写 settings 防双写);
* -1=audiod 缺席/失败(调用方回退 wd_vol_write 本地直写 + wd_vol_save 本地持久化)。 */
#define WD_AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock" /* 同 audiod AUD_SOCK_DEFAULT */
#define WD_AUD_CONN_TMO_MS 300
#define WD_AUD_IO_TMO_MS 1000
static const char *wd_aud_sock(void)
{
const char *e = getenv("MEOW_AUDIO_SOCK"); /* 与 SDK sock_path() 同款 env(宿主测覆盖) */
return (e && e[0]) ? e : WD_AUD_SOCK_DEFAULT;
}
static int wd_wr_all(int fd, const void *p, size_t n)
{
const uint8_t *b = p; size_t o = 0;
while (o < n) { ssize_t r = write(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } o += (size_t)r; }
return 0;
}
static int wd_rd_all(int fd, void *p, size_t n)
{
uint8_t *b = p; size_t o = 0;
while (o < n) { ssize_t r = read(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } if (r == 0) return -1; o += (size_t)r; }
return 0;
}
static int wd_audiod_set_volume(int pct)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", wd_aud_sock());
int r = connect(fd, (struct sockaddr *)&a, sizeof a);
if (r < 0 && errno == EINPROGRESS) {
struct pollfd p;
p.fd = fd; p.events = POLLOUT; p.revents = 0;
if (poll(&p, 1, WD_AUD_CONN_TMO_MS) > 0) {
int err = 0;
socklen_t el = sizeof err;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
} else {
r = -1; /* 超时/错误 */
}
}
if (r < 0) { close(fd); return -1; }
if (fl >= 0) fcntl(fd, F_SETFL, fl); /* 回阻塞模式;读写超时由 SO_*TIMEO 兜底 */
struct timeval tv;
tv.tv_sec = WD_AUD_IO_TMO_MS / 1000; tv.tv_usec = (WD_AUD_IO_TMO_MS % 1000) * 1000;
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
/* 发 MSG_AUDIO_VOLUME(i32 pct);req_id 任意(一次性连接单请求) */
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
uint32_t magic = MEOW_IPC_MAGIC, flen = sizeof(int32_t), freq = 1;
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
memcpy(frame + 0, &magic, 4);
memcpy(frame + 4, &ver, 2);
memcpy(frame + 6, &type, 2);
memcpy(frame + 8, &freq, 4);
memcpy(frame + 12, &flen, 4);
int32_t p32 = pct;
memcpy(frame + MEOW_IPC_HDR_LEN, &p32, sizeof p32);
if (wd_wr_all(fd, frame, sizeof frame) != 0) { close(fd); return -1; }
/* 等 MSG_ACK(有界:RCVTIMEO;audiod 异步执行点应用后回 ACK) */
uint8_t hdr[MEOW_IPC_HDR_LEN];
if (wd_rd_all(fd, hdr, sizeof hdr) != 0) { close(fd); return -1; }
uint32_t rmagic, rlen;
uint16_t rver, rtype;
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2); memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION || rtype != MSG_ACK || rlen > 64) { close(fd); return -1; }
uint8_t pl[64];
if (rlen && wd_rd_all(fd, pl, rlen) != 0) { close(fd); return -1; }
close(fd);
return 0;
}
/* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 —— /* M2.92+ 修复(真机实证 settings.conf 被毁):旧版 O_TRUNC 整文件重写只剩 4 行 ——
* 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。 * 抹掉全部其它设置;与其它写者交错还叠出重复 audio.volume 行、丢 volume_max/step。
* 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换), * 改为读改写单键(流式拷贝、命中行全部改写去重、缺失末尾追加、rename 原子替换),
* 与 SDK meow_ui_vol_set 同款语义。 */ * 与 SDK meow_ui_vol_set 同款语义。 */
static void wd_vol_save(void) static void wd_vol_save(void)
{ {
system("mkdir -p /usr/data/meow 2>/dev/null"); const char *path = wd_settings_path();
const char *path = MEOW_SETTINGS_FILE; if (strcmp(path, MEOW_SETTINGS_FILE) == 0) /* 仅默认路径补目录(env 覆盖=宿主测,目录自备) */
system("mkdir -p /usr/data/meow 2>/dev/null");
char tmp[300]; char tmp[300];
snprintf(tmp, sizeof tmp, "%s.tmp", path); snprintf(tmp, sizeof tmp, "%s.tmp", path);
FILE *in = fopen(path, "r"); FILE *in = fopen(path, "r");
@@ -490,7 +592,9 @@ static int wd_jack_debounce(void)
(now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS); (now_ms() - wd_jack_changed_at < (unsigned long long)WD_JACK_DEBOUNCE_MS);
} }
/* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。 */ /* 音量步进:+1/-1(每按下边沿一次;clamp 到极大/最小)。
* 音量收编 v2:首选 audiod 权威(IPC;DAC 写+持久化 audiod 完成);audiod 缺席才本地
* amixer 直写 + 本地持久化兜底(wd_vol 本地缓存仅作步进基数,两路都维护)。 */
static void wd_vol_adj(int dir) static void wd_vol_adj(int dir)
{ {
int target = wd_vol + dir * wd_vol_step; int target = wd_vol + dir * wd_vol_step;
@@ -498,9 +602,13 @@ static void wd_vol_adj(int dir)
if (target > wd_vol_max) target = wd_vol_max; if (target > wd_vol_max) target = wd_vol_max;
if (target == wd_vol) { wd_log("volume %s at limit (%d)", dir > 0 ? "UP" : "DOWN", wd_vol); return; } if (target == wd_vol) { wd_log("volume %s at limit (%d)", dir > 0 ? "UP" : "DOWN", wd_vol); return; }
wd_vol = target; wd_vol = target;
wd_vol_write(wd_vol); if (wd_audiod_set_volume(wd_vol) == 0) {
wd_log("volume %s -> %d (via audiod)", dir > 0 ? "UP" : "DOWN", wd_vol);
return;
}
wd_vol_write(wd_vol); /* 兜底:仅 audiod 未起(HibyOS/开机早期) */
wd_vol_save(); wd_vol_save();
wd_log("volume %s -> %d", dir > 0 ? "UP" : "DOWN", wd_vol); wd_log("volume %s -> %d (local amixer fallback, audiod absent)", dir > 0 ? "UP" : "DOWN", wd_vol);
} }
static void handle_touch(int immersive, int app_pid, static void handle_touch(int immersive, int app_pid,
+4
View File
@@ -289,6 +289,8 @@ void net_policy_parse(const char *txt, sbuf_t *s);
int api_net_policy_get(int sock); int api_net_policy_get(int sock);
int api_net_policy_set(int sock, const char *body, size_t body_len); int api_net_policy_set(int sock, const char *body, size_t body_len);
int net_locked_entity(const char *entity); /* 防自锁:系统关键网络/服务组件(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init) */ int net_locked_entity(const char *entity); /* 防自锁:系统关键网络/服务组件(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init) */
int net_bt_aggregate(const char *st, const char *paired, const char *connected, const char *a2dp, const char *devices, sbuf_t *s); /* F1-③ 0xE6 五读文本聚合(selftest 可测) */
int api_net_bt(int sock); /* F1-③ GET /api/net/bt 蓝牙只读详情 */
int panel_policy_write_enabled(void); /* policy 默认关 gate:settings.conf `panel.policy_enabled`,缺省 0=关(只读透传) */ int panel_policy_write_enabled(void); /* policy 默认关 gate:settings.conf `panel.policy_enabled`,缺省 0=关(只读透传) */
void panel_strip_ansi(char *s); void panel_strip_ansi(char *s);
int txt_tokens(char *buf, char **toks, int maxtoks); int txt_tokens(char *buf, char **toks, int maxtoks);
@@ -415,6 +417,8 @@ int api_notification_dismiss_all(int sock);
int api_files_list(int sock, const req_t *r); int api_files_list(int sock, const req_t *r);
int api_files_read(int sock, const req_t *r); int api_files_read(int sock, const req_t *r);
int api_files_download(int sock, const req_t *r); int api_files_download(int sock, const req_t *r);
int files_search_json(const char *abs_start, const char *name, const char *typef, int max, sbuf_t *out); /* F1-④ 搜索核心(selftest 可测) */
int api_files_search(int sock, const req_t *r); /* F1-④ GET /api/files/search(只读) */
int api_files_upload(int sock, const req_t *r); int api_files_upload(int sock, const req_t *r);
int rm_recursive(const char *path); int rm_recursive(const char *path);
int api_files_mkdir(int sock, const req_t *r); int api_files_mkdir(int sock, const req_t *r);
+136
View File
@@ -571,6 +571,142 @@ int api_files_delete(int sock, const req_t *r)
return send_json(sock, 200, "{\"removed\":true}"); return send_json(sock, 200, "{\"removed\":true}");
} }
/* ---- F1-④ 文件搜索(FUTURE_MEOWPANEL §2.3,只读):白名单内 readdir 递归(限深 4 / 限量 200 /
* 限遍历 8000 条),不 run_cmd("find")(防注入、可控)。name=名子串(ASCII 大小写不敏感);
* type=file|dir 可选过滤。不跟符号链接(防循环/逃逸出白名单根)。 ---- */
#define SEARCH_MAX_DEPTH 4
#define SEARCH_MAX_ITEMS 200
#define SEARCH_MAX_VISIT 8000
typedef struct {
const char *needle; /* 已小写化的匹配子串 */
int want_type; /* 0=不限 1=仅文件 2=仅目录 */
int max;
int count;
long visited;
sbuf_t *s;
} search_ctx_t;
static void name_lower(const char *in, char *out, size_t cap)
{
size_t i = 0;
for (; in[i] && i + 1 < cap; i++) {
unsigned char c = (unsigned char)in[i];
out[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 32) : (char)c;
}
out[i] = 0;
}
/* 返回 1=到达上限应全程停止(truncated)。 */
static int search_walk(search_ctx_t *c, const char *dir, int depth)
{
if (c->count >= c->max || c->visited >= SEARCH_MAX_VISIT) return 1;
DIR *d = opendir(dir);
if (!d) return 0;
int stop = 0;
struct dirent *e;
while ((e = readdir(d))) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
if (++c->visited > SEARCH_MAX_VISIT) { stop = 1; break; }
char full[1400];
snprintf(full, sizeof full, "%s/%s", dir, e->d_name);
struct stat st;
if (lstat(full, &st) < 0) continue;
if (S_ISLNK(st.st_mode)) continue; /* 不跟符号链接 */
int isdir = S_ISDIR(st.st_mode);
int type_ok = !c->want_type || (c->want_type == 1 && !isdir) || (c->want_type == 2 && isdir);
if (type_ok) {
char low[300];
name_lower(e->d_name, low, sizeof low);
if (strstr(low, c->needle)) {
if (c->count >= c->max) { stop = 1; break; }
char rel[1200];
make_rel(full, rel, sizeof rel);
if (c->count) sA(c->s, ",");
sA(c->s, "{\"name\":");
sA_str(c->s, e->d_name);
sA(c->s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld,\"rel\":",
isdir ? "dir" : "file", isdir ? 0LL : (long long)st.st_size, (long long)st.st_mtime);
sA_str(c->s, rel);
sA(c->s, "}");
c->count++;
}
}
if (isdir && depth < SEARCH_MAX_DEPTH && search_walk(c, full, depth + 1)) { stop = 1; break; }
}
closedir(d);
return stop;
}
/* 0=OK(JSON 入 out);-1 白名单外;-2 路径不存在/非目录;-3 name 空。 */
int files_search_json(const char *abs_start, const char *name, const char *typef, int max, sbuf_t *out)
{
if (!path_in_whitelist(abs_start)) return -1;
struct stat st;
if (stat(abs_start, &st) < 0 || !S_ISDIR(st.st_mode)) return -2;
char needle[300];
name_lower(name ? name : "", needle, sizeof needle);
if (!needle[0]) return -3;
int want_type = 0;
if (typef && !strcmp(typef, "file")) want_type = 1;
else if (typef && !strcmp(typef, "dir")) want_type = 2;
else if (typef && typef[0]) return -4; /* 非法 type 值(api 层映射 422) */
if (max <= 0 || max > SEARCH_MAX_ITEMS) max = SEARCH_MAX_ITEMS;
search_ctx_t c;
memset(&c, 0, sizeof c);
c.needle = needle; c.want_type = want_type; c.max = max; c.s = out;
char rel[1200];
make_rel(abs_start, rel, sizeof rel);
sA(out, "{\"path\":");
sA_str(out, abs_start);
sA(out, ",\"rel\":");
sA_str(out, rel);
sA(out, ",\"name\":");
sA_str(out, name);
sA(out, ",\"items\":[");
int stopped = search_walk(&c, abs_start, 1);
sA(out, "],\"count\":%d,\"truncated\":%s}", c.count,
(stopped || c.visited >= SEARCH_MAX_VISIT) ? "true" : "false");
return 0;
}
int api_files_search(int sock, const req_t *r)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
char path[1024] = "/";
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
char name[512] = "";
if (!qparam(r->query, "name", name, sizeof name) || !name[0])
return send_err(sock, 400, "BAD_REQUEST", "缺 name(按名子串搜索)");
char typef[16] = "";
qparam(r->query, "type", typef, sizeof typef);
char maxs[16] = "";
int max = SEARCH_MAX_ITEMS;
if (qparam(r->query, "max", maxs, sizeof maxs)) { int m = atoi(maxs); if (m > 0) max = m; }
char abs[1024];
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs))
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
sbuf_t s = {0};
int rc = files_search_json(abs, name, typef, max, &s);
if (rc == -1) { sbuf_free(&s); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
if (rc == -2) { sbuf_free(&s); return send_err(sock, 404, "NOT_FOUND", "路径不存在或非目录"); }
if (rc == -4) { sbuf_free(&s); return send_err(sock, 422, "UNSUPPORTED", "type 须为 file|dir"); }
if (rc != 0) { sbuf_free(&s); return send_err(sock, 400, "BAD_REQUEST", "name 为空"); }
/* 放行前自校验(与 overview 同口径:括号平衡/内嵌 NUL/坏字节,浏览器 JSON.parse 不容忍) */
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
if (chk) jv_free(chk);
if (bad) {
panel_log("files/search: SELF-CHECK fail (n=%zu)", s.n);
sbuf_free(&s);
return send_err(sock, 500, "INTERNAL", "搜索结果输出未通过 JSON 自校验");
}
int out = send_json(sock, 200, s.b);
sbuf_free(&s);
return out;
}
int multipart_extract_file(const req_t *r, const char *outpath) int multipart_extract_file(const req_t *r, const char *outpath)
{ {
const char *b = strstr(r->content_type, "boundary="); const char *b = strstr(r->content_type, "boundary=");
+154
View File
@@ -175,6 +175,160 @@ int api_net_dns_flush(int sock)
return rc; return rc;
} }
/* ---- F1-③ 蓝牙只读详情(FUTURE_MEOWPANEL §2.6):netmgrd MSG_NET_BT_CTL(0xE6) 读级五命令
* (status/paired/connected/a2dp/devices)聚合为一份 JSON。读级任意调用者(bt_cmd_is_write=读),
* 无阻塞(net_ipc 2s 超时兜底);子命令失败/坏字节 → 该子块空数组降级,绝不拖垮整卡、绝不发非法 JSON。 ---- */
/* 单发 0xE6 读命令;成功返回文本长度(>0),IPC/服务端失败 -1,坏 UTF-8 -2(该子块降级)。 */
static int bt_ctl_read(const char *cmd, char *out, size_t cap)
{
m_net_cmd_t q; memset(&q, 0, sizeof q);
snprintf(q.cmd, sizeof q.cmd, "%s", cmd);
uint16_t t; uint32_t code;
int n = net_ipc(MSG_NET_BT_CTL, &q, sizeof q, &t, &code, NULL, out, cap);
if (n <= 0 || t != MSG_NET_BT_RESULT || code != MEOW_OK) return -1;
if (!utf8_valid_n(out, (size_t)n)) return -2;
return n;
}
static int bt_mac_ok(const char *m)
{
for (int i = 0; i < 17; i++) {
if (i % 3 == 2) { if (m[i] != ':') return 0; }
else if (!is_hex(m[i])) return 0;
}
return 1;
}
/* "Device <MAC> <Name>" 行集 → JSON 数组项(不合法行跳过;名经 sA_str_n 消毒)。返回条数。 */
static int bt_devlines_json(sbuf_t *s, const char *txt, int max)
{
int n = 0;
const char *ln = txt;
while (ln && *ln && n < max) {
const char *nl = strchr(ln, '\n');
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
const char *p = ln;
while ((size_t)(p - ln) < ll && (*p == ' ' || *p == '\t')) p++;
if ((size_t)(ln + ll - p) >= 7 + 17 && !strncmp(p, "Device ", 7) && bt_mac_ok(p + 7)) {
const char *mac = p + 7;
const char *nm = mac + 17;
while ((size_t)(nm - ln) < ll && (*nm == ' ' || *nm == '\t')) nm++;
size_t nlen = ll - (size_t)(nm - ln);
while (nlen && (nm[nlen - 1] == ' ' || nm[nlen - 1] == '\t' || nm[nlen - 1] == '\r')) nlen--;
if (n) sA(s, ",");
sA(s, "{\"mac\":");
sA_str_n(s, mac, 17);
sA(s, ",\"name\":");
sA_str_n(s, nm, nlen);
sA(s, "}");
n++;
}
ln = nl ? nl + 1 : NULL;
}
return n;
}
/* 行内 " key=value"(空白/行尾收尾)取值。 */
static void bt_line_kv(const char *line, const char *key, char *out, size_t cap)
{
out[0] = 0;
const char *v = strstr(line, key);
if (!v) return;
v += strlen(key);
size_t i = 0;
while (v[i] && v[i] != ' ' && v[i] != '\t' && v[i] != '\n' && v[i] != '\r' && i + 1 < cap) { out[i] = v[i]; i++; }
out[i] = 0;
}
/* "A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=<0|1> role=<..>" 行集 → JSON 数组项。 */
static int bt_a2dp_json(sbuf_t *s, const char *txt, int max)
{
int n = 0;
const char *ln = txt;
while (ln && *ln && n < max) {
const char *nl = strchr(ln, '\n');
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
char line[320];
if (ll >= sizeof line) ll = sizeof line - 1;
memcpy(line, ln, ll); line[ll] = 0;
if (!strncmp(line, "A2DP ", 5) && strncmp(line, "A2DP none", 9) && bt_mac_ok(line + 5)) {
char codec[24], rate[16], chn[8], bits[8], acq[8], role[8];
bt_line_kv(line, " codec=", codec, sizeof codec);
bt_line_kv(line, " rate=", rate, sizeof rate);
bt_line_kv(line, " channels=", chn, sizeof chn);
bt_line_kv(line, " bits=", bits, sizeof bits);
bt_line_kv(line, " acquired=", acq, sizeof acq);
bt_line_kv(line, " role=", role, sizeof role);
if (n) sA(s, ",");
sA(s, "{\"mac\":");
sA_str_n(s, line + 5, 17);
sA(s, ",\"codec\":"); sA_str(s, codec);
sA(s, ",\"rate\":"); sA_str(s, rate);
sA(s, ",\"channels\":"); sA_str(s, chn);
sA(s, ",\"bits\":"); sA_str(s, bits);
sA(s, ",\"acquired\":%s", !strcmp(acq, "1") ? "true" : "false");
sA(s, ",\"role\":"); sA_str(s, role);
sA(s, "}");
n++;
}
ln = nl ? nl + 1 : NULL;
}
return n;
}
/* 五读文本聚合(任一 NULL=该子块空数组;st=NULL → {"available":false})。 */
int net_bt_aggregate(const char *st, const char *paired, const char *connected,
const char *a2dp, const char *devices, sbuf_t *s)
{
if (!st) { sA(s, "{\"available\":false}"); return -1; }
int avail = 0, powered = 0; long long paired_n = -1, connected_n = -1;
char addr[24] = "";
(void)sscanf(st, "bt available=%d powered=%d address=%23s paired=%lld connected=%lld",
&avail, &powered, addr, &paired_n, &connected_n);
if (!strcmp(addr, "-")) addr[0] = 0;
sA(s, "{\"available\":%s", avail ? "true" : "false");
sA(s, ",\"powered\":%s", powered ? "true" : "false");
sA(s, ",\"address\":"); sA_str(s, addr);
if (paired_n >= 0) sA(s, ",\"paired_n\":%lld", paired_n); else sA(s, ",\"paired_n\":null");
if (connected_n >= 0) sA(s, ",\"connected_n\":%lld", connected_n); else sA(s, ",\"connected_n\":null");
sA(s, ",\"paired\":["); if (paired) bt_devlines_json(s, paired, 32); sA(s, "]");
sA(s, ",\"connected\":["); if (connected) bt_devlines_json(s, connected, 16); sA(s, "]");
sA(s, ",\"a2dp\":["); if (a2dp) bt_a2dp_json(s, a2dp, 8); sA(s, "]");
sA(s, ",\"nearby\":["); if (devices) bt_devlines_json(s, devices, 16); sA(s, "]");
sA(s, "}");
return 0;
}
int api_net_bt(int sock)
{
char st[MEOW_IPC_MAX], pa[MEOW_IPC_MAX], co[MEOW_IPC_MAX], a2[MEOW_IPC_MAX], dv[MEOW_IPC_MAX];
int ns = bt_ctl_read("status", st, sizeof st);
if (ns <= 0) /* netmgrd 不可达/status 坏字节 → 整体降级(net 域惯例:200 available:false) */
return send_json(sock, 200, "{\"available\":false}");
int np = bt_ctl_read("paired", pa, sizeof pa);
int nc = bt_ctl_read("connected", co, sizeof co);
int na = bt_ctl_read("a2dp", a2, sizeof a2);
int nd = bt_ctl_read("devices", dv, sizeof dv);
sbuf_t s = {0};
net_bt_aggregate(st, np > 0 ? pa : NULL, nc > 0 ? co : NULL, na > 0 ? a2 : NULL, nd > 0 ? dv : NULL, &s);
/* 放行前自校验(与 overview 同口径:浏览器 JSON.parse 不容忍尾随垃圾/括号错位/内嵌 NUL/坏字节) */
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
int bad = !s.b || !chk || (strlen(s.b) != s.n) ||
!utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n);
if (chk) jv_free(chk);
if (bad) {
FILE *df = fopen("/tmp/net_bt_bad.json", "w");
if (df && s.b) { fwrite(s.b, 1, s.n, df); fclose(df); }
panel_log("net/bt: SELF-CHECK fail, raw dumped /tmp/net_bt_bad.json");
sbuf_free(&s);
return send_err(sock, 500, "INTERNAL", "蓝牙聚合输出未通过 JSON 自校验(原始数据已留 /tmp/net_bt_bad.json)");
}
int rc = send_json(sock, 200, s.b);
sbuf_free(&s);
return rc;
}
int api_net_log(int sock) int api_net_log(int sock)
{ {
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
+112 -1
View File
@@ -301,6 +301,7 @@ int handle(int sock, req_t *r)
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */ /* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/bt")) return api_net_bt(sock); /* F1-③ 蓝牙只读详情(0xE6 读级聚合) */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
@@ -381,6 +382,7 @@ int handle(int sock, req_t *r)
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/search")) return api_files_search(sock, r); /* F1-④ 搜索(只读) */
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r); if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
/* ---- 应用 ---- */ /* ---- 应用 ---- */
@@ -634,7 +636,116 @@ int panel_selftest(void)
if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; }
} }
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\n"); /* 10) net_bt_aggregate(F1-③):0xE6 五读文本聚合 → 合法 JSON + 字段断言;
* 设备名含截断坏字节 → sA_str_n 消毒为 '?' 且产物仍合法;子文本 NULL → 空数组;status NULL → available:false。 */
{
sbuf_t b = {0};
const char *st = "bt available=1 powered=1 address=4C:BC:98:B0:98:DB paired=2 connected=1";
const char *pa = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 流媒体客户\xE7\n";
const char *co = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n";
const char *a2 = "A2DP 33:DE:2E:21:AD:30 codec=sbc rate=44100 channels=2 bits=16 acquired=1 role=src\n";
const char *dv = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 17OP15\n";
if (net_bt_aggregate(st, pa, co, a2, dv, &b) != 0) { printf("SELFTEST FAIL: net_bt_aggregate 正常文本返回非 0\n"); sbuf_free(&b); return 1; }
if (!b.b || !utf8_valid_n(b.b, b.n) || !json_strict_balanced(b.b, b.n)) { printf("SELFTEST FAIL: net_bt_aggregate 产物非法字节/括号错位\n"); sbuf_free(&b); return 1; }
jv_t *br = jv_parse(b.b, (int)b.n);
if (!br) { printf("SELFTEST FAIL: net_bt_aggregate 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; }
jv_t *pw = jv_obj_get(br, "powered");
if (!pw || pw->type != J_BOOL || !pw->bval) { printf("SELFTEST FAIL: net_bt_aggregate powered 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *pr = jv_obj_get(br, "paired");
if (!pr || pr->type != J_ARR || !pr->child || !pr->child->next || pr->child->next->next) { printf("SELFTEST FAIL: net_bt_aggregate paired 应 2 条\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *cn = jv_obj_get(br, "connected");
if (!cn || cn->type != J_ARR || !cn->child || cn->child->next) { printf("SELFTEST FAIL: net_bt_aggregate connected 应 1 条\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *a2p = jv_obj_get(br, "a2dp");
if (!a2p || a2p->type != J_ARR || !a2p->child) { printf("SELFTEST FAIL: net_bt_aggregate 缺 a2dp 条目\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *cd = jv_obj_get(a2p->child, "codec");
if (!cd || cd->type != J_STR || !cd->str || strcmp(cd->str, "sbc") != 0) { printf("SELFTEST FAIL: net_bt_aggregate a2dp codec 非 sbc\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *aq = jv_obj_get(a2p->child, "acquired");
if (!aq || aq->type != J_BOOL || !aq->bval) { printf("SELFTEST FAIL: net_bt_aggregate a2dp acquired 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_free(br);
sbuf_free(&b);
/* status NULL → available:false;子文本 NULL → 空数组 */
sbuf_t b2 = {0};
net_bt_aggregate(NULL, NULL, NULL, NULL, NULL, &b2);
if (!b2.b || strstr(b2.b, "\"available\":false") == NULL) { printf("SELFTEST FAIL: net_bt_aggregate status NULL 未降级 available:false\n"); sbuf_free(&b2); return 1; }
sbuf_free(&b2);
sbuf_t b3 = {0};
net_bt_aggregate(st, NULL, NULL, NULL, NULL, &b3);
jv_t *b3r = b3.b ? jv_parse(b3.b, (int)b3.n) : NULL;
jv_t *b3p = b3r ? jv_obj_get(b3r, "paired") : NULL;
if (!b3p || b3p->type != J_ARR || b3p->child) { printf("SELFTEST FAIL: net_bt_aggregate 子文本 NULL 应为空数组\n"); if (b3r) jv_free(b3r); sbuf_free(&b3); return 1; }
jv_free(b3r);
sbuf_free(&b3);
}
/* 11) files_search_json(F1-④):沙盒卡根递归搜索(限深/限量/类型过滤/rel 回显)→ 合法 JSON + 计数断言;
* 白名单外起点 → -1;name 空 → -3;非法 type → -4。 */
{
char tmpl[] = "/tmp/mp_search_XXXXXX";
char *root = mkdtemp(tmpl);
if (!root) { printf("SELFTEST FAIL: mkdtemp 失败\n"); return 1; }
char saved_root[1024];
snprintf(saved_root, sizeof saved_root, "%s", g_tf_root_buf);
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", root);
char d1[1200], d2[1200], f1[1200], f2[1200], f3[1200];
snprintf(d1, sizeof d1, "%s/MeowOS", root);
snprintf(d2, sizeof d2, "%s/MeowOS/sub", root);
snprintf(f1, sizeof f1, "%s/MeowOS/Test Song.mp3", root);
snprintf(f2, sizeof f2, "%s/MeowOS/another.txt", root);
snprintf(f3, sizeof f3, "%s/MeowOS/sub/x Song2.log", root);
int setup_ok = (mkdir(d1, 0755) == 0 && mkdir(d2, 0755) == 0);
const char *mk[3] = { f1, f2, f3 };
for (int i = 0; i < 3 && setup_ok; i++) {
FILE *f = fopen(mk[i], "w");
if (!f) { setup_ok = 0; break; }
fputs("x", f); fclose(f);
}
if (!setup_ok) { printf("SELFTEST FAIL: 搜索沙盒构建失败\n"); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root); rm_recursive(root); rmdir(root); return 1; }
int fails = 0;
sbuf_t s = {0};
if (files_search_json(d1, "song", NULL, 0, &s) != 0) { printf("SELFTEST FAIL: files_search_json 正常搜索返回非 0\n"); fails = 1; }
jv_t *sr = (!fails && s.b) ? jv_parse(s.b, (int)s.n) : NULL;
if (!fails && (!sr || !utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n))) { printf("SELFTEST FAIL: files_search_json 产物非法\n"); fails = 1; }
jv_t *cnt = (!fails && sr) ? jv_obj_get(sr, "count") : NULL;
if (!fails && (!cnt || cnt->type != J_NUM || cnt->inum != 2)) { printf("SELFTEST FAIL: files_search_json song 应中 2 条(递归命中子目录)\n"); fails = 1; }
jv_t *its = (!fails && sr) ? jv_obj_get(sr, "items") : NULL;
jv_t *rel0 = (its && its->child) ? jv_obj_get(its->child, "rel") : NULL;
if (!fails && (!rel0 || rel0->type != J_STR || !rel0->str || strncmp(rel0->str, "/MeowOS/", 8) != 0)) { printf("SELFTEST FAIL: files_search_json rel 未按卡根相对回显\n"); fails = 1; }
if (sr) jv_free(sr);
sbuf_free(&s);
/* type=dir 过滤:建 Songs 目录应仅中目录 */
char d3[1200];
snprintf(d3, sizeof d3, "%s/MeowOS/Songs", root);
if (!fails && mkdir(d3, 0755) != 0) { printf("SELFTEST FAIL: 沙盒 Songs 目录创建失败\n"); fails = 1; }
sbuf_t s2 = {0};
if (!fails && files_search_json(d1, "song", "dir", 0, &s2) == 0) {
jv_t *sr2 = jv_parse(s2.b, (int)s2.n);
jv_t *c2 = sr2 ? jv_obj_get(sr2, "count") : NULL;
jv_t *i2 = sr2 ? jv_obj_get(sr2, "items") : NULL;
jv_t *t0 = (i2 && i2->child) ? jv_obj_get(i2->child, "type") : NULL;
if (!c2 || c2->type != J_NUM || c2->inum != 1 || !t0 || t0->type != J_STR || strcmp(t0->str, "dir") != 0) {
printf("SELFTEST FAIL: files_search_json type=dir 应仅中 1 个目录\n"); fails = 1;
}
if (sr2) jv_free(sr2);
} else if (!fails) { printf("SELFTEST FAIL: files_search_json type=dir 搜索失败\n"); fails = 1; }
sbuf_free(&s2);
/* 无匹配 → count 0;白名单外 → -1;空 name → -3;非法 type → -4 */
sbuf_t s3 = {0};
if (!fails && files_search_json(d1, "zzzz", NULL, 0, &s3) == 0) {
if (!strstr(s3.b, "\"count\":0")) { printf("SELFTEST FAIL: files_search_json 无匹配应 count 0\n"); fails = 1; }
} else if (!fails) { printf("SELFTEST FAIL: files_search_json 无匹配搜索失败\n"); fails = 1; }
sbuf_free(&s3);
sbuf_t s4 = {0};
if (!fails && files_search_json("/etc", "x", NULL, 0, &s4) != -1) { printf("SELFTEST FAIL: files_search_json 白名单外未拒\n"); fails = 1; }
if (!fails && files_search_json(d1, "", NULL, 0, &s4) != -3) { printf("SELFTEST FAIL: files_search_json 空 name 未拒\n"); fails = 1; }
if (!fails && files_search_json(d1, "x", "bogus", 0, &s4) != -4) { printf("SELFTEST FAIL: files_search_json 非法 type 未拒\n"); fails = 1; }
sbuf_free(&s4);
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root);
rm_recursive(root);
rmdir(root);
if (fails) return 1;
}
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 端点池 + 蓝牙 0xE6 聚合 + 文件搜索 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\n");
return 0; return 0;
} }
+4 -1
View File
@@ -537,7 +537,10 @@ int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢
void audio_out_close(void); void audio_out_close(void);
int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod
(MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。 (MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。
audio_out_open SINK_AUDIOD 成功后已自动调;独立导出供宿主验证与复调。 */ 每音频会话(新 audiod 流 sid)首同步一次 + 值变才重发——切歌/新建
会话时 DAC 音量不残留默认响度(修「切歌音量骤增」);同会话内不
重复建连(不做 fd 风暴)。audio_out_open SINK_AUDIOD 成功后已自动调;
独立导出供宿主验证与复调。 */
void audio_out_set_target(int ms); /* 预留:缓冲目标 */ void audio_out_set_target(int ms); /* 预留:缓冲目标 */
int audio_out_buffered_ms(void); int audio_out_buffered_ms(void);
+17 -9
View File
@@ -233,20 +233,27 @@ static void audiod_sync_volume(int pct)
close(fd); close(fd);
} }
/* 每进程同步一次 + 值变才重发:杜绝每次 audio_out_open 新建短连接(fd=N accept 风暴根因)。 /* 会话身份 = audiod 流 sid(g_sid):切歌/新建音频会话 = 每次 audio_out_open 都向 audiod 开一条
* g_vol_synced=1 即已同步过;g_vol_last=上次下发的 pct,二者相同则零连接直接返回。 */ * 新流、audiod 分配新 sid(单调不重用)。此前用进程级 g_vol_synced「首次同步一次」——切歌(新会话)
static int g_vol_synced = 0; * 时音量不再被重发到 audiod,DAC 落在会话建立/重配后的默认/上次残留值(衰减量 0=最响),造成
static int g_vol_last = -1; * 「切歌音量骤增,按一下音量键才回落到设定值」(用户真机 2026-09-05)。audiod 只在 reconfigure
* 且参数变化时才 re-apply 音量(_audio_mark_reconfig_needed 同参数跳过),本会话重发是权威兜底。
* 修法:**每会话首同步一次**——新 sid(新会话)必重发一次当前音量;同会话内仅值变才重发。
* 每会话恰 1 条短连接,不回到「每次 open 都建连」的 fd 风暴(§2026-09-02 根因,vol_sync_sim 60 次
* 同值→1 连接)。音量值取自 settings.conf(audiod 权威同源同键),反转/真静音(255) 由 audiod
* dac_vol_set 负责,本层只传 pct(0-100)。 */
static int g_vol_synced_sid = -1; /* 上次音量同步针对的 audiod 流 sid;-1=从未(必发) */
static int g_vol_last = -1;
int audio_out_sync_volume(void) int audio_out_sync_volume(void)
{ {
int pct = settings_read_volume(); int pct = settings_read_volume();
if (g_vol_synced && pct == g_vol_last) return 0; /* 已同步且未变:零新建连接 */ if (g_vol_synced_sid == g_sid && pct == g_vol_last) return 0; /* 本会话已同步且值未变:零新建连接 */
g_vol_synced = 1; g_vol_synced_sid = g_sid;
g_vol_last = pct; g_vol_last = pct;
audiod_sync_volume(pct); audiod_sync_volume(pct);
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s)", meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s, sid=%d)",
pct, settings_conf_path()); pct, settings_conf_path(), g_sid);
return 0; return 0;
} }
@@ -379,6 +386,7 @@ int audio_out_pause(int paused)
} }
g_sid = sid; g_sid = sid;
meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid); meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid);
audio_out_sync_volume(); /* 新会话(新 sid):音量权威兜底重发一次(DAC 复位后不残留默认响度) */
return 0; return 0;
} }
+38 -6
View File
@@ -1032,10 +1032,18 @@ static int g_lyr_thr_on;
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */ static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */ static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */
/* 歌词拉取失败后的重试退避(ms,指数翻倍封顶 OS):掉线瞬间失败的请求**不**写死「无歌词」,
* 交回退避重试;网络恢复后下一轮到点即重拉成功(真机症状:掉线一会儿再重连后恒「暂无歌词」)。 */
#define LYRIC_RETRY_BASE_MS 1000
#define LYRIC_RETRY_MAX_MS (30 * 1000)
static void *lyric_worker(void *arg) static void *lyric_worker(void *arg)
{ {
(void)arg; (void)arg;
char last_id[24] = ""; char last_id[24] = "";
int last_ok = 1; /* last_id 对应结果是否成功(成功不重拉;失败记退避待重试) */
int retry_delay = 0; /* 失败后逐次翻倍的退避基值(ms) */
int64_t retry_at = 0; /* 单调时刻:下次重试;0=无待重试 */
while (!g_app_quit) { while (!g_app_quit) {
usleep(300 * 1000); usleep(300 * 1000);
char id[24]; char id[24];
@@ -1051,14 +1059,28 @@ static void *lyric_worker(void *arg)
g_lyr_state = 0; g_lyr_state = 0;
pthread_mutex_unlock(&g_lyrmu); pthread_mutex_unlock(&g_lyrmu);
last_id[0] = 0; last_id[0] = 0;
last_ok = 1;
retry_delay = 0;
retry_at = 0;
continue; continue;
} }
if (!id[0]) continue; if (!id[0]) continue;
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取 int is_new = (strcmp(id, last_id) != 0);
* (A 回退硬暂停 OR svc_perf C Cached 降载), if (is_new) {
* 不置 last_id → 降载恢复后下轮再拉。 */ if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
ncm_strlcpy(last_id, id, sizeof last_id); * (A 回退硬暂停 OR svc_perf C Cached 降载),
* 不置 last_id → 降载恢复后下轮再拉。 */
ncm_strlcpy(last_id, id, sizeof last_id);
last_ok = 0;
retry_delay = 0;
retry_at = 0;
} else {
if (last_ok) continue; /* 同一曲已成功拉完(含 NCM_OK 空词=真无词),不重拉 */
if (meow_cachegov_prefetch_paused()) continue;
if (retry_at > ncm_monotonic_ms()) continue; /* 失败退避未到 */
/* 到点重试:保留 retry_delay 以便失败时继续翻倍 */
}
lyric_clear(); lyric_clear();
pthread_mutex_lock(&g_lyrmu); pthread_mutex_lock(&g_lyrmu);
g_lyr_state = 1; /* 加载中 */ g_lyr_state = 1; /* 加载中 */
@@ -1068,22 +1090,32 @@ static void *lyric_worker(void *arg)
char *tl = (char *)malloc(32 * 1024); char *tl = (char *)malloc(32 * 1024);
if (!lrc || !tl) { if (!lrc || !tl) {
free(lrc); free(tl); free(lrc); free(tl);
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
retry_at = ncm_monotonic_ms() + retry_delay;
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu); pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
continue; continue;
} }
lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */ lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024); int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
if (rc == NCM_OK) { if (rc == NCM_OK) {
last_ok = 1; /* 真正结果:空词(NCM_OK 无词也算「已拉完」) */
retry_delay = 0;
retry_at = 0;
lyric_parse(lrc, tl); lyric_parse(lrc, tl);
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */ ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
} else { } else {
/* 拉取失败(网络/HTTP/会话):不写死「无词」,交回退避重试;UI 呈「加载中」而非「暂无歌词」 */
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
retry_at = ncm_monotonic_ms() + retry_delay;
ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */ ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */
} }
free(lrc); free(tl); free(lrc); free(tl);
pthread_mutex_lock(&g_lyrmu); pthread_mutex_lock(&g_lyrmu);
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id); ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
g_lyr_state = 2; /* 拉完(可能 0 行=无词) */ g_lyr_state = last_ok ? 2 : 0; /* 2=已拉完(可 0 行=无词);0=失败待重试(UI 呈加载中) */
pthread_mutex_unlock(&g_lyrmu); pthread_mutex_unlock(&g_lyrmu);
} }
return NULL; return NULL;
+64 -30
View File
@@ -26,8 +26,12 @@ static volatile int g_load_err;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。 /* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。
* §17 滑动窗口:worker 两种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化首段; * §17 滑动窗口:worker 三种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化第 0 页
* ②滑窗补载:物化视口区间 [g_ens_lo,g_ens_hi)(ensure_range,盘缓存优先)。 */ * (≤NCM_SONG_BATCH 首,一次一页——不再首屏连拉多批补满窗口,大歌单阻塞久且触发风控);
* ②顺序下一页(g_ens_page=1):仅当用户滑到已分页底部(余量 ≤NCM_PL_PAGE_EDGE 首)时拉一页,
* 成功推进 g_paged_hi,拉完该歌单全部曲目即止;③回滚补物化:已分页区间内被窗口挤出的
* 失效槽按视口 ±24 补(盘缓存命中为主,不占网络分页额度)。 */
#define NCM_PL_PAGE_EDGE 20 /* 懒加载触发余量(首):视口底距已分页底部 ≤20 首才拉下一页 */
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_plthr; static pthread_t g_plthr;
static int g_plthr_on; static int g_plthr_on;
@@ -37,9 +41,11 @@ static volatile int g_plloading;
static volatile int g_plloaded_idx; static volatile int g_plloaded_idx;
static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */ static volatile int g_plloaded_gen; /* worker 启动时代次(§26:过期结果丢弃判据) */
static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */ static volatile int g_pl_stale; /* 1=本次结果被判过期(未写全局,需重拉当前目标) */
static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */ static volatile int g_ens_lo, g_ens_hi; /* 补载/下一页目标区间(全局下标) */
static volatile int g_ens_first; /* 1=首屏重头(offset=0) */ static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
static volatile int g_ens_page; /* 1=顺序下一页任务(成功推进 g_paged_hi) */
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */ static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
static volatile int g_paged_hi; /* 已分页高水位:从顶部连续物化到的全局下标(下一页起点) */
/* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt, /* 打开快照(§26 开A→未载完→开B 防串):refresh 在启动 worker 前把「目标歌单」拷进 g_tgt,
* worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的 * worker 启动时再拷到本地——detail 加载不再依赖 g_st.playlists[](会被回家时 home_worker 的
@@ -239,12 +245,15 @@ static void pl_draw_footer(void *ctx)
ncm_draw_footer(r_cont, foot); ncm_draw_footer(r_cont, foot);
} }
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。 */ /* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。
* 拉到数据后必须让列表项重新绑定/重绘:src2 每帧 live 取行(占位行 ↔ 已物化歌名/歌手),
* 失效列表带 + 整帧重画把刚物化的歌名/歌手填进各项(修「拉完曲目行仍 ... 加载中」)。 */
static void pl_apply(int reset_scroll) static void pl_apply(int reset_scroll)
{ {
if (g_list) { if (g_list) {
meow_listview_set_count(g_list, g_st.track_ids_count); /* 全量行数(占位行兜底未物化区) */ meow_listview_set_count(g_list, g_st.track_ids_count); /* 全量行数(占位行兜底未物化区) */
if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); } if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); }
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
} }
const ncm_playlist_t *pcur = pl_cur(); const ncm_playlist_t *pcur = pl_cur();
if (g_tb && pcur) { if (g_tb && pcur) {
@@ -261,30 +270,24 @@ static void *playlist_worker(void *arg)
(void)arg; (void)arg;
/* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */ /* 拷贝打开快照 + 任务参数:单飞(g_plthr_on)保证 worker 运行期间 g_tgt 不被改写。 */
ncm_playlist_t tpl; ncm_playlist_t tpl;
int tsearch, tidx, tgen, first, lo, hi; int tsearch, tidx, tgen, first, lo, hi, page;
pthread_mutex_lock(&g_plmu); pthread_mutex_lock(&g_plmu);
tpl = g_tgt.pl; tsearch = g_tgt.is_search; tpl = g_tgt.pl; tsearch = g_tgt.is_search;
tidx = g_tgt.idx; tgen = g_tgt.gen; tidx = g_tgt.idx; tgen = g_tgt.gen;
first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi; first = g_ens_first; lo = g_ens_lo; hi = g_ens_hi; page = g_ens_page;
pthread_mutex_unlock(&g_plmu); pthread_mutex_unlock(&g_plmu);
int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */ int cur_active = g_st.active_playlist; /* ensure 路径结束校验用(当前目标) */
int rc; int rc;
if (g_host_mock) rc = 0; if (g_host_mock) rc = 0;
else if (first) { else if (first) {
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */ /* 首屏:快照式加载(detail 全量 trackIds + 物化第 0 页)。ctx 校验代次/目标,过期自动丢弃。
* §懒加载:首屏只拉一页(≤NCM_SONG_BATCH 首,一次仅一批)——不再 ensure_range 补满整个
* 滑动窗口(旧版首屏连拉 ≤3 批:5k+ 大歌单「我喜欢的音乐」长时间卡住不出列表,连续
* 请求还触发风控);后续页由 pl_maybe_ensure 在用户滑到已分页底部时逐页懒加载。 */
if (tidx < 0) rc = NCM_ERR_ARG; if (tidx < 0) rc = NCM_ERR_ARG;
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH); else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
/* §22 整张歌单逐渐物化:首屏成功后把「能装进滑动窗口」的歌单一次性补填入窗 if (rc == NCM_OK)
* (≤NCM_SONG_WINDOW 的歌单整张物化,末叶行不再恒「.../加载中」——真机反馈 g_paged_hi = (g_st.track_ids_count < NCM_SONG_BATCH) ? g_st.track_ids_count : NCM_SONG_BATCH;
* 「曲目稍多时行一直加载中,点开播放这首才回填」);超窗口的歌单仍按视口滑动补载
* (内存纪律:窗口 ≤256 首)。窗口基址=0 覆盖 [0,min(track,WINDOW)),不滑窗不挤占视口。 */
if (rc == NCM_OK) {
int full = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
if (full > NCM_SONG_BATCH) {
int r2 = appstate_song_ensure_range(0, full);
if (r2 != NCM_OK && rc == NCM_OK) rc = r2; /* 补填失败非致命:已物化槽保留 */
}
}
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc, /* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命 * 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */ * g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
@@ -292,8 +295,10 @@ static void *playlist_worker(void *arg)
"playlist: first-screen idx=%d plid=%s trackIds=%d materialized=%d rc=%d", "playlist: first-screen idx=%d plid=%s trackIds=%d materialized=%d rc=%d",
tidx, tpl.id, g_st.track_ids_count, g_st.song_count, rc); tidx, tpl.id, g_st.track_ids_count, g_st.song_count, rc);
} else { } else {
/* 滑窗补载:物化视口区间(盘缓存优先,批网格补载)。 */ /* 补载:物化目标区间(顺序下一页 / 回滚补物化;盘缓存优先,批网格补载)。
* 顺序下一页成功 → 已分页高水位推进到 hi;部分失败不推进(占位行保留,滑到底部重试本页)。 */
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK; rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
if (rc == NCM_OK && page && hi > g_paged_hi) g_paged_hi = hi;
} }
pthread_mutex_lock(&g_plmu); pthread_mutex_lock(&g_plmu);
if (first) { if (first) {
@@ -324,7 +329,25 @@ static void *playlist_worker(void *arg)
return NULL; return NULL;
} }
/* 视口驱动滑窗补载:视口 ±24 行有未物化槽位 → 发 ensure worker(已在载/失败态/宿主 mock 不发)。 */ /* 发补载 worker(单飞;调用方先判 g_plthr_on)。page=1 为顺序下一页任务。 */
static void pl_ensure_post(int lo, int hi, int page)
{
g_ens_lo = lo; g_ens_hi = hi;
g_ens_first = 0;
g_ens_page = page;
g_plloading = 1;
g_plthr_on = 1;
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
}
}
/* 懒加载分页触发(Bug 修①:旧版视口 ±24 触发 + 首屏连拉多批 → 大歌单连续拉多页、阻塞 + 风控):
* ① 顺序下一页:仅在视口底滑到「已分页底部」(余量 ≤NCM_PL_PAGE_EDGE 首)时拉下一页,
* 一次只拉一页(≤NCM_SONG_BATCH);g_paged_hi 达 track_ids_count = 全部拉完,停止。
* 远离已分页底部(快速猛滑到深处占位区)不拉——防连续拉多页触发风控。
* ② 回滚补物化:视口 ±24 落在「已分页区间内」的失效槽(窗口挤出)按旧逻辑补——
* 盘缓存(songmeta)命中为主,不消耗网络分页额度;区间外(未分页)一律不预拉。 */
static void pl_maybe_ensure(void) static void pl_maybe_ensure(void)
{ {
if (g_plthr_on || g_host_mock) return; if (g_plthr_on || g_host_mock) return;
@@ -334,23 +357,32 @@ static void pl_maybe_ensure(void)
int scroll = meow_listview_scroll(g_list); int scroll = meow_listview_scroll(g_list);
int first = scroll / APP_ROW_H; int first = scroll / APP_ROW_H;
int last = (scroll + r_list.h) / APP_ROW_H + 1; int last = (scroll + r_list.h) / APP_ROW_H + 1;
if (last > total) last = total;
int paged = g_paged_hi;
if (paged > total) paged = total; /* 防御:索引重建后高水位越界 */
/* ① 顺序下一页(懒加载) */
if (paged < total) {
int margin = paged - last; /* 视口底距已分页底部余量(首;负=已越过) */
if (margin <= NCM_PL_PAGE_EDGE && margin >= -NCM_PL_PAGE_EDGE) {
int hi = paged + NCM_SONG_BATCH;
if (hi > total) hi = total;
pl_ensure_post(paged, hi, 1);
return;
}
}
/* ② 回滚补物化(仅已分页区间内) */
int lo = first - 24, hi = last + 24; int lo = first - 24, hi = last + 24;
if (lo < 0) lo = 0; if (lo < 0) lo = 0;
if (hi > total) hi = total; if (hi > paged) hi = paged;
if (lo >= hi) return; if (lo >= hi) return;
if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作(窗口原地不动) */ if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作(窗口原地不动) */
g_ens_lo = lo; g_ens_hi = hi; pl_ensure_post(lo, hi, 0);
g_ens_first = 0;
g_plloading = 1;
g_plthr_on = 1;
if (ncm_thread_create(&g_plthr, playlist_worker, NULL) != 0) {
g_plthr_on = 0; g_plloading = 0; ncm_snack("加载失败");
}
} }
void playlist_refresh(void) void playlist_refresh(void)
{ {
if (g_host_mock) { g_load_err = 0; g_ens_err = 0; pl_apply(1); return; } if (g_host_mock) { g_load_err = 0; g_ens_err = 0; g_paged_hi = g_st.track_ids_count; pl_apply(1); return; }
if (g_plthr_on) return; if (g_plthr_on) return;
g_load_err = 0; g_load_err = 0;
g_ens_err = 0; g_ens_err = 0;
@@ -373,7 +405,9 @@ void playlist_refresh(void)
if (!appstate_playlist_window_is(g_tgt.pl.id)) if (!appstate_playlist_window_is(g_tgt.pl.id))
appstate_playlist_reset(); appstate_playlist_reset();
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */ g_ens_first = 1; /* 首屏:重建索引 + 物化第 0 页 */
g_ens_page = 0;
g_paged_hi = 0; /* 分页高水位归零:首屏成功后由 worker 重标 */
g_pl_stale = 0; g_pl_stale = 0;
g_plloading = 1; g_plloading = 1;
g_plthr_on = 1; g_plthr_on = 1;
+145
View File
@@ -0,0 +1,145 @@
/* bench/lyric_sim.c — 歌词「掉线失败 → 重连 → 重试成功」宿主离线回归(0 真实请求)。
*
* 驱动真实 app/play/player_manager.c 的 lyric_worker(后台歌词线程),网络换成可注入的
* ncm_api_lyric 桩:第 1 次调用返回 NCM_ERR_NET(模拟掉线瞬间歌词请求失败),第 2 次返回
* NCM_OK + 一段 LRC(模拟网络恢复后重试成功)。
*
* 复现的真机症状(REPORT/任务):播放中歌词正常;网络掉线一会儿再重连后某曲恒「暂无歌词」。
* 根因:旧 lyric_worker 在**发起网络请求前**就把 last_id 记为已拉,且失败也置 lyric_state=2
* (=已拉完),故网络失败被写死成「无歌词」,重连后永不重试。本机回归:
* ① 第 1 次失败后 lyric_state() 必须**不是** 2(可重试),且无词(lyric_count()==0);
* ② 推进单调时钟越过退避点(模拟重连)后必须**自动重试**并成功(第 2 次调用)+ 词展示
* (lyric_count()>0、lyric_state()==2)。
* 旧代码在 ② 上 FAIL(g_lyr_state 恒 2、第 2 次调用永不发生)。
*
* 编译(宿主,ncm_app 目录下;gc-sections 只保留歌词路径,避免链整个播放链的符号):
* CC=gcc F="-ffunction-sections -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd -Ivendor/jsmn"
* $CC $F -c bench/lyric_sim.c -o /tmp/ly.o
* $CC $F -c app/play/player_manager.c -o /tmp/ly_pm.o
* $CC $F -c app/logic/lyric.c -o /tmp/ly_lrc.o
* $CC -Wl,--gc-sections -o lyric_sim /tmp/ly.o /tmp/ly_pm.o /tmp/ly_lrc.o -lpthread -lm
* 运行:./lyric_sim (PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h> /* usleep */
/* ---- 全局状态(app.c 定义;本 sim 提供桩) ---- */
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 1;
int g_dark = 0;
/* ---- 可控单调时钟(lyric_worker 用 ncm_monotonic_ms 做失败退避) ---- */
static int64_t g_now_ms = 1000;
int64_t ncm_monotonic_ms(void) { return g_now_ms; }
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
while (src[n]) n++;
return n;
}
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
/* appstate 桩:单曲队列(idx 0 恒有效,song id=1000) */
int appstate_queue_count(void) { return 1; }
/* page-cache 治理桩:恒非暂停(不挡歌词预取) */
int meow_cachegov_prefetch_paused(void) { return 0; }
/* 媒体会话歌词上报桩:失败上报空、成功上报 LRC;计数供断言 */
static int g_report_empty;
static int g_report_lyric;
static char g_report_last_lrc[256];
void ipc_media_lyric_report(const char *lrc, const char *tl)
{
(void)tl;
if (lrc && lrc[0]) { g_report_lyric++; ncm_strlcpy(g_report_last_lrc, lrc, sizeof g_report_last_lrc); }
else { g_report_empty++; }
}
/* 歌词 API 桩:第 1 次=网络失败,其后=成功返回 LRC。 */
static int g_fetch_calls;
int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
char *lrc, size_t lrc_sz, char *tlrc, size_t tlrc_sz)
{
(void)device_id; (void)cookie; (void)id; (void)tlrc; (void)tlrc_sz;
g_fetch_calls++;
if (g_fetch_calls == 1) {
if (lrc && lrc_sz) lrc[0] = 0; /* 掉线失败:不写词 */
return NCM_ERR_NET; /* 网络错误(掉线) */
}
static const char body[] =
"[00:00.00]Line one\n[00:05.00]Line two\n[00:10.00]Line three\n";
if (lrc && lrc_sz) snprintf(lrc, lrc_sz, "%s", body);
return NCM_OK;
}
static void wait_ms(int ms)
{
for (int i = 0; i < ms; i += 20) {
usleep(20000);
if (g_app_quit) break;
}
}
static int wait_until(const volatile int *val, int target, int timeout_ms)
{
for (int i = 0; i < timeout_ms; i += 10) {
if (*val >= target) return 1;
usleep(10000);
}
return (*val >= target);
}
int main(void)
{
memset(&g_st, 0, sizeof g_st);
pthread_mutex_init(&g_play.mu, NULL);
g_play.state = NCM_PC_PLAYING;
g_play.cur_song = 0;
g_play.play_song_valid = 1;
ncm_strlcpy(g_play.play_song.id, "1000", sizeof g_play.play_song.id);
if (player_manager_load_lyric() != NCM_OK) { printf("FAIL start worker\n"); return 1; }
int ok = 1;
/* ① 第 1 次拉取失败(网络错误):须可重试(state!=2)、无词 */
if (!wait_until(&g_fetch_calls, 1, 3000)) { printf("FAIL first fetch never happened\n"); return 1; }
wait_ms(400); /* 让 worker 完成失败收尾(state=0, retry_at 已算) */
int st1 = lyric_state();
int n1 = lyric_count();
int ok1 = (g_fetch_calls == 1 && st1 != 2 && n1 == 0);
printf("step1 failure: calls=%d state=%d (expect !=2) count=%d rep_empty=%d\n",
g_fetch_calls, st1, n1, g_report_empty);
printf(" -> %s\n", ok1 ? "PASS" : "FAIL");
ok &= ok1;
/* 模拟网络重连:推进单调时钟越过失败退避点 */
g_now_ms = 10000;
/* ② 重试自动发生并成功:第 2 次调用、state=2、词已展示 */
if (!wait_until(&g_fetch_calls, 2, 4000)) {
printf("FAIL retry never fired (calls=%d) — old code keeps state=2, no retry\n", g_fetch_calls);
return 1;
}
int st2 = lyric_state();
int n2 = lyric_count();
int ok2 = (g_fetch_calls == 2 && st2 == 2 && n2 > 0 && g_report_lyric >= 1);
printf("step2 retry: calls=%d state=%d count=%d rep_lyric=%d lrc=%s\n",
g_fetch_calls, st2, n2, g_report_lyric, g_report_last_lrc);
printf(" -> %s\n", ok2 ? "PASS" : "FAIL");
ok &= ok2;
g_app_quit = 1;
printf("lyric_sim overall -> %s\n", ok ? "PASS" : "FAIL");
fflush(stdout);
return ok ? 0 : 1;
}
+277
View File
@@ -0,0 +1,277 @@
/* bench/plpage_sim.c — 歌单页「懒加载分页 + 加载后列表刷新」离线回归(0 真实 NCM 请求)。
*
* 驱动真实 app/ui/playlist.c(页面打开 / 滚动触发 / worker / tick 消费全链路)+ 真实
* appstate.c(滑动窗口)+ 真实 songmeta.c(/tmp 盘缓存)+ 宿主 SDK(meow_listview/draw 全真,
* 与 ncm_host 同一批 out/host/sdk_*.o)。网络端点(ncm_api_playlist_detail /
* ncm_api_song_detail)为本地合成 stub(计调用次数、可注入单批失败)。
*
* Bug1(大歌单懒加载分页)验证点:
* ① 打开 400 首歌单:首屏 = 1 detail + 恰 1 批 song_detail(物化 [0,100),第 100 首未物化)
* ——不再首屏连拉多批补满窗口(旧行为 ≤3 批连发,5k+ 歌单阻塞 + 风控);
* ② 顶部小幅滚动(视口底距已分页底部 >20 首):不发任何新请求;
* ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败时 g_paged_hi 不推进(占位保留),
* 再滑到底部重试同一页成功(一次一页,不连拉);
* ④ 逐页到底(400 = 4 页);全部拉完后滑到尾部:不再发请求(停止);
* ⑤ 回滚到顶部:失效槽(窗口挤出)由盘缓存补物化,song_detail 调用数 0 增长(不占网络额度);
* Bug2(加载完列表刷新)验证点:
* ⑥ 首屏/每页 worker 完成经 playlist_tick 消费后,整帧重画计数(--wrap=meow_ui_paint_full)
* 递增,且新物化行经 appstate_song_copy 取到真实歌名/歌手(= pl_src2 将绑定真实数据,
* 不再是「... 加载中」占位)。
* 风控纪律:0 次真实 NCM 请求(离线 stub + /tmp 盘缓存)。
*
* 编译/运行见 build_ncm_sims.sh「plpage_sim」段(宿主,ncm_app 目录下)。
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 0; /* 真实分页链路(不走 mock 跳网分支) */
/* ---- core 符号最小实现(同 playlist_race_sim 口径) ---- */
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) {
while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; }
dst[n] = 0;
}
while (src[n]) n++;
return n;
}
int64_t ncm_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* ---- cover / 播放 stub(本 sim 不测封面与播放;playlist.c 引用,链接需要) ---- */
void cover_request(const char *url, const char *song_id) { (void)url; (void)song_id; }
meow_img *cover_get(void) { return NULL; }
void cover_put(void) {}
int cover_changed(void) { return 0; }
void cover_ack(void) {}
void cover_cache_reset(void) {}
void player_play_song(int idx) { (void)idx; }
/* ---- core 符号(appstate.c 引用;运行时不到达,仅链接需要) ---- */
int ncm_login_status_check(const char *device_id, const char *cookie, char *uid, size_t uid_sz,
char *nick, size_t nick_sz)
{ (void)device_id;(void)cookie;(void)uid;(void)uid_sz;(void)nick;(void)nick_sz; return NCM_ERR_AUTH; }
int ncm_ctx_clear_cookie(ncm_ctx_t *ctx) { (void)ctx; return NCM_OK; }
int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid,
int limit, int offset, ncm_playlist_t *out, int max, int *out_n)
{ (void)device_id;(void)cookie;(void)uid;(void)limit;(void)offset;(void)out;(void)max;(void)out_n; return NCM_ERR_NET; }
/* ---- 网络端点 stub(离线,合成数据;计调用次数 + 可注入单批失败) ---- */
#define SIM_TOTAL 400 /* 4 页:100 × 4(> NCM_SONG_WINDOW 256,回滚必滑窗) */
static int g_detail_calls;
static int g_song_calls; /* song_detail 成功批次数 */
static int g_song_attempts; /* song_detail 发起次数(含失败批) */
static int g_song_fail_n; /* 接下来 N 批 song_detail 网络失败模拟 */
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
char *name, size_t name_sz, char *cover, size_t cover_sz,
int64_t *ids, int id_cap, int *out_id_n)
{
(void)device_id; (void)cookie; (void)playlist_id; (void)cover_sz;
g_detail_calls++;
if (name) snprintf(name, name_sz, "Sim Playlist");
if (cover) cover[0] = 0;
int n = (SIM_TOTAL < id_cap) ? SIM_TOTAL : id_cap;
for (int i = 0; i < n; i++) ids[i] = 1000000LL + i;
*out_id_n = n;
return NCM_OK;
}
int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
ncm_song_t *out, int max, int *out_n)
{
(void)device_id; (void)cookie;
g_song_attempts++;
if (g_song_fail_n > 0) { /* 单批网络失败模拟(失败不推进分页高水位 → 同页重试) */
g_song_fail_n--;
if (out_n) *out_n = 0;
return NCM_ERR_NET;
}
g_song_calls++;
int m = (n < max) ? n : max;
for (int i = 0; i < m; i++) {
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)ids[i]);
snprintf(out[i].name, sizeof out[i].name, "Song%lld", (long long)ids[i]);
snprintf(out[i].art_name, sizeof out[i].art_name, "Artist%lld", (long long)ids[i]);
snprintf(out[i].album_name, sizeof out[i].album_name, "Album%lld", (long long)ids[i]);
out[i].duration_ms = 200000;
out[i].fee = 0;
}
if (out_n) *out_n = m;
return NCM_OK;
}
/* ---- Bug2 证据:整帧重画计数(链接时 --wrap=meow_ui_paint_full 重定向到此) ---- */
extern void __real_meow_ui_paint_full(void);
static int g_paint_full_calls;
void __wrap_meow_ui_paint_full(void)
{
g_paint_full_calls++;
__real_meow_ui_paint_full();
}
/* ---- 驱动辅助:模拟 app_on_tick 周期性消费 worker 完成 ---- */
static int pump_until(int (*cond)(void), int timeout_ms)
{
for (int t = 0; t < timeout_ms; t += 5) {
playlist_tick(0);
if (cond()) {
/* 再喂若干 tick:确保完成态被消费(g_plthr_on 清零),后续滚动可再触发 */
for (int k = 0; k < 30; k++) { playlist_tick(0); usleep(2000); }
return 1;
}
usleep(5000);
}
return 0;
}
static int g_fail = 0;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
/* 条件(C 无闭包,用全局目标值) */
static int g_want_calls;
static int cond_calls(void) { return g_song_calls >= g_want_calls; }
static int g_want_attempts;
static int cond_attempts(void) { return g_song_attempts >= g_want_attempts; }
static int cond_first_screen(void)
{
return g_st.track_ids_count == SIM_TOTAL && g_song_calls >= 1;
}
/* 滚动到指定首行(行高 APP_ROW_H px;swipe 距离按当前行差换算) */
static void scroll_to_row(int row)
{
static int cur;
int d = row - cur;
cur = row;
playlist_swipe(d >= 0 ? MEOW_SWIPE_UP : MEOW_SWIPE_DOWN,
(d >= 0 ? d : -d) * APP_ROW_H);
}
int main(void)
{
printf("== plpage_sim: 懒加载分页 + 加载后列表刷新(%d 首,窗口 %d,批 %d,触底余量 20)==\n",
SIM_TOTAL, NCM_SONG_WINDOW, NCM_SONG_BATCH);
/* 宿主 SDK 离屏初始化(与 ncm_host 同口径) */
setenv("MEOW_UI_DUMP", "/tmp/plpage_sim.ppm", 0);
meow_ui_opts o = { APP_W, APP_H };
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
meow_theme_set_mode(MEOW_THEME_DARK);
ncm_on_theme(MEOW_THEME_DARK);
/* 歌单 mock:1 张 SIM_TOTAL 首歌单;songmeta 盘缓存用临时目录 */
char cdt[] = "/tmp/plpage_cache_XXXXXX";
if (!mkdtemp(cdt)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "%s", cdt);
g_st.playlist_count = 1;
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl0");
snprintf(g_st.playlists[0].name, sizeof g_st.playlists[0].name, "SimPlaylist");
g_st.playlists[0].track_count = SIM_TOTAL;
ncm_song_t sq;
int paint0;
/* ---- ① Bug1:打开歌单 → 首屏恰 1 detail + 1 批 song_detail(物化 [0,100)) ---- */
printf("== case 1: 首屏只拉一页 ==\n");
paint0 = g_paint_full_calls;
playlist_view_create();
playlist_open(0);
CHECK(pump_until(cond_first_screen, 8000), "first-screen loaded (track_ids=%d calls=%d)",
g_st.track_ids_count, g_song_calls);
CHECK(g_detail_calls == 1, "detail calls=%d (expect 1)", g_detail_calls);
CHECK(g_song_calls == 1, "song_detail calls=%d (expect 1, 不再首屏连拉多批)", g_song_calls);
CHECK(g_st.track_ids_count == SIM_TOTAL, "track_ids_count=%d (expect %d)",
g_st.track_ids_count, SIM_TOTAL);
CHECK(appstate_song_copy(99, &sq) == 0 && strcmp(sq.name, "Song1000099") == 0,
"row 99 materialized name='%s'", sq.name);
CHECK(appstate_song_copy(100, &sq) != 0, "row 100 NOT materialized yet (placeholder)");
/* Bug2:首屏完成 → 重画计数递增 + 行绑定真实歌名/歌手(非占位) */
CHECK(g_paint_full_calls > paint0, "paint_full after first-screen %d -> %d (list repainted)",
paint0, g_paint_full_calls);
CHECK(appstate_song_copy(5, &sq) == 0 && strcmp(sq.art_name, "Artist1000005") == 0,
"row 5 binds real artist='%s' (src2 填真实数据,非「... 加载中」)", sq.art_name);
/* ---- ② 顶部小幅滚动:不触发任何新请求(距已分页底部 >20 首) ---- */
printf("== case 2: 顶部滚动不拉页 ==\n");
scroll_to_row(10);
int a0 = g_song_attempts;
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
CHECK(g_song_attempts == a0, "song_detail attempts=%d (unchanged, 顶部滚动不拉页)", g_song_attempts);
/* ---- ③ 滑到已分页底部(余量 ≤20 首):恰拉 1 页;单批失败不推进,再滑重试同页成功 ---- */
printf("== case 3: 触底拉下一页(一次一页;失败重试同页) ==\n");
g_song_fail_n = 1; /* 注入:下一次 song_detail 网络失败 */
int att_before = g_song_attempts;
scroll_to_row(90); /* 视口底 ≈97 行,余量 ≤20 → 触发第 2 页(失败) */
g_want_attempts = att_before + 1;
CHECK(pump_until(cond_attempts, 8000), "page-2 first attempt fired (attempts=%d)", g_song_attempts);
CHECK(g_song_calls == 1, "page-2 failed -> calls=%d (g_paged_hi 不推进,占位保留)", g_song_calls);
CHECK(appstate_song_copy(199, &sq) != 0, "row 199 still placeholder after failed page");
scroll_to_row(91); /* 再滑(滚动事件)→ 重试同一页 */
g_want_calls = 2;
CHECK(pump_until(cond_calls, 8000), "page 2 retried & loaded (calls=%d attempts=%d)",
g_song_calls, g_song_attempts);
CHECK(g_song_attempts == att_before + 2,
"attempts +%d (1 失败 + 1 重试同页 = 一次一页,未连拉)", g_song_attempts - att_before);
CHECK(appstate_song_copy(199, &sq) == 0 && strcmp(sq.name, "Song1000199") == 0,
"row 199 materialized name='%s'", sq.name);
CHECK(g_paint_full_calls > paint0, "paint_full after page-2 consumed (=%d)", g_paint_full_calls);
/* ---- ④ 逐页到底;全部拉完后滑到尾部:不再发请求 ---- */
printf("== case 4: 逐页到底 + 拉完即止 ==\n");
att_before = g_song_attempts;
scroll_to_row(190); /* 余量 ≤20 → 触发第 3 页 */
g_want_calls = 3;
CHECK(pump_until(cond_calls, 8000), "page 3 loaded (calls=%d)", g_song_calls);
scroll_to_row(290); /* 余量 ≤20 → 触发第 4 页(末页) */
g_want_calls = 4;
CHECK(pump_until(cond_calls, 8000), "page 4 (last) loaded (calls=%d)", g_song_calls);
CHECK(g_song_attempts == att_before + 2, "attempts +%d (第 3、4 页恰各 1 批)",
g_song_attempts - att_before);
CHECK(appstate_song_copy(399, &sq) == 0 && strcmp(sq.name, "Song1000399") == 0,
"tail row 399 materialized name='%s'", sq.name);
att_before = g_song_attempts;
scroll_to_row(390); /* 已在尾部:全部拉完 → 不再发请求 */
for (int k = 0; k < 40; k++) { playlist_tick(0); usleep(3000); }
CHECK(g_song_attempts == att_before, "all loaded -> scroll at tail: attempts=%d (unchanged)",
g_song_attempts);
/* ---- ⑤ 回滚到顶部:失效槽(窗口挤出)盘缓存补物化,song_detail 0 增长 ---- */
printf("== case 5: 回滚补物化走盘缓存(0 网络) ==\n");
att_before = g_song_attempts;
scroll_to_row(0); /* 回顶部:窗口已滑走,行 0-35 失效 → 盘缓存补 */
for (int k = 0; k < 400 && !appstate_range_loaded(0, 30); k++) { playlist_tick(0); usleep(5000); }
CHECK(appstate_range_loaded(0, 30), "rows 0-29 re-materialized after scroll-back");
CHECK(g_song_attempts == att_before, "scroll-back refill: song_detail attempts=%d "
"(unchanged, 盘缓存命中不占网络)", g_song_attempts);
CHECK(appstate_song_copy(0, &sq) == 0 && strcmp(sq.name, "Song1000000") == 0,
"row 0 re-materialized name='%s'", sq.name);
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
/* 清理临时盘缓存(≤400 个小文件) */
{
char cmd[600];
snprintf(cmd, sizeof cmd, "rm -rf %s", cdt);
if (system(cmd) != 0) fprintf(stderr, "cleanup failed: %s\n", cdt);
}
meow_ui_shutdown();
return g_fail ? 1 : 0;
}
+45 -2
View File
@@ -40,8 +40,11 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
} }
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; } void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
/* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接 */ /* audio_out.c 引用但本 sim 不经 SDK 调用的符号:只链不跑就满足链接。
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return -1; } * 会话建立→音量同步用例(§切歌音量骤增回归)需要 audio_out_open 真正「开新流/分配新 sid」,
* 故 meow_audio_open 桩改为返回递增 sid(模拟 audiod 单调分配),供 §切歌 会话切换复现。 */
static int g_sim_sid = 0;
int meow_audio_open(int rate, int ch, int fmt) { (void)rate;(void)ch;(void)fmt; return ++g_sim_sid; }
int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; } int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; }
int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; } int meow_audio_pause(int sid, int paused) { (void)sid;(void)paused; return 0; }
int meow_audio_close(int sid) { (void)sid; return 0; } int meow_audio_close(int sid) { (void)sid; return 0; }
@@ -161,6 +164,46 @@ int main(void)
printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n"); printf("PASS changed-volume re-sync: settings=50 -> 2nd connection pct=50\n");
} }
/* §切歌音量骤增回归:会话建立→音量同步。切歌 = 新 audio_out_open = 新 audiod 流(新 sid),
* 必须**每会话重发一次**当前音量——此前 g_vol_synced 进程级「首同步一次」,新会话被跳过,
* DAC 停在会话建立后的默认响度(切歌骤增,按音量键才回落)。用真实 audio_out_open/close
* (meow_audio_open 桩返回递增 sid)断言:新会话必 +1 连接(重发音量),同会话内重复 sync 不新增。 */
{
int base = g_nconn; /* 上面值变重发测试后基线 = 2 条连接 */
/* 会话 1(首开):应重发 1 条 VOLUME,pct==当前 settings(50) */
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session1 audio_out_open\n"); fail = 1; }
usleep(50000);
if (g_nconn != base + 1) {
printf("FAIL session1 establish: conns=%d (expect %d) — 新会话未重发音量\n", (int)g_nconn, base + 1);
fail = 1;
} else if (g_got_pct != 50) {
printf("FAIL session1 establish: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
} else {
printf("PASS session1 establish -> 1 volume sync (pct=50)\n");
}
/* 同会话内反复 sync(同值):不得新建连接(防 fd 风暴) */
for (int i = 0; i < 20; i++) audio_out_sync_volume();
usleep(50000);
if (g_nconn != base + 1) {
printf("FAIL same-session repeat sync: conns=%d (expect %d)\n", (int)g_nconn, base + 1);
fail = 1;
} else {
printf("PASS same-session repeat sync -> no new connection (storm-free)\n");
}
audio_out_close();
/* 会话 2(切歌):再开 → 必须再重发 1 条(新 sid),pct==50 */
if (audio_out_open(44100, 2, 16) != 0) { printf("FAIL session2 audio_out_open\n"); fail = 1; }
usleep(50000);
if (g_nconn != base + 2) {
printf("FAIL session2 (song switch): conns=%d (expect %d) — 切歌未重发音量\n", (int)g_nconn, base + 2);
fail = 1;
} else if (g_got_pct != 50) {
printf("FAIL session2 switch: pct=%d (expect 50)\n", (int)g_got_pct); fail = 1;
} else {
printf("PASS session2 (song switch) -> new volume sync (pct=50)\n");
}
}
g_stop = 1; g_stop = 1;
pthread_join(th, NULL); pthread_join(th, NULL);
unlink(g_sock); unlink(g_sock);
+12
View File
@@ -75,6 +75,18 @@ gcc -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o \
out/core-host/ncm_apicache.o -lpthread # appstate.c 调 ncm_apicache_init/clear(自包含模块) out/core-host/ncm_apicache.o -lpthread # appstate.c 调 ncm_apicache_init/clear(自包含模块)
run_sim window_sim run_sim window_sim
echo "==> build plpage_sim(歌单页懒加载分页 + 加载后列表刷新;真实 playlist.c 全链路 + 宿主 SDK,0 网络。"
echo " 依赖 out/host/sdk_*.o:若缺请先跑 build_ncm_host.sh)"
PPINC="-DMEOW_UI_HOST_TEST -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn -Ivendor/mbedtls/include -Ivendor/minimp3 -Ivendor/dr_flac"
for f in bench/plpage_sim.c app/ui/playlist.c app/logic/appstate.c app/logic/songmeta.c app/ui/chrome.c app/ui/ncm_colors.c; do
b=$(basename "$f" .c)
gcc -O2 -Wall -Wextra -ffunction-sections -fdata-sections $PPINC -c "$f" -o /tmp/pp_$b.o
done
gcc -O2 -Wl,--gc-sections -Wl,--wrap=meow_ui_paint_full -o plpage_sim \
/tmp/pp_plpage_sim.o /tmp/pp_playlist.o /tmp/pp_appstate.o /tmp/pp_songmeta.o /tmp/pp_chrome.o /tmp/pp_ncm_colors.o \
out/host/sdk_*.o -lz -lpthread -lm
run_sim plpage_sim
echo "==> build search_sim" echo "==> build search_sim"
gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \ gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd -Icore -Ivendor/jsmn \
-c bench/search_sim.c -o /tmp/sh.o -c bench/search_sim.c -o /tmp/sh.o
+7 -3
View File
@@ -12,9 +12,10 @@
/* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用 /* 响应缓冲上界(内存瘦身,OOM 修复):多数接口响应远小于 1MB,按需给尺寸避免每次调用
* 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。 * 都 malloc 1MB 堆峰值(播放中 song_url/lyric/song_detail 并发,几 × 1MB 是可砍的瞬态)。
* 仅 playlist detail(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留传统 2MB。 */ * playlist detail(全量 trackIds,最多 NCM_TRACKID_MAX 条)保留 2MB;
* user_playlist(120 首歌单列表)用 NCM_RESP_MAX(1MB),见 ncm_api_user_playlist。 */
#define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */ #define RESP_SMALL (64 * 1024) /* song_url:url+字段,实际 ~1KB */
#define RESP_MED (256 * 1024) /* lyric/song_detail/playlist/cloudsearch:实际 ≤100KB */ #define RESP_MED (256 * 1024) /* lyric/song_detail/cloudsearch:实际 ≤100KB */
const char *ncm_api_level_str(int level) { const char *ncm_api_level_str(int level) {
switch (level) { switch (level) {
@@ -279,8 +280,11 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char
uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0); uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0);
api_json_t c; memset(&c, 0, sizeof c); api_json_t c; memset(&c, 0, sizeof c);
/* 我的歌单列表:真机 120 首歌单响应可超 RESP_MED(256KB) → ncm_http 回 NCM_ERR_FULL(-11)
* (日志 home: playlists rc=-11)。该响应不含 tracks(每首 tracks:null),1MB 足够,
* 用 NCM_RESP_MAX(与 vip/info 同口径的堆瞬态,解析后即释,非 bss)。 */
int rc = api_post_cached(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI, int rc = api_post_cached(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI,
RESP_MED, &c); NCM_RESP_MAX, &c);
if (rc != NCM_OK) return rc; if (rc != NCM_OK) return rc;
int arr = ncm_json_path(&c.j, "playlist"); int arr = ncm_json_path(&c.j, "playlist");
+6 -4
View File
@@ -302,10 +302,12 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
} }
} }
/* params 是 full2 的 AES-ECB 大写 hex(≈ plaintext×2)。 /* params 是 full2 的 AES-ECB 大写 hex(≈ plaintext×2 + 1)。
* 批量 song_detail 的 c 数组转义后 full2 ~1.6KB → hex ~3.2KB,旧 2048 溢出 * NCM_SONG_BATCH=100 时:c 数组转义后 full2 ~2.5KB(10 位 id) / ~3.4KB(19 位 id)
* → ncm_crypto_eapi 返回 NCM_ERR_CRYPTO(实测 batch=64 时 rc=-8)。 */ * → hex ~5.1KB / ~6.9KB。旧 params[4096] 对 100 首批量恒溢出 → aes_ecb_hex
char params[4096]; * 返回 -1 → ncm_crypto_eapi 返回 NCM_ERR_CRYPTO(-8)(真机 first-screen rc=-8)。
* 用 NCM_REQ_BODY_MAX(8192) 兜住 100 首最坏情况(body="params="+hex ≤ 8192)。 */
char params[NCM_REQ_BODY_MAX];
if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO; if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO;
char cookie_hdr[3072]; char cookie_hdr[3072];
+30
View File
@@ -234,6 +234,35 @@ static void test_eapi_musicu_hdr(void) {
CHECK(strstr(h2, "MUSIC_U=") == NULL, "guest eapi has no MUSIC_U"); CHECK(strstr(h2, "MUSIC_U=") == NULL, "guest eapi has no MUSIC_U");
} }
static void test_eapi_song_detail_batch(void) {
/* §NEW:NCM_SONG_BATCH=100 的 song_detail eapi 请求构建(不打网络)。
* 旧 params[4096] 对 100 首的 AES-ECB hex(~5.1KB) 溢出 → NCM_ERR_CRYPTO(-8),
* 真机 first-screen 因此 rc=-8。这里只构建请求验证 params/body 缓冲够。 */
printf("== eapi 100-song detail batch ==\n");
const char *device = "AA11BB22CC33DD44EE55FF6600112233445566778899AABB00";
char clist[4096] = "[", tmp[64];
for (int i = 0; i < 100 && strlen(clist) < sizeof clist - 80; i++) {
snprintf(tmp, sizeof tmp, "%s{\\\"id\\\":%lld}", i ? "," : "",
(long long)(2048962657LL + (long long)i));
ncm_strlcat(clist, tmp, sizeof clist);
}
ncm_strlcat(clist, "]", sizeof clist);
char json[4500];
snprintf(json, sizeof json, "{\"c\":\"%s\"}", clist);
char params[8192];
CHECKRC(ncm_crypto_eapi("/api/v3/song/detail", json, params, sizeof params),
"eapi crypto 100-song");
CHECK(strlen(params) > 4096, "params exceeds old 4096 cap (got %zu)", strlen(params));
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
CHECKRC(ncm_req_build(device, "", "/api/v3/song/detail", json, NCM_CRYPT_EAPI,
body, sizeof body, hdrs, sizeof hdrs, url, sizeof url),
"eapi req_build 100-song");
CHECK(strstr(body, "params=") == body, "body starts with params=");
CHECK(strlen(body) < NCM_REQ_BODY_MAX, "body fits NCM_REQ_BODY_MAX (got %zu)", strlen(body));
}
static void test_network(void) { static void test_network(void) {
printf("== network (live) ==\n"); printf("== network (live) ==\n");
int net_ok = 0; int net_ok = 0;
@@ -515,6 +544,7 @@ int main(int argc, char **argv) {
test_qr(); test_qr();
test_cookie_merge(); test_cookie_merge();
test_eapi_musicu_hdr(); test_eapi_musicu_hdr();
test_eapi_song_detail_batch();
test_network(); test_network();
test_track_all_parse(); test_track_all_parse();
test_vip_parse(); test_vip_parse();
+606 -129
View File
@@ -9,6 +9,7 @@
#include "rdsrc.h" #include "rdsrc.h"
#include "rdtxt.h" #include "rdtxt.h"
#include "seg.h" #include "seg.h"
#include "rdchapter.h"
#include "rdlayout.h" #include "rdlayout.h"
#include "rdimg.h" #include "rdimg.h"
#include "../engine/rdft.h" #include "../engine/rdft.h"
@@ -22,6 +23,17 @@ typedef struct seg {
int page_count; /* 本段页数 */ int page_count; /* 本段页数 */
} seg_t; } seg_t;
/* Phase 2C:双段 cps 窗口槽——只保留段解码码点(utf8 转完即 free,不再驻留整段 lines/pages)。 */
typedef struct cps_slot {
int seg; /* 段下标;-1 = 空槽 */
uint32_t *cps;
size_t n;
} cps_slot;
/* 滞回释放/预取阈值(按全书页数百分比;真机可调)。 */
#define RD_WIN_NEAR_PCT 2 /* 距下一段界 ≤2%:预取下一段 */
#define RD_WIN_FAR_PCT 4 /* >4% 滑出:远槽可被下一段预取替换(由距离最近者策略体现) */
struct rd_book { struct rd_book {
char path[400]; char path[400];
char font[400]; char font[400];
@@ -40,17 +52,28 @@ struct rd_book {
rd_book_page *pages; int npages; /* flat 页表(文本) */ rd_book_page *pages; int npages; /* flat 页表(文本) */
int cur; int cur;
/* 当前渲染段复用缓冲 */ /* Phase 2C:双段 cps 窗口(渲染微排版取码点用;替换旧单段 seg_lines/seg_pages 缓存) */
uint8_t *seg_utf8; size_t seg_utf8_cap; cps_slot win[2];
uint32_t *seg_cps; size_t seg_cps_n, seg_cps_cap;
rd_line *seg_lines; rd_page *seg_pages; int seg_nlines, seg_npages; /* Phase 2A:拼接页表滚动状态(上一段的「末页页首→段尾」码点 tail;建下一段时拼接) */
int seg_cache_idx; uint32_t *tail_cps; size_t tail_n;
int tail_seg; /* tail 所属段;-1 = 无 */
int32_t tail_seg_char; /* tail 在所属段的起始字符偏移 */
/* Phase 2C:渲染微排版复用缓冲(拼接页临时码点 + 一页行缓冲,避免每帧 malloc) */
uint32_t *stitch_cps; size_t stitch_cap;
rd_line *render_lines; int render_lines_cap;
/* R3.2 分段流式(TXT):段目录 + 全局段数 + 已建段数 + 段/页表容量(增量补齐用) */ /* R3.2 分段流式(TXT):段目录 + 全局段数 + 已建段数 + 段/页表容量(增量补齐用) */
char seg_dir[400]; char seg_dir[400];
int seg_n; /* 段文件总数(manifest);fallback = nsegs(全建) */ int seg_n; /* 段文件总数(manifest);fallback = nsegs(全建) */
int seg_built; /* 已布局段数;fallback = nsegs */ int seg_built; /* 已布局段数;fallback = nsegs */
int segs_cap, pages_cap; /* segs[]/pages[] 当前容量(只在 seg 路径增量增长) */ int segs_cap, pages_cap; /* segs[]/pages[] 当前容量(只在 seg 路径增量增长) */
/* R3.3 章节(TXT):解析结果 + 就绪/无章节标志(延迟解析,不拖慢首屏) */
rdchapter chaps;
int chaps_ready; /* 1=chaps 已填充(含确无章节) */
int chaps_none; /* 1=确无章节(扫描过 n==0)或该书不可用 */
}; };
/* ---- 页索引持久化(MeowData;见 rd_book_set_index_dir) ---- /* ---- 页索引持久化(MeowData;见 rd_book_set_index_dir) ----
@@ -61,6 +84,7 @@ struct rd_book {
* 纯 libc(无 SDK):reader.c 用 meow_config_dir 推导 MeowData dir 注入;冒烟/无卡时 dir 为空 → 直排不持久。 */ * 纯 libc(无 SDK):reader.c 用 meow_config_dir 推导 MeowData dir 注入;冒烟/无卡时 dir 为空 → 直排不持久。 */
static char g_index_dir[400]; /* 索引缓存目录; 空 = 禁用 */ static char g_index_dir[400]; /* 索引缓存目录; 空 = 禁用 */
static int seg_ensure_built(rd_book *b, int upto_seg); /* 段路径补齐(下文定义) */
void rd_book_set_index_dir(const char *dir) void rd_book_set_index_dir(const char *dir)
{ {
if (dir && dir[0]) { strncpy(g_index_dir, dir, sizeof g_index_dir - 1); g_index_dir[sizeof g_index_dir - 1] = 0; } if (dir && dir[0]) { strncpy(g_index_dir, dir, sizeof g_index_dir - 1); g_index_dir[sizeof g_index_dir - 1] = 0; }
@@ -80,7 +104,7 @@ void rd_book_set_seg_base(const char *base)
#define IDX_MAGIC1 'I' #define IDX_MAGIC1 'I'
#define IDX_MAGIC2 'D' #define IDX_MAGIC2 'D'
#define IDX_MAGIC3 'X' #define IDX_MAGIC3 'X'
#define IDX_VER 1u #define IDX_VER 2u
static int fio_rd(FILE *f, void *p, size_t n) { return fread(p, 1, n, f) == n; } static int fio_rd(FILE *f, void *p, size_t n) { return fread(p, 1, n, f) == n; }
static int fio_wr(FILE *f, const void *p, size_t n) { return fwrite(p, 1, n, f) == n; } static int fio_wr(FILE *f, const void *p, size_t n) { return fwrite(p, 1, n, f) == n; }
@@ -253,6 +277,267 @@ static void index_cache_save(rd_book *b)
if (ok) { rename(tmp, path); } else { remove(tmp); } if (ok) { rename(tmp, path); } else { remove(tmp); }
} }
/* ---- R3.3 章节缓存(MeowData/os.meow.reader/index/<hash>.chap,键=书字节+编码) ----
* 章节 char_off/seg_idx/seg_char 仅依赖书字节与编码(不含字号/行距/字体/版面),故失效键
* (file_size, file_mtime, enc) 即够,与页索引(含 font/fs/ls/cw/ch)分开、独立秒开。
* 目录为 index 同父;g_index_dir 为空(宿主/无卡)→ 不持久,仅内存。 */
#define CHAP_MAGIC0 'R'
#define CHAP_MAGIC1 'C'
#define CHAP_MAGIC2 'H'
#define CHAP_MAGIC3 'P'
#define CHAP_VER 1u
static int chap_cache_path(const rd_book *b, char *out, int outsz)
{
if (!g_index_dir[0]) return -1;
if (snprintf(out, (size_t)outsz, "%s/%08x.chap", g_index_dir, idx_fnv1a(b->path)) >= outsz) return -1;
return 0;
}
/* 章节缓存键:书字节 + 编码(含 BOM/编码覆盖),跟页索引键不同(无字体/字号/版面)。 */
static int chap_cache_load(rd_book *b)
{
char path[440];
if (chap_cache_path(b, path, sizeof path) != 0) return 0;
FILE *f = fopen(path, "rb");
if (!f) return 0;
uint8_t magic[4];
uint16_t ver = 0;
uint64_t fsize = 0; int64_t fmtime = 0; int32_t fenc = 0;
int32_t nsegs = 0, nch = 0; int64_t total = 0;
int ok = 0;
if (fio_rd(f, magic, sizeof magic) && magic[0] == CHAP_MAGIC0 && magic[1] == CHAP_MAGIC1 &&
magic[2] == CHAP_MAGIC2 && magic[3] == CHAP_MAGIC3 &&
fio_rd(f, &ver, sizeof ver) && ver == CHAP_VER &&
fio_rd(f, &fsize, sizeof fsize) && fio_rd(f, &fmtime, sizeof fmtime) &&
fio_rd(f, &fenc, sizeof fenc) &&
fio_rd(f, &nsegs, sizeof nsegs) && fio_rd(f, &total, sizeof total) &&
fio_rd(f, &nch, sizeof nch) &&
nsegs >= 0 && nch >= 0 && nsegs <= 1000000 && nch <= RDCHAP_MAX_ENTRIES) {
struct stat st;
if (stat(b->path, &st) == 0 && (uint64_t)st.st_size == fsize &&
(int64_t)st.st_mtime == fmtime && (int32_t)b->enc == fenc) {
rdchapter c; rdchapter_init(&c);
c.seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
c.entries = (rdchapter_entry *)malloc((size_t)(nch ? nch : 1) * sizeof(rdchapter_entry));
if (c.seg_char_start && c.entries &&
fio_rd(f, c.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t))) {
int eok = 1;
for (int i = 0; i < nch && eok; i++) {
rdchapter_entry *e = &c.entries[i];
if (!fio_rd(f, &e->char_off, sizeof e->char_off) ||
!fio_rd(f, &e->seg_idx, sizeof e->seg_idx) ||
!fio_rd(f, &e->seg_char, sizeof e->seg_char) ||
!fio_rd(f, &e->number, sizeof e->number) ||
!fio_rd(f, e->title, sizeof e->title)) eok = 0;
}
if (eok) {
c.n = nch; c.nsegs = nsegs;
c.has_pre = (nch > 0 && c.entries[0].char_off == 0 && c.entries[0].number == -1) ? 1 : 0;
snprintf(c.pre_title, sizeof c.pre_title, "%s", (nch > 0) ? c.entries[0].title : "卷首");
b->chaps = c;
b->chaps_ready = 1;
b->chaps_none = (nch == 0);
ok = 1;
} else { rdchapter_free(&c); }
} else { rdchapter_free(&c); }
}
}
fclose(f);
return ok;
}
static void chap_cache_save(rd_book *b)
{
if (!b->chaps_ready || b->chaps.nsegs <= 0) return;
char path[440];
if (chap_cache_path(b, path, sizeof path) != 0) return;
char tmp[480];
if (snprintf(tmp, sizeof tmp, "%s.tmp", path) >= (int)sizeof tmp) return;
FILE *f = fopen(tmp, "wb");
if (!f) return;
struct stat st;
uint64_t fsize = 0; int64_t fmtime = 0;
if (stat(b->path, &st) == 0) { fsize = (uint64_t)st.st_size; fmtime = (int64_t)st.st_mtime; }
const uint8_t magic[4] = { CHAP_MAGIC0, CHAP_MAGIC1, CHAP_MAGIC2, CHAP_MAGIC3 };
uint16_t ver = CHAP_VER;
int32_t nsegs = b->chaps.nsegs, fenc = (int32_t)b->enc, nch = b->chaps.n;
int64_t total = b->chaps.seg_char_start ? b->chaps.seg_char_start[nsegs] : 0;
int ok = 1;
ok = ok && fio_wr(f, magic, sizeof magic) && fio_wr(f, &ver, sizeof ver) &&
fio_wr(f, &fsize, sizeof fsize) && fio_wr(f, &fmtime, sizeof fmtime) &&
fio_wr(f, &fenc, sizeof fenc) &&
fio_wr(f, &nsegs, sizeof nsegs) && fio_wr(f, &total, sizeof total) &&
fio_wr(f, &nch, sizeof nch);
if (ok && nsegs > 0) ok = fio_wr(f, b->chaps.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t));
if (ok && nch > 0) {
for (int i = 0; i < nch; i++) {
const rdchapter_entry *e = &b->chaps.entries[i];
ok = ok && fio_wr(f, &e->char_off, sizeof e->char_off) &&
fio_wr(f, &e->seg_idx, sizeof e->seg_idx) &&
fio_wr(f, &e->seg_char, sizeof e->seg_char) &&
fio_wr(f, &e->number, sizeof e->number) &&
fio_wr(f, e->title, sizeof e->title);
}
}
fclose(f);
if (ok) { rename(tmp, path); } else { remove(tmp); }
}
/* 确保章节解析(幂等):命中缓存 → 就绪;未命中 → 扫描(读全部段文件)。
* 可靠性(R3.4):仅「扫描全程成功」才落盘(含无章节 n==0);扫描中途出错(段读/解码/内存)
* 不落盘、本次降级无章节,下次重试。返回:0=成功(就绪);-1=不可用(非 TXT/无段目录);
* -2=扫描出错(不落盘)。 */
static int chapter_ensure(rd_book *b)
{
if (!b) return -1;
if (b->chaps_ready) return 0;
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) {
b->chaps_none = 1; b->chaps_ready = 1;
return -1;
}
if (chap_cache_load(b)) return 0;
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &b->chaps);
if (rc != 0) { b->chaps_none = 1; b->chaps_ready = 1; return -2; } /* 扫描失败:不落盘,降级无章节 */
b->chaps_none = (b->chaps.n == 0);
b->chaps_ready = 1;
chap_cache_save(b); /* 可靠(成功扫描,无论有无章节)→ 落盘 */
return 0;
}
int rd_book_chapter_ready(rd_book *b) { return (b && b->chaps_ready) ? 1 : 0; }
/* R3.4 手动重解析本书章节:先扫描到临时结果,成功才「清旧缓存/旧内存 + 换入新结果 + 落盘」;
* 失败则保留旧缓存/旧内存(绝不产生错位章节),不落盘、返回 -2。返回:n=章节数 / 0=无章节(成功)/
* -1=不可用(非 TXT+seg)/ -2=扫描失败。幂等。 */
int rd_book_reparse_chapters(rd_book *b)
{
if (!b) return -1;
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
rdchapter tmp; rdchapter_init(&tmp);
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &tmp);
if (rc != 0) { rdchapter_free(&tmp); return -2; } /* 失败:保留旧缓存/旧内存,未落盘 */
rdchapter_free(&b->chaps); /* 清除内存旧解析结果 */
b->chaps = tmp;
b->chaps_none = (b->chaps.n == 0);
b->chaps_ready = 1;
char path[440];
if (chap_cache_path(b, path, sizeof path) == 0) remove(path); /* 清缓存文件 */
chap_cache_save(b); /* 可靠结果 → 落盘 */
return b->chaps.n; /* >=0 */
}
int rd_book_chapter_n(rd_book *b)
{
if (!b) return -1;
if (b->is_img || b->fmt != RD_FMT_TXT) return -1;
if (chapter_ensure(b) != 0) return -1;
return b->chaps.n;
}
int rd_book_chapter(rd_book *b, int i, char *title, int tsz,
int64_t *char_off, int *seg, int32_t *seg_char, int *number)
{
if (!b || !title || i < 0) return -1;
if (chapter_ensure(b) != 0) return -1;
if (i >= b->chaps.n || !b->chaps.entries) return -1;
const rdchapter_entry *e = &b->chaps.entries[i];
if (tsz > 0) { snprintf(title, (size_t)tsz, "%s", e->title); }
if (char_off) *char_off = e->char_off;
if (seg) *seg = e->seg_idx;
if (seg_char) *seg_char = e->seg_char;
if (number) *number = e->number;
return 0;
}
int64_t rd_book_page_char_off(rd_book *b, int page)
{
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
if (chapter_ensure(b) != 0) return -1;
if (!b->chaps.seg_char_start || b->chaps.nsegs <= 0) return -1;
int seg = b->pages[page].seg;
if (seg < 0 || seg >= b->chaps.nsegs) return -1;
return b->chaps.seg_char_start[seg] + b->pages[page].seg_char;
}
int rd_book_page_for_char(rd_book *b, int64_t char_off)
{
if (!b || b->npages <= 0) return -1;
if (chapter_ensure(b) != 0) return -1;
int nseg = b->chaps.nsegs;
if (!b->chaps.seg_char_start || nseg <= 0) return -1;
int64_t total = b->chaps.seg_char_start[nseg];
if (total <= 0) return 0;
if (char_off < 0) char_off = 0;
if (char_off >= total) char_off = total - 1;
/* 二分所在段:最大的 i 使 seg_char_start[i] <= char_off */
int lo = 0, hi = nseg - 1, s = 0;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (b->chaps.seg_char_start[mid] <= char_off) { s = mid; lo = mid + 1; }
else hi = mid - 1;
}
if (seg_ensure_built(b, s) != 0) return -1;
if (s >= b->nsegs) s = b->nsegs - 1;
int local = (int)(char_off - b->chaps.seg_char_start[s]);
int p0 = b->segs[s].page_start, pc = b->segs[s].page_count;
if (pc <= 0) return (p0 < b->npages) ? p0 : (b->npages - 1);
/* 段内二分:最后一个 seg_char <= local 的页 = 含该字符的页 */
int lo2 = p0, hi2 = p0 + pc - 1, best = p0;
while (lo2 <= hi2) {
int mid = (lo2 + hi2) / 2;
if (b->pages[mid].seg_char <= local) { best = mid; lo2 = mid + 1; }
else hi2 = mid - 1;
}
return (best < b->npages) ? best : (b->npages - 1);
}
int rd_book_chapter_for_page(rd_book *b, int page, int *idx)
{
if (!b || !idx) return -1;
if (chapter_ensure(b) != 0) return -1;
if (b->chaps.n <= 0) { *idx = -1; return 0; }
if (page < 0 || page >= b->npages) return -1;
int64_t po = rd_book_page_char_off(b, page);
if (po < 0) { *idx = -1; return 0; }
int lo = 0, hi = b->chaps.n - 1, best = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (b->chaps.entries[mid].char_off <= po) { best = mid; lo = mid + 1; }
else hi = mid - 1;
}
*idx = best;
return 0;
}
/* ---- 段访问(文件/段模式跳转) ---- */
int rd_book_seg_n(rd_book *b)
{
if (!b || b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
return (b->seg_n > 0) ? b->seg_n : b->nsegs;
}
int rd_book_seg_page(rd_book *b, int seg)
{
if (!b || seg < 0) return -1;
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
if (seg >= b->seg_n) return -1;
if (seg_ensure_built(b, seg) != 0) return -1;
if (seg >= b->nsegs) return -1;
return b->segs[seg].page_start;
}
return (seg < b->nsegs) ? b->segs[seg].page_start : -1;
}
int rd_book_seg_page_count(rd_book *b, int seg)
{
if (!b || seg < 0 || seg >= b->nsegs) return -1;
return b->segs[seg].page_count;
}
int rd_book_seg_of_page(rd_book *b, int page)
{
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
return b->pages[page].seg;
}
/* ---- 文本:建索引(逐段 layout) ---- */ /* ---- 文本:建索引(逐段 layout) ---- */
static int build_index(rd_book *b) static int build_index(rd_book *b)
{ {
@@ -288,8 +573,10 @@ static int build_index(rd_book *b)
b->segs[seg_idx].page_count = out.npages; b->segs[seg_idx].page_count = out.npages;
for (int p = 0; p < out.npages; p++) { for (int p = 0; p < out.npages; p++) {
if (b->npages >= (int)cap_pages) { cap_pages *= 2; b->pages = (rd_book_page *)realloc(b->pages, cap_pages * sizeof(rd_book_page)); } if (b->npages >= (int)cap_pages) { cap_pages *= 2; b->pages = (rd_book_page *)realloc(b->pages, cap_pages * sizeof(rd_book_page)); }
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
b->pages[b->npages].seg = seg_idx; b->pages[b->npages].seg = seg_idx;
b->pages[b->npages].seg_char = out.pages[p].first_char; b->pages[b->npages].seg_char = out.pages[p].first_char;
b->pages[b->npages].flags = (uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u);
b->npages++; b->npages++;
} }
free(lines); free(pages); free(lines); free(pages);
@@ -300,41 +587,169 @@ static int build_index(rd_book *b)
return 0; return 0;
} }
/* ---- R3.2 分段流式(TXT):把一段 UTF-8 布局并追加到 flat 页表 ---- */ /* ---- 段解码:把段 idx 的原始字节 → utf8 → 码点(malloc 返回,调用方 free)。
static int append_seg_pages(rd_book *b, int seg_idx, const uint8_t *utf8, size_t ulen) * seg 路径读 512KB 段文件;非 seg 路径按 rd_src_seg 重显。窗口/建表/tail 重建共用。 ---- */
static int seg_load_cps(rd_book *b, int seg_idx, uint32_t **cps_out, size_t *n_out)
{ {
uint32_t *cps = NULL; size_t n = 0; if (!cps_out || !n_out) return -1;
if (rd_utf8_to_cp(utf8, ulen, &cps, &n) != RD_ENC_OK) return -5; *cps_out = NULL; *n_out = 0;
rd_line *lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line)); uint8_t *utf8 = NULL; size_t cap = 0, ulen = 0;
rd_page *pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page)); if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
if (!lines || !pages) { free(lines); free(pages); free(cps); return -6; } size_t rawlen = 0;
rd_layout_in in; if (rdseg_rawlen(b->seg_dir, seg_idx, &rawlen) != 0) return -4;
rd_layout_out out; cap = rawlen * 3 / 2 + 16;
in.chars = cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls; if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs; utf8 = (uint8_t *)malloc(cap);
in.meas = book_meas; in.mctx = b; if (!utf8) return -6;
out.lines = lines; out.pages = pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1); uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
int lrc = rd_layout(&in, &out); if (!raw) { free(utf8); return -6; }
free(cps); size_t rn = 0;
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); return -4; } int rc = rdseg_load(b->seg_dir, seg_idx, raw, rawlen, &rn);
b->segs[seg_idx].page_start = b->npages; if (rc != 0) { free(raw); free(utf8); return -4; }
b->segs[seg_idx].page_count = out.npages; int r2 = rd_to_utf8(b->enc, raw, rn, utf8, cap, &ulen);
for (int p = 0; p < out.npages; p++) { free(raw);
if (b->npages >= b->pages_cap) { if (r2 == RD_ENC_ERR_BUFSZ) { free(utf8); return -2; }
int nc = b->pages_cap ? b->pages_cap * 2 : 1024; if (r2 != RD_ENC_OK) { free(utf8); return -3; }
rd_book_page *np = (rd_book_page *)realloc(b->pages, (size_t)nc * sizeof(rd_book_page)); } else {
if (!np) { free(lines); free(pages); return -6; } cap = RDTXT_SEG_UTF8_MAX + 8;
b->pages = np; b->pages_cap = nc; utf8 = (uint8_t *)malloc(cap);
} if (!utf8) return -6;
b->pages[b->npages].seg = seg_idx; if (rd_src_seg(b->src, seg_idx, utf8, cap, &ulen) != 0) { free(utf8); return -4; }
b->pages[b->npages].seg_char = out.pages[p].first_char;
b->npages++;
} }
free(lines); free(pages); uint32_t *cps = NULL; size_t n = 0;
int rc = rd_utf8_to_cp(utf8, ulen, &cps, &n);
free(utf8);
if (rc != RD_ENC_OK) { free(cps); return -5; }
*cps_out = cps; *n_out = n;
return 0; return 0;
} }
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → utf8 → 布局)补齐进 flat 页表。 /* ---- Phase 2A:拼接页表滚动状态(tail) ---- */
static void tail_clear(rd_book *b)
{
free(b->tail_cps);
b->tail_cps = NULL;
b->tail_n = 0;
b->tail_seg = -1;
b->tail_seg_char = 0;
}
/* 缓存续建时 tail 不在内存(未持久化)→ 由段 seg 的末页首字符重算(读该段 cps 截取末页 tail)。 */
static int tail_rebuild(rd_book *b, int seg)
{
if (b->tail_seg == seg && b->tail_cps && b->tail_n > 0) return 0;
tail_clear(b);
if (seg < 0 || seg >= b->seg_built) return -1;
int pc = b->segs[seg].page_count;
if (pc <= 0) return -1;
int lastp = b->segs[seg].page_start + pc - 1;
if (lastp < 0 || lastp >= b->npages) return -1;
int32_t sc = b->pages[lastp].seg_char;
if (sc < 0) return -1;
uint32_t *cps = NULL; size_t n = 0;
if (seg_load_cps(b, seg, &cps, &n) != 0) return -1;
if ((size_t)sc > n) { free(cps); return -1; }
b->tail_n = n - (size_t)sc;
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
if (!b->tail_cps) { free(cps); return -6; }
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
b->tail_seg = seg;
b->tail_seg_char = sc;
free(cps);
return 0;
}
static int pages_push(rd_book *b, int seg, int32_t seg_char, uint8_t flags)
{
if (b->npages >= b->pages_cap) {
int nc = b->pages_cap ? b->pages_cap * 2 : 1024;
rd_book_page *np = (rd_book_page *)realloc(b->pages, (size_t)nc * sizeof(rd_book_page));
if (!np) return -6;
b->pages = np; b->pages_cap = nc;
}
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
b->pages[b->npages].seg = seg;
b->pages[b->npages].seg_char = seg_char;
b->pages[b->npages].flags = flags;
b->npages++;
return 0;
}
/* ---- Phase 2A:滚动拼接。把 seg_idx 段码点布局并追加到 flat 页表。
* 建 N+1 时:删 N 的临时末页 → tail_cps + cps_new 拼接重排 → 首张拼接页回填 N(锚不变),
* 其余页按 first_char − tail_n 归入 N+1。末段/首段无 tail 时按整段直排。 ---- */
static int append_seg_pages(rd_book *b, int seg_idx, const uint32_t *cps, size_t n)
{
int have_tail = (seg_idx > 0 && b->tail_seg == seg_idx - 1 && b->tail_cps && b->tail_n > 0);
if (have_tail) {
if (b->segs[seg_idx - 1].page_count <= 0) return -4;
b->npages--; /* 删上一段临时末页(拼接页原位回填) */
b->segs[seg_idx - 1].page_count--;
}
size_t stitched_n = have_tail ? b->tail_n + n : n;
uint32_t *scps = NULL;
const uint32_t *chars;
if (have_tail) {
scps = (uint32_t *)malloc((stitched_n ? stitched_n : 1) * sizeof(uint32_t));
if (!scps) return -6;
memcpy(scps, b->tail_cps, b->tail_n * sizeof(uint32_t));
memcpy(scps + b->tail_n, cps, n * sizeof(uint32_t));
chars = scps;
} else {
chars = cps;
}
rd_line *lines = (rd_line *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_line));
rd_page *pages = (rd_page *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_page));
if (!lines || !pages) { free(lines); free(pages); free(scps); return -6; }
rd_layout_in in;
rd_layout_out out;
in.chars = chars; in.n = (int32_t)stitched_n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
out.lines = lines; out.pages = pages; out.max_lines = (int)(stitched_n ? stitched_n : 1); out.max_pages = (int)(stitched_n ? stitched_n : 1);
int lrc = rd_layout(&in, &out);
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); free(scps); return -4; }
int first_own = 0;
if (have_tail) {
if (out.npages < 1) { free(lines); free(pages); free(scps); return -4; }
int rc = pages_push(b, seg_idx - 1, b->tail_seg_char,
(uint8_t)(out.lines[out.pages[0].first_line].para_start ? 1u : 0u));
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
b->segs[seg_idx - 1].page_count++;
first_own = 1;
}
b->segs[seg_idx].page_start = b->npages;
b->segs[seg_idx].page_count = 0;
for (int p = first_own; p < out.npages; p++) {
int32_t local = out.pages[p].first_char - (have_tail ? (int32_t)b->tail_n : 0);
if (local < 0) local = 0;
int rc = pages_push(b, seg_idx, local,
(uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u));
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
b->segs[seg_idx].page_count++;
}
/* 保存本段末页 tail(供建下一段拼接;末段也保存但不会用到)。 */
tail_clear(b);
if (b->segs[seg_idx].page_count > 0) {
int lastp = b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1;
int32_t sc = b->pages[lastp].seg_char;
if (sc < 0 || (size_t)sc > n) { free(lines); free(pages); free(scps); return -4; }
b->tail_n = n - (size_t)sc;
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
if (!b->tail_cps) { b->tail_n = 0; free(lines); free(pages); free(scps); return -6; }
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
b->tail_seg = seg_idx;
b->tail_seg_char = sc;
}
free(lines); free(pages); free(scps);
return 0;
}
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → cps → 拼接布局)补齐进 flat 页表。
* 幂等:已建段跳过。开书只建到目标(页 0/保存进度),翻页/跳转再按需补齐 —— 免整书一次性布局。 */ * 幂等:已建段跳过。开书只建到目标(页 0/保存进度),翻页/跳转再按需补齐 —— 免整书一次性布局。 */
static int seg_ensure_built(rd_book *b, int upto_seg) static int seg_ensure_built(rd_book *b, int upto_seg)
{ {
@@ -345,8 +760,6 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
int built_any = 0; int built_any = 0;
while (b->seg_built <= upto_seg) { while (b->seg_built <= upto_seg) {
int idx = b->seg_built; int idx = b->seg_built;
size_t rawlen = 0;
if (rdseg_rawlen(b->seg_dir, idx, &rawlen) != 0) break;
if (b->segs_cap <= idx) { if (b->segs_cap <= idx) {
int nc = b->segs_cap ? b->segs_cap * 2 : 64; int nc = b->segs_cap ? b->segs_cap * 2 : 64;
if (nc <= idx) nc = idx + 1; if (nc <= idx) nc = idx + 1;
@@ -354,24 +767,13 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
if (!ns) return -6; if (!ns) return -6;
b->segs = ns; b->segs_cap = nc; b->segs = ns; b->segs_cap = nc;
} }
size_t cap = rawlen * 3 / 2 + 16; if (idx > 0 && tail_rebuild(b, idx - 1) != 0) return -4;
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8; uint32_t *cps = NULL; size_t n = 0;
if (!b->seg_utf8 || b->seg_utf8_cap < cap) { int rc = seg_load_cps(b, idx, &cps, &n);
uint8_t *t = (uint8_t *)malloc(cap); if (rc != 0) return rc;
if (!t) return -6; rc = append_seg_pages(b, idx, cps, n);
free(b->seg_utf8); b->seg_utf8 = t; b->seg_utf8_cap = cap; free(cps);
} if (rc != 0) return rc;
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
if (!raw) return -6;
size_t rn = 0;
if (rdseg_load(b->seg_dir, idx, raw, rawlen, &rn) != 0) { free(raw); return -4; }
size_t ulen = 0;
int rc = rd_to_utf8(b->enc, raw, rn, b->seg_utf8, cap, &ulen);
free(raw);
if (rc == RD_ENC_ERR_BUFSZ) return -2;
if (rc != RD_ENC_OK) return -3;
int prc = append_seg_pages(b, idx, b->seg_utf8, ulen);
if (prc != 0) return prc;
b->seg_built++; b->seg_built++;
built_any = 1; built_any = 1;
} }
@@ -380,59 +782,53 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
return 0; return 0;
} }
/* ---- 正文重显 + 布局到复用缓冲 ---- */ /* ---- Phase 2C:双段 cps 窗口(渲染微排版取码点;替换旧单段整段 lines/pages 缓存) ---- */
static int load_seg(rd_book *b, int seg_idx) static void win_clear(rd_book *b)
{ {
if (seg_idx == b->seg_cache_idx) return 0; for (int i = 0; i < 2; i++) {
size_t ulen = 0; free(b->win[i].cps);
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) { b->win[i].cps = NULL;
/* R3.2 段路径:只读当前页所在 512KB 段文件(有界,不全量读整书) */ b->win[i].n = 0;
size_t rawlen = 0; b->win[i].seg = -1;
if (rdseg_rawlen(b->seg_dir, seg_idx, &rawlen) != 0) return -4;
size_t cap = rawlen * 3 / 2 + 16;
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
if (!b->seg_utf8 || b->seg_utf8_cap < cap) {
uint8_t *t = (uint8_t *)malloc(cap);
if (!t) return -6;
free(b->seg_utf8); b->seg_utf8 = t; b->seg_utf8_cap = cap;
}
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
if (!raw) return -6;
size_t rn = 0;
int rc = rdseg_load(b->seg_dir, seg_idx, raw, rawlen, &rn);
if (rc != 0) { free(raw); return -4; }
int r2 = rd_to_utf8(b->enc, raw, rn, b->seg_utf8, cap, &ulen);
free(raw);
if (r2 == RD_ENC_ERR_BUFSZ) return -2;
if (r2 != RD_ENC_OK) return -3;
} else {
size_t cap = RDTXT_SEG_UTF8_MAX + 8;
if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
if (!b->seg_utf8) return -6;
if (rd_src_seg(b->src, seg_idx, b->seg_utf8, cap, &ulen) != 0) return -4;
} }
size_t n = 0; }
free(b->seg_cps); b->seg_cps = NULL;
if (rd_utf8_to_cp(b->seg_utf8, ulen, &b->seg_cps, &n) != RD_ENC_OK) return -5; static cps_slot *win_find(rd_book *b, int seg)
b->seg_cps_n = n; {
if (b->seg_lines) free(b->seg_lines); if (b->win[0].seg == seg) return &b->win[0];
if (b->seg_pages) free(b->seg_pages); if (b->win[1].seg == seg) return &b->win[1];
b->seg_lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line)); return NULL;
b->seg_pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page)); }
if (!b->seg_lines || !b->seg_pages) return -6;
rd_layout_in in; static int win_load_seg(rd_book *b, int seg_idx, cps_slot *slot)
rd_layout_out out; {
in.chars = b->seg_cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls; uint32_t *cps = NULL; size_t n = 0;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs; int rc = seg_load_cps(b, seg_idx, &cps, &n);
in.meas = book_meas; in.mctx = b; if (rc != 0) return rc;
out.lines = b->seg_lines; out.pages = b->seg_pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1); free(slot->cps);
int lrc = rd_layout(&in, &out); slot->cps = cps; slot->n = n; slot->seg = seg_idx;
if (lrc != RD_LAYOUT_OK) { b->seg_nlines = b->seg_npages = 0; b->seg_cache_idx = -1; return -4; }
b->seg_nlines = out.nlines; b->seg_npages = out.npages;
b->seg_cache_idx = seg_idx;
return 0; return 0;
} }
/* 确保段 seg_idx 码点在窗口内。双槽都占时,淘汰「离 seg_idx 更远」的槽(距相等淘汰更小段下标,
* 倾向保留下一段);滞回释放由 reader_on_tick 的 2%/4% 预取门控体现。 */
static int win_ensure(rd_book *b, int seg_idx)
{
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (win_find(b, seg_idx)) return 0;
int victim = -1;
if (b->win[0].seg < 0) victim = 0;
else if (b->win[1].seg < 0) victim = 1;
else {
int d0 = b->win[0].seg - seg_idx; if (d0 < 0) d0 = -d0;
int d1 = b->win[1].seg - seg_idx; if (d1 < 0) d1 = -d1;
if (d0 > d1) victim = 0;
else if (d1 > d0) victim = 1;
else victim = (b->win[0].seg < b->win[1].seg) ? 0 : 1;
}
return win_load_seg(b, seg_idx, &b->win[victim]);
}
int rd_book_open(rd_book **b, const char *path, const char *font_path, int rd_book_open(rd_book **b, const char *path, const char *font_path,
int fs, int ls, int pw, int ph, int mx, int my, int top) int fs, int ls, int pw, int ph, int mx, int my, int top)
{ {
@@ -465,7 +861,8 @@ int rd_book_open(rd_book **b, const char *path, const char *font_path,
if (rdft_init(&x->ft) != 0) goto close; if (rdft_init(&x->ft) != 0) goto close;
if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); x->ft = NULL; goto close; } if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); x->ft = NULL; goto close; }
if (x->ft) rdft_set_px(x->ft, fs); if (x->ft) rdft_set_px(x->ft, fs);
x->seg_cache_idx = -1; x->win[0].seg = x->win[1].seg = -1;
x->tail_seg = -1;
int seg_mode = 0; int seg_mode = 0;
/* R3.2 分段流式(TXT 且段 base 已配置):开书切 512KB 段文件,逐段按需布局(不全量)。 */ /* R3.2 分段流式(TXT 且段 base 已配置):开书切 512KB 段文件,逐段按需布局(不全量)。 */
if (g_seg_base[0] && x->fmt == RD_FMT_TXT) { if (g_seg_base[0] && x->fmt == RD_FMT_TXT) {
@@ -513,10 +910,11 @@ void rd_book_close(rd_book *b)
if (!b) return; if (!b) return;
free(b->segs); free(b->segs);
free(b->pages); free(b->pages);
free(b->seg_utf8); win_clear(b);
free(b->seg_cps); tail_clear(b);
free(b->seg_lines); free(b->stitch_cps);
free(b->seg_pages); free(b->render_lines);
rdchapter_free(&b->chaps);
if (b->ft) rdft_free(b->ft); if (b->ft) rdft_free(b->ft);
if (b->src) rd_src_free(b->src); if (b->src) rd_src_free(b->src);
free(b); free(b);
@@ -563,6 +961,8 @@ int rd_book_set_enc(rd_book *b, rd_enc_t enc)
/* 重建索引(复用现值 fs/ls/content_*),再回锚 */ /* 重建索引(复用现值 fs/ls/content_*),再回锚 */
free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL; free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL;
b->nsegs = b->npages = 0; b->seg_built = 0; b->segs_cap = b->pages_cap = 0; b->nsegs = b->npages = 0; b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
win_clear(b); /* 编码变了 → 码点空间不同,窗口/tail 全失效 */
tail_clear(b);
if (b->seg_dir[0]) { if (b->seg_dir[0]) {
/* R3.2 段路径:编码变了 → 段文件边界/解码不同 → 重建段文件 + 按需布局(段 0 起步) */ /* R3.2 段路径:编码变了 → 段文件边界/解码不同 → 重建段文件 + 按需布局(段 0 起步) */
if (oldenc != b->enc) rdseg_clear(b->seg_dir); if (oldenc != b->enc) rdseg_clear(b->seg_dir);
@@ -615,27 +1015,104 @@ int rd_book_set_page(rd_book *b, int page)
return page; return page;
} }
/* Phase 2C:空闲预取下一段(reader_on_tick 调)。当前页距下一段界 ≤2% 时把 s+1 段码点
* 提前解码进窗口(命中/未到阈值则幂等返回),翻到段界即零突发。 */
int rd_book_prefetch(rd_book *b)
{
if (!b || b->is_img || b->npages <= 0) return -1;
if (!(b->seg_dir[0] && b->fmt == RD_FMT_TXT)) return 0; /* 仅 TXT 段路径有段界预取意义 */
int cur = b->cur; if (cur < 0) cur = 0; if (cur >= b->npages) cur = b->npages - 1;
int s = b->pages[cur].seg;
if (s < 0 || s + 1 >= b->nsegs) return 0;
int bnd = b->segs[s + 1].page_start;
int d = cur - bnd; if (d < 0) d = -d;
if ((long long)d * 100 / b->npages > RD_WIN_NEAR_PCT) return 0;
return win_ensure(b, s + 1);
}
/* ---- Phase 2C:页级微排版。把 page_no 从页首字符起只排一页(复用 rd_layout_page)。
* 拼接页(TXT 段界末页)在窗口两段 cps 相接的临时缓冲上排,天然连续跨段。
* 输出:*chars 与 *chars_n = 绘制用码点视图(窗口 cps 或 b->stitch_cps,下次调用前勿 free),
* b->render_lines 填行;*nlines 行数。返回 0=成功,1=空页,负=错误。 */
static int page_micro_layout(rd_book *b, int page_no, const uint32_t **chars, size_t *chars_n, int *nlines)
{
*chars = NULL; *chars_n = 0; *nlines = 0;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
int stitched = (b->seg_dir[0] && b->fmt == RD_FMT_TXT) &&
(page_no == b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1) &&
(seg_idx + 1 < b->nsegs);
if (win_ensure(b, seg_idx) != 0) return -2;
if (stitched && win_ensure(b, seg_idx + 1) != 0) return -2;
int lh = (b->fs * b->ls) / 100; if (lh < 1) lh = 1;
int max_lines = b->content_h / lh + 2;
if (b->render_lines_cap < max_lines) {
rd_line *t = (rd_line *)realloc(b->render_lines, (size_t)max_lines * sizeof(rd_line));
if (!t) return -6;
b->render_lines = t; b->render_lines_cap = max_lines;
}
cps_slot *s0 = win_find(b, seg_idx);
const uint32_t *view; size_t view_n; int32_t start;
if (!stitched) {
view = s0->cps; view_n = s0->n;
start = pg->seg_char;
if (start < 0) start = 0;
if ((size_t)start > view_n) start = (int32_t)view_n;
} else {
cps_slot *s1 = win_find(b, seg_idx + 1);
int32_t tail = pg->seg_char; if (tail < 0) tail = 0;
if ((size_t)tail > s0->n) return 1;
size_t tail_n = s0->n - (size_t)tail;
/* 拼接页只吃「一页能装下」的下一段前缀;按一页行数×行宽×2(零宽字符余量)估上界。 */
size_t budget = (size_t)(max_lines + 2) * (size_t)(b->content_w + 2) * 2u + 64u;
size_t take = s1->n < budget ? s1->n : budget;
size_t total = tail_n + take;
if (b->stitch_cap < total) {
uint32_t *t = (uint32_t *)realloc(b->stitch_cps, (total ? total : 1) * sizeof(uint32_t));
if (!t) return -6;
b->stitch_cps = t; b->stitch_cap = total;
}
memcpy(b->stitch_cps, s0->cps + tail, tail_n * sizeof(uint32_t));
memcpy(b->stitch_cps + tail_n, s1->cps, take * sizeof(uint32_t));
view = b->stitch_cps; view_n = total;
start = 0;
}
int mid_para = !(pg->flags & 1u);
rd_layout_in in;
in.chars = view; in.n = (int32_t)view_n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
int32_t next_start = 0;
int rc = rd_layout_page(&in, start, mid_para, b->render_lines, max_lines, nlines, &next_start);
if (rc == RD_LAYOUT_ERR_OVER) return -4;
if (rc != RD_LAYOUT_OK) return -3;
*chars = view; *chars_n = view_n;
return (*nlines > 0) ? 0 : 1;
}
/* ---- 文本页渲染(FreeType 字形 → 全屏 565) ---- */ /* ---- 文本页渲染(FreeType 字形 → 全屏 565) ---- */
static int render_text_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565) static int render_text_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565)
{ {
int npix = b->pw * b->ph; int npix = b->pw * b->ph;
for (int i = 0; i < npix; i++) out[i] = bg565; for (int i = 0; i < npix; i++) out[i] = bg565;
rd_book_page *pg = &b->pages[page_no]; const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
int seg_idx = (int)pg->seg; int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1; if (rc != 0) return rc;
if (load_seg(b, seg_idx) != 0) return -2;
int local_page = page_no - b->segs[seg_idx].page_start;
if (local_page < 0 || local_page >= b->seg_npages) return 1;
rd_page *pp = &b->seg_pages[local_page];
int asc = rdft_ascender(b->ft); int asc = rdft_ascender(b->ft);
int screen_x0 = b->mx; int screen_x0 = b->mx;
int screen_y0 = b->top; int screen_y0 = b->top;
for (int li = 0; li < pp->nlines; li++) { for (int li = 0; li < nlines; li++) {
rd_line *ln = &b->seg_lines[pp->first_line + li]; rd_line *ln = &b->render_lines[li];
int pen = screen_x0 + ln->x; int pen = screen_x0 + ln->x;
int baseline = screen_y0 + ln->y + asc; int baseline = screen_y0 + ln->y + asc;
for (int32_t ci = ln->s; ci < ln->e; ci++) { for (int32_t ci = ln->s; ci < ln->e; ci++) {
uint32_t cp = b->seg_cps[ci]; if (ci < 0 || (size_t)ci >= chars_n) continue;
uint32_t cp = chars[ci];
int adv = 0, bw = 0, bh = 0, left = 0, btop = 0; int adv = 0, bw = 0, bh = 0, left = 0, btop = 0;
const uint8_t *bmp = NULL; const uint8_t *bmp = NULL;
rdft_glyph(b->ft, cp, &adv, &bmp, &bw, &bh, &left, &btop); rdft_glyph(b->ft, cp, &adv, &bmp, &bw, &bh, &left, &btop);
@@ -704,6 +1181,8 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
b->content_w = b->pw - 2 * b->mx; b->content_w = b->pw - 2 * b->mx;
b->content_h = b->ph - b->top - b->my; b->content_h = b->ph - b->top - b->my;
if (b->ft) rdft_set_px(b->ft, fs); if (b->ft) rdft_set_px(b->ft, fs);
win_clear(b); /* 页表重建 → 窗口/tail 状态整体复位(码点可复用,但按重建统一清) */
tail_clear(b);
seg_t *nos = b->segs; int n_oldsegs = b->nsegs; seg_t *nos = b->segs; int n_oldsegs = b->nsegs;
rd_book_page *nop = b->pages; int n_oldpages = b->npages; rd_book_page *nop = b->pages; int n_oldpages = b->npages;
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0; b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
@@ -740,22 +1219,20 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx) int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
{ {
if (!b || !fn || b->is_img || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1; if (!b || !fn || b->is_img || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
rd_book_page *pg = &b->pages[page_no]; const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
int seg_idx = (int)pg->seg; int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1; if (rc != 0) return rc;
if (load_seg(b, seg_idx) != 0) return -2;
int local_page = page_no - b->segs[seg_idx].page_start;
if (local_page < 0 || local_page >= b->seg_npages) return 1;
rd_page *pp = &b->seg_pages[local_page];
uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32)); uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32));
if (!line_utf8) return -6; if (!line_utf8) return -6;
for (int li = 0; li < pp->nlines; li++) { for (int li = 0; li < nlines; li++) {
rd_line *ln = &b->seg_lines[pp->first_line + li]; rd_line *ln = &b->render_lines[li];
int x = b->mx + ln->x, y = b->top + ln->y; int x = b->mx + ln->x, y = b->top + ln->y;
size_t o = 0; size_t o = 0;
for (int32_t ci = ln->s; ci < ln->e; ci++) { for (int32_t ci = ln->s; ci < ln->e; ci++) {
if (ci < 0 || (size_t)ci >= chars_n) continue;
uint8_t tmp[4]; uint8_t tmp[4];
int n = rd_cp_to_utf8(b->seg_cps[ci], tmp); int n = rd_cp_to_utf8(chars[ci], tmp);
if (o + (size_t)n + 1 > (size_t)(b->content_w * 4 + 32)) break;
memcpy(line_utf8 + o, tmp, (size_t)n); o += (size_t)n; memcpy(line_utf8 + o, tmp, (size_t)n); o += (size_t)n;
} }
line_utf8[o] = 0; line_utf8[o] = 0;
+21
View File
@@ -24,6 +24,7 @@ typedef struct rd_book rd_book;
typedef struct rd_book_page { typedef struct rd_book_page {
int32_t seg; /* 段下标 */ int32_t seg; /* 段下标 */
int32_t seg_char; /* 段内字符偏移(页首) */ int32_t seg_char; /* 段内字符偏移(页首) */
uint8_t flags; /* bit0 = 首页行是否段首行(缩进/段距判定;Phase 2A/2C) */
} rd_book_page; } rd_book_page;
/* 打开书:path=TXT;font_path=FreeType 字体;geom=版面(fs 字号 / ls 行距% / pw,ph 全屏 / /* 打开书:path=TXT;font_path=FreeType 字体;geom=版面(fs 字号 / ls 行距% / pw,ph 全屏 /
@@ -36,6 +37,7 @@ int rd_book_pages(rd_book *b);
int rd_book_current(rd_book *b); /* 当前页 */ int rd_book_current(rd_book *b); /* 当前页 */
int rd_book_turn(rd_book *b, int delta); /* +1/-1;clamp 到 [0,npages-1] */ int rd_book_turn(rd_book *b, int delta); /* +1/-1;clamp 到 [0,npages-1] */
int rd_book_set_page(rd_book *b, int page); /* 跳到绝对页;clamp;返回实际页(段刮擦用) */ int rd_book_set_page(rd_book *b, int page); /* 跳到绝对页;clamp;返回实际页(段刮擦用) */
int rd_book_prefetch(rd_book *b); /* 空闲预取下一段(reader_on_tick 调);0=已备/无需 */
int rd_book_is_image(rd_book *b); /* 1=CBZ 图片页书 */ int rd_book_is_image(rd_book *b); /* 1=CBZ 图片页书 */
/* 目录:0=无;i 项标题写到 title,目标段/字符偏移填 *seg 与 *ch。 */ /* 目录:0=无;i 项标题写到 title,目标段/字符偏移填 *seg 与 *ch。 */
@@ -73,6 +75,25 @@ void rd_book_set_index_dir(const char *dir);
* 码位对齐段文件,渲染只加载当前页所在段(不全量读整书)——治大书开书 CPU100%/返回无效。 */ * 码位对齐段文件,渲染只加载当前页所在段(不全量读整书)——治大书开书 CPU100%/返回无效。 */
void rd_book_set_seg_base(const char *base); void rd_book_set_seg_base(const char *base);
/* R3.3(TXT 章节):章节规范解析(core/rdchapter.c)+ 章节→页映射。
* 只对 TXT(seg_mode 分段流式路径)生效;其它格式返回「无章节」,不破坏既有路径。
* rd_book_chapter_n 触发解析(首次读全部段文件解码扫行,或命中章节缓存秒回),
* 解析/缓存后就绪;顶栏章节名与「当前页属于哪章」据此工作。 */
int rd_book_chapter_n(rd_book *b); /* 章节数;-1=非 TXT/CBZ(不可用) */
int rd_book_chapter_ready(rd_book *b); /* 1=章节已解析(含无章节=0 但已确认) */
int rd_book_reparse_chapters(rd_book *b); /* 手动重解析:清除缓存+重扫+落盘;见 book.c */
int rd_book_chapter(rd_book *b, int i, char *title, int tsz,
int64_t *char_off, int *seg, int32_t *seg_char, int *number);
int rd_book_chapter_for_page(rd_book *b, int page, int *idx); /* 当前页所属章节下标;-1=无 */
int rd_book_page_for_char(rd_book *b, int64_t char_off); /* 全书字符偏移 → 页号;-1=不可用 */
int64_t rd_book_page_char_off(rd_book *b, int page); /* 页首全书字符偏移;-1=不可用 */
/* 段(文件/段模式跳转用)。非段模式返回 -1。 */
int rd_book_seg_n(rd_book *b); /* 段数 */
int rd_book_seg_page(rd_book *b, int seg); /* 段首页(ensure built);-1 */
int rd_book_seg_page_count(rd_book *b, int seg); /* 段页数;-1 */
int rd_book_seg_of_page(rd_book *b, int page); /* 页所属段;-1 */
/* 字体/几何 */ /* 字体/几何 */
int rd_book_font(rd_book *b); /* 当前字号 */ int rd_book_font(rd_book *b); /* 当前字号 */
int rd_book_ls(rd_book *b); /* 行距% */ int rd_book_ls(rd_book *b); /* 行距% */
+478 -42
View File
@@ -10,6 +10,7 @@
#include "book.h" #include "book.h"
#include "progress.h" #include "progress.h"
#include "reader_conf.h" #include "reader_conf.h"
#include "rdchapter.h" /* RDCHAP_TITLE_MAX(章节标题) */
#include <dirent.h> #include <dirent.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -18,7 +19,7 @@
#include <time.h> #include <time.h>
#define RD_MENU_H 64 #define RD_MENU_H 64
#define RD_MENU_BTN 4 /* 返回 | 目录 | 界面 | 设置(MVP 单条) */ #define RD_MENU_BTN 5 /* 返回 | 目录 | 跳转 | 界面 | 设置(R3.3 加「跳转」) */
#define RD_HEADER_H 30 /* 页头条高(书名+章节名) */ #define RD_HEADER_H 30 /* 页头条高(书名+章节名) */
#define RD_FOOT_H 18 /* 页底条高(章节+页码) */ #define RD_FOOT_H 18 /* 页底条高(章节+页码) */
#define RD_UI_H 268 /* 「界面」排版面板高 */ #define RD_UI_H 268 /* 「界面」排版面板高 */
@@ -36,6 +37,13 @@ static int g_menu_open;
static int g_ui_open; /* 「界面」排版面板(B) */ static int g_ui_open; /* 「界面」排版面板(B) */
static int g_dir_open; /* 目录覆盖层 */ static int g_dir_open; /* 目录覆盖层 */
static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */ static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
static int g_jump_open; /* 跳转覆盖层(R3.3) */
static int g_jump_mode; /* 1=章节模式; 2=文件/段模式; 0=未定 */
static int g_jump_sel; /* 当前选中章节/段下标 */
static int g_jump_scroll; /* 列表滚动(首行项下标) */
static char g_jump_buf[8]; /* 章内/段内页码输入缓冲(ASCII 数字) */
static int g_chap_cache_idx = -1; /* 章节范围缓存(章起/止页;随字号/行距重排失效) */
static int g_chap_cache_start, g_chap_cache_end;
static char g_font_path[400]; static char g_font_path[400];
static int g_notice_ms; static int g_notice_ms;
@@ -48,12 +56,13 @@ static int g_anim_dir; /* 1=下一页(旧左移/新右入);-
#define RD_SCRUB_W 26 /* 右缘刮擦条宽度 */ #define RD_SCRUB_W 26 /* 右缘刮擦条宽度 */
static int g_scrub; /* 1=刮擦中 */ static int g_scrub; /* 1=刮擦中 */
static int g_scrub_page; /* 刮擦目标页(实时预览用) */ static int g_scrub_page; /* 刮擦目标页(实时预览用) */
static int g_scrub_moved; /* 本次刮擦是否发生了拖动(无拖动=点按,不当翻页跳页) */
/* ---- R3 编码切换(TXT;Big5 繁体书手动选定) ---- */ /* ---- R3 编码切换(TXT;Big5 繁体书手动选定) ---- */
static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效编码) */ static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效编码) */
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */ static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */ static char g_chapter[RDCHAP_TITLE_MAX]; /* 顶栏章节名(TXT 无章节=文件名) */
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */ static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */ static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
static int g_prog_dirty; /* 阅读进度变更待写(翻页/刮擦/字号/编码;R3.2 定时落盘防崩溃丢进度) */ static int g_prog_dirty; /* 阅读进度变更待写(翻页/刮擦/字号/编码;R3.2 定时落盘防崩溃丢进度) */
@@ -168,7 +177,21 @@ static const char *resolve_font(void)
static void chapter_name(void) static void chapter_name(void)
{ {
/* TXT 无章节 → 文件名;R1 固定。 */ /* TXT 有章节(已解析)→ 当前页所属章名;否则文件名(R1 固定)。
* 仅在章节已就绪时用章名(rd_book_chapter_ready 不触发扫描),首屏/未解析时用文件名,
* 避免开书即整书扫描占 UI 线程。 */
if (!g_book) { g_chapter[0] = 0; return; }
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
int page = rd_book_current(g_book);
int idx = -1;
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
char t[RDCHAP_TITLE_MAX];
if (rd_book_chapter(g_book, idx, t, sizeof t, NULL, NULL, NULL, NULL) == 0) {
snprintf(g_chapter, sizeof g_chapter, "%s", t);
return;
}
}
}
snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book)); snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book));
} }
@@ -176,6 +199,7 @@ static void reader_reflow(void)
{ {
if (!g_book) return; if (!g_book) return;
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */ if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */
g_chap_cache_idx = -1; /* 页索引随字号/行距变 → 章范围缓存失效 */
int seg = 0; int32_t ch = 0; int seg = 0; int32_t ch = 0;
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */ rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
g_prog_dirty = 1; g_prog_dirty = 1;
@@ -266,6 +290,354 @@ static void reader_draw_scrub(const meow_theme *th)
} }
} }
/* ---------------- R3.3 跳转弹层(章节模式 / 文件·段模式) ----------------
* 复用 g_dir 系列「半透明底 + 条目列表 + 当前高亮」;顶部提供:当前章节/段信息、页码输入
* (自绘数字键盘 0-9 + ⌫ + 跳)、快捷(首 / ±10 / ±100 / 末)。只对 TXT(seg 模式)启用:
* rd_book_chapter_n>0 → 章节模式;否则 rd_book_seg_n>0 → 文件/段模式。 */
#define J_SHORTCUT_N 6
#define J_SHORTCUT_X0 16
#define J_SHORTCUT_GAP 6
#define J_SHORTCUT_H 32
#define J_KEY_W 64
#define J_KEY_H 42
#define J_KEY_GAP 8
#define J_ROW 30
enum {
J_NONE = 0,
J_GO_FIRST = 1, J_GO_M100 = 2, J_GO_M10 = 3,
J_GO_P10 = 4, J_GO_P100 = 5, J_GO_LAST = 6,
J_DIG_0 = 10, J_DIG_1 = 11, J_DIG_2 = 12, J_DIG_3 = 13, J_DIG_4 = 14,
J_DIG_5 = 15, J_DIG_6 = 16, J_DIG_7 = 17, J_DIG_8 = 18, J_DIG_9 = 19,
J_BACK = 30, J_JUMP = 31,
J_ITEM = 100, /* 100 + i = 列表第 i 项 */
};
static int jump_hdr_y(void) { return g_top; }
static int jump_info_y(void) { return g_top + 30; }
static int jump_shortcut_y(void){ return g_top + 56; }
static int jump_input_y(void) { return g_top + 92; }
static int jump_pad_y(void) { return g_top + 128; }
static int jump_list_y(void) { return g_top + 128 + 4 * (J_KEY_H + J_KEY_GAP) + 10; }
static int chapter_range(int idx, int *start, int *end)
{
if (!g_book || idx < 0) return -1;
if (g_chap_cache_idx == idx) { if (start) *start = g_chap_cache_start; if (end) *end = g_chap_cache_end; return 0; }
int64_t co = 0;
if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) != 0) return -1;
int s = rd_book_page_for_char(g_book, co);
if (s < 0) return -1;
int n = rd_book_chapter_n(g_book);
int e;
if (idx + 1 < n) {
int64_t co2 = 0;
if (rd_book_chapter(g_book, idx + 1, NULL, 0, &co2, NULL, NULL, NULL) != 0) return -1;
e = rd_book_page_for_char(g_book, co2) - 1;
} else {
e = rd_book_pages(g_book) - 1;
}
if (e < s) e = s;
g_chap_cache_idx = idx; g_chap_cache_start = s; g_chap_cache_end = e;
if (start) *start = s;
if (end) *end = e;
return 0;
}
/* 跳转选中项 idx 的页区间(章节模式=章范围;文件模式=段范围)。 */
static int jump_region(int idx, int *start, int *end, int *inp, int *ofp)
{
if (!g_book) return -1;
int s, e;
if (g_jump_mode == 1) {
if (chapter_range(idx, &s, &e) != 0) return -1;
} else if (g_jump_mode == 2) {
int nseg = rd_book_seg_n(g_book);
if (idx < 0 || idx >= nseg) return -1;
s = rd_book_seg_page(g_book, idx);
if (s < 0) return -1;
e = s + rd_book_seg_page_count(g_book, idx) - 1;
if (e < s) e = s;
} else return -1;
int cur = rd_book_current(g_book);
if (cur < s) cur = s;
if (cur > e) cur = e;
if (start) *start = s;
if (end) *end = e;
if (inp) *inp = cur - s + 1;
if (ofp) *ofp = e - s + 1;
return 0;
}
/* 页底条右侧文本:章内 y/Z · 全书 x/M(章节就绪且范围可算时),否则 全书 x/M。 */
static void footer_progress(char *out, int outsz)
{
int np = rd_book_pages(g_book);
int page = rd_book_current(g_book);
if (np <= 0) { snprintf(out, outsz, "0/0"); return; }
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
int idx = -1;
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
int s = -1, e = -1;
if (chapter_range(idx, &s, &e) == 0 && e >= s) {
int inp = page - s + 1, ofp = e - s + 1;
if (inp > 0 && ofp > 0) { snprintf(out, outsz, "%d/%d · %d/%d", inp, ofp, page + 1, np); return; }
}
}
}
snprintf(out, outsz, "%d/%d", page + 1, np);
}
static void jump_chip(int x, int y, int w, int h, const char *label, int active, const meow_theme *th)
{
if (active) meow_ui_fill_rect(x, y, w, h, th->primaryContainer);
else meow_ui_fill_rect(x, y, w, h, th->surfaceContainerHighest);
uint32_t tc = active ? th->onPrimaryContainer : th->fg;
meow_ui_text_center(x + w / 2, y + (h - 20) / 2, label, 20, tc);
}
static void reader_draw_jump(const meow_theme *th)
{
if (!g_book) return;
int page = rd_book_current(g_book);
int np = rd_book_pages(g_book);
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
/* 头栏 */
int y0 = jump_hdr_y();
meow_ui_fill_rect(0, y0, g_pw, 30, th->surfaceContainer);
char hdr[48];
snprintf(hdr, sizeof hdr, "跳转 · %s", (g_jump_mode == 1) ? "章节" : "文件/段");
meow_ui_text(16, y0 + 5, hdr, 20, th->onSurface);
meow_ui_draw_rect(0, y0 + 29, g_pw, 1, th->outlineVariant);
if (n <= 0) {
meow_ui_text(20, jump_info_y() + 12, "本书无可用跳转目标", 20, th->dim);
meow_ui_text(20, g_ph - 30, "点空白关闭", 16, th->dim);
return;
}
/* 信息行 */
char info[256]; info[0] = 0;
int s, e, inp, ofp;
if (g_jump_mode == 1) {
char lab[RDCHAP_TITLE_MAX];
int num = -1;
if (rd_book_chapter(g_book, g_jump_sel, lab, sizeof lab, NULL, NULL, NULL, &num) == 0 &&
jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
char prefix[24]; prefix[0] = 0;
if (num >= 0) snprintf(prefix, sizeof prefix, "第%d章 ", num);
snprintf(info, sizeof info, "%s%s · 章内 %d/%d · 全书 %d/%d", prefix, lab, inp, ofp, page + 1, np);
}
} else {
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0)
snprintf(info, sizeof info, "段 %d/%d · 段内 %d/%d · 全书 %d/%d", g_jump_sel + 1, n, inp, ofp, page + 1, np);
}
meow_ui_text(16, jump_info_y() + 3, info[0] ? info : "—", 16, th->fg);
/* 快捷 chips */
static const char *sc_lbl[J_SHORTCUT_N] = { "首", "-100", "-10", "+10", "+100", "末" };
int x = J_SHORTCUT_X0;
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
for (int i = 0; i < J_SHORTCUT_N; i++) {
jump_chip(x, jump_shortcut_y(), cw, J_SHORTCUT_H, sc_lbl[i], 0, th);
x += cw + J_SHORTCUT_GAP;
}
/* 输入行 */
meow_ui_text(16, jump_input_y() + 5, (g_jump_mode == 1) ? "章内页" : "段内页", 20, th->fg);
char ib[48];
snprintf(ib, sizeof ib, "%s", g_jump_buf[0] ? g_jump_buf : "_");
meow_ui_text(120, jump_input_y() + 3, ib, 24, th->primary);
meow_ui_text_right(g_pw - 16, jump_input_y() + 7, "输页码点「跳」 · ⌫退格", 16, th->dim);
/* 数字键盘 1..9 / ⌫ 0 跳 */
static const char *pad[4][3] = {
{ "1", "2", "3" }, { "4", "5", "6" }, { "7", "8", "9" }, { "⌫", "0", "跳" },
};
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
int pad_x = (g_pw - pad_w) / 2;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 3; c++) {
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
int ky = jump_pad_y() + r * (J_KEY_H + J_KEY_GAP);
jump_chip(kx, ky, J_KEY_W, J_KEY_H, pad[r][c], (r == 3 && c == 2), th);
}
}
/* 条目列表(当前选中高亮) */
int top = jump_list_y();
int maxshow = (g_ph - top - 8) / J_ROW;
if (maxshow < 1) maxshow = 1;
/* 让当前选中项进入可视区 */
if (g_jump_sel >= 0) {
if (g_jump_sel < g_jump_scroll) g_jump_scroll = g_jump_sel;
else if (g_jump_sel >= g_jump_scroll + maxshow) g_jump_scroll = g_jump_sel - maxshow + 1;
}
if (g_jump_scroll + maxshow > n) g_jump_scroll = (n - maxshow < 0 ? 0 : n - maxshow);
if (g_jump_scroll < 0) g_jump_scroll = 0;
for (int i = 0; i < maxshow; i++) {
int item = g_jump_scroll + i;
if (item >= n) break;
char t[96];
if (g_jump_mode == 1) {
if (rd_book_chapter(g_book, item, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
} else {
snprintf(t, sizeof t, "段 %d", item + 1);
}
int yy = top + i * J_ROW;
if (item == g_jump_sel) meow_ui_fill_rect(0, yy, g_pw, J_ROW - 4, th->surfaceContainer);
meow_ui_text(24, yy + 4, t, 20, th->fg);
meow_ui_draw_rect(12, yy + J_ROW - 6, g_pw - 24, 1, th->outline);
}
meow_ui_text(20, g_ph - 30, "点条目跳转 · 点空白关闭", 16, th->dim);
}
static int reader_jump_hit(int x, int y)
{
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
if (n <= 0) return J_NONE;
/* 快捷 chips */
int sy = jump_shortcut_y();
if (y >= sy && y < sy + J_SHORTCUT_H) {
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
if (x >= J_SHORTCUT_X0) {
int k = (x - J_SHORTCUT_X0) / (cw + J_SHORTCUT_GAP);
if (k >= 0 && k < J_SHORTCUT_N) return J_GO_FIRST + k;
}
return J_NONE;
}
/* 键盘 */
int py = jump_pad_y();
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
int pad_x = (g_pw - pad_w) / 2;
for (int r = 0; r < 4; r++) {
int ky = py + r * (J_KEY_H + J_KEY_GAP);
if (y >= ky && y < ky + J_KEY_H) {
for (int c = 0; c < 3; c++) {
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
if (x >= kx && x < kx + J_KEY_W) {
if (r == 3) {
if (c == 2) return J_JUMP;
if (c == 0) return J_BACK;
return J_DIG_0; /* 第三行 c==1 → 数字 0 */
}
return J_DIG_0 + (r * 3 + c) + 1; /* 0..2 行:1 2 3 / 4 5 6 / 7 8 9 */
}
}
return J_NONE;
}
}
/* 条目列表 */
int top = jump_list_y();
if (y >= top) {
int idx = (y - top) / J_ROW + g_jump_scroll;
if (idx < n) return J_ITEM + idx;
return J_NONE;
}
return J_NONE;
}
static void reader_jump_to(int page)
{
if (!g_book) return;
int np = rd_book_pages(g_book);
if (np <= 0) return;
int cur = rd_book_current(g_book);
(void)cur;
rd_book_set_page(g_book, page);
g_prog_dirty = 1;
reader_refill_page();
reader_redraw();
reader_reflow_persist();
}
static void reader_jump_action(int a)
{
if (a == J_NONE) { g_jump_open = 0; reader_redraw(); return; }
int s, e, inp, ofp;
if (a >= J_GO_FIRST && a <= J_GO_LAST) {
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) != 0) return;
int target = rd_book_current(g_book);
switch (a) {
case J_GO_FIRST: target = s; break;
case J_GO_M100: target -= 100; break;
case J_GO_M10: target -= 10; break;
case J_GO_P10: target += 10; break;
case J_GO_P100: target += 100; break;
case J_GO_LAST: target = e; break;
}
if (target < s) target = s;
if (target > e) target = e;
reader_jump_to(target);
return;
}
if (a >= J_DIG_0 && a <= J_DIG_9) {
size_t len = strlen(g_jump_buf);
if (len + 1 < sizeof g_jump_buf) { g_jump_buf[len] = (char)('0' + (a - J_DIG_0)); g_jump_buf[len + 1] = 0; }
reader_redraw();
return;
}
if (a == J_BACK) {
size_t len = strlen(g_jump_buf);
if (len > 0) g_jump_buf[len - 1] = 0;
reader_redraw();
return;
}
if (a == J_JUMP) {
if (g_jump_buf[0]) {
int tpage = atoi(g_jump_buf);
if (tpage < 1) tpage = 1;
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
int t = s + (tpage - 1);
if (t < s) t = s;
if (t > e) t = e;
reader_jump_to(t);
}
}
return;
}
if (a >= J_ITEM) {
int item = a - J_ITEM;
if (g_jump_mode == 1) {
if (jump_region(item, &s, &e, &inp, &ofp) == 0) {
g_jump_sel = item;
reader_jump_to(s);
}
} else {
int sp = rd_book_seg_page(g_book, item);
if (sp >= 0) { g_jump_sel = item; reader_jump_to(sp); }
}
return;
}
g_jump_open = 0; reader_redraw();
}
/* 打开跳转弹层:解析章节(首次会扫描+缓存),按是否有章节选章节/文件·段模式。 */
static void reader_jump_open(void)
{
if (!g_book) return;
g_jump_buf[0] = 0;
g_jump_scroll = 0;
int nc = rd_book_chapter_n(g_book);
if (nc > 0) {
g_jump_mode = 1;
int idx = -1;
rd_book_chapter_for_page(g_book, rd_book_current(g_book), &idx);
g_jump_sel = (idx >= 0) ? idx : 0;
} else if (rd_book_seg_n(g_book) > 0) {
g_jump_mode = 2;
int seg = rd_book_seg_of_page(g_book, rd_book_current(g_book));
g_jump_sel = (seg >= 0) ? seg : 0;
} else {
g_jump_mode = 2;
g_jump_sel = 0;
}
g_jump_open = 1;
g_menu_open = 0;
reader_redraw();
}
/* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ---------------- /* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ----------------
* 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。 * 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。
* 几何 draw/hit 共用(常量)。 */ * 几何 draw/hit 共用(常量)。 */
@@ -416,6 +788,7 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
{ {
(void)w; (void)ctx; (void)w; (void)ctx;
meow_ui_clear(g_page_bg); meow_ui_clear(g_page_bg);
chapter_name(); /* 每帧同步顶栏/页底章节名(就绪后取章名,否则文件名) */
if (g_book) { if (g_book) {
int page = rd_book_current(g_book); int page = rd_book_current(g_book);
if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) { if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
@@ -427,8 +800,6 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
} }
/* 页头条(书名 + 章节名;可选,rd.header) */ /* 页头条(书名 + 章节名;可选,rd.header) */
int sbh = meow_ui_statusbar_height(); int sbh = meow_ui_statusbar_height();
int pages = g_book ? rd_book_pages(g_book) : 0;
int page = g_book ? rd_book_current(g_book) : 0;
const char *bk = g_book ? rd_book_basename(g_book) : "阅读"; const char *bk = g_book ? rd_book_basename(g_book) : "阅读";
if (g_show_header) { if (g_show_header) {
meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg); meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg);
@@ -438,24 +809,23 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
} }
/* 页底条(章节 + 页码;可选,rd.footer) */ /* 页底条(章节 + 页码;可选,rd.footer) */
if (g_show_footer) { if (g_show_footer) {
char ft[48]; char ft[64];
if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", page + 1, pages); footer_progress(ft, sizeof ft);
else snprintf(ft, sizeof ft, "0/0");
int fy = g_ph - RD_FOOT_H; int fy = g_ph - RD_FOOT_H;
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg); meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg); meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg);
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg); meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
} }
/* R3 刮擦指示条(非目录/面板覆盖时显示;菜单为底部条,与右缘条并存即可) */ /* R3 刮擦指示条(非目录/面板/跳转覆盖时显示;菜单为底部条,与右缘条并存即可) */
if (g_book && !g_dir_open && !g_ui_open) reader_draw_scrub(th); if (g_book && !g_dir_open && !g_ui_open && !g_jump_open) reader_draw_scrub(th);
if (g_menu_open) { if (g_menu_open) {
int y = g_ph - RD_MENU_H; int y = g_ph - RD_MENU_H;
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160); meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
/* 4 按钮(MVP 单条):返回 | 目录 | 界面 | 设置 */ /* 5 按钮:返回 | 目录 | 跳转 | 界面 | 设置 */
int bw = g_pw / RD_MENU_BTN; int bw = g_pw / RD_MENU_BTN;
const char *labels[RD_MENU_BTN] = { "返回", "目录", "界面", "设置" }; const char *labels[RD_MENU_BTN] = { "返回", "目录", "跳转", "界面", "设置" };
for (int i = 0; i < RD_MENU_BTN; i++) { for (int i = 0; i < RD_MENU_BTN; i++) {
int bx = i * bw; int bx = i * bw;
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer); meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
@@ -463,9 +833,13 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
} }
} }
if (g_ui_open && g_book) reader_draw_ui_panel(th); if (g_ui_open && g_book) reader_draw_ui_panel(th);
if (g_jump_open && g_book) reader_draw_jump(th);
if (g_dir_open && g_book) { if (g_dir_open && g_book) {
/* 目录覆盖层:半透明底 + 条目列表(当前行高亮) */ /* 目录覆盖层:TXT 有章节 → 章节列表(复用 rd_book 章节数据,与跳转共享);否则 rdsrc 目录;无则「无章节」。 */
int n = rd_book_toc_n(g_book); int nchap = 0;
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
int ntoc = (nchap == 0) ? rd_book_toc_n(g_book) : 0;
int n = (nchap > 0) ? nchap : ntoc;
if (n > 0) { if (n > 0) {
int top = g_top + 8; int top = g_top + 8;
int row = 34; int row = 34;
@@ -474,19 +848,29 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
if (maxshow > n) maxshow = n; if (maxshow > n) maxshow = n;
g_dir_top = top; g_dir_row_h = row; g_dir_top = top; g_dir_row_h = row;
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150); meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
char t[96]; int sg; int32_t cch; char t[160];
int page = rd_book_current(g_book); int page = rd_book_current(g_book);
int curchap = -1;
if (nchap > 0) rd_book_chapter_for_page(g_book, page, &curchap);
for (int i = 0; i < maxshow; i++) { for (int i = 0; i < maxshow; i++) {
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue; int hl = 0;
if (nchap > 0) {
if (rd_book_chapter(g_book, i, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
hl = (i == curchap);
} else {
int sg = -1; int32_t cch = 0;
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
hl = (page == sg);
}
int y0 = top + i * row; int y0 = top + i * row;
if (page == sg) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer); if (hl) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer);
meow_ui_text(24, y0, t, 20, th->fg); meow_ui_text(24, y0, t, 20, th->fg);
meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline); meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline);
} }
meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim); meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim);
} else { } else {
g_dir_open = 0; g_dir_open = 0;
meow_ui_text(20, g_top + 40, "本书无目录", 20, th->dim); meow_ui_text(20, g_top + 40, (rd_book_chapter_n(g_book) == 0) ? "本书无章节" : "本书无目录", 20, th->dim);
} }
} }
} }
@@ -629,12 +1013,13 @@ static void reader_scrub_preview(int page)
static void reader_scrub_begin(int x, int y) static void reader_scrub_begin(int x, int y)
{ {
(void)x; (void)x;
if (!g_book || g_menu_open || g_dir_open) return; if (!g_book || g_menu_open || g_dir_open || g_jump_open) return;
if (g_pw - x > RD_SCRUB_W) return; if (g_pw - x > RD_SCRUB_W) return;
if (y < rect_y0() || y >= rect_y1()) return; if (y < rect_y0() || y >= rect_y1()) return;
int np = rd_book_pages(g_book); int np = rd_book_pages(g_book);
if (np <= 1) return; if (np <= 1) return;
g_scrub = 1; g_scrub = 1;
g_scrub_moved = 0; /* 每次触控重置;拖动(touch_move)才置 1,区分点按/刮擦 */
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
reader_scrub_preview(scrub_page_for_y(y)); reader_scrub_preview(scrub_page_for_y(y));
} }
@@ -710,9 +1095,10 @@ int reader_open(const char *path)
reader_alloc_page_bufs(); /* 重建 cur+next 两槽;g_anim_old 翻页时懒分配 */ reader_alloc_page_bufs(); /* 重建 cur+next 两槽;g_anim_old 翻页时懒分配 */
} }
/* 复位动画/刮擦状态(跨书) */ /* 复位动画/刮擦/覆盖层状态(跨书) */
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0;
g_chap_cache_idx = -1;
/* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */ /* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */
if (g_book && !rd_book_is_image(g_book)) { if (g_book && !rd_book_is_image(g_book)) {
@@ -739,6 +1125,23 @@ int reader_open(const char *path)
return 0; return 0;
} }
/* ---------- R3.4 章节重解析(app/settings.c 调用) ---------- */
int reader_has_chapter_book(void)
{
if (!g_book || rd_book_is_image(g_book)) return 0;
/* TXT + 分段流式(seg 模式)即支持章节解析;用 rd_book_seg_n(不触发扫描)判定。 */
return (rd_book_seg_n(g_book) > 0) ? 1 : 0;
}
int reader_reparse_chapters(void)
{
if (!g_book) return -1;
int rc = rd_book_reparse_chapters(g_book);
if (rc >= 0) g_chap_cache_idx = -1; /* 章节列表变化 → 章范围缓存失效 */
if (g_reader_active) reader_redraw();
return rc;
}
/* ---------- 视图生命周期 ---------- */ /* ---------- 视图生命周期 ---------- */
static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; } static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void reader_on_enter(meow_view *v, void *ctx) static void reader_on_enter(meow_view *v, void *ctx)
@@ -766,7 +1169,8 @@ static void reader_on_destroy(meow_view *v, void *ctx)
if (g_book) rd_book_close(g_book); if (g_book) rd_book_close(g_book);
g_book = NULL; g_book = NULL;
reader_free_page_bufs(); reader_free_page_bufs();
g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_scrub = 0; g_reader_active = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0; g_scrub = 0; g_reader_active = 0;
g_chap_cache_idx = -1;
} }
meow_view *reader_view_create(void) meow_view *reader_view_create(void)
@@ -785,28 +1189,52 @@ void reader_on_tap(int x, int y)
{ {
(void)x; (void)x;
if (!g_book) { return; } if (!g_book) { return; }
if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾 */ if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾(仅真实拖动过才跳;点按右缘=翻下一页) */
g_scrub = 0; g_scrub = 0;
reader_scrub_jump(g_scrub_page); if (g_scrub_moved) { reader_scrub_jump(g_scrub_page); return; }
/* 未拖动:当作点按,落到下方三区翻页逻辑(右 1/3 下一页),避免右缘误触跳远页 */
}
if (g_jump_open) {
/* 跳转弹层:快捷/键盘/条目 → 执行;点空白关闭 */
int a = reader_jump_hit(x, y);
reader_jump_action(a);
return; return;
} }
if (g_dir_open) { if (g_dir_open) {
/* 目录覆盖层:点条目跳转,点空白关闭 */ /* 目录覆盖层:TXT 章节(复用 rd_book 章节数据)或 rdsrc 目录;点条目跳转,点空白关闭 */
int n = rd_book_toc_n(g_book); int nchap = 0;
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
int n = (nchap > 0) ? nchap : rd_book_toc_n(g_book);
if (n > 0 && x >= 0 && y >= g_dir_top) { if (n > 0 && x >= 0 && y >= g_dir_top) {
int row = g_dir_row_h > 0 ? g_dir_row_h : 34; int row = g_dir_row_h > 0 ? g_dir_row_h : 34;
int idx = (y - g_dir_top) / row; int idx = (y - g_dir_top) / row;
if (idx < n && idx >= 0) { if (idx < n && idx >= 0) {
char t[96]; int sg; int32_t cch; if (nchap > 0) {
if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) { int64_t co = 0;
if (g_book && sg >= 0) { if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) == 0) {
rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls); int sp = rd_book_page_for_char(g_book, co);
g_dir_open = 0; g_menu_open = 0; if (sp >= 0) {
snprintf(g_chapter, sizeof g_chapter, "%s", t); rd_book_set_page(g_book, sp);
reader_refill_page(); g_dir_open = 0; g_menu_open = 0;
reader_redraw(); g_chap_cache_idx = -1;
reader_reflow_persist(); reader_refill_page();
return; reader_redraw();
reader_reflow_persist();
return;
}
}
} else {
char t[96]; int sg; int32_t cch;
if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) {
if (g_book && sg >= 0) {
rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls);
g_dir_open = 0; g_menu_open = 0;
snprintf(g_chapter, sizeof g_chapter, "%s", t);
reader_refill_page();
reader_redraw();
reader_reflow_persist();
return;
}
} }
} }
} }
@@ -865,14 +1293,18 @@ void reader_on_tap(int x, int y)
reader_reflow_persist(); reader_reflow_persist();
meow_view_page_pop(); meow_view_page_pop();
return; return;
case 1: /* 目录 */ case 1: /* 目录(TXT 章节 → 章节列表;无章节显示「本书无章节」) */
if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); } g_dir_open = 1; g_menu_open = 0; reader_redraw();
return;
case 2: /* 跳转(章节/文件·段模式) */
if (rd_book_chapter_n(g_book) > 0 || rd_book_seg_n(g_book) > 0) { reader_jump_open(); }
else if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
else { g_menu_open = 0; reader_redraw(); } else { g_menu_open = 0; reader_redraw(); }
return; return;
case 2: /* 界面(阅读排版设置) */ case 3: /* 界面(阅读排版设置) */
g_ui_open = 1; g_menu_open = 0; reader_redraw(); g_ui_open = 1; g_menu_open = 0; reader_redraw();
return; return;
case 3: /* 设置 → App 设置页 */ case 4: /* 设置 → App 设置页 */
reader_reflow_persist(); reader_reflow_persist();
meow_view_page_push(ACT_SETTINGS); meow_view_page_push(ACT_SETTINGS);
return; return;
@@ -901,6 +1333,7 @@ void reader_on_swipe(int dir, int dx, int dy)
* 刮擦(reader_scrub_begin,由 touch_down/move 驱动)两条既有路径。 */ * 刮擦(reader_scrub_begin,由 touch_down/move 驱动)两条既有路径。 */
if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; } if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; }
if (g_dir_open) { g_dir_open = 0; reader_redraw(); return; } if (g_dir_open) { g_dir_open = 0; reader_redraw(); return; }
if (g_jump_open) { g_jump_open = 0; reader_redraw(); return; }
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; } if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
if (dir == MEOW_SWIPE_LEFT || dir == MEOW_SWIPE_RIGHT) { if (dir == MEOW_SWIPE_LEFT || dir == MEOW_SWIPE_RIGHT) {
reader_reflow_persist(); reader_reflow_persist();
@@ -913,7 +1346,7 @@ void reader_on_touch_down(int x, int y) { reader_scrub_begin(x, y); }
void reader_on_touch_move(int x, int y) void reader_on_touch_move(int x, int y)
{ {
(void)x; (void)x;
if (g_scrub && g_book) reader_scrub_preview(scrub_page_for_y(y)); if (g_scrub && g_book) { g_scrub_moved = 1; reader_scrub_preview(scrub_page_for_y(y)); }
} }
/* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数; /* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数;
@@ -932,9 +1365,12 @@ void reader_on_tick(uint64_t now)
if (g_volpage_pending) { if (g_volpage_pending) {
int dir = g_volpage_pending > 0 ? 1 : -1; int dir = g_volpage_pending > 0 ? 1 : -1;
g_volpage_pending = 0; g_volpage_pending = 0;
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open) if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
reader_turn_animated(dir); reader_turn_animated(dir);
} }
/* Phase 2C:空闲预取下一段(距段界 ≤2% 时提前解码进窗口,翻段界零突发) */
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
rd_book_prefetch(g_book);
/* R3.2:进度实时落盘(翻页/刮擦/字号/编码变更后,节流 ~1s 定时写)——意外崩溃重开不丢进度 */ /* R3.2:进度实时落盘(翻页/刮擦/字号/编码变更后,节流 ~1s 定时写)——意外崩溃重开不丢进度 */
if (g_prog_dirty && now - g_prog_last_save_ms >= 1000) { if (g_prog_dirty && now - g_prog_last_save_ms >= 1000) {
g_prog_last_save_ms = now; g_prog_last_save_ms = now;
+38 -8
View File
@@ -20,10 +20,16 @@ static meow_widget *g_appbar;
static meow_widget *g_list; static meow_widget *g_list;
static int g_active; static int g_active;
/* 行项枚举(顺序 = 列表顺序) */ /* 行项枚举(顺序 = 列表顺序)。ITEM_BASE = 固定的全局项数;重解析本书章节仅在
enum { ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR, ITEM_COUNT }; * 已打开一本「支持章节解析」(TXT+seg 模式)的书时追加(见 settings_on_enter)。 */
enum {
ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR,
ITEM_REPARSE, ITEM_COUNT
};
#define ITEM_BASE ITEM_REPARSE /* 7:不含重解析的固定项数 */
static int g_nitems = ITEM_BASE;
static const char *const g_labels[ITEM_COUNT] = { static const char *const g_labels[ITEM_COUNT] = {
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条" "默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条", "重解析本书章节"
}; };
static void settings_redraw(void); static void settings_redraw(void);
@@ -61,6 +67,7 @@ static void item_value(int item, char *out, int outsz)
case ITEM_SHELF: snprintf(out, (size_t)outsz, "%s", shelf_label(c)); break; case ITEM_SHELF: snprintf(out, (size_t)outsz, "%s", shelf_label(c)); break;
case ITEM_HDR: snprintf(out, (size_t)outsz, "%s", c->header ? "显示" : "隐藏"); break; case ITEM_HDR: snprintf(out, (size_t)outsz, "%s", c->header ? "显示" : "隐藏"); break;
case ITEM_FTR: snprintf(out, (size_t)outsz, "%s", c->footer ? "显示" : "隐藏"); break; case ITEM_FTR: snprintf(out, (size_t)outsz, "%s", c->footer ? "显示" : "隐藏"); break;
case ITEM_REPARSE: snprintf(out, (size_t)outsz, "%s", "点按重新扫描章节"); break;
default: snprintf(out, (size_t)outsz, "-"); break; default: snprintf(out, (size_t)outsz, "-"); break;
} }
} }
@@ -69,15 +76,15 @@ static void item_value(int item, char *out, int outsz)
static int settings_src(int idx, meow_list_item *out, void *ctx) static int settings_src(int idx, meow_list_item *out, void *ctx)
{ {
(void)ctx; (void)ctx;
if (idx < 0 || idx >= ITEM_COUNT) return 0; if (idx < 0 || idx >= g_nitems || idx >= ITEM_COUNT) return 0;
static char valbuf[16]; static char valbuf[48];
item_value(idx, valbuf, sizeof valbuf); item_value(idx, valbuf, sizeof valbuf);
out->icon_kind = MEOW_LIST_ICON_NONE; out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = g_labels[idx]; out->title = g_labels[idx];
out->subtitle = valbuf; out->subtitle = valbuf;
out->right = MEOW_LIST_RIGHT_ARROW; out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->on = 0;
out->enabled = 1; out->enabled = (idx == ITEM_REPARSE) ? reader_has_chapter_book() : 1;
return 1; return 1;
} }
@@ -147,6 +154,7 @@ static void apply_choice(int item, int sel)
/* 打开一个 SELECT 对话框并应用。 */ /* 打开一个 SELECT 对话框并应用。 */
static void settings_open_choice(int item) static void settings_open_choice(int item)
{ {
if (item == ITEM_REPARSE) return; /* 重解析走 settings_reparse(非 SELECT) */
const rdconf *c = rdconf_get(); const rdconf *c = rdconf_get();
if (!c) return; if (!c) return;
const char *title = g_labels[item]; const char *title = g_labels[item];
@@ -191,6 +199,25 @@ static void settings_redraw(void)
meow_ui_flush(); meow_ui_flush();
} }
/* R3.4 «重解析本书章节»:确认 → 清缓存+重扫 → 结果对话框(成功/失败/进行中=确认弹窗)。 */
static void settings_reparse(void)
{
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "重解析本书章节?\n将清除章节缓存并重新扫描全书。");
if (!d) return;
meow_dialog_show(d);
int ok = meow_dialog_result(d);
meow_dialog_free(d);
if (ok != 1) { settings_redraw(); return; } /* 取消 */
int rc = reader_reparse_chapters(); /* 阻塞:重扫全书段 */
char msg[120];
if (rc >= 0) snprintf(msg, sizeof msg, "重解析完成:共识别 %d 章(已保存缓存)", rc);
else if (rc == -2) snprintf(msg, sizeof msg, "重解析失败:扫描出错,已回退为无章节(未保存)");
else snprintf(msg, sizeof msg, "该书不支持章节解析");
meow_dialog *r = meow_dialog_create(MEOW_DLG_INFO, msg);
if (r) { meow_dialog_show(r); meow_dialog_result(r); meow_dialog_free(r); }
settings_redraw();
}
/* view 生命周期 */ /* view 生命周期 */
static void settings_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; } static void settings_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void settings_on_enter(meow_view *v, void *ctx) static void settings_on_enter(meow_view *v, void *ctx)
@@ -203,7 +230,9 @@ static void settings_on_enter(meow_view *v, void *ctx)
if (g_appbar) meow_topappbar_set_back(g_appbar, 1); if (g_appbar) meow_topappbar_set_back(g_appbar, 1);
if (g_list) { meow_widget_free(g_list); g_list = NULL; } if (g_list) { meow_widget_free(g_list); g_list = NULL; }
int ly = sbh + SET_APPBAR_H + 8; int ly = sbh + SET_APPBAR_H + 8;
g_list = meow_listview_create(SET_X, ly, SET_W, 720 - ly, ITEM_COUNT, /* 仅当打开的书「支持章节解析」(TXT+seg)才显示「重解析本书章节」项 */
g_nitems = ITEM_BASE + (reader_has_chapter_book() ? 1 : 0);
g_list = meow_listview_create(SET_X, ly, SET_W, 720 - ly, g_nitems,
settings_src, NULL, MEOW_LIST_SEL_SINGLE); settings_src, NULL, MEOW_LIST_SEL_SINGLE);
if (g_list) { if (g_list) {
meow_listview_set_container(g_list, 1); meow_listview_set_container(g_list, 1);
@@ -240,7 +269,8 @@ int settings_on_tap(int x, int y)
} }
if (g_list) { if (g_list) {
int idx = meow_listview_tap_at(g_list, x, y); int idx = meow_listview_tap_at(g_list, x, y);
if (idx >= 0 && idx < ITEM_COUNT) { if (idx >= 0 && idx < g_nitems && idx < ITEM_COUNT) {
if (idx == ITEM_REPARSE) { settings_reparse(); return 1; }
settings_open_choice(idx); settings_open_choice(idx);
return 1; return 1;
} }
+7 -3
View File
@@ -95,6 +95,10 @@ echo "==> host seg test (R3.2)"
$HOST_CC -O2 -Wall -Wextra -Icore test/seg_smoke.c core/seg.c core/rdenc.c -o test/seg_smoke_host -lm $HOST_CC -O2 -Wall -Wextra -Icore test/seg_smoke.c core/seg.c core/rdenc.c -o test/seg_smoke_host -lm
./test/seg_smoke_host ./test/seg_smoke_host
echo "==> host chapter test (R3.3)"
$HOST_CC -O2 -Wall -Wextra -Icore test/chapter_smoke.c core/rdchapter.c core/seg.c core/rdenc.c -o test/chapter_smoke_host -lm
./test/chapter_smoke_host
echo "==> host loaders test" echo "==> host loaders test"
HOBJ="$OBJ/host"; mkdir -p "$HOBJ" HOBJ="$OBJ/host"; mkdir -p "$HOBJ"
for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do
@@ -119,21 +123,21 @@ timeout 120 qemu-mipsel ./test/rdft_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-
# ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ---------- # ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ----------
echo "==> mipsel book smoke (TXT)" echo "==> mipsel book smoke (TXT)"
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/book_smoke.c \ $CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/book_smoke.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \ app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \ core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel "$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel
timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
echo "==> mipsel formats smoke" echo "==> mipsel formats smoke"
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/formats_smoke.c \ $CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/formats_smoke.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \ app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \ core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/formats_smoke_mipsel "$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/formats_smoke_mipsel
timeout 150 qemu-mipsel ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" /tmp/rd_samples timeout 150 qemu-mipsel ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" /tmp/rd_samples
# ---------- 7) reader 二进制(mipsel 静态) ---------- # ---------- 7) reader 二进制(mipsel 静态) ----------
echo "==> reader binary" echo "==> reader binary"
CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c" CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c"
ENGINE_L="engine/rdft.c" ENGINE_L="engine/rdft.c"
APP_L="app.c app/shelf.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c" APP_L="app.c app/shelf.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
$CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \ $CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
+528
View File
@@ -0,0 +1,528 @@
/* core/rdchapter.c — TXT 章节规范解析器实现(见 core/rdchapter.h)。 */
#include "rdchapter.h"
#include "seg.h"
#include "rdtxt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------------- 基础字符工具 ---------------- */
static int ascii_digit_val(uint32_t cp)
{
return (cp >= '0' && cp <= '9') ? (int)(cp - '0') : -1;
}
/* CJK / 全角宽度 = 2,其余 = 1(近似;不含零宽组合符)。 */
static int cp_width(uint32_t cp)
{
if (cp == 0x3000 || cp == 0x3001 || cp == 0x3002) return 2;
if ((cp >= 0x2E80 && cp <= 0x303E) || (cp >= 0x3040 && cp <= 0x30FF) ||
(cp >= 0x3100 && cp <= 0x318F) || (cp >= 0x3400 && cp <= 0x4DBF) ||
(cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xA000 && cp <= 0xA4CF) ||
(cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) ||
(cp >= 0xFE30 && cp <= 0xFE4F) || (cp >= 0xFF00 && cp <= 0xFF60) ||
(cp >= 0xFFE0 && cp <= 0xFFE6)) return 2;
return 1;
}
static int disp_width(const uint32_t *cps, int s, int e)
{
int w = 0;
for (int i = s; i < e; i++) w += cp_width(cps[i]);
return w;
}
/* 章节标记字符集:章 回 节 卷 集 部 篇。 */
static int is_marker(uint32_t cp)
{
return cp == 0x7AE0 || cp == 0x56DE || cp == 0x8282 ||
cp == 0x5377 || cp == 0x96C6 || cp == 0x90E8 || cp == 0x7BC7;
}
/* 卷 部 集(卷/集类章节前缀)。 */
static int is_volmark(uint32_t cp)
{
return cp == 0x5377 /* 卷 */ || cp == 0x90E8 /* 部 */ || cp == 0x96C6 /* 集 */;
}
/* 中文数字字符(含位权)。 */
static int is_cn_num_char(uint32_t cp)
{
return cp == 0x96F6 /* 零 */ || cp == 0x4E00 /* 一 */ || cp == 0x4E8C /* 二 */ ||
cp == 0x4E09 /* 三 */ || cp == 0x56DB /* 四 */ || cp == 0x4E94 /* 五 */ ||
cp == 0x516D /* 六 */ || cp == 0x4E03 /* 七 */ || cp == 0x516B /* 八 */ ||
cp == 0x4E5D /* 九 */ || cp == 0x5341 /* 十 */ || cp == 0x767E /* 百 */ ||
cp == 0x5343 /* 千 */ || cp == 0x4E07 /* 万 */ || cp == 0x4E24 /* 两 */;
}
static int cn_digit_val(uint32_t cp)
{
switch (cp) {
case 0x96F6: return 0; /* 零 */
case 0x4E00: return 1; /* 一 */
case 0x4E8C: return 2; /* 二 */
case 0x4E09: return 3; /* 三 */
case 0x56DB: return 4; /* 四 */
case 0x4E94: return 5; /* 五 */
case 0x516D: return 6; /* 六 */
case 0x4E03: return 7; /* 七 */
case 0x516B: return 8; /* 八 */
case 0x4E5D: return 9; /* 九 */
case 0x4E24: return 2; /* 两 */
default: return -1;
}
}
static int cn_weight_val(uint32_t cp)
{
switch (cp) {
case 0x5341: return 10; /* 十 */
case 0x767E: return 100; /* 百 */
case 0x5343: return 1000; /* 千 */
case 0x4E07: return 10000; /* 万 */
default: return 0;
}
}
/* 中文数字子串 [s,e) → 阿拉伯;非法/空返回 -1。实现:digit 累积 + 位权叠加。 */
static int cn_num_from_cps(const uint32_t *cps, int s, int e)
{
long total = 0, cur = 0, digit = 0;
int any = 0;
for (int i = s; i < e; i++) {
uint32_t cp = cps[i];
int dv = cn_digit_val(cp);
if (dv >= 0) { digit = dv; any = 1; continue; }
int w = cn_weight_val(cp);
if (w > 0) {
cur += (digit ? digit : 1) * (long)w;
digit = 0; any = 1;
continue;
}
return -1; /* 非中文数字字符 */
}
total += cur + digit;
return any ? (int)total : -1;
}
/* 行内正文标点计数:句号/逗号/顿号/分号/问号/感叹号/省略号 + ASCII . , ; ! ?
* 返回计数(区分「极少」:≤1 视为标题,>1 视为正文)。 */
static int sent_punct_count(const uint32_t *cps, int s, int e)
{
int n = 0;
for (int i = s; i < e; i++) {
uint32_t cp = cps[i];
if (cp == 0x3002 /* 。 */ || cp == 0xFF0C /* , */ || cp == 0x3001 /* 、 */ ||
cp == 0xFF1B /* ; */ || cp == 0xFF1F /* ? */ || cp == 0xFF01 /* ! */ ||
cp == 0x2026 /* … */ || cp == '.' || cp == ',' || cp == ';' ||
cp == '!' || cp == '?') n++;
}
return n;
}
/* cps[s..] 是否以 utf8 字面量为前缀(逐码点比较)。 */
static int cps_prefix(const uint32_t *cps, int s, int e, const uint8_t *utf8)
{
int i = s;
const uint8_t *p = utf8;
while (*p && i < e) {
uint32_t cp;
uint8_t b0 = *p;
int n;
if (b0 < 0x80) { cp = b0; n = 1; }
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | (p[1] & 0x3F); n = 2; }
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)(p[1] & 0x3F) << 6) | (p[2] & 0x3F); n = 3; }
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)(p[1] & 0x3F) << 12) | ((uint32_t)(p[2] & 0x3F) << 6) | (p[3] & 0x3F); n = 4; }
if (cps[i] != cp) return 0;
i++; p += n;
}
return (*p == 0) && (i > s);
}
/* ASCII 大小写无关前缀比较(用于 "chapter")。 */
static int cps_prefix_ci(const uint32_t *cps, int s, int e, const char *ascii)
{
int i = s;
for (const char *p = ascii; *p; p++) {
if (i >= e) return 0;
uint32_t cp = cps[i++];
uint32_t lo = (cp >= 'A' && cp <= 'Z') ? cp + 32 : cp;
char ch = *p;
if (lo != (uint32_t)ch) return 0;
}
return 1;
}
/* 罗马数字 → 阿拉伯(ivxl);解析失败 -1。 */
static int roman_to_int(const uint32_t *cps, int s, int e, int *end)
{
struct { uint32_t cp; int v; } tbl[] = {
{ 'i', 1 }, { 'v', 5 }, { 'x', 10 }, { 'l', 50 }, { 'c', 100 },
};
int vals[32], k = 0;
int i = s;
while (i < e && k < 32) {
uint32_t cp = cps[i];
if (cp >= 'A' && cp <= 'Z') cp += 32; /* 小写化 */
int v = 0;
for (int t = 0; t < 5; t++) if (tbl[t].cp == cp) { v = tbl[t].v; break; }
if (v == 0) break;
vals[k++] = v; i++;
}
if (k == 0) return -1;
int total = 0, prev = 0;
for (int j = k - 1; j >= 0; j--) {
int v = vals[j];
if (v < prev) total -= v; else total += v;
prev = v;
}
*end = i;
return total;
}
/* ---- 英文数字词(Chapter one / twenty-one / two hundred fifty ...)→ 阿拉伯 ---- */
static int cps_ascii_word_eq_ci(const uint32_t *cps, int s, int e, const char *w)
{
int len = (int)strlen(w);
if (e - s != len) return 0;
for (int k = 0; k < len; k++) {
uint32_t c = cps[s + k];
if (c >= 'A' && c <= 'Z') c += 32;
if (c != (uint32_t)(unsigned char)w[k]) return 0;
}
return 1;
}
/* 英文数字词一 → 值;不识别返回 -1。覆盖 one..nineteen、twenty..ninety、hundred、thousand。 */
static int en_word_val(const uint32_t *cps, int s, int e)
{
static const struct { const char *w; int v; } tbl[] = {
{ "one", 1 }, { "two", 2 }, { "three", 3 }, { "four", 4 }, { "five", 5 },
{ "six", 6 }, { "seven", 7 }, { "eight", 8 }, { "nine", 9 }, { "ten", 10 },
{ "eleven", 11 }, { "twelve", 12 }, { "thirteen", 13 }, { "fourteen", 14 },
{ "fifteen", 15 }, { "sixteen", 16 }, { "seventeen", 17 }, { "eighteen", 18 },
{ "nineteen", 19 }, { "twenty", 20 }, { "thirty", 30 }, { "forty", 40 },
{ "fifty", 50 }, { "sixty", 60 }, { "seventy", 70 }, { "eighty", 80 },
{ "ninety", 90 }, { "hundred", 100 }, { "thousand", 1000 },
};
for (int t = 0; t < (int)(sizeof tbl / sizeof tbl[0]); t++)
if (cps_ascii_word_eq_ci(cps, s, e, tbl[t].w)) return tbl[t].v;
return -1;
}
/* 解析一段英文数字词(one / twenty-one / two hundred fifty ...)。*end=末字符位置。
* 失败(无词/未知词/越界/数值越界)返回 -1。 */
static int word_num_from_cps(const uint32_t *cps, int s, int e, int *end)
{
long total = 0, cur = 0;
int i = s, words = 0, last_end = s;
while (i < e) {
while (i < e && (cps[i] == ' ' || cps[i] == '-' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
int ws = i;
while (i < e && ((cps[i] >= 'a' && cps[i] <= 'z') || (cps[i] >= 'A' && cps[i] <= 'Z'))) i++;
if (i == ws) break;
int v = en_word_val(cps, ws, i);
if (v < 0) return -1;
if (v == 100 || v == 1000) { long base = (cur == 0) ? 1 : cur; total += base * (long)v; cur = 0; }
else cur += (long)v;
words++;
last_end = i;
}
if (words == 0) return -1;
long n = total + cur;
if (n <= 0 || n > 100000) return -1;
*end = last_end;
return (int)n;
}
/* 把 cps[s..e) 转成 UTF-8 到 out(有界)。 */
static void cps_to_utf8(const uint32_t *cps, int s, int e, char *out, int outsz)
{
int o = 0;
for (int i = s; i < e; i++) {
uint8_t b[4];
int n = rd_cp_to_utf8(cps[i], b);
if (o + n + 1 > outsz) break;
for (int k = 0; k < n; k++) out[o++] = (char)b[k];
}
out[o] = 0;
}
/* 特殊前缀章节(楔子/序章/序言/引子/前言/序幕/尾声/后记/番外/外传/终章/大结局/正文)。 */
static const uint8_t *const g_special[] = {
(const uint8_t *)"\xE6\xA5\x94\xE5\xAD\x90", /* 楔子 */
(const uint8_t *)"\xE5\xBA\x8F\xE7\xAB\xA0", /* 序章 */
(const uint8_t *)"\xE5\xBA\x8F\xE8\xA8\x80", /* 序言 */
(const uint8_t *)"\xE5\xBC\x95\xE5\xAD\x90", /* 引子 */
(const uint8_t *)"\xE5\x89\x8D\xE8\xA8\x80", /* 前言 */
(const uint8_t *)"\xE5\xBA\x8F\xE5\xB9\x95", /* 序幕 */
(const uint8_t *)"\xE5\xB0\xBE\xE5\xA3\xB0", /* 尾声 */
(const uint8_t *)"\xE5\x90\x8E\xE8\xAE\xB0", /* 后记 */
(const uint8_t *)"\xE7\x95\xAA\xE5\xA4\x96", /* 番外 */
(const uint8_t *)"\xE5\xA4\x96\xE4\xBC\xA0", /* 外传 */
(const uint8_t *)"\xE7\xBB\x88\xE7\xAB\xA0", /* 终章 */
(const uint8_t *)"\xE5\xA4\xA7\xE7\xBB\x93\xE5\xB1\x80", /* 大结局 */
(const uint8_t *)"\xE6\xAD\xA3\xE6\x96\x87", /* 正文 */
};
#define G_SPECIAL_N ((int)(sizeof g_special / sizeof g_special[0]))
/* 判断一行(cps[ls..le),ls=首个非空白字符)是否为章节标题行。
* 命中则填 title(完整行)与 *num;返回 1,否则 0。 */
static int chap_match_line(const uint32_t *cps, int ls, int le, char *title, int outsz, int *num)
{
if (ls >= le) return 0;
if (disp_width(cps, ls, le) > RDCHAP_DISP_WMAX) return 0;
/* A/B:`第` + 阿拉伯/中文数字 + 章回节卷集部篇 */
if (cps[ls] == 0x7B2C /* 第 */) {
int i = ls + 1;
int numv = -1;
int dstart = i;
while (i < le && ascii_digit_val(cps[i]) >= 0) i++;
if (i > dstart) {
if (i - dstart > 4) return 0;
numv = 0;
for (int k = dstart; k < i; k++) numv = numv * 10 + ascii_digit_val(cps[k]);
} else {
int cstart = i;
while (i < le && is_cn_num_char(cps[i])) i++;
if (i == cstart) return 0;
if (i - cstart > 8) return 0;
numv = cn_num_from_cps(cps, cstart, i);
if (numv < 0) return 0;
}
if (i >= le || !is_marker(cps[i])) return 0;
i++; /* 消费标记 */
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* C:阿拉伯数字 + 可选分隔符(、.. 空格 全角空格) + 章回节卷集部篇 */
if (ascii_digit_val(cps[ls]) >= 0) {
int i = ls, numv = 0, cnt = 0;
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
if (cnt < 1 || cnt > 4) return 0;
if (i < le && (cps[i] == 0x3001 || cps[i] == '.' || cps[i] == ' ' || cps[i] == 0x3000)) i++;
if (i >= le || !is_marker(cps[i])) return 0;
i++;
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* F:卷/部/集 + 空白 + 中文数字(卷一 / 卷 一 / 集三) */
if (is_volmark(cps[ls])) {
int i = ls + 1;
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000)) i++;
int cstart = i;
while (i < le && is_cn_num_char(cps[i])) i++;
if (i == cstart) return 0;
int numv = cn_num_from_cps(cps, cstart, i);
if (numv < 0) return 0;
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* E:Chapter N(N=阿拉伯 / 罗马数字 / 英文数字词 one..hundred) */
if (cps_prefix_ci(cps, ls, le, "chapter")) {
int i = ls + 7;
if (i >= le) return 0;
if (!(cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) return 0; /* 必须空白:ChapterX 不认 */
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
int numv;
if (ascii_digit_val(cps[i]) >= 0) {
numv = 0; int cnt = 0;
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
if (cnt < 1 || cnt > 5) return 0;
} else {
int endi = i;
numv = roman_to_int(cps, i, le, &endi);
if (numv < 0) { endi = i; numv = word_num_from_cps(cps, i, le, &endi); }
if (numv < 0) return 0;
i = endi;
}
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* D:特殊前缀(楔子/序章/…)——要求整行无正文标点(避免「正文内容。」/「番外正文。」误判) */
for (int t = 0; t < G_SPECIAL_N; t++) {
if (cps_prefix(cps, ls, le, g_special[t])) {
int i = ls;
for (const uint8_t *p = g_special[t]; *p; ) { /* 消费前缀码点 */
uint8_t b0 = *p;
int n = (b0 < 0x80) ? 1 : ((b0 & 0xE0) == 0xC0) ? 2 : ((b0 & 0xF0) == 0xE0) ? 3 : 4;
i++; p += n;
}
if (sent_punct_count(cps, i, le) > 0) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = -1;
return 1;
}
}
return 0;
}
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c)
{
if (!c) return -1;
rdchapter_init(c);
if (!seg_dir || !seg_dir[0]) return 0; /* 无段目录 → 无章节 */
int nsegs = rdseg_n(seg_dir);
if (nsegs < 0) return -1;
if (nsegs == 0) { c->nsegs = 0; return 0; }
c->seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
if (!c->seg_char_start) return -6;
c->nsegs = nsegs;
int ent_cap = 0;
int64_t global = 0;
int err = 0;
uint8_t *raw = NULL; size_t rawcap = 0;
uint8_t *utf8 = NULL; size_t utf8cap = 0;
uint32_t *cps = NULL;
for (int seg = 0; seg < nsegs && !err; seg++) {
size_t rawlen = 0;
if (rdseg_rawlen(seg_dir, seg, &rawlen) != 0) { err = 1; break; }
if (rawlen > rawcap) {
uint8_t *t = (uint8_t *)realloc(raw, rawlen ? rawlen : 1);
if (!t) { err = -6; break; }
raw = t; rawcap = rawlen;
}
size_t rn = 0;
if (rdseg_load(seg_dir, seg, raw, rawlen, &rn) != 0) { err = 2; break; }
size_t ulen = 0;
size_t need = rawlen * 3 / 2 + 16;
if (need < RDTXT_SEG_UTF8_MAX + 8) need = RDTXT_SEG_UTF8_MAX + 8;
if (need > utf8cap) {
uint8_t *t = (uint8_t *)realloc(utf8, need);
if (!t) { err = -6; break; }
utf8 = t; utf8cap = need;
}
if (rd_to_utf8(enc, raw, rn, utf8, utf8cap, &ulen) != RD_ENC_OK) { err = 3; break; }
size_t n = 0;
/* 解码为码点失败(多为内存不足)→ 视作扫描失败:不落盘坏结果(见 rdchapter_scan 头部注释)。 */
if (rd_utf8_to_cp(utf8, ulen, &cps, &n) != RD_ENC_OK) { cps = NULL; err = -6; break; }
c->seg_char_start[seg] = global; /* 本段全局字符起点 */
if (cps && n > 0) {
int ls = 0;
while (ls < (int)n) {
int le = ls;
while (le < (int)n && cps[le] != '\n') le++;
int indent = 0, first = ls;
while (first < le && (cps[first] == ' ' || cps[first] == '\t' || cps[first] == 0x3000)) {
indent++; first++;
}
int te = le;
while (te > first && (cps[te - 1] == ' ' || cps[te - 1] == '\t' ||
cps[te - 1] == '\r' || cps[te - 1] == 0x3000)) te--;
if (indent <= RDCHAP_INDENT_MAX && first < te) {
char tbuf[RDCHAP_TITLE_MAX];
int num = -1;
if (chap_match_line(cps, first, te, tbuf, sizeof tbuf, &num) && c->n < RDCHAP_MAX_ENTRIES) {
if (ent_cap <= c->n) {
int nc = ent_cap ? ent_cap * 2 : 64;
if (nc > RDCHAP_MAX_ENTRIES) nc = RDCHAP_MAX_ENTRIES;
rdchapter_entry *ne = (rdchapter_entry *)realloc(c->entries, (size_t)nc * sizeof(rdchapter_entry));
if (!ne) { err = -6; break; }
c->entries = ne; ent_cap = nc;
}
rdchapter_entry *en = &c->entries[c->n];
memset(en, 0, sizeof *en);
snprintf(en->title, sizeof en->title, "%s", tbuf);
en->char_off = global + first;
en->seg_idx = seg;
en->seg_char = (int32_t)first;
en->number = num;
c->n++;
}
}
if (le >= (int)n) break;
ls = le + 1;
}
free(cps); cps = NULL;
}
global += (int64_t)n;
}
free(cps);
free(raw);
free(utf8);
if (err) { rdchapter_free(c); return err; }
/* 段数恒定:seg_char_start[nsegs] = 全书字符数(尾哨兵)。 */
if (global > (int64_t)0x7fffffffLL) { rdchapter_free(c); return -7; } /* 安全上限 */
c->seg_char_start[nsegs] = global;
/* 卷首/前置:第一章节前有内容 → 作为 entries[0](title=文件名或"卷首",号=-1)。 */
if (c->n > 0 && c->entries[0].char_off > 0) {
if (c->n >= RDCHAP_MAX_ENTRIES) { /* 已满则跳过卷首(理论) */ }
else {
memmove(&c->entries[1], &c->entries[0], (size_t)c->n * sizeof(rdchapter_entry));
rdchapter_entry *pre = &c->entries[0];
memset(pre, 0, sizeof *pre);
const char *bn = (book_basename && book_basename[0]) ? book_basename : "卷首";
strncpy(pre->title, bn, sizeof pre->title - 1);
pre->title[sizeof pre->title - 1] = 0;
pre->char_off = 0;
pre->seg_idx = 0;
pre->seg_char = 0;
pre->number = -1;
c->n++;
c->has_pre = 1;
snprintf(c->pre_title, sizeof c->pre_title, "%s", bn);
}
} else if (book_basename && book_basename[0]) {
snprintf(c->pre_title, sizeof c->pre_title, "%s", book_basename);
} else {
snprintf(c->pre_title, sizeof c->pre_title, "%s", "卷首");
}
return 0;
}
void rdchapter_init(rdchapter *c)
{
if (!c) return;
memset(c, 0, sizeof *c);
}
void rdchapter_free(rdchapter *c)
{
if (!c) return;
free(c->entries); c->entries = NULL;
free(c->seg_char_start); c->seg_char_start = NULL;
c->n = c->nsegs = c->has_pre = 0;
c->pre_title[0] = 0;
}
int rdchapter_cn_num(const char *s, int len)
{
if (!s || len <= 0) return -1;
uint32_t local[16];
int n = 0;
int i = 0;
while (i < len && n < 16) {
uint8_t b0 = (uint8_t)s[i];
uint32_t cp; int nb;
if (b0 < 0x80) { cp = b0; nb = 1; }
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | ((uint8_t)s[i + 1] & 0x3F); nb = 2; }
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 6) | ((uint8_t)s[i + 2] & 0x3F); nb = 3; }
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 12) | ((uint32_t)((uint8_t)s[i + 2] & 0x3F) << 6) | ((uint8_t)s[i + 3] & 0x3F); nb = 4; }
if (!is_cn_num_char(cp)) break;
local[n++] = cp;
i += nb;
}
if (n == 0) return -1;
return cn_num_from_cps(local, 0, n);
}
+61
View File
@@ -0,0 +1,61 @@
/* core/rdchapter.h — TXT 章节规范解析器:扫描已切好的 512KB 码位对齐段文件(core/seg.c),
* 识别章节标题行,输出章节列表(每项:标题、全书字符偏移、所在段/段内字符偏移、阿拉伯章号)。
*
* 设计约束:
* - 逐段流式(`rdseg_load` 读一个段 → `rd_to_utf8` 解码 → `rd_utf8_to_cp` 数码点 → 行扫),
* 不整书驻留;单段内存与 book.c `seg_ensure_built` 同量级(~512KB 原文 + ~3 倍 UTF-8)。
* - 章节标题行约束:行首缩进 ≤4 半角/全角空格;标题显示宽度 ≤ RDCHAP_DISP_WMAX(CJK=2/ASCII=1);
* 行内正文标点 ≤1(句号/逗号/省略号等,容「极少」);行首锚定 + 短 + 无成段标点 → 防误判
* (「他说第三章内容」不居行首即不命中;目录页交错等已知限制,见报告)。
* - 中文数字 → 阿拉伯(`rdchapter_cn_num`,用于排序/显示);按 char_off 升序输出。
* - 开头书名/简介区(第一章节之前的内容)归为「卷首/前置」,作为条目[0](title=文件名或"卷首")。
*
* 纯 libc + rdenc + seg,无 SDK/FreeType;宿主可测。
*/
#ifndef RDCHAPTER_H
#define RDCHAPTER_H
#include "rdenc.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RDCHAP_TITLE_MAX 160 /* 单个标题字符(UTF-8 字节)上限 */
#define RDCHAP_DISP_WMAX 40 /* 标题最大显示宽度(CJK=2 / ASCII=1) */
#define RDCHAP_INDENT_MAX 4 /* 行首缩进上限(空格/全角空格/制表,按字符计) */
#define RDCHAP_MAX_ENTRIES 200000 /* 章节数上限(防失控内存) */
typedef struct rdchapter_entry {
char title[RDCHAP_TITLE_MAX]; /* 完整标题行(如「第一章 风雨」「楔子」),UTF-8 */
int64_t char_off; /* 全书字符偏移(段 0 起,跨段累计;标题首字符) */
int32_t seg_idx; /* 所在 512KB 段下标 */
int32_t seg_char; /* 段内字符偏移(标题首字符) */
int32_t number; /* 阿拉伯章号;-1=无号(如楔子/卷首/序章) */
} rdchapter_entry;
typedef struct rdchapter {
rdchapter_entry *entries;
int n; /* 章节数;0=本书无章节(扫描过) */
int64_t *seg_char_start;/* seg_char_start[seg]=该段在全书中的字符起点;[nsegs]=全书字符数 */
int nsegs; /* 段数(= seg_char_start 数组长 -1) */
char pre_title[RDCHAP_TITLE_MAX]; /* 卷首/前置标题(文件名或"卷首") */
int has_pre; /* 1=存在卷首/前置(第一章节前有内容) */
} rdchapter;
void rdchapter_init(rdchapter *c);
void rdchapter_free(rdchapter *c);
/* 扫描 seg_dir 下全部 512KB 段文件,识别章节标题。book_basename 用于卷首标题(可为 NULL/"卷首")。
* 0=成功(c->n=章节数,可为 0);负=错误(seg_dir 无效/无段/解码失败等)。 */
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c);
/* 中文数字 → 阿拉伯(「二十三」→23,「一百零一」→101,「两」→2)。s/len 为 UTF-8;非法返回 -1。 */
int rdchapter_cn_num(const char *s, int len);
#ifdef __cplusplus
}
#endif
#endif /* RDCHAPTER_H */
+6 -1
View File
@@ -312,7 +312,12 @@ int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n)
size_t cnt = 0, i = 0; size_t cnt = 0, i = 0;
while (i < len) { while (i < len) {
uint8_t b = utf8[i]; uint8_t b = utf8[i];
if (b < 0x80) { arr[cnt++] = b; i++; continue; } if (b < 0x80) {
/* \r\n → \n、孤 \r → \n:TXT 行尾 \r 若进排版,在无 U+000D 映射的字体里落成
* .notdef(豆腐/占宽);归一化后章节/页表/进度锚同处一个无 \r 码点空间。 */
if (b == '\r') { arr[cnt++] = '\n'; i++; if (i < len && utf8[i] == '\n') i++; continue; }
arr[cnt++] = b; i++; continue;
}
unsigned q, lo, hi; unsigned q, lo, hi;
if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; } if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; } else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; }
+63
View File
@@ -226,6 +226,12 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
npages++; npages++;
page_start = np_start; page_start = np_start;
cur_y = 0; cur_y = 0;
/* 孤行上推修正:被推行(l-1)成为新页首行 → 按页首行重算其 y 并计入页内高度,
* 否则它带着上一页的 y 残值(渲染错位)且页填充少算一行高(页底溢出)。 */
if (np_start == l - 1) {
out->lines[l - 1].y = 0;
cur_y = lh;
}
} }
/* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */ /* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */
extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0; extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
@@ -245,6 +251,63 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
return RD_LAYOUT_OK; return RD_LAYOUT_OK;
} }
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
rd_line *lines, int max_lines, int *nlines, int32_t *next_start)
{
if (!in || !in->chars || in->n < 0 || !lines || !nlines || !next_start || max_lines <= 0)
return RD_LAYOUT_ERR_INVAL;
if (in->fs <= 0 || in->page_w <= 0 || in->page_h <= 0) return RD_LAYOUT_ERR_INVAL;
if (start < 0) start = 0;
if (start > in->n) start = in->n;
int lh = (in->fs * in->line_scale) / 100;
if (lh < 1) lh = 1;
int pspacing = lh / 2;
if (pspacing < 0) pspacing = 0;
int nl = 0;
int y = 0; /* 页内累计(含段距;规则同 rd_layout 阶段 B 单页填充) */
int32_t ps = start;
int first_para = 1; /* 首段:首行状态由 mid_para 给定(续行不缩进/非段首) */
while (ps < in->n) {
while (ps < in->n && (in->chars[ps] == '\n' || in->chars[ps] == '\r')) ps++;
if (ps >= in->n) break;
int32_t pe = ps;
while (pe < in->n && in->chars[pe] != '\n') pe++;
int32_t pos = ps;
int first_line = first_para ? !mid_para : 1;
first_para = 0;
while (pos < pe) {
rd_line ln;
para_break_line(in, pe, &pos, first_line, &ln);
first_line = 0;
/* 页填充判定(同 rd_layout 阶段 B:页内首行不加段距;溢出即页满) */
int extra = (ln.para_start && nl > 0) ? pspacing : 0;
if (y + lh + extra > in->page_h && nl > 0) {
/* 孤行守卫:页末行是段首行且段未结束 → 该行随段入下一页(同 rd_layout) */
int cut = nl;
if (lines[nl - 1].para_start && !lines[nl - 1].para_end) cut = nl - 1;
*nlines = cut;
*next_start = (cut < nl) ? lines[cut].s : ln.s;
return RD_LAYOUT_OK;
}
if (nl >= max_lines) { *nlines = nl; *next_start = ln.s; return RD_LAYOUT_ERR_OVER; }
ln.y = y + extra;
y += lh + extra;
lines[nl++] = ln;
if (ln.para_end) break;
}
ps = pos;
while (ps < in->n && in->chars[ps] == '\n') ps++;
if (pos >= pe && ps >= in->n) break;
}
*nlines = nl;
*next_start = in->n;
return RD_LAYOUT_OK;
}
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char) int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char)
{ {
if (!out || out->npages <= 0) return 0; if (!out || out->npages <= 0) return 0;
+8
View File
@@ -65,6 +65,14 @@ typedef struct rd_layout_out {
int rd_layout(const rd_layout_in *in, rd_layout_out *out); int rd_layout(const rd_layout_in *in, rd_layout_out *out);
/* 页级微排版(Phase 2C):从 chars[start](须为行首:start==0 或 chars[start-1]=='\n')起只排一页,
* 填满 page_h 即停。mid_para=1 表示 start 行是段中续行(不缩进、非段首行);=0 表示段首行
* (缩进 + para_start,与 rd_layout 首行规则一致)。输出 lines[0..*nlines)(内容区坐标,
* 与 rd_layout 同断行/分页/孤行守卫规则);*next_start = 下一页首字符偏移(== in->n 表示文末)。
* 一致性:同一 (chars, start, mid_para) 下产出 == rd_layout 全排版中该页的行区间(逐行 diff 可证)。 */
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
rd_line *lines, int max_lines, int *nlines, int32_t *next_start);
/* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。 /* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。
* 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */ * 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char); int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char);
+6 -4
View File
@@ -1,10 +1,11 @@
#!/bin/bash #!/bin/bash
# pack_reader_mpk.sh — 把 os.meow.reader 打成 .mpk(tar.gz),并把 LGPL 合规材料并入包内。 # pack_reader_mpk.sh — 把 os.meow.reader 打成 .mpk(tar.gz),并把 LGPL 合规材料并入包内。
# .mpk = app.json + reader 二进制 + icon.png + resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt} # .mpk = app.json + reader 二进制 + icon.png + resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt,FTL.txt}
# (不含 data/cache;符合 APPS.md §3 与 mips-repo/mpk/README.md 的 .mpk 规范) # (不含 data/cache;符合 APPS.md §3 与 mips-repo/mpk/README.md 的 .mpk 规范)
# #
# 合规目的:让「接收者随包拿到」NOTICE + GPL-3.0 全文 + LGPL-3.0 全文, # 合规目的:让「接收者随包拿到」NOTICE + GPL-3.0 全文 + LGPL-3.0 全文 + FreeType(FTL) 全文,
# 满足 LGPL-3.0 §4(a)(显著通知)与 §4(b)(随 Combined Work 附 GPL + LGPL 两份文本)。 # 满足 LGPL-3.0 §4(a)(显著通知)、§4(b)(随 Combined Work 附 GPL + LGPL 两份文本)与
# FTL 署名条款(随二进制分发附许可文本与版权声明)。
# #
# 用法: # 用法:
# ./build_reader.sh # 先编出 reader 二进制(本脚本不重编) # ./build_reader.sh # 先编出 reader 二进制(本脚本不重编)
@@ -31,6 +32,7 @@ cp icon.png "$STAGE/" 2>/dev/null || true
cp reader "$STAGE/" cp reader "$STAGE/"
mkdir -p "$STAGE/resources/licenses" mkdir -p "$STAGE/resources/licenses"
cp resources/licenses/NOTICE resources/licenses/GPL-3.0.txt resources/licenses/LGPL-3.0.txt \ cp resources/licenses/NOTICE resources/licenses/GPL-3.0.txt resources/licenses/LGPL-3.0.txt \
resources/licenses/FTL.txt \
"$STAGE/resources/licenses/" "$STAGE/resources/licenses/"
# busybox tar + gzip(与 launcher/nyai 同管道语义);-cz 直接出 . # busybox tar + gzip(与 launcher/nyai 同管道语义);-cz 直接出 .
@@ -40,6 +42,6 @@ echo "== packed $MPK =="
tar tzvf "$MPK" tar tzvf "$MPK"
echo "md5: $(md5sum "$MPK" | cut -d' ' -f1)" echo "md5: $(md5sum "$MPK" | cut -d' ' -f1)"
echo echo
echo "含合规材料:resources/licenses/NOTICE + GPL-3.0.txt + LGPL-3.0.txt" echo "含合规材料:resources/licenses/NOTICE + GPL-3.0.txt + LGPL-3.0.txt + FTL.txt"
echo "未签名。正式分发前 run:" echo "未签名。正式分发前 run:"
echo " ../meow_sign/meow_sign sign $MPK \"\$HOME/Project/R3ProII/.keys/signing.key\"" echo " ../meow_sign/meow_sign sign $MPK \"\$HOME/Project/R3ProII/.keys/signing.key\""
+5
View File
@@ -34,6 +34,11 @@ void reader_on_touch_move(int x, int y);
* 返回 0=成功(书已打开、页位图已备、可 push);非 0=失败(未能打开,调用方不 push)。 */ * 返回 0=成功(书已打开、页位图已备、可 push);非 0=失败(未能打开,调用方不 push)。 */
int reader_open(const char *path); int reader_open(const char *path);
/* R3.4 章节重解析(app/settings.c 调用):是否打开了支持章节解析的书(TXT+seg 模式)。
* reader_reparse_chapters 返回 n=章节数 / 0=无章节(成功)/ -1=不可用 / -2=扫描失败(未落盘)。 */
int reader_has_chapter_book(void);
int reader_reparse_chapters(void);
/* ---- 全局设置页(app/settings.c,C)---- */ /* ---- 全局设置页(app/settings.c,C)---- */
meow_view *settings_view_create(void); meow_view *settings_view_create(void);
int settings_is_active(void); int settings_is_active(void);
+105 -1
View File
@@ -20,10 +20,82 @@ static void write_txt(const char *path)
"它包含汉字、标点,以及一些 English words 混合排版。" "它包含汉字、标点,以及一些 English words 混合排版。"
"断行、避头尾与分页行为都要正确。\n", i + 1); "断行、避头尾与分页行为都要正确。\n", i + 1);
} }
fprintf(f, "\nChapter two: the quick brown fox jumps over the lazy dog.\n"); fprintf(f, "\n第二章 尾声\n这是一段章尾正文,用于验证章节跳转与章内翻页。\n");
fclose(f); fclose(f);
} }
/* ---- Phase 2A/2C:跨段拼接页连续性(硬用例)。
* 构造恰好 512KB 的段 0(5242×100B + 88B,末行为短段落),段 1 开头放唯一标记 SEG1MARKER;
* 建段 1 触发滚动拼接后,段界页(段 0 末页)应包含段 1 开头的文本(拼接页含两段文本),
* 且跨段往返翻页后锚 (seg,ch) 稳定。 ---- */
typedef struct { char *buf; size_t len, cap; } linecol;
static void linecol_add(int x, int y, const char *utf8, int len, void *ctx)
{
(void)x; (void)y;
linecol *lc = (linecol *)ctx;
if (lc->len + (size_t)len + 2 > lc->cap) {
size_t nc = lc->cap ? lc->cap * 2 : 4096;
while (nc < lc->len + (size_t)len + 2) nc *= 2;
char *nb = (char *)realloc(lc->buf, nc);
if (!nb) return;
lc->buf = nb; lc->cap = nc;
}
memcpy(lc->buf + lc->len, utf8, (size_t)len);
lc->len += (size_t)len;
lc->buf[lc->len++] = '\n';
lc->buf[lc->len] = 0;
}
static void phase2a_stitch(const char *font)
{
const char *txt = "/tmp/rdbook_stitch.txt";
FILE *f = fopen(txt, "wb");
char line[100];
memset(line, 'a', 99); line[99] = '\n';
for (int i = 0; i < 5242; i++) fwrite(line, 1, 100, f); /* 524200 B */
for (int i = 0; i < 88; i++) fputc('a', f); /* 共 524288 B = 512KB 码位边界 */
fprintf(f, "SEG1MARKER 段一开头唯一标记,证明拼接页跨段包含两段文本。\n");
for (int i = 0; i < 2000; i++)
fprintf(f, "段一正文第%d行:滚动拼接后段界无短页,页表与进度锚稳定。\n", i + 1);
fclose(f);
rd_book_set_seg_base("/tmp/meow_seg_stitch");
rd_book_set_index_dir("/tmp/meow_seg_stitch_index");
rd_book *b = NULL;
int rc = rd_book_open(&b, txt, font, 20, 130, 480, 720, 16, 20, 62);
CHECK(rc == 0, "stitch open");
if (rc != 0) { remove(txt); return; }
int nseg = rd_book_seg_n(b);
CHECK(nseg >= 2, "stitch multi-seg");
if (nseg >= 2) {
int p1 = rd_book_seg_page(b, 1); /* 建段 1 → 触发段 0 末页拼接 */
CHECK(p1 >= 0, "stitch seg1 built");
int ps = rd_book_seg_page(b, 0);
int pc = rd_book_seg_page_count(b, 0);
CHECK(ps == 0 && pc > 0, "stitch seg0 pages");
int bnd = ps + pc - 1;
linecol lc = { NULL, 0, 0 };
int rr = rd_book_render_text(b, bnd, linecol_add, &lc);
CHECK(rr == 0, "stitch boundary render_text ok");
CHECK(lc.buf && strstr(lc.buf, "SEG1MARKER") != NULL, "stitch boundary contains seg1 text");
free(lc.buf);
/* 跨段往返:段界页翻前/后,锚 (seg,ch) 不变 */
int as = -1; int32_t ach = -1;
rd_book_set_page(b, bnd);
rd_book_anchor(b, &as, &ach);
CHECK(as == 0 && ach >= 0, "stitch boundary anchor seg0");
int cur = rd_book_current(b);
int fwd = rd_book_turn(b, 1);
int back = rd_book_turn(b, -1);
CHECK(fwd == cur + 1 && back == cur, "stitch boundary turn roundtrip");
int as2 = -1; int32_t ach2 = -1;
rd_book_anchor(b, &as2, &ach2);
CHECK(as2 == as && ach2 == ach, "stitch boundary anchor stable");
}
rd_book_close(b);
remove(txt);
}
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf"; const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
@@ -82,8 +154,40 @@ int main(int argc, char **argv)
int np3 = rd_book_set_size(b, 20, 150); int np3 = rd_book_set_size(b, 20, 150);
CHECK(np3 >= 0 && np3 < rd_book_pages(b), "set_size reflow ok"); CHECK(np3 >= 0 && np3 < rd_book_pages(b), "set_size reflow ok");
/* R3.3 章节:解析 + 章节→页映射 + 章内翻页单调(页号递增不跳变) */
int nch = rd_book_chapter_n(b);
printf(" chapters=%d\n", nch);
CHECK(nch == 2, "book has 2 chapters");
if (nch == 2) {
char t0[160], t1[160]; int n0 = -1, n1 = -1; int64_t c0 = -1, c1 = -1;
CHECK(rd_book_chapter(b, 0, t0, sizeof t0, &c0, NULL, NULL, &n0) == 0, "chapter0 get");
CHECK(n0 == 1, "chapter0 number 1");
CHECK(c0 == 0, "chapter0 char_off 0");
int p0 = rd_book_page_for_char(b, c0);
CHECK(p0 == 0 && p0 < rd_book_pages(b), "chapter0 start page 0");
CHECK(rd_book_chapter(b, 1, t1, sizeof t1, &c1, NULL, NULL, &n1) == 0, "chapter1 get");
CHECK(n1 == 2, "chapter1 number 2");
int p1 = rd_book_page_for_char(b, c1);
CHECK(p1 >= p0 && p1 < rd_book_pages(b), "chapter1 start page in range");
printf(" chapter1('%s') at page %d (of %d)\n", t1, p1, rd_book_pages(b));
/* 跳到章二首页,验证章内翻页 +1 单调(不越过章界仍递增) */
int landed = rd_book_set_page(b, p1);
CHECK(landed == p1, "jump lands on chapter1 start");
int cur1 = rd_book_current(b);
int nxt1 = rd_book_turn(b, 1);
CHECK((nxt1 == cur1 + 1) || (nxt1 == cur1), "chapter-internal turn +1 (clamp-safe)");
/* R3.4 手动重解析(清 .chap 缓存 + 重扫)应幂等返回同章数,且章节仍可用 */
int nrep = rd_book_reparse_chapters(b);
CHECK(nrep == nch, "reparse returns same chapter count");
CHECK(rd_book_chapter_n(b) == nch, "reparse keeps chapters");
}
free(buf); free(buf);
rd_book_close(b); rd_book_close(b);
/* Phase 2A/2C:跨段拼接页连续性 + 段界锚稳定(另开一本 >512KB 的多段书) */
phase2a_stitch(font);
printf("book_smoke: %s\n", fails ? "FAIL" : "PASS"); printf("book_smoke: %s\n", fails ? "FAIL" : "PASS");
return fails ? 1 : 0; return fails ? 1 : 0;
} }
@@ -0,0 +1,20 @@
#!/bin/bash
# test/build_turn_repro.sh — cross-compile turn_repro for mipsel (mirrors book_smoke_mipsel link line).
set -e
cd "$(dirname "$0")/.."
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
VENDOR="$PWD/vendor"
OBJ="$VENDOR/build"
FT_BUILD="$VENDOR/freetype-build"
FT_SRC="$VENDOR/freetype-src"
FT_INCS="-I$FT_BUILD -I$FT_BUILD/include -I$FT_SRC/include"
COREINC="-Icore -Ivendor/miniz -Ivendor/stb -Ivendor/libmobi/src -Ivendor/zlib"
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC \
test/turn_repro.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" \
-o test/turn_repro_mipsel
echo "build OK -> test/turn_repro_mipsel"
+275
View File
@@ -0,0 +1,275 @@
/* test/chapter_smoke.c — os.meow.reader 章节规范解析器宿主单测(纯 C,无 SDK/Freetype)。
*
* 覆盖(按需求 5 样本 + 跨段 + 中文数字转换):
* C1 标准中文数字章(第一章/第二章/第十一章 → 1/2/11,char_off 单调)
* C2 阿拉伯章(第1章/第2章/第3章 → 1/2/3)
* C3 楔子/序章/尾声/番外(无号,number==-1,char_off 单调)
* C4 无章节纯文本(n==0,seg_char_start 仍完整)
* C5 开头长书名+简介再进第一章(卷首识别 as entries[0]、章号、防误判正文行 )
* C6 跨段:>512KB 书,章落在后段(seg_idx>=1,char_off 跨段单调)
* C7 中文数字转换:十一/二十三/一百零一/一千零一/两
*
* 跑法:build_reader.sh 内 `test/chapter_smoke_host`;本文件只依赖 rdchapter + seg + rdenc。
*/
#include "rdchapter.h"
#include "seg.h"
#include "rdenc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, msg) do { if (cond) { g_pass++; } else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } } while (0)
static char g_dir[600];
static void write_file(const char *path, const char *data)
{
FILE *f = fopen(path, "wb");
fwrite(data, 1, strlen(data), f);
fclose(f);
}
/* 建段目录 + 切段,返回段数(<0=错误)。 */
static int seg_for(const char *path, const char *base)
{
rdseg_set_base(base);
char dir[600];
if (rdseg_dir_for(path, dir, sizeof dir) != 0) return -1;
strcpy(g_dir, dir);
return rdseg_build(path, RD_ENC_UTF8, dir);
}
static int scan_book(rdchapter *c, const char *basename)
{
return rdchapter_scan(g_dir, RD_ENC_UTF8, basename, c);
}
static void test_c1(void)
{
const char *txt = "/tmp/ch_s1.txt";
write_file(txt,
"第一章 风起\n"
"这是第一章的正文内容。\n"
"第二章 云涌\n"
"这是第二章的正文内容。\n"
"第十一章 破晓\n"
"这是第十一章的正文内容。\n");
CHECK(seg_for(txt, "/tmp/ch_base1") >= 1, "c1 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s1") == 0, "c1 scan");
CHECK(c.n == 3, "c1 n==3");
if (c.n == 3) {
CHECK(c.entries[0].number == 1, "c1 #1");
CHECK(c.entries[1].number == 2, "c1 #2");
CHECK(c.entries[2].number == 11, "c1 #11");
CHECK(c.entries[0].char_off == 0, "c1 first at 0 (no preface)");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off, "c1 char_off monotonic");
CHECK(c.entries[0].seg_idx == 0 && c.entries[0].seg_char == 0, "c1 seg0 char0");
CHECK(strcmp(c.entries[0].title, "第一章 风起") == 0, "c1 title");
CHECK(c.has_pre == 0, "c1 no preface");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c2(void)
{
const char *txt = "/tmp/ch_s2.txt";
write_file(txt,
"第1章 起源\n"
"正文A。\n"
"第2章 发展\n"
"正文B。\n"
"第3章 终局\n"
"正文C。\n");
CHECK(seg_for(txt, "/tmp/ch_base2") >= 1, "c2 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s2") == 0, "c2 scan");
CHECK(c.n == 3, "c2 n==3");
if (c.n == 3) {
CHECK(c.entries[0].number == 1, "c2 #1");
CHECK(c.entries[1].number == 2, "c2 #2");
CHECK(c.entries[2].number == 3, "c2 #3");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off, "c2 char_off monotonic");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c3(void)
{
const char *txt = "/tmp/ch_s3.txt";
write_file(txt,
"楔子\n"
"一段开篇。\n"
"第一章 正传\n"
"正文内容。\n"
"尾声\n"
"后记文字。\n"
"番外 归途\n"
"番外正文。\n");
CHECK(seg_for(txt, "/tmp/ch_base3") >= 1, "c3 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s3") == 0, "c3 scan");
CHECK(c.n == 4, "c3 n==4");
if (c.n == 4) {
CHECK(c.entries[0].number == -1, "c3 楔子 number -1");
CHECK(strcmp(c.entries[0].title, "楔子") == 0, "c3 楔子 title");
CHECK(c.entries[1].number == 1, "c3 第一章 number 1");
CHECK(c.entries[2].number == -1, "c3 尾声 number -1");
CHECK(c.entries[3].number == -1, "c3 番外 number -1");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off &&
c.entries[2].char_off < c.entries[3].char_off, "c3 char_off monotonic");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c4(void)
{
const char *txt = "/tmp/ch_s4.txt";
write_file(txt,
"这是一个没有章节标题的长篇小说文本。\n"
"它只有连续不断的段落,没有任何章回节的标识。\n"
"所有的文字都是纯粹的故事内容。\n");
CHECK(seg_for(txt, "/tmp/ch_base4") >= 1, "c4 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s4") == 0, "c4 scan");
CHECK(c.n == 0, "c4 no chapters");
CHECK(c.nsegs == 1, "c4 nsegs==1");
CHECK(c.seg_char_start && c.seg_char_start[0] == 0 && c.seg_char_start[1] > 0, "c4 seg_char_start");
rdchapter_free(&c);
remove(txt);
}
static void test_c5(void)
{
const char *txt = "/tmp/ch_s5.txt";
write_file(txt,
"《大漠孤烟》这是一本很长的书名,作者著。\n"
"简介:这是一个故事的开头,讲述少年成长。\n"
"第一章风雨欲来,全书开场,命运由此转折。\n"
"第一章 离家\n"
"正文内容。\n"
"第二章 远行\n"
"正文内容。\n");
CHECK(seg_for(txt, "/tmp/ch_base5") >= 1, "c5 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "sample5") == 0, "c5 scan");
CHECK(c.n == 3, "c5 n==3 (preface + 2 chapters)");
if (c.n == 3) {
CHECK(c.has_pre == 1, "c5 has preface");
CHECK(c.entries[0].number == -1, "c5 preface number -1");
CHECK(strcmp(c.entries[0].title, "sample5") == 0, "c5 preface title = basename");
CHECK(c.entries[0].char_off == 0, "c5 preface char_off 0");
CHECK(c.entries[1].number == 1 && c.entries[2].number == 2, "c5 chapter numbers 1/2");
CHECK(c.entries[1].char_off > 0, "c5 ch1 after preface");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off, "c5 monotonic");
/* 防误判:正文行「第一章风雨欲来,全书开场。」带两个正文标点 → 未被识别为章 */
CHECK(c.entries[1].seg_char > 0, "c5 decoy line not counted (ch1 seg_char>0)");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c6(void)
{
/* 跨段:写 ~600KB filler,在 ~600KB 处放一个章 → 应落在后段(seg_idx>=1) */
const char *txt = "/tmp/ch_s6.txt";
FILE *f = fopen(txt, "wb");
const char *para = "这是一段用于填充页面的中文正文,包含标点,中文English混合,反复出现,占据空间。\n";
size_t plen = strlen(para);
size_t total = 0;
while (total < 600 * 1024) {
fwrite(para, 1, plen, f); total += plen;
}
fwrite("第二百章 跨段检测\n", 1, strlen("第二百章 跨段检测\n"), f);
const char *tail = "这是跨段之后的一小段正文。\n";
fwrite(tail, 1, strlen(tail), f);
fclose(f);
CHECK(seg_for(txt, "/tmp/ch_base6") >= 2, "c6 seg build >=2");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s6") == 0, "c6 scan");
/* 前置 filler(600KB 无章节)→ entries[0]=卷首;真正章节在后段 */
CHECK(c.n == 2, "c6 n==2 (preface + 1 chapter)");
if (c.n == 2) {
CHECK(c.entries[0].number == -1, "c6 preface number -1");
CHECK(c.entries[1].number == 200, "c6 number 200");
CHECK(c.entries[1].seg_idx >= 1, "c6 chapter in later segment");
CHECK(c.entries[1].seg_idx < c.nsegs, "c6 seg_idx in range");
CHECK(c.entries[1].seg_char >= 0, "c6 seg_char >=0");
CHECK(c.seg_char_start[0] == 0 &&
c.seg_char_start[0] < c.seg_char_start[1] &&
c.seg_char_start[c.nsegs - 1] < c.seg_char_start[c.nsegs], "c6 seg_char_start monotonic");
/* 跨段 char_off:章首偏移应在段 1 起点之后 */
CHECK(c.entries[1].char_off >= c.seg_char_start[c.entries[1].seg_idx], "c6 char_off within seg");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c7(void)
{
CHECK(rdchapter_cn_num("十一", 6) == 11, "c7 十一=11");
CHECK(rdchapter_cn_num("二十三", 9) == 23, "c7 二十三=23");
CHECK(rdchapter_cn_num("一百零一", 12) == 101, "c7 一百零一=101");
CHECK(rdchapter_cn_num("一千零一", 12) == 1001, "c7 一千零一=1001");
CHECK(rdchapter_cn_num("两", 3) == 2, "c7 两=2");
CHECK(rdchapter_cn_num("一百", 6) == 100, "c7 一百=100");
CHECK(rdchapter_cn_num("foobar", 6) == -1, "c7 invalid -1");
}
static void test_c8(void)
{
/* R3.4 英文章节:文字数字(one/twenty-one/two hundred fifty)+ 罗马(IV)+ 拒绝无空格(ChapterX) */
const char *txt = "/tmp/ch_s8.txt";
write_file(txt,
"Chapter one\n"
"正文。\n"
"Chapter IV\n"
"正文。\n"
"Chapter twenty-one\n"
"正文。\n"
"Chapter two hundred fifty\n"
"正文。\n"
"ChapterX\n"
"正文。\n"
"Chapter twelve\n"
"正文。\n");
CHECK(seg_for(txt, "/tmp/ch_base8") >= 1, "c8 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s8") == 0, "c8 scan");
/* ChapterX 不认(无空格);其余 5 个认 */
CHECK(c.n == 5, "c8 n==5");
if (c.n == 5) {
static const int exp[5] = { 1, 4, 21, 250, 12 };
for (int i = 0; i < 5; i++) CHECK(c.entries[i].number == exp[i], "c8 word number value");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off &&
c.entries[2].char_off < c.entries[3].char_off &&
c.entries[3].char_off < c.entries[4].char_off, "c8 char_off monotonic");
CHECK(strcmp(c.entries[0].title, "Chapter one") == 0, "c8 title Chapter one");
CHECK(strcmp(c.entries[3].title, "Chapter two hundred fifty") == 0, "c8 title two hundred fifty");
}
rdchapter_free(&c);
remove(txt);
}
int main(void)
{
test_c1();
test_c2();
test_c3();
test_c4();
test_c5();
test_c6();
test_c7();
test_c8();
printf("chapter_smoke: %d pass, %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+111
View File
@@ -341,10 +341,121 @@ static void t13(void)
CHECK(rd_enc_detect(gbk, sizeof gbk, NULL) == RD_ENC_GB18030, "t13 gbk still gb18030"); CHECK(rd_enc_detect(gbk, sizeof gbk, NULL) == RD_ENC_GB18030, "t13 gbk still gb18030");
} }
/* t14 — TXT `\r` 归一化(READER_ADVANCED §3-1):`\r\n`→`\n`、孤 `\r`→`\n`,
* 码点流不含 0x0D(行尾 \r 在无 U+000D 映射的字体里落成 .notdef 豆腐;MiSans 虽映射到
* 零宽空字形不可见,归一化后对所有字体稳健,且章节/页表/进度锚同处无 \r 码点空间)。 */
static void t14(void)
{
const uint8_t u8[] = "ab\r\ncd\ref\n\r\ngh\r";
uint32_t *cps = NULL; size_t n = 0;
CHECK(rd_utf8_to_cp(u8, sizeof u8 - 1, &cps, &n) == RD_ENC_OK, "t14 utf8→cp ok");
const uint32_t expect[] = { 'a','b','\n','c','d','\n','e','f','\n','\n','g','h','\n' };
CHECK(n == sizeof expect / sizeof expect[0], "t14 count");
int no_cr = 1;
for (size_t i = 0; i < n; i++) if (cps[i] == 0x0D) no_cr = 0;
CHECK(no_cr, "t14 no 0x0D in cps");
CHECK(n == 13 && memcmp(cps, expect, n * sizeof(uint32_t)) == 0, "t14 normalized sequence");
free(cps);
/* GB18030 路径同样归一化(rd_to_utf8 → rd_utf8_to_cp) */
const uint8_t gbk[] = { 0xC4,0xE3,0xBA,0xC3, 0x0D,0x0A, 0xCA,0xC0,0xBD,0xE7 }; /* 你好\r\n世界 */
uint8_t out[64]; size_t olen = 0;
CHECK(rd_to_utf8(RD_ENC_GB18030, gbk, sizeof gbk, out, sizeof out, &olen) == RD_ENC_OK, "t14 gbk decode");
uint32_t *c2 = NULL; size_t n2 = 0;
CHECK(rd_utf8_to_cp(out, olen, &c2, &n2) == RD_ENC_OK, "t14 gbk→cp");
CHECK(n2 == 5 && c2[0] == 0x4F60 && c2[2] == '\n' && c2[3] == 0x4E16, "t14 gbk crlf→lf");
free(c2);
}
/* t15 — 页级微排版 vs 全排版逐行一致(Phase 2C 硬用例):
* 全排版 rd_layout → 对每页 first_char 起 rd_layout_page,断言行区间(s/e/x/y/w/para 标志)
* 与该页完全一致,且 next_start == 下一页 first_char(或文末)。含伪随机长文覆盖段界/空段/
* Kinsoku/长词/页满边界;显式构造 mid_para=1(段中续行起始)。 */
static uint32_t g_lcg = 0x12345678u;
static uint32_t lcg(void) { g_lcg = g_lcg * 1664525u + 1013904223u; return g_lcg >> 8; }
static void t15_check_text(const char *txt, int fs, int ls, int pw, int ph, const char *tag)
{
uint32_t *cps = NULL; size_t n = 0;
rd_utf8_to_cp((const uint8_t *)txt, strlen(txt), &cps, &n);
rd_line *fl = (rd_line *)malloc((n + 1) * sizeof(rd_line));
rd_page *fp = (rd_page *)malloc((n + 1) * sizeof(rd_page));
rd_line *ml = (rd_line *)malloc((size_t)(ph + 4) * sizeof(rd_line));
CHECK(fl && fp && ml, "t15 alloc");
rd_layout_in in = { cps, (int32_t)n, fs, ls, pw, ph, fs, meas_mock, NULL };
rd_layout_out out;
out.lines = fl; out.pages = fp; out.max_lines = (int)n + 1; out.max_pages = (int)n + 1;
int rc = rd_layout(&in, &out);
CHECK(rc == RD_LAYOUT_OK, tag);
if (rc != RD_LAYOUT_OK) { free(cps); free(fl); free(fp); free(ml); return; }
for (int p = 0; p < out.npages; p++) {
int32_t start = out.pages[p].first_char;
int mid = !(start == 0 || cps[start - 1] == '\n');
int mnl = -1; int32_t nxt = -1;
int mrc = rd_layout_page(&in, start, mid, ml, ph + 3, &mnl, &nxt);
CHECK(mrc == RD_LAYOUT_OK, "t15 micro rc");
CHECK(mnl == out.pages[p].nlines, "t15 page nlines equal");
if (mnl > 0 && mnl == out.pages[p].nlines) {
for (int li = 0; li < mnl; li++) {
const rd_line *a = &out.lines[out.pages[p].first_line + li];
const rd_line *b = &ml[li];
CHECK(a->s == b->s && a->e == b->e && a->x == b->x && a->y == b->y &&
a->w == b->w && a->para_start == b->para_start && a->para_end == b->para_end,
"t15 line-for-line equal");
}
}
int32_t want_next = (p + 1 < out.npages) ? out.pages[p + 1].first_char : (int32_t)n;
CHECK(nxt == want_next, "t15 next_start == next page first_char");
}
free(cps); free(fl); free(fp); free(ml);
}
static void t15(void)
{
/* 固定样本:多段/空段/中英混排/标点/长词/末尾换行 */
t15_check_text("第一章 开始\n\n这是一段正文,包含 English words 与标点。第二句继续填充,"
"直到页面溢出产生多页。\n另一段(左括号测试)右标点,簇!?混合。"
"supercalifragilisticexpialidocious longword\n\n\n末段无换行结尾",
20, 130, 440, 300, "t15 fixed layout");
/* 伪随机长文(确定性种子):40+ 页,覆盖段中页首/页满边界 */
size_t cap = 65536;
char *buf = (char *)malloc(cap);
size_t o = 0;
static const char *cjk[] = { "的","一","是","不","了","人","我","在","有","他","这","中","大",
"来","上","国","个","到","说","们","为","子","和","你","地","出" };
static const char *lat[] = { "a","test","word","x","hello","longwordish","b","qk","reader","zz" };
static const char *pnc[] = { "。",",","、","?","!","(",")"," ", " ", ".", "," };
g_lcg = 0xC0FFEE;
while (o + 16 < cap) {
uint32_t r = lcg() % 100;
const char *s;
if (r < 55) s = cjk[lcg() % (sizeof cjk / sizeof cjk[0])];
else if (r < 80) s = lat[lcg() % (sizeof lat / sizeof lat[0])];
else if (r < 92) s = pnc[lcg() % (sizeof pnc / sizeof pnc[0])];
else if (r < 97) s = "\n";
else s = "\n\n";
size_t sl = strlen(s);
memcpy(buf + o, s, sl); o += sl;
}
buf[o] = 0;
t15_check_text(buf, 20, 130, 440, 640, "t15 random layout A");
t15_check_text(buf, 17, 150, 200, 300, "t15 random layout B"); /* 窄页小字:更多断行/页 */
free(buf);
/* 段中续行起始显式用例:整段一行到底(无 \n),页首必为 mid_para=1 */
{
char *one = (char *)malloc(16384);
size_t k = 0;
for (int i = 0; i < 400; i++) { const char *w = "持续不断的正文"; size_t wl = strlen(w); memcpy(one + k, w, wl); k += wl; }
one[k] = 0;
t15_check_text(one, 20, 130, 440, 640, "t15 single-para layout");
free(one);
}
}
int main(void) int main(void)
{ {
t1(); t2(); t3(); t4(); t5(); t1(); t2(); t3(); t4(); t5();
t6(); t7(); t8(); t9(); t10(); t11(); t12(); t13(); t6(); t7(); t8(); t9(); t10(); t11(); t12(); t13();
t14(); t15();
printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail); printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0; return g_fail ? 1 : 0;
} }
+83
View File
@@ -0,0 +1,83 @@
/* test/turn_repro.c — 复现「翻页跳页」(R3.2 分段流式)。
* 模拟真机序列:open → (可选 set_enc 到 UTF-16LE) → 跳页/翻页 → 渲染校验(非空/seg 边界正确)。
* 跑于 qemu-mipsel(静态)。
* 用法:qemu-mipsel ./test/turn_repro_mipsel <font.otf> <book.txt> <dirprefix> [enc] [turns]
*/
#include "book.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int render_buf(uint16_t *buf, int W, int H, rd_book *b, int page, uint16_t bg, uint16_t fg)
{
for (int i = 0; i < W * H; i++) buf[i] = bg;
int rc = rd_book_render(b, page, buf, W * 2, bg, fg);
if (rc != 0) return -100 + rc;
int ink = 0;
for (int i = 0; i < W * H; i++) if (buf[i] != bg) ink++;
return ink;
}
int main(int argc, char **argv)
{
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
const char *book = (argc > 2) ? argv[2] : "/tmp/rdsample/tongyi.txt";
const char *pref = (argc > 3) ? argv[3] : "/tmp/rdsample/repro";
int force_enc = (argc > 4) ? atoi(argv[4]) : 0;
int turns = (argc > 5) ? atoi(argv[5]) : 200;
char segbase[420], idxdir[420];
snprintf(segbase, sizeof segbase, "%s/segbase", pref);
snprintf(idxdir, sizeof idxdir, "%s/index", pref);
rd_book_set_seg_base(segbase);
rd_book_set_index_dir(idxdir);
int fs = 16, ls = 135, pw = 480, ph = 724, mx = 16, my = 20, top = 62;
uint16_t *buf = (uint16_t *)malloc((size_t)pw * ph * 2);
rd_book *b = NULL;
int rc = rd_book_open(&b, book, font, fs, ls, pw, ph, mx, my, top);
printf("open rc=%d enc=%s\n", rc, rc == 0 ? rd_book_encname(b) : "-");
if (rc != 0) return 1;
if (force_enc) { printf("set_enc(%d)->rc=%d\n", force_enc, rd_book_set_enc(b, (rd_enc_t)force_enc)); }
int np = rd_book_pages(b);
printf("npages=%d\n", np);
/* ① 翻页序列:跨段边界观察 cur 跳变 */
rd_book_set_page(b, 2);
int prev = rd_book_current(b), jumps = 0;
for (int i = 0; i < turns; i++) {
int nxt = rd_book_turn(b, 1);
int d = nxt - prev;
if (d != 1) { printf(" JUMP turn#%d %d->%d d=%d npages=%d\n", i, prev, nxt, d, rd_book_pages(b)); if (d > 1) jumps++; }
prev = nxt;
if (rd_book_pages(b) < 1) break;
}
printf("turn jumps>1=%d\n", jumps);
/* ② 渲染首页/第3页/中间页 校验非空 */
int targets[3] = { 0, 2, np > 3 ? np / 2 : 0 };
for (int t = 0; t < 3; t++) {
if (targets[t] >= rd_book_pages(b)) continue;
int ink = render_buf(buf, pw, ph, b, targets[t], 0xFFFF, 0x0000);
printf("render page %d -> %s (ink=%d)\n", targets[t], ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
}
/* ③ set_page 到远页(触发按需补段),渲染校验 + 观察是否落到目标 */
int far = 316;
int got = rd_book_set_page(b, far);
printf("set_page(%d) -> cur=%d npages=%d\n", far, got, rd_book_pages(b));
int ink = render_buf(buf, pw, ph, b, got, 0xFFFF, 0x0000);
printf("render page %d -> %s (ink=%d)\n", got, ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
/* ④ 跳到更远(超过当前 npages,强制补段到 seg 20) */
int far2 = 4000;
int got2 = rd_book_set_page(b, far2);
printf("set_page(%d) -> cur=%d npages=%d\n", far2, got2, rd_book_pages(b));
ink = render_buf(buf, pw, ph, b, got2, 0xFFFF, 0x0000);
printf("render page %d -> %s (ink=%d)\n", got2, ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
free(buf);
rd_book_close(b);
printf("done\n");
return 0;
}
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# tools/check_lgpl_release.sh — 发布前合规校验闸(只读,不改任何产物)。
#
# 用法(在 reader_app 目录外也可):
# ./tools/check_lgpl_release.sh [--skip-net]
#
# 校验项(对齐 devdocs/plan/FUTURE_LIBMOBI.md §4-④ P0):
# 1) os.meow.reader.mpk 必含 resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt,FTL.txt}
# 2) build_reader.sh 锁定的 libmobi commit 与 NOTICE「Locked version」一致(防锁定值漂移)
# 3) [联网] codeload tarball sha256 与 NOTICE 记录指纹一致(未记录则报告,属 P1 补强,不判失败)
# 4) [联网] 主仓 git ls-remote 公开可读(LGPL §4(d) 应用源码可达性)
#
# 退出码:0 = 通过;1 = 硬失败(缺许可证文件 / commit 漂移 / tarball 漂移 / 主仓不可达)。
set -u
cd "$(dirname "$0")/.." # 切到 reader_app 根(build_reader.sh/NOTICE/mpk 都在此)
SKIP_NET=0
[ "${1:-}" = "--skip-net" ] && SKIP_NET=1
MPK="os.meow.reader.mpk"
NOTICE="NOTICE"
BUILD="build_reader.sh"
LIC="resources/licenses"
MAIN_REPO="https://git.yeij.top/AskaEth/MeowOS-For-R3P2.git"
fail=0
say() { printf '%s\n' "$*"; }
hard() { printf 'FAIL %s\n' "$*"; fail=1; }
ok() { printf 'ok %s\n' "$*"; }
warn() { printf 'warn %s\n' "$*"; }
say "== 合规校验(reader_app) =="
# ---------- 1) 包内许可证文件 ----------
if [ ! -f "$MPK" ]; then
hard "找不到 $MPK —— 请先 ./build_reader.sh 再 ./pack_reader_mpk.sh"
else
for f in NOTICE GPL-3.0.txt LGPL-3.0.txt FTL.txt; do
if tar tzf "$MPK" 2>/dev/null | grep -qx "resources/licenses/$f"; then
ok "包内含 resources/licenses/$f"
else
hard "包内缺 resources/licenses/$f"
fi
done
fi
# ---------- 2) libmobi 锁定 commit 一致性 ----------
LM_URL="$(sed -n 's/^LM_URL="\([^"]*\)"/\1/p' "$BUILD" | head -1)"
LM_COMMIT="$(printf '%s' "$LM_URL" | sed 's#.*/##')"
NOTICE_COMMIT="$(sed -n 's/^[[:space:]]*\([0-9a-f]\{40\}\).*/\1/p' "$NOTICE" | head -1)"
if [ -z "$LM_COMMIT" ]; then
hard "build_reader.sh 未解析到 LM_URL 锁定 commit"
elif [ "$LM_COMMIT" != "$NOTICE_COMMIT" ]; then
hard "libmobi 锁定 commit 漂移:build=$LM_COMMIT NOTICE=$NOTICE_COMMIT"
else
ok "libmobi 锁定 commit 一致 ($LM_COMMIT)"
fi
# ---------- 3) [联网] tarball sha256 ----------
NOTICE_SHA="$(sed -n 's/^[[:space:]]*Tarball sha256[^:]*:[[:space:]]*\([0-9a-f]\{64\}\).*/\1/p' "$NOTICE" | head -1)"
LIVE_SHA=""
if [ "$SKIP_NET" = 1 ]; then
warn "跳过在线 tarball 校验 (--skip-net)"
if [ -n "$NOTICE_SHA" ]; then ok "NOTICE 已记录 tarball sha256 ($NOTICE_SHA)"; else warn "NOTICE 未记录 tarball sha256(P1 补强项)"; fi
else
if command -v curl >/dev/null 2>&1 && command -v sha256sum >/dev/null 2>&1 && [ -n "$LM_URL" ]; then
LIVE_SHA="$(curl -fsSL "$LM_URL" 2>/dev/null | sha256sum | awk '{print $1}')"
if [ -z "$LIVE_SHA" ]; then
warn "在线取 tarball 失败(网络不可用)—— 未校验 sha256"
elif [ -n "$NOTICE_SHA" ]; then
if [ "$LIVE_SHA" = "$NOTICE_SHA" ]; then ok "tarball sha256 与 NOTICE 一致 ($LIVE_SHA)"; else hard "tarball sha256 漂移:live=$LIVE_SHA NOTICE=$NOTICE_SHA"; fi
else
say "info tarball sha256=$LIVE_SHA (NOTICE 未记录指纹,属 P1 补强项,不判失败)"
fi
else
warn "无 curl/sha256sum 或未解析 LM_URL —— 跳过在线 tarball 校验"
fi
fi
# ---------- 4) [联网] 主仓公开可读 ----------
if [ "$SKIP_NET" = 1 ]; then
warn "跳过主仓可读性校验 (--skip-net)"
else
if command -v git >/dev/null 2>&1; then
if git ls-remote "$MAIN_REPO" HEAD >/dev/null 2>&1; then
ok "主仓公开可读 (git ls-remote HEAD)"
else
hard "主仓 git ls-remote 失败(LGPL §4(d) 应用源码可达性)"
fi
else
warn "无 git —— 跳过主仓可读性校验"
fi
fi
# ---------- 5) 合规清单 ----------
say "== 合规清单 =="
say "libmobi commit : ${LM_COMMIT:-?}"
say "libmobi tarball : ${LIVE_SHA:-${NOTICE_SHA:-未校验}}"
say "包内许可证 :"
if [ -f "$MPK" ]; then tar tzf "$MPK" 2>/dev/null | grep 'resources/licenses/' || true; fi
if [ "$fail" = 1 ]; then say "结果:FAIL"; exit 1; fi
say "结果:PASS"
+20
View File
@@ -75,6 +75,16 @@ static int g_ime_done = 0;
static int g_ime_result = 0; static int g_ime_result = 0;
static int g_shift = 0; static int g_shift = 0;
/* WiFi 设置页 bug3(2026-09-15):IME 打开瞬间 TAP 冷却窗。触发打开的那次点按,其「抬起
* TAP」(含 goodix 抬起长尾)可能由 SDK 输入线程在打开 drain 之后才读出并入队(子页自有
* tfd 与 SDK 输入线程独立读同一 evdev,节奏可滞后 ~20ms+)——阻塞循环把它当「点背景=取消」
* 瞬间关键盘(y<KB_TOP)或幻影按键(y≥KB_TOP)。窗口期内的 TAP 一律消费丢弃(镜像
* meow_ui_run 的 STARTUP_COOLDOWN_MS 500 先例,取 200 兼顾手感;正常打字第一键必在窗后)。
* 防御为双保险:drain 已复位触摸状态机(在途按压不再产 TAP),冷却窗再兜住 drain 前已入队
* 未及清/时序竞争残漏的 TAP。 */
#define IME_TAP_COOLDOWN_MS 200
static uint64_t g_ime_open_ms = 0; /* meow_ui_time_ms() 单调毫秒;0=未记录 */
static meow_ime_ctx g_ime_ctx; /* 拷贝(on_change 回调用) */ static meow_ime_ctx g_ime_ctx; /* 拷贝(on_change 回调用) */
static meow_ime_ctx *g_ime_orig = NULL; /* 应用真实 ctx(buf 回填用) */ static meow_ime_ctx *g_ime_orig = NULL; /* 应用真实 ctx(buf 回填用) */
static char g_ime_buf[256]; static char g_ime_buf[256];
@@ -570,6 +580,11 @@ int meow_ime_long_press(int x, int y)
int meow_ime_handle_event(const meow_input_ev *e) int meow_ime_handle_event(const meow_input_ev *e)
{ {
if (!g_ime_active || g_ime_done) return 0; if (!g_ime_active || g_ime_done) return 0;
/* 打开冷却窗(bug3):窗口内 TAP 一律消费丢弃——防触发打开那次点按的迟到「抬起 TAP」
* 被当「点背景=取消」或幻影按键。消费(return 1)而非透传:事件已属 IME 会话期。 */
if (e->type == MEOW_EV_TAP && g_ime_open_ms &&
meow_ui_time_ms() - g_ime_open_ms < IME_TAP_COOLDOWN_MS)
return 1;
switch (e->type) { switch (e->type) {
case MEOW_EV_TAP: return meow_ime_handle_tap(e->a, e->b); case MEOW_EV_TAP: return meow_ime_handle_tap(e->a, e->b);
case MEOW_EV_TOUCH_DOWN: g_ime_swipe_y = e->b; return 1; /* 记录压点(候选栏滑动判定);覆盖层消费 */ case MEOW_EV_TOUCH_DOWN: g_ime_swipe_y = e->b; return 1; /* 记录压点(候选栏滑动判定);覆盖层消费 */
@@ -605,6 +620,11 @@ void meow_ui_text_input_async(meow_ime_ctx *ctx)
ime_capture(); /* M2.71-bug7:打开前拍下键盘区背景(关闭即还原) */ ime_capture(); /* M2.71-bug7:打开前拍下键盘区背景(关闭即还原) */
meow_input_start(); meow_input_start();
meow_input_drain(); meow_input_drain();
/* bug3 加固(对齐 s_dd_modal/subpage 吸收 goodix 抬起长尾的「排空→40ms→再排空」惯例):
* 第一遍 drain 后等 40ms 让输入线程读出迟到事件,第二遍 drain 连状态机一并复位。 */
usleep(40000);
meow_input_drain();
g_ime_open_ms = meow_ui_time_ms(); /* TAP 冷却窗起点(见 IME_TAP_COOLDOWN_MS) */
} }
int meow_ui_text_input(meow_ime_ctx *ctx) int meow_ui_text_input(meow_ime_ctx *ctx)
+20 -3
View File
@@ -68,6 +68,8 @@
/* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */ /* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */
void meow_ui_gesture_end(void); void meow_ui_gesture_end(void);
/* 触摸状态机复位(定义在触摸状态机变量之后,meow_input_drain 内调用)。 */
void meow_input_touch_reset(void);
/* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)── /* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)──
* 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理 * 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理
@@ -212,6 +214,10 @@ void meow_input_drain(void)
g_qh = 0; g_qt = 0; g_qh = 0; g_qt = 0;
pthread_mutex_unlock(&g_qm); pthread_mutex_unlock(&g_qm);
meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */ meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */
/* WiFi 设置页 bug3(2026-09-15):drain 同时复位触摸状态机——语义自洽:drain=丢弃在途
* 手势。否则 drain 前已开始、drain 后才被输入线程读出的「抬起」仍会按残留原点产生
* 一个迟到 TAP 入队(IME 打开瞬间被当「点背景=取消」或幻影按键,键盘开即关)。 */
meow_input_touch_reset();
} }
static void push_ev(int type, int a, int b) static void push_ev(int type, int a, int b)
@@ -260,6 +266,19 @@ static int t_mv_x = -1, t_mv_y = -1;
/* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */ /* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */
static int t_long_fired = 0; static int t_long_fired = 0;
/* 触摸状态机整体复位:丢弃在途手势(down/原点/坐标/长按触发标记全部归零)。
* 调用点:① meow_input_drain(drain=丢弃在途手势,IME/模态打开前);② §10.5 叠加抑制
* 进入沿(防抑制期丢失抬手致 t_down 卡死)。效果:复位前已开始、复位后才抬起的按压
* 不再产生 TAP/SWIPE(WiFi 设置页 bug3:IME 打开 drain 之后才入队的迟到「抬起 TAP」
* 曾把键盘当「点背景=取消」瞬间关闭)。 */
void meow_input_touch_reset(void)
{
t_down = 0; t_press_valid = 0; t_origin_valid = 0;
t_cx = -1; t_cy = -1; t_dx = 0; t_dy = 0; t_down_ms = 0;
t_mv_x = -1; t_mv_y = -1;
t_long_fired = 0;
}
/* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */ /* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */
static int g_long_press_ms = MEOW_LONG_PRESS_MS; static int g_long_press_ms = MEOW_LONG_PRESS_MS;
/* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */ /* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */
@@ -592,9 +611,7 @@ static void *input_thread(void *arg)
if (overlay_input_suppressed()) { if (overlay_input_suppressed()) {
if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */ if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */
overlay_was = 1; overlay_was = 1;
t_down = 0; t_press_valid = 0; t_origin_valid = 0; meow_input_touch_reset();
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
t_long_fired = 0;
} }
if (r > 0) if (r > 0)
for (int i = 0; i < nfd; i++) { for (int i = 0; i < nfd; i++) {
+3 -1
View File
@@ -207,8 +207,10 @@ void meow_input_start(void);
int meow_input_pop(meow_input_ev *ev); int meow_input_pop(meow_input_ev *ev);
/* 注入一条语义事件(宿主验证手势分发/进度条用;设备上无调用方)。 */ /* 注入一条语义事件(宿主验证手势分发/进度条用;设备上无调用方)。 */
void meow_input_inject(int type, int a, int b); void meow_input_inject(int type, int a, int b);
/* 清空队列(IME 打开前丢弃残留触摸,避免误敲)。 */ /* 清空队列(IME 打开前丢弃残留触摸,避免误敲);同时复位触摸状态机(drain=丢弃在途手势)。 */
void meow_input_drain(void); void meow_input_drain(void);
/* 触摸状态机整体复位(丢弃在途手势;drain/叠加抑制进入沿调用;宿主单测可直调)。 */
void meow_input_touch_reset(void);
/* ---- M2.71-bug5:App 活动 → 通知 launcher 重置息屏计时 ---- /* ---- M2.71-bug5:App 活动 → 通知 launcher 重置息屏计时 ----
* launcher 前台 app 会话的 app_last_input 只在物理键边沿重置(它看不到应用内触摸)。SDK 在 * launcher 前台 app 会话的 app_last_input 只在物理键边沿重置(它看不到应用内触摸)。SDK 在
+39 -4
View File
@@ -3,14 +3,18 @@
* ② K_ENTER 只提交不关(meow_ime_active() 仍真),ESC 后关; * ② K_ENTER 只提交不关(meow_ime_active() 仍真),ESC 后关;
* ③ 阻塞 IME 循环喂 on_tick(动态内容重绘)且 IME 面板仍在最上 * ③ 阻塞 IME 循环喂 on_tick(动态内容重绘)且 IME 面板仍在最上
* (On_tick 在 IME 面板区画红色哨兵行,IME 绘制后应盖回面板色,不被 App 覆盖)。 * (On_tick 在 IME 面板区画红色哨兵行,IME 绘制后应盖回面板色,不被 App 覆盖)。
* ④ bug3(2026-09-15):IME 打开 ~200ms TAP 冷却窗——打开后立即注入的 stray-TAP
* (背景/键区)被吞不取消;冷却窗后背景 TAP=取消语义保留。
* *
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \ * gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
* meow_ui_anim.c meow_ui_media.c meow_ui_nav.c meow_pinyin.c meow_ui_box.c meow_ui_scroll.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm) * (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm;
* widget/input/ime/view 后续又引入 anim/media/nav/pinyin/box/scroll 依赖,一并链接)
*/ */
#include "meow_ui.h" #include "meow_ui.h"
#include "meow_ui_internal.h" #include "meow_ui_internal.h"
@@ -54,6 +58,38 @@ static int near(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14
/* ---- ① 异步 shift 测试(直接 handle_tap,确定语义,无阻塞循环) ---- */ /* ---- ① 异步 shift 测试(直接 handle_tap,确定语义,无阻塞循环) ---- */
static char g_echo[64] = ""; static char g_echo[64] = "";
static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); } static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); }
/* ④ WiFi 设置页 bug3(2026-09-15):IME 打开 ~200ms TAP 冷却窗——触发打开那次点按的迟到
* 「抬起 TAP」(goodix 长尾,经 SDK 输入线程 drain 后才入队)不得把键盘当「点背景=取消」
* 瞬间关掉;冷却窗后点背景=取消的语义保留。 */
static void test_stray_tap(void)
{
meow_ime_ctx c; memset(&c, 0, sizeof c);
c.mode = MEOW_IME_ASCII; c.maxlen = 32; c.on_change = ime_cb;
meow_ui_text_input_async(&c);
assert(meow_ime_active() == 1);
/* 打开后立即注入背景 TAP(y<KB_TOP=420):冷却窗内必须被吞——不取消、键盘仍 active */
meow_input_ev stray = { MEOW_EV_TAP, 240, 100 };
assert(meow_ime_handle_event(&stray) == 1); /* 消费(吞掉) */
assert(meow_ime_active() == 1); /* 键盘未被关(修复前此处已 done=取消) */
/* 窗口内键区 TAP 也丢弃(防幻影按键) */
meow_input_ev key = { MEOW_EV_TAP, K_Q_X, K_Q_Y };
assert(meow_ime_handle_event(&key) == 1);
assert(meow_ime_active() == 1);
assert(meow_ime_text()[0] == 0); /* 无幻影字符入缓冲 */
/* 冷却窗(200ms)后:点背景=取消 语义保留 */
usleep(250000);
assert(meow_ime_handle_event(&stray) == 1);
assert(meow_ime_active() == 0); /* 已取消(g_ime_done) */
printf("PASS stray-tap: 窗内背景/键区 TAP 吞掉不取消, 窗后背景 TAP 取消\n"); fflush(stdout);
meow_ime_close(0); /* 复位(清 g_ime_done/active) */
assert(meow_ime_active() == 0);
}
static void test_shift(void) static void test_shift(void)
{ {
meow_ime_ctx c; memset(&c, 0, sizeof c); meow_ime_ctx c; memset(&c, 0, sizeof c);
@@ -94,8 +130,6 @@ static char g_drv_text[64] = ""; /* shift 序列后 meow_ime_text()(期
/* 把 back_buf 写成 PPM(视觉目检 IME 面板渲染/最上;在 IME 空闲时调用,back_buf 稳定) */ /* 把 back_buf 写成 PPM(视觉目检 IME 面板渲染/最上;在 IME 空闲时调用,back_buf 稳定) */
static void dump_ppm(const char *path) static void dump_ppm(const char *path)
{ {
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
int W = meow_ui_width(), H = meow_ui_height(); int W = meow_ui_width(), H = meow_ui_height();
FILE *f = fopen(path, "wb"); FILE *f = fopen(path, "wb");
if (!f) return; if (!f) return;
@@ -134,7 +168,7 @@ static void *driver(void *arg)
(void)arg; (void)arg;
usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */ usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */
meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */ meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */
usleep(160000); usleep(350000); /* 等过 IME 打开 TAP 冷却窗(200ms,bug3)+ drain 40ms */
meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */ meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */
usleep(90000); usleep(90000);
meow_input_inject(MEOW_EV_TAP, K_SHIFT_X, K_SHIFT_Y); /* shift 开 */ meow_input_inject(MEOW_EV_TAP, K_SHIFT_X, K_SHIFT_Y); /* shift 开 */
@@ -169,6 +203,7 @@ int main(void)
const meow_theme *th = meow_theme_get(); const meow_theme *th = meow_theme_get();
test_shift(); /* ① */ test_shift(); /* ① */
test_stray_tap(); /* ④ bug3:打开冷却窗吞 stray-TAP */
/* ②③ 在 meow_ui_run 里驱动(g_events 仅 meow_ui_run 能设置) */ /* ②③ 在 meow_ui_run 里驱动(g_events 仅 meow_ui_run 能设置) */
g_tf = meow_textfield_create(10, 300, 300, 44, "Type here...", g_tfbuf, 32, MEOW_IME_ASCII); g_tf = meow_textfield_create(10, 300, 300, 44, "Type here...", g_tfbuf, 32, MEOW_IME_ASCII);