fix(sdk+launcher): 图标不渲染真根因(agent-219) — meow_ui_img.c scale_buf cover 采样把乘写成除(÷缩放比→采到源图左上角透明区,全丢); 改乘+居中裁剪偏移修正; 宿主旧测试用纯色图掩盖了该bug。+ main_menu App退出/结构变化重绘时 fb_pan_buf0_force 无条件pan回buffer0(防App翻页留buffer1致菜单残帧)
meow_ui.a→3e0ee8f7 launcher→9d0efc2f hello→9609ea6e files→90f5cf07 ncm→be8be221(均已上卡); 真机截图证据 .tmp/meowbug/fix1.png+fix_b2_after.png
This commit is contained in:
1 parent
05a19a9ea3
commit
ab4da09e75
45 files changed
+1926
-147
No files matched your search
@@ -10,14 +10,74 @@
|
||||
* 文本预览换页(push ACT_VIEWER)。退出=在根目录左滑或 SIGTERM(看门狗)。
|
||||
*/
|
||||
#include "files_internal.h"
|
||||
#include "meow_notify_ipc.h" /* §6.2 二期:picker 结果经 MSG_PICKER_RESULT 回传 */
|
||||
#include "meow_ipc.h"
|
||||
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static meow_view *g_browser_view;
|
||||
static meow_view *g_viewer_view;
|
||||
|
||||
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
|
||||
|
||||
/* ============================================================ */
|
||||
/* §6.2 二期:picker 模式(launcher 注入 MEOW_PICKER_* → 选文件返回路径) */
|
||||
/* ============================================================ */
|
||||
static char g_picker_action[24];
|
||||
static char g_picker_caller[MEOW_APP_ID_MAX];
|
||||
static uint32_t g_picker_req_id;
|
||||
static int g_picker_active;
|
||||
|
||||
int picker_active(void) { return g_picker_active; }
|
||||
const char *picker_action(void) { return g_picker_action; }
|
||||
uint32_t picker_req_id(void) { return g_picker_req_id; }
|
||||
|
||||
static void picker_init(void)
|
||||
{
|
||||
const char *a = getenv("MEOW_PICKER_ACTION");
|
||||
if (!a || !a[0]) return;
|
||||
snprintf(g_picker_action, sizeof g_picker_action, "%s", a);
|
||||
g_picker_active = 1;
|
||||
const char *c = getenv("MEOW_PICKER_CALLER");
|
||||
if (c) snprintf(g_picker_caller, sizeof g_picker_caller, "%s", c);
|
||||
const char *r = getenv("MEOW_PICKER_REQ_ID");
|
||||
g_picker_req_id = r ? (uint32_t)atoi(r) : 0;
|
||||
meow_log(MEOW_LOG_INFO, "picker: mode action=%s req=%u caller=%s",
|
||||
g_picker_action, g_picker_req_id, g_picker_caller);
|
||||
}
|
||||
|
||||
/* 选文件:发 MSG_PICKER_RESULT(OK+路径)+ 请求退出。best-effort(launcher 不在静默)。 */
|
||||
void picker_return_file(const char *path)
|
||||
{
|
||||
m_picker_result_t r;
|
||||
memset(&r, 0, sizeof r);
|
||||
snprintf(r.caller, sizeof r.caller, "%s", g_picker_caller);
|
||||
r.req_id = g_picker_req_id;
|
||||
snprintf(r.action, sizeof r.action, "%s", g_picker_action);
|
||||
r.code = MEOW_OK;
|
||||
if (path) snprintf(r.value, sizeof r.value, "%s", path);
|
||||
(void)meow_notify_ipc_send_raw(MSG_PICKER_RESULT, &r, (uint32_t)sizeof r, 1);
|
||||
meow_log(MEOW_LOG_INFO, "picker: return path=%s", path ? path : "");
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
|
||||
/* 取消:发 MSG_PICKER_RESULT(MEOW_ERR_NOENT+空值)+ 请求退出。 */
|
||||
void picker_cancel(void)
|
||||
{
|
||||
m_picker_result_t r;
|
||||
memset(&r, 0, sizeof r);
|
||||
snprintf(r.caller, sizeof r.caller, "%s", g_picker_caller);
|
||||
r.req_id = g_picker_req_id;
|
||||
snprintf(r.action, sizeof r.action, "%s", g_picker_action);
|
||||
r.code = MEOW_ERR_NOENT; /* 用户取消 */
|
||||
(void)meow_notify_ipc_send_raw(MSG_PICKER_RESULT, &r, (uint32_t)sizeof r, 1);
|
||||
meow_log(MEOW_LOG_INFO, "picker: cancel");
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 事件分发:meow_ui_run 平铺回调 → 当前页 */
|
||||
/* ============================================================ */
|
||||
@@ -38,9 +98,10 @@ static void app_on_swipe(int dir, int dx, int dy)
|
||||
{
|
||||
(void)dx;
|
||||
if (dir == MEOW_SWIPE_LEFT) {
|
||||
/* 左缘返回:预览页→回目录;浏览器→上级目录;到根→退出 App。 */
|
||||
/* 左缘返回:预览页→回目录;浏览器→上级目录;到根→退出 App(picker 模式则取消)。 */
|
||||
if (viewer_is_active()) { meow_view_page_pop(); return; }
|
||||
if (browser_is_active() && browser_go_up()) return;
|
||||
if (picker_active()) { picker_cancel(); return; } /* §6.2 二期:根屏左滑=取消 picker */
|
||||
meow_log(MEOW_LOG_INFO, "exit:swipe");
|
||||
meow_ui_request_exit();
|
||||
return;
|
||||
@@ -88,6 +149,7 @@ int main(void)
|
||||
meow_theme_add_listener(NULL);
|
||||
|
||||
signal(SIGTERM, on_sigterm);
|
||||
picker_init(); /* §6.2 二期:读 MEOW_PICKER_* 决定是否进入 picker 模式 */
|
||||
|
||||
/* 建两页 view(on_create 建各自控件/状态 + 读卡扫首个目录) */
|
||||
g_browser_view = browser_view_create();
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"name":"Files","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.files/files","package":"os.meow.files","version":"0.1","framework":1,"immersive":1,"author":"MeowOS"}
|
||||
{"name":"Files","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.files/files","package":"os.meow.files","version":"0.1","framework":1,"immersive":1,"author":"MeowOS","route.pick":"pick","picker.pick_file":"pick"}
|
||||
@@ -9,7 +9,7 @@ cd "$(dirname "$0")"
|
||||
ROOT="$HOME/Project/R3ProII"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
||||
"$CC" -Os -static -Wall -Wextra -I../sdk -I. \
|
||||
"$CC" -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. \
|
||||
app.c ui/browser.c ui/viewer.c ui/common/chrome.c logic/fsx.c \
|
||||
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" -lpthread -o files
|
||||
ls -la files
|
||||
|
||||
Binary file not shown.
@@ -23,6 +23,16 @@
|
||||
#define ACT_BROWSER 0
|
||||
#define ACT_VIEWER 1
|
||||
|
||||
/* ---- picker 模式(AMSD_DESIGN §6.2 二期)----
|
||||
* launcher 注入 MEOW_PICKER_ACTION/REQ_ID/CALLER(amsd `am picker` 路由到本 App)。
|
||||
* 非空 ACTION → 本 App 进入「选文件」picker 模式:浏览目录、点文件=返回路径(经
|
||||
* MSG_PICKER_RESULT 发回 launcher→amsd)+ 退出;返回/左滑退出=取消。 */
|
||||
int picker_active(void); /* 1=进入 picker 模式(MEOW_PICKER_ACTION 非空) */
|
||||
const char *picker_action(void);
|
||||
uint32_t picker_req_id(void);
|
||||
void picker_return_file(const char *path); /* 选文件:发 MSG_PICKER_RESULT + 请求退出 */
|
||||
void picker_cancel(void); /* 取消:发 code=MEOW_ERR_NOENT + 请求退出 */
|
||||
|
||||
/* ---- 布局 ---- */
|
||||
#define APP_W 480
|
||||
#define APP_H 720
|
||||
|
||||
@@ -322,10 +322,12 @@ int browser_on_tap(int x, int y)
|
||||
int row = meow_listview_hit(g_list, x, y); /* 行号(含滚动换算;禁用行 -1) */
|
||||
if (row < 0) { browser_drag_reset(); return 1; } /* 命中列表但无有效行(空隙) */
|
||||
int op = meow_listview_hit_op(g_list, x, y); /* 行右 1/3 = 操作区 */
|
||||
if (op) browser_open_sheet(row);
|
||||
else {
|
||||
if (op) {
|
||||
if (!picker_active()) browser_open_sheet(row); /* §6.2 二期:picker 模式不弹 sheet */
|
||||
} else {
|
||||
fsx_entry *e = &g_entries[row];
|
||||
if (e->is_dir) browser_enter_dir(e->path);
|
||||
else if (picker_active()) picker_return_file(e->path); /* picker:点文件=返回路径 */
|
||||
else browser_open_file(e->path);
|
||||
}
|
||||
browser_drag_reset();
|
||||
@@ -389,6 +391,7 @@ void browser_swipe(int dir, int dist)
|
||||
* TAP(不再触发行体/打开动作),故这里无需再吞后续 tap;打开前清按下高亮与拖动态。 */
|
||||
void browser_on_long_press(int x, int y)
|
||||
{
|
||||
if (picker_active()) return; /* §6.2 二期:picker 模式无长按 BottomSheet */
|
||||
if (g_err != 0 || g_n == 0) return;
|
||||
if (!meow_widget_hit(g_list, x, y)) return;
|
||||
int row = meow_listview_hit(g_list, x, y);
|
||||
@@ -410,7 +413,11 @@ static void browser_page_create(meow_view *v, void *ctx)
|
||||
if (r_list.h < 0) r_list.h = 0;
|
||||
strncpy(g_cur, FSX_SD_ROOT, sizeof g_cur - 1);
|
||||
g_cur[sizeof g_cur - 1] = '\0';
|
||||
g_tb = app_topbar_create("Files", 1, "NEW");
|
||||
/* §6.2 二期:picker 模式 topbar 标题「Pick a file」且无 NEW 动作(点文件即选)。 */
|
||||
if (picker_active())
|
||||
g_tb = app_topbar_create("Pick a file", 1, NULL);
|
||||
else
|
||||
g_tb = app_topbar_create("Files", 1, "NEW");
|
||||
/* 结构化列表:行数据源 browser_src2;单选模式(行高亮用 selected,§5.8.3) */
|
||||
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
||||
browser_src2, NULL, MEOW_LIST_SEL_SINGLE);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+53
-7
@@ -86,15 +86,41 @@ static int cmd_history(const char *pkg)
|
||||
static int cmd_start(const char *spec)
|
||||
{
|
||||
if (geteuid() != 0) { printf("ERR permission denied (root only)\n"); return 1; }
|
||||
char line[512];
|
||||
char line[800];
|
||||
snprintf(line, sizeof line, "start %s\n", spec);
|
||||
return am_query(line);
|
||||
}
|
||||
|
||||
static int cmd_resolve(const char *spec)
|
||||
{
|
||||
/* spec: <pkg> <activity>(两参数)。 */
|
||||
char line[800];
|
||||
snprintf(line, sizeof line, "resolve %s\n", spec);
|
||||
return am_query(line);
|
||||
}
|
||||
|
||||
static int cmd_picker(const char *action)
|
||||
{
|
||||
if (geteuid() != 0) { printf("ERR permission denied (root only)\n"); return 1; }
|
||||
char line[800];
|
||||
snprintf(line, sizeof line, "picker %s\n", action);
|
||||
return am_query(line);
|
||||
}
|
||||
|
||||
static int cmd_picker_status(const char *reqid)
|
||||
{
|
||||
char line[800];
|
||||
if (reqid && reqid[0])
|
||||
snprintf(line, sizeof line, "picker-status %s\n", reqid);
|
||||
else
|
||||
snprintf(line, sizeof line, "picker-status\n");
|
||||
return am_query(line);
|
||||
}
|
||||
|
||||
static int cmd_kill(const char *pkg)
|
||||
{
|
||||
if (geteuid() != 0) { printf("ERR permission denied (root only)\n"); return 1; }
|
||||
char line[512];
|
||||
char line[800];
|
||||
snprintf(line, sizeof line, "kill %s\n", pkg);
|
||||
return am_query(line);
|
||||
}
|
||||
@@ -103,8 +129,9 @@ static void usage(const char *a0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: %s [--sock <path>] status|list|history [pkg]|start <pkg>[/<act>]|kill <pkg>\n"
|
||||
" %s resolve <pkg> <activity> | picker <action> | picker-status [reqid]\n"
|
||||
" --sock <path> amsd socket (default %s, or $MEOW_AMS_SOCK)\n",
|
||||
a0, AMS_SOCK_DEFAULT);
|
||||
a0, a0, AMS_SOCK_DEFAULT);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
@@ -112,17 +139,25 @@ int main(int argc, char **argv)
|
||||
const char *env = getenv("MEOW_AMS_SOCK");
|
||||
g_sock = (env && env[0]) ? env : AMS_SOCK_DEFAULT;
|
||||
|
||||
const char *cmd = NULL, *arg = NULL;
|
||||
const char *cmd = NULL;
|
||||
char spec[768] = ""; /* 子命令后所有位置参数,空格拼接(resolve 需 pkg+act 两参) */
|
||||
int spec_n = 0;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char *a = argv[i];
|
||||
if (!strcmp(a, "--sock") && i + 1 < argc) g_sock = argv[++i];
|
||||
else if (!strcmp(a, "-h") || !strcmp(a, "--help")) { usage(argv[0]); return 0; }
|
||||
else if (a[0] == '-') { usage(argv[0]); return 2; }
|
||||
else if (!cmd) cmd = a;
|
||||
else if (!arg) arg = a;
|
||||
else { fprintf(stderr, "extra arg %s\n", a); return 2; }
|
||||
else if (!cmd) { cmd = a; }
|
||||
else {
|
||||
int w = snprintf(spec + spec_n, sizeof spec - spec_n, "%s%s",
|
||||
spec_n ? " " : "", a);
|
||||
if (w < 0) break;
|
||||
spec_n += w;
|
||||
if ((size_t)spec_n >= sizeof spec - 1) break;
|
||||
}
|
||||
}
|
||||
if (!cmd) { usage(argv[0]); return 2; }
|
||||
const char *arg = spec[0] ? spec : NULL;
|
||||
|
||||
if (!strcmp(cmd, "status")) return cmd_status();
|
||||
if (!strcmp(cmd, "list")) return cmd_list();
|
||||
@@ -135,6 +170,17 @@ int main(int argc, char **argv)
|
||||
if (!arg) { fprintf(stderr, "kill needs <pkg>\n"); return 2; }
|
||||
return cmd_kill(arg);
|
||||
}
|
||||
if (!strcmp(cmd, "resolve")) {
|
||||
if (!arg) { fprintf(stderr, "resolve needs <pkg> <activity>\n"); return 2; }
|
||||
return cmd_resolve(arg);
|
||||
}
|
||||
if (!strcmp(cmd, "picker")) {
|
||||
if (!arg) { fprintf(stderr, "picker needs <action>\n"); return 2; }
|
||||
return cmd_picker(arg);
|
||||
}
|
||||
if (!strcmp(cmd, "picker-status")) {
|
||||
return cmd_picker_status(arg);
|
||||
}
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
Binary file not shown.
Regular → Executable
File mode changed.
Binary file not shown.
@@ -56,6 +56,9 @@
|
||||
#define AMS_HIST 8 /* 每 App 最近事件数 */
|
||||
#define AMS_MAX_APPS 64
|
||||
#define AMS_MAX_CONNS 32
|
||||
#define AMS_MAX_ROUTES 8 /* 每 App 路由数(MEOW_MAX_ROUTES 对齐) */
|
||||
#define AMS_MAX_PICKERS 8 /* picker action → handler 注册表 */
|
||||
#define AMS_MAX_PENDING 8 /* 进行中 picker 请求(结果关联) */
|
||||
#define AMS_TICK_MS 1000
|
||||
#define AMS_CLEANUP_MS 60000
|
||||
#define AMS_PATH_MAX 512
|
||||
@@ -70,7 +73,13 @@ struct ams_ev {
|
||||
char detail[192]; /* 事件描述(exit=0 / sig=11 / ams_start=... 等) */
|
||||
long epoch; /* 墙钟秒 */
|
||||
long up; /* uptime 秒 */
|
||||
char kind[16]; /* launch / exit / crash / ams_start / ams_kill */
|
||||
char kind[16]; /* launch / exit / crash / ams_start / ams_kill / picker_result */
|
||||
};
|
||||
|
||||
/* 路由行(来自 app.json route.<id>=<chain>;运行时经 DECLARE 增量并集)。 */
|
||||
struct ams_route {
|
||||
char act[16]; /* route 键后缀 */
|
||||
char chain[MEOW_CHAIN_MAX];
|
||||
};
|
||||
|
||||
struct ams_app {
|
||||
@@ -90,10 +99,33 @@ struct ams_app {
|
||||
int last_kind; /* 0=干净 exit 1=exit 2=sig */
|
||||
int last_code;
|
||||
long long last_dur_ms;
|
||||
/* §6.2 二期:路由权威(resolve 用;静态 app.json + 运行时 DECLARE 并集覆盖) */
|
||||
int nroute;
|
||||
struct ams_route routes[AMS_MAX_ROUTES];
|
||||
struct ams_ev hist[AMS_HIST];
|
||||
int hist_n, hist_idx;
|
||||
};
|
||||
|
||||
/* picker action → handler app(app.json picker.<action>=<chain>;amsd 权威路由)。 */
|
||||
struct ams_picker {
|
||||
char action[24];
|
||||
char app_id[MEOW_APP_ID_MAX];
|
||||
char chain[MEOW_CHAIN_MAX]; /* handler 入口 activity 链(v2 常空=裸启动,handler 读 env) */
|
||||
int used; /* 注册表命中 */
|
||||
};
|
||||
|
||||
/* 进行中 picker 请求(`am picker`/App 发起;结果经 PICKER_RESULT 关联)。 */
|
||||
struct ams_pending {
|
||||
int active;
|
||||
uint32_t req_id;
|
||||
char caller[MEOW_APP_ID_MAX];
|
||||
char action[24];
|
||||
char app_id[MEOW_APP_ID_MAX]; /* handler app(记录用) */
|
||||
int done; /* 结果已回 */
|
||||
int code;
|
||||
char value[256];
|
||||
};
|
||||
|
||||
/* stdout 捕获会话(一期同一时刻仅一个前台 App → 单槽) */
|
||||
struct ams_out {
|
||||
int active;
|
||||
@@ -119,6 +151,10 @@ static int g_sigpipe[2] = {-1, -1};
|
||||
static int g_listen_fd = -1;
|
||||
static struct ams_app g_apps[AMS_MAX_APPS];
|
||||
static int g_napps = 0;
|
||||
static struct ams_picker g_pickers[AMS_MAX_PICKERS]; /* §6.2 二期:picker 权威路由表 */
|
||||
static int g_npickers = 0;
|
||||
static struct ams_pending g_pending[AMS_MAX_PENDING]; /* §6.2 二期:进行中 picker */
|
||||
static uint32_t g_pick_req_seq = 1; /* req_id 单调分配 */
|
||||
|
||||
struct ams_conn {
|
||||
int fd;
|
||||
@@ -353,6 +389,92 @@ static void appjson_get(const char *content, const char *key, char *out, size_t
|
||||
out[l] = '\0';
|
||||
}
|
||||
|
||||
/* §6.2 二期:在 content 二分/扁平解析 route.<id>=<chain>(或 JSON "route.<id>":"<chain>")到 app。
|
||||
* 跳过 route.<id>.fullscreen / .keep_awake 子键;chain 取直链(';' 分隔保持原样)。 */
|
||||
static void ams_parse_routes(const char *content, struct ams_app *a)
|
||||
{
|
||||
if (!content || !a) return;
|
||||
const char *p = content;
|
||||
while ((p = strstr(p, "route.")) != NULL) {
|
||||
const char *q = p + 6;
|
||||
char id[16] = "";
|
||||
int idl = 0;
|
||||
while (*q && *q != '"' && *q != ':' && *q != '=' && *q != '.' && idl < 15)
|
||||
id[idl++] = *q++;
|
||||
id[idl] = 0;
|
||||
if (idl > 0 && *q == '.') { p = q; continue; } /* route.<id>.xxx 子键,跳过 */
|
||||
if (idl > 0) {
|
||||
const char *vp = q;
|
||||
while (*vp && *vp != ':' && *vp != '=') vp++;
|
||||
if (*vp == ':' || *vp == '=') vp++;
|
||||
while (*vp == ' ' || *vp == '\t') vp++;
|
||||
if (*vp == '"') vp++;
|
||||
char chain[MEOW_CHAIN_MAX] = "";
|
||||
int vl = 0;
|
||||
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
|
||||
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
|
||||
chain[vl++] = *vp++;
|
||||
chain[vl] = 0;
|
||||
while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t')) chain[--vl] = 0;
|
||||
int ri = -1;
|
||||
for (int i = 0; i < a->nroute; i++)
|
||||
if (!strcmp(a->routes[i].act, id)) { ri = i; break; }
|
||||
if (ri < 0) {
|
||||
if (a->nroute >= AMS_MAX_ROUTES) { p = vp; continue; }
|
||||
ri = a->nroute++;
|
||||
}
|
||||
scopy(a->routes[ri].act, sizeof a->routes[ri].act, id);
|
||||
scopy(a->routes[ri].chain, sizeof a->routes[ri].chain, chain);
|
||||
p = vp;
|
||||
} else {
|
||||
p += 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* §6.2 二期:解析 picker.<action>=<chain> 到全局 picker 表(action → handler app_id)。
|
||||
* app_id = 当前扫描目录名(=该 app.json 所属 App)。链 v2 常空=裸启动(handler 读 env)。 */
|
||||
static void ams_parse_pickers(const char *content, const char *app_id)
|
||||
{
|
||||
if (!content || !app_id) return;
|
||||
const char *p = content;
|
||||
while ((p = strstr(p, "picker.")) != NULL) {
|
||||
const char *q = p + 7;
|
||||
char action[24] = "";
|
||||
int al = 0;
|
||||
while (*q && *q != '"' && *q != ':' && *q != '=' && *q != '.' && al < 23)
|
||||
action[al++] = *q++;
|
||||
action[al] = 0;
|
||||
if (al > 0) {
|
||||
const char *vp = q;
|
||||
while (*vp && *vp != ':' && *vp != '=') vp++;
|
||||
if (*vp == ':' || *vp == '=') vp++;
|
||||
while (*vp == ' ' || *vp == '\t') vp++;
|
||||
if (*vp == '"') vp++;
|
||||
char chain[MEOW_CHAIN_MAX] = "";
|
||||
int vl = 0;
|
||||
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
|
||||
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
|
||||
chain[vl++] = *vp++;
|
||||
chain[vl] = 0;
|
||||
struct ams_picker *pk = NULL;
|
||||
for (int i = 0; i < g_npickers; i++)
|
||||
if (!strcmp(g_pickers[i].action, action)) { pk = &g_pickers[i]; break; }
|
||||
if (!pk && g_npickers < AMS_MAX_PICKERS) {
|
||||
pk = &g_pickers[g_npickers++];
|
||||
memset(pk, 0, sizeof *pk);
|
||||
scopy(pk->action, sizeof pk->action, action);
|
||||
}
|
||||
if (pk) {
|
||||
scopy(pk->app_id, sizeof pk->app_id, app_id);
|
||||
scopy(pk->chain, sizeof pk->chain, chain);
|
||||
pk->used = 1;
|
||||
}
|
||||
}
|
||||
p = q;
|
||||
}
|
||||
}
|
||||
|
||||
/* 扫描 --apps 下每个目录(app_id=目录名;name/exec 从 app.json 抽,缺省回退)。 */
|
||||
static void registry_scan(void)
|
||||
{
|
||||
@@ -396,13 +518,22 @@ static void registry_scan(void)
|
||||
scopy(a->exec, sizeof a->exec, dir);
|
||||
path_append(a->exec, sizeof a->exec, "/meow");
|
||||
}
|
||||
/* §6.2 二期:路由权威表(静态 app.json route.*)+ picker 表(picker.<action>=<chain>) */
|
||||
ams_parse_routes(content, a);
|
||||
ams_parse_pickers(content, a->pkg);
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
g_napps = n;
|
||||
emit_log("registry scan: %d app(s) from %s", g_napps, g_apps_dir);
|
||||
for (int i = 0; i < g_napps; i++)
|
||||
emit_log(" app %s name=%s exec=%s", g_apps[i].pkg, g_apps[i].name, g_apps[i].exec);
|
||||
for (int i = 0; i < g_napps; i++) {
|
||||
struct ams_app *a = &g_apps[i];
|
||||
emit_log(" app %s name=%s exec=%s routes=%d", a->pkg, a->name, a->exec, a->nroute);
|
||||
for (int r = 0; r < a->nroute; r++)
|
||||
emit_log(" route %s=%s", a->routes[r].act, a->routes[r].chain);
|
||||
}
|
||||
for (int i = 0; i < g_npickers; i++)
|
||||
emit_log(" picker %s -> %s (chain=%s)", g_pickers[i].action, g_pickers[i].app_id, g_pickers[i].chain);
|
||||
}
|
||||
|
||||
/* ---- App 表辅助 ---- */
|
||||
@@ -428,6 +559,81 @@ static struct ams_app *app_get(const char *pkg)
|
||||
return a;
|
||||
}
|
||||
|
||||
/* ---- §6.2 二期:路由权威(resolve/declare/picker)---- */
|
||||
|
||||
/* 路由规则与 launcher resolve_chain 一致(FRAMEWORK_DESIGN §2):
|
||||
* route 缺失(app 未声明任何路由)→ chain=activity、深度 1 兜底;
|
||||
* 声明了路由但 activity 非 route 键 → -MEOW_ERR_NOENT。返回 0=成功。 */
|
||||
static int amsd_resolve_chain(const char *app_id, const char *activity, char *out, int *depth)
|
||||
{
|
||||
if (!out || !depth) return MEOW_ERR_INVAL;
|
||||
if (!activity) activity = "";
|
||||
struct ams_app *a = app_find(app_id);
|
||||
if (!a) return MEOW_ERR_NOENT;
|
||||
if (a->nroute == 0) {
|
||||
snprintf(out, MEOW_CHAIN_MAX, "%s", activity);
|
||||
*depth = activity[0] ? 1 : 0;
|
||||
return MEOW_OK;
|
||||
}
|
||||
for (int i = 0; i < a->nroute; i++) {
|
||||
if (!strcmp(a->routes[i].act, activity)) {
|
||||
snprintf(out, MEOW_CHAIN_MAX, "%s", a->routes[i].chain);
|
||||
const char *c = a->routes[i].chain;
|
||||
int d = c[0] ? 1 : 0;
|
||||
for (; *c; c++) if (*c == ';') d++;
|
||||
*depth = d;
|
||||
return MEOW_OK;
|
||||
}
|
||||
}
|
||||
return MEOW_ERR_NOENT;
|
||||
}
|
||||
|
||||
/* 应用一组运行时 route entries(NUL 分隔 "id=chain")到 ams_app(并集覆盖,同 launcher)。 */
|
||||
static void amsd_apply_routes(struct ams_app *a, const char *entries)
|
||||
{
|
||||
const char *p = entries;
|
||||
while (*p) {
|
||||
const char *eq = strchr(p, '=');
|
||||
if (eq && eq > p) {
|
||||
int idlen = (int)(eq - p);
|
||||
if (idlen >= 16) idlen = 15;
|
||||
char id[16];
|
||||
memcpy(id, p, idlen); id[idlen] = 0;
|
||||
const char *chain = eq + 1;
|
||||
int ri = -1;
|
||||
for (int i = 0; i < a->nroute; i++)
|
||||
if (!strcmp(a->routes[i].act, id)) { ri = i; break; }
|
||||
if (ri < 0) {
|
||||
if (a->nroute >= AMS_MAX_ROUTES) { p += strlen(p) + 1; continue; }
|
||||
ri = a->nroute++;
|
||||
}
|
||||
scopy(a->routes[ri].act, sizeof a->routes[ri].act, id);
|
||||
scopy(a->routes[ri].chain, sizeof a->routes[ri].chain, chain);
|
||||
}
|
||||
p += strlen(p) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* hex 解码(小写/大写 2 字符/字节)到 out[outsz],返回字节数(含尾部 NUL 由调用方处理)。 */
|
||||
static int ams_hex_decode(const char *hex, char *out, int outsz)
|
||||
{
|
||||
int n = 0;
|
||||
const char *p = hex;
|
||||
while (p[0] && p[1] && n < outsz - 1) {
|
||||
int hi = (p[0] >= '0' && p[0] <= '9') ? p[0] - '0'
|
||||
: (p[0] >= 'a' && p[0] <= 'f') ? p[0] - 'a' + 10
|
||||
: (p[0] >= 'A' && p[0] <= 'F') ? p[0] - 'A' + 10 : -1;
|
||||
int lo = (p[1] >= '0' && p[1] <= '9') ? p[1] - '0'
|
||||
: (p[1] >= 'a' && p[1] <= 'f') ? p[1] - 'a' + 10
|
||||
: (p[1] >= 'A' && p[1] <= 'F') ? p[1] - 'A' + 10 : -1;
|
||||
if (hi < 0 || lo < 0) break;
|
||||
out[n++] = (char)((hi << 4) | lo);
|
||||
p += 2;
|
||||
}
|
||||
out[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void hist_push(struct ams_app *a, const char *kind, const char *detail,
|
||||
long epoch, long up)
|
||||
{
|
||||
@@ -668,6 +874,146 @@ static void handle_out(char **t, int nt)
|
||||
out_append(pkg, pid, epoch, up, text);
|
||||
}
|
||||
|
||||
/* ---- §6.2 二期:picker 编排(amsd 路由权威) ---- */
|
||||
|
||||
static struct ams_picker *picker_find(const char *action)
|
||||
{
|
||||
if (!action || !action[0]) return NULL;
|
||||
for (int i = 0; i < g_npickers; i++)
|
||||
if (g_pickers[i].used && !strcmp(g_pickers[i].action, action)) return &g_pickers[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void pending_record(uint32_t req_id, const char *action, int code, const char *value)
|
||||
{
|
||||
for (int i = 0; i < AMS_MAX_PENDING; i++) {
|
||||
struct ams_pending *p = &g_pending[i];
|
||||
if (!p->active) continue;
|
||||
if (p->req_id != req_id) continue;
|
||||
if (action[0] && strcmp(p->action, action) != 0) continue;
|
||||
p->done = 1;
|
||||
p->code = code;
|
||||
scopy(p->value, sizeof p->value, value);
|
||||
emit_log("picker result: req=%u action=%s code=%d value=%s", req_id, p->action, code, p->value);
|
||||
struct ams_app *h = app_find(p->app_id);
|
||||
if (h) {
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof detail, "req=%u caller=%s act=%s code=%d val=%.96s",
|
||||
req_id, p->caller, action, code, value);
|
||||
hist_push(h, "picker_result", detail, (long)time(NULL), read_uptime_sec());
|
||||
}
|
||||
return;
|
||||
}
|
||||
emit_log("picker result: no pending req=%u (dropped)", req_id);
|
||||
}
|
||||
|
||||
/* picker 启动:解析 action → handler;alloc/更新 pending;经 launcher MSG_AMS_START(picker) 拉起。
|
||||
* write_resp:写响应到 resp(cmd_picker 用;PICKER_CALL 事件传 NULL 不写)。返回 0=ok启动。 */
|
||||
static int picker_start(const char *caller, uint32_t req_id, const char *action,
|
||||
char *resp, int rn)
|
||||
{
|
||||
struct ams_picker *pk = picker_find(action);
|
||||
if (!pk) {
|
||||
if (resp) snprintf(resp, rn, "ERR no picker handler for action: %s\n", action ? action : "");
|
||||
emit_log("AMS picker %s -> no handler", action ? action : "");
|
||||
return -1;
|
||||
}
|
||||
struct ams_app *h = app_find(pk->app_id);
|
||||
if (!h) {
|
||||
if (resp) snprintf(resp, rn, "ERR handler app not registered: %s\n", pk->app_id);
|
||||
emit_log("AMS picker %s -> handler %s not registered", action, pk->app_id);
|
||||
return -1;
|
||||
}
|
||||
/* 找/建 pending 槽(同 req_id 去重;满则覆盖最旧 done) */
|
||||
struct ams_pending *p = NULL;
|
||||
for (int i = 0; i < AMS_MAX_PENDING; i++)
|
||||
if (g_pending[i].active && g_pending[i].req_id == req_id) { p = &g_pending[i]; break; }
|
||||
if (!p) {
|
||||
for (int i = 0; i < AMS_MAX_PENDING; i++)
|
||||
if (!g_pending[i].active || g_pending[i].done) { p = &g_pending[i]; break; }
|
||||
}
|
||||
if (!p) p = &g_pending[0]; /* 全占满→覆盖 0(最旧) */
|
||||
memset(p, 0, sizeof *p);
|
||||
p->active = 1;
|
||||
p->req_id = req_id;
|
||||
scopy(p->caller, sizeof p->caller, caller ? caller : "");
|
||||
scopy(p->action, sizeof p->action, action ? action : "");
|
||||
scopy(p->app_id, sizeof p->app_id, pk->app_id);
|
||||
/* 裸启动 handler(activity 空;picker 上下文经 MSG_AMS_START 扩载荷注入 env) */
|
||||
m_ams_start_t s;
|
||||
memset(&s, 0, sizeof s);
|
||||
scopy(s.app_id, sizeof s.app_id, pk->app_id);
|
||||
if (pk->chain[0]) scopy(s.activity, sizeof s.activity, pk->chain);
|
||||
s.picker = 1;
|
||||
s.picker_req_id = req_id;
|
||||
scopy(s.picker_action, sizeof s.picker_action, pk->action);
|
||||
scopy(s.picker_caller, sizeof s.picker_caller, caller ? caller : "");
|
||||
uint32_t code = MEOW_ERR_INTERNAL;
|
||||
int rt = ams_request_ctl(MSG_AMS_START, &s, sizeof s, &code);
|
||||
if (rt != 0) {
|
||||
p->active = 0;
|
||||
if (resp) snprintf(resp, rn, "ERR launcher offline\n");
|
||||
emit_log("AMS picker %s -> launcher offline", action);
|
||||
return -1;
|
||||
}
|
||||
if (code != MEOW_OK) {
|
||||
if (resp) snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pk->app_id, code);
|
||||
emit_log("AMS picker %s -> launcher refused code=%u", action, code);
|
||||
return -1;
|
||||
}
|
||||
if (resp) snprintf(resp, rn, "OK pending %u %s\n", req_id, pk->app_id);
|
||||
emit_log("AMS picker %s -> %s (req=%u, caller=%s)", action, pk->app_id, req_id, caller ? caller : "");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* `am picker <action>`:alloc req_id(CLI 无 caller),启动,回 OK pending。 */
|
||||
static int cmd_picker(const char *action, char *resp, int rn)
|
||||
{
|
||||
if (!action || !action[0]) { snprintf(resp, rn, "ERR picker needs <action>\n"); return 1; }
|
||||
uint32_t req_id = g_pick_req_seq++;
|
||||
if (g_pick_req_seq == 0) g_pick_req_seq = 1;
|
||||
picker_start("", req_id, action, resp, rn); /* 内部写 OK pending / ERR */
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* `picker-status [reqid]`:列正在/已决 picker。 */
|
||||
static int cmd_picker_status(const char *reqstr, char *resp, int rn)
|
||||
{
|
||||
int l = snprintf(resp, rn, "# amd picker status\n");
|
||||
int found = 0;
|
||||
for (int i = 0; i < AMS_MAX_PENDING; i++) {
|
||||
struct ams_pending *p = &g_pending[i];
|
||||
if (!p->active) continue;
|
||||
if (reqstr && reqstr[0] && (uint32_t)atoi(reqstr) != p->req_id) continue;
|
||||
found = 1;
|
||||
const char *state = p->done ? "done" : "pending";
|
||||
int m = snprintf(resp + l, rn - l, "PICK %u %s %s %d %s\n",
|
||||
p->req_id, p->action, p->app_id, p->done ? p->code : -1,
|
||||
p->done ? p->value : state);
|
||||
if (m > 0) l += m;
|
||||
}
|
||||
if (!found) { int m = snprintf(resp + l, rn - l, "(none)\n"); if (m > 0) l += m; }
|
||||
return l;
|
||||
}
|
||||
|
||||
/* `am resolve <pkg> <act>`:查 amsd 权威路由表。 */
|
||||
static int cmd_resolve(const char *pkg, const char *act, char *resp, int rn)
|
||||
{
|
||||
if (!pkg || !pkg[0] || !act || !act[0]) {
|
||||
snprintf(resp, rn, "ERR resolve needs <pkg> <activity>\n");
|
||||
return 1;
|
||||
}
|
||||
char chain[MEOW_CHAIN_MAX];
|
||||
int depth = 0;
|
||||
int rc = amsd_resolve_chain(pkg, act, chain, &depth);
|
||||
if (rc != 0) {
|
||||
snprintf(resp, rn, "ERR resolve %s/%s failed (%d)\n", pkg, act, rc);
|
||||
return 1;
|
||||
}
|
||||
snprintf(resp, rn, "CHAIN %s %s %d %s\n", pkg, act, depth, chain);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- am CLI 命令处理 ---- */
|
||||
static int cmd_status(char *resp, int rn)
|
||||
{
|
||||
@@ -819,7 +1165,41 @@ static int dispatch_line(char *line, char *resp, int rn)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 命令:status/list/history/start/kill */
|
||||
/* §6.2 二期事件(launcher → amsd 一次性转发,无响应):DECLARE / PICKER_CALL / PICKER_RESULT */
|
||||
if (strncmp(line, "DECLARE ", 8) == 0 || strncmp(line, "PICKER_", 7) == 0) {
|
||||
char *tok[8];
|
||||
int nt = 0;
|
||||
char *save = NULL;
|
||||
for (int i = 0; i < (int)(sizeof tok / sizeof tok[0]); i++) {
|
||||
char *tk = strtok_r(i == 0 ? line : NULL, " ", &save);
|
||||
if (!tk) break;
|
||||
tok[nt++] = tk;
|
||||
}
|
||||
if (nt >= 1) {
|
||||
if (!strcmp(tok[0], "DECLARE") && nt >= 4) {
|
||||
struct ams_app *a = app_get(tok[1]);
|
||||
char entries[MEOW_IPC_CARGO];
|
||||
if (a) {
|
||||
int bl = ams_hex_decode(tok[3], entries, sizeof entries);
|
||||
(void)bl;
|
||||
amsd_apply_routes(a, entries);
|
||||
emit_log("DECLARE %s n=%s routes=%d", tok[1], tok[2], a->nroute);
|
||||
}
|
||||
} else if (!strcmp(tok[0], "PICKER_CALL") && nt >= 4) {
|
||||
uint32_t req = (uint32_t)atoi(tok[2]);
|
||||
(void)picker_start(tok[1], req, tok[3], NULL, 0);
|
||||
} else if (!strcmp(tok[0], "PICKER_RESULT") && nt >= 5) {
|
||||
uint32_t req = (uint32_t)atoi(tok[1]);
|
||||
int code = atoi(tok[3]);
|
||||
char value[256];
|
||||
(void)ams_hex_decode(tok[4], value, sizeof value);
|
||||
pending_record(req, tok[2], code, value);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 命令:status/list/history/start/kill/resolve/picker/picker-status */
|
||||
char *save = NULL;
|
||||
char *cmd = strtok_r(line, " ", &save);
|
||||
if (!cmd) return 0;
|
||||
@@ -841,6 +1221,19 @@ static int dispatch_line(char *line, char *resp, int rn)
|
||||
cmd_kill(arg, resp, rn);
|
||||
return 1;
|
||||
}
|
||||
if (!strcmp(cmd, "resolve")) {
|
||||
if (!arg) { snprintf(resp, rn, "ERR resolve needs <pkg> <activity>\n"); return 1; }
|
||||
char *act = strtok_r(NULL, " ", &save);
|
||||
return cmd_resolve(arg, act, resp, rn) > 0;
|
||||
}
|
||||
if (!strcmp(cmd, "picker")) {
|
||||
if (!arg) { snprintf(resp, rn, "ERR picker needs <action>\n"); return 1; }
|
||||
cmd_picker(arg, resp, rn);
|
||||
return 1;
|
||||
}
|
||||
if (!strcmp(cmd, "picker-status")) {
|
||||
return cmd_picker_status(arg, resp, rn) > 0;
|
||||
}
|
||||
snprintf(resp, rn, "ERR unknown cmd: %s\n", cmd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -63,6 +63,14 @@ static void ams_send(const char *payload, int len)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* 公开 amsd 上报通道包装(AMSD_DESIGN §6.2 二期):util.c 转发运行时 declare / picker
|
||||
* call/result 给 amsd 用。语义与 ams_send 一致:best-effort、单次 connect+write+close、
|
||||
* 失败静默丢(amsd 不在不影响 launcher)。 */
|
||||
void meow_ams_send(const char *payload, int len)
|
||||
{
|
||||
ams_send(payload, len);
|
||||
}
|
||||
|
||||
/* launch 上报:pkg/activity/chain/pid。amsd 建表项 + 开 stdout 会话。 */
|
||||
static void ams_report_launch(const struct app *a, const char *chain, pid_t pid)
|
||||
{
|
||||
@@ -223,6 +231,54 @@ int meow_ams_kill_pending(const char *pkg)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- AMSD_DESIGN §6.2 二期:picker 上下文(随 pending-start 置位,launch_app_chain 消费后清)----
|
||||
* amsd `am picker` → MSG_AMS_START 扩载荷(picker=1)→ ipc_dispatch 经 set_ex 置位 + 武装
|
||||
* g_ams_pick_armed;main_menu 消费 pending-start(照旧 launch_app/launch_activity)时,
|
||||
* launch_app_chain 读到武装位 → 注入 MEOW_PICKER_ACTION/REQ_ID/CALLER 到子进程 → 解除武装。
|
||||
* 单一写者(ipc_dispatch / launch_app_chain),且同一时刻仅一个 pending-start 在飞(v1 单前台 App),
|
||||
* 故武装位不会错配到非 picker 启动。 */
|
||||
static char g_ams_pick_action[24];
|
||||
static char g_ams_pick_caller[MEOW_APP_ID_MAX];
|
||||
static uint32_t g_ams_pick_req;
|
||||
static int g_ams_pick_armed;
|
||||
|
||||
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
|
||||
const char *pick_action, uint32_t pick_req,
|
||||
const char *pick_caller)
|
||||
{
|
||||
int rc = meow_ams_pending_start_set(pkg, act);
|
||||
if (rc != 0) return rc; /* 拒绝(有前台 App / 参数非法) */
|
||||
g_ams_pick_action[0] = 0;
|
||||
g_ams_pick_caller[0] = 0;
|
||||
g_ams_pick_req = 0;
|
||||
g_ams_pick_armed = 0;
|
||||
if (pick_action && pick_action[0]) {
|
||||
snprintf(g_ams_pick_action, sizeof g_ams_pick_action, "%s", pick_action);
|
||||
if (pick_caller) snprintf(g_ams_pick_caller, sizeof g_ams_pick_caller, "%s", pick_caller);
|
||||
g_ams_pick_req = pick_req;
|
||||
g_ams_pick_armed = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 子进程 exec 前注入 picker env(action/req/caller 已由 launch_app_chain 入口捕获并解除全局武装,
|
||||
* 即使启动失败不再错配到下一次普通启动)。返回注入后的 ne。 */
|
||||
static int ams_push_picker_env(char **envp, int ne, const char *action,
|
||||
uint32_t req, const char *caller)
|
||||
{
|
||||
if (!action || !action[0]) return ne;
|
||||
/* envp 容量 48:此处 +3 仍在界内(见 launch_app_chain envp[48] 注释) */
|
||||
static char e_act[40], e_req[40], e_caller[MEOW_APP_ID_MAX + 20];
|
||||
snprintf(e_act, sizeof e_act, "MEOW_PICKER_ACTION=%s", action);
|
||||
snprintf(e_req, sizeof e_req, "MEOW_PICKER_REQ_ID=%u", req);
|
||||
snprintf(e_caller, sizeof e_caller, "MEOW_PICKER_CALLER=%s", caller ? caller : "");
|
||||
envp[ne++] = e_act;
|
||||
envp[ne++] = e_req;
|
||||
envp[ne++] = e_caller;
|
||||
logmsg("launch: picker env action=%s req=%u caller=%s", action, req, caller ? caller : "");
|
||||
return ne;
|
||||
}
|
||||
|
||||
|
||||
/* ---- 阶段 D · M1:路由注册表辅助(主循环单写者,无锁)---- */
|
||||
struct meow_acct *meow_acct_find(const char *app_id)
|
||||
@@ -1179,6 +1235,17 @@ static int run_app_notif_overlay(struct app *a, pid_t pid)
|
||||
static int launch_app_chain(struct app *a, const char *chain)
|
||||
{
|
||||
logmsg("launch: %s (%s) chain=\"%s\"", a->name, a->exec, chain ? chain : "");
|
||||
/* §6.2 二期:入口即捕获 picker 上下文并解除全局武装——即便后续 exec 失败(!a->ok)返回,
|
||||
* 武装位也已清,不会错配到下一次普通启动(单飞不泄漏)。 */
|
||||
char pick_action[24] = "";
|
||||
char pick_caller[MEOW_APP_ID_MAX] = "";
|
||||
uint32_t pick_req = 0;
|
||||
if (g_ams_pick_armed) {
|
||||
snprintf(pick_action, sizeof pick_action, "%s", g_ams_pick_action);
|
||||
snprintf(pick_caller, sizeof pick_caller, "%s", g_ams_pick_caller);
|
||||
pick_req = g_ams_pick_req;
|
||||
g_ams_pick_armed = 0;
|
||||
}
|
||||
if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; }
|
||||
meow_nav_begin(a->id, chain); /* M3:冷启动/裸启动都以当前链深初始化导航栈(chain 空→depth 0) */
|
||||
|
||||
@@ -1369,6 +1436,8 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
envp[ne++] = env_chain;
|
||||
logmsg("launch: M3 env %s / %s", env_act, env_chain);
|
||||
}
|
||||
/* AMSD_DESIGN §6.2 二期:picker handler 启动注入(捕获值来自 launch_app_chain 入口)。 */
|
||||
ne = ams_push_picker_env(envp, ne, pick_action, pick_req, pick_caller);
|
||||
envp[ne] = NULL;
|
||||
execve(a->exec, argv, envp);
|
||||
int e = errno;
|
||||
|
||||
@@ -339,6 +339,7 @@ static meow_img *menu_app_icon_load(const struct app *a)
|
||||
snprintf(p, sizeof p, "/usr/data/mnt/sd_0/%s", a->icon);
|
||||
img = meow_img_load_file(p);
|
||||
}
|
||||
logmsg("icon: %s -> %s %s", a->id[0] ? a->id : a->name, p, img ? "OK" : "FAIL"); /* M2.87 排查 */
|
||||
return img;
|
||||
}
|
||||
|
||||
@@ -1574,14 +1575,23 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
||||
need_menu_paint = 1;
|
||||
dirty = 0;
|
||||
}
|
||||
/* Bug2 残帧防护 (agent-x):App 退出/结构变化触发全帧重画时,用 fb_pan_buf0_force()
|
||||
* 无条件把 LCDC pan 回 buffer0(菜单/所有 app 都画 buffer0 且从不自己 pan)。仅
|
||||
* fb_norm_buf0() 会在缓存的 fb_active_yoffset==0 时短路——若 App 运行期把物理
|
||||
* yoffset 留到了 buffer1(如其内部双缓冲翻页),缓存仍为 0 导致真正的 re-pan 从未
|
||||
* 发出,重画后的菜单落在 buffer0 而 LCDC 仍显示 buffer1 的 App 残帧(=菜单区残帧)。
|
||||
* 只在真重画(need_menu_paint)时 force-pan,不每圈 ioctl;重复 pan 到同一 yoffset
|
||||
* 是硬件 no-op,无频闪(fb_pan_buf0_force 注释)。 */
|
||||
if (need_menu_paint) {
|
||||
draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */
|
||||
fb_pan_buf0_force(); /* 真重画 → 无条件 pan 回 buffer0(防 App 残留 buffer1) */
|
||||
need_menu_paint = 0;
|
||||
} else {
|
||||
fb_norm_buf0(); /* 无重画 → 短路;yoffset 非 0 才 pan */
|
||||
}
|
||||
/* A-1/A-2:呈现统一走 SDK meow_ui_flush()(内含帧率门控;无脏返回,门控未达期保留脏
|
||||
* 待下一轮)。fb_norm_buf0 确保 LCDC 显示 buffer0(yoffset 非 0 才 pan,短路无开销)。
|
||||
* 不再用 launcher 的 g_ndirty/g_dirty_full 做呈现门——SDK present 自会 no-op 于空脏。 */
|
||||
fb_norm_buf0(); /* Bug 3:确保 LCDC 显示 buffer0 */
|
||||
meow_ui_flush(); /* M2.16 无闪烁 + M2.16 帧率门控归口 SDK */
|
||||
if (toast_ms > 0) {
|
||||
toast_ms -= POLL_MS;
|
||||
|
||||
@@ -64,12 +64,14 @@ enum {
|
||||
MSG_PERF_RESULT = 0x0051, /* ↓ 聚合 JSON(perf/system 子集) */
|
||||
MSG_POWER_ACTION = 0x0060, /* ↑ 电源/系统动作信号(专项服务 meow_svc_power) */
|
||||
MSG_POWER_STATUS = 0x0061, /* ↓ 动作受理/结果 + 状态文本 */
|
||||
/* ---- 【预留 · 本批不实现】picker:带返回值的跨 App 调用(M3 延伸,只留协议位)----
|
||||
* 语义:App A 经 MSG_PICKER_CALL 请求 App B 弹一个 selector 并返回所选值(如文件/音轨
|
||||
* 选择器)。属「跨 App 带返回值」调用,与单向业务通知(MSG_NOTIFY)不同;负载/应答结构
|
||||
* 待 M3 延伸阶段定义(m_picker_call_t / m_picker_result_t),此处仅固定消息号与方向。 */
|
||||
MSG_PICKER_CALL = 0x0070, /* ↑ 请求跨 App picker 调用(发起方 A → 持有方 B) */
|
||||
MSG_PICKER_RESULT = 0x0071, /* ↓ picker 返回值(候选结果回 A) */
|
||||
/* ---- picker:带返回值的跨 App 调用(M3 延伸,AMSD_DESIGN §6.2 二期已落地)----
|
||||
* 语义:App A 经 MSG_PICKER_CALL 请求 App B 弹一个 selector 并返回所选值(如文件选择器)。
|
||||
* amsd 为路由/决策者:它查 picker 表(app.json `picker.<action>=<chain>`)决定 handler App,
|
||||
* 经 MSG_AMS_START(扩载荷含 picker 上下文)拉起 handler;handler 结果经 MSG_PICKER_RESULT
|
||||
* 回传,launcher 转发 amsd 记录。属「跨 App 带返回值」调用。负载结构见下方
|
||||
* m_picker_call_t / m_picker_result_t。 */
|
||||
MSG_PICKER_CALL = 0x0070, /* ↑ 请求跨 App picker 调用(发起方 A → 持有方 B;→launcher→amsd) */
|
||||
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
|
||||
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
||||
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
||||
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
||||
@@ -326,15 +328,45 @@ typedef struct m_power_status {
|
||||
/* ---- amsd 控制(AMSD_DESIGN §4.2)---- */
|
||||
|
||||
/* MSG_AMS_START →:请求 launcher 拉起某 App。app_id=包名(权威);activity 空=裸启动
|
||||
* (launcher launch_app),非空=resolve_chain+launch_activity。 */
|
||||
* (launcher launch_app),非空=resolve_chain+launch_activity。
|
||||
* picker 上下文(二期):picker=1 时本次启动为「picker handler 调用」——launcher 注入 env
|
||||
* MEOW_PICKER_ACTION/MEOW_PICKER_REQ_ID/MEOW_PICKER_CALLER 到子进程;handler App 据此进入
|
||||
* picker 模式,选完后经 MSG_PICKER_RESULT 回传。 */
|
||||
typedef struct m_ams_start {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
char activity[MEOW_ACT_MAX]; /* 64;空=裸启动 */
|
||||
} m_ams_start_t; /* 96 B */
|
||||
uint32_t picker; /* 1=带 picker 上下文(注入 MEOW_PICKER_*) */
|
||||
uint32_t picker_req_id; /* picker 请求号(回显到结果) */
|
||||
char picker_action[24]; /* picker action(如 "pick_file");picker=0 忽略 */
|
||||
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
|
||||
} m_ams_start_t; /* 160 B */
|
||||
|
||||
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。 */
|
||||
typedef struct m_ams_kill {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
} m_ams_kill_t; /* 32 B */
|
||||
|
||||
/* ---- picker 负载(二期)---- */
|
||||
|
||||
/* MSG_PICKER_CALL ↑:发起跨 App picker 调用(App A → launcher → amsd 路由;或 amsd 内部)。
|
||||
* caller=发起方 app_id(""=系统/CLI);req_id=调用方自填(结果/PICKER_RESULT 回显);
|
||||
* action=要调用的 picker 类型(如 "pick_file");hint=初始目录等提示(空=缺省)。 */
|
||||
typedef struct m_picker_call {
|
||||
char caller[MEOW_APP_ID_MAX]; /* 32 */
|
||||
uint32_t req_id; /* 调用方请求号 */
|
||||
char action[24]; /* picker action */
|
||||
char hint[128]; /* 提示(初始目录等);空=缺省 */
|
||||
uint32_t flags; /* 预留 */
|
||||
} m_picker_call_t; /* 192 B */
|
||||
|
||||
/* MSG_PICKER_RESULT ↑:picker 返回值(handler App → launcher → amsd)。caller/req_id/action
|
||||
* 回显调用方;code=MEOW_OK/MEOW_ERR_*;value=返回项(路径/文本;空=用户取消)。 */
|
||||
typedef struct m_picker_result {
|
||||
char caller[MEOW_APP_ID_MAX]; /* 32:回显发起方(""=系统/CLI) */
|
||||
uint32_t req_id; /* 回显请求号 */
|
||||
char action[24]; /* 回显 action */
|
||||
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
|
||||
char value[256]; /* 返回项(路径/文本);空=cancel */
|
||||
} m_picker_result_t; /* 320 B */
|
||||
|
||||
#endif /* MEOW_IPC_H */
|
||||
@@ -644,12 +644,20 @@ void meow_nav_report(const char *app_id, uint32_t depth, const char *current);
|
||||
void meow_nav_exit(void); /* 清栈(应用退出/EXIT) */
|
||||
void meow_nav_request_exit(void); /* MSG_NAV{EXIT}:置位让前台 waitpid 循环终止 app */
|
||||
int meow_nav_exit_pending(void); /* 读取并清除退出请求(前台 launch_* 循环轮询) */
|
||||
/* ---- amsd 控制挂点(AMSD_DESIGN §4.3):主循环单写者,跨模块声明 ----
|
||||
/* ---- amsd 控制挂点(AMSD_DESIGN §4.3 / §6.2 二期):主循环单写者,跨模块声明 ----
|
||||
* pending-start/kill 仅在主循环上下文写入与消费(ipc_dispatch 写、run_menu 前台循环/菜单循环读)。 */
|
||||
int meow_ams_pending_start_set(const char *pkg, const char *act); /* 有前台 App 时拒绝(返回 -MEOW_ERR_BUSY) */
|
||||
int meow_ams_take_start(char *pkg, int pn, char *act, int an); /* 1=取到待拉起 App(清位);0=无 */
|
||||
void meow_ams_pending_kill_set(const char *pkg); /* 请求终止某 pkg */
|
||||
int meow_ams_kill_pending(const char *pkg); /* 匹配则清除并返回 1(前台循环轮询) */
|
||||
/* §6.2 二期:带 picker 上下文的 pending-start(amsd `am picker` 经 MSG_AMS_START 扩载荷)。
|
||||
* pick_action 非空 → launch_app_chain 注入 MEOW_PICKER_ACTION/REQ_ID/CALLER 到子进程。 */
|
||||
int meow_ams_pending_start_set_ex(const char *pkg, const char *act,
|
||||
const char *pick_action, uint32_t pick_req,
|
||||
const char *pick_caller);
|
||||
/* §6.2 二期:amsd 上报通道(best-effort,app_launcher.c 的 ams_send 公开包装)。
|
||||
* util.c 转发运行时 declare 与 picker call/result 给 amsd 用。 */
|
||||
void meow_ams_send(const char *payload, int len);
|
||||
|
||||
void meow_dac_vol_write(int pct);
|
||||
void meow_dac_vol_write_throttled(void);
|
||||
|
||||
Binary file not shown.
@@ -1097,6 +1097,32 @@ void meow_ipc_process(void)
|
||||
}
|
||||
|
||||
|
||||
/* ---- AMSD_DESIGN §6.2 二期:launcher → amsd 转发工具(best-effort)---- */
|
||||
|
||||
/* hex 编码二进制/字符串到 out(小写 2 字符/字节)。返回编码字节数。 */
|
||||
static int ams_hex_encode(const unsigned char *src, int slen, char *out, int outsz)
|
||||
{
|
||||
static const char *hx = "0123456789abcdef";
|
||||
int n = 0;
|
||||
for (int i = 0; i < slen && n + 2 < outsz; i++) {
|
||||
out[n++] = hx[src[i] >> 4];
|
||||
out[n++] = hx[src[i] & 15];
|
||||
}
|
||||
out[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* m_act_declare_t.entries 为 count 个 NUL 结尾 "id=chain" 串的拼接;返回其字节长(含 NUL)。 */
|
||||
static int declare_entries_len(const m_act_declare_t *d)
|
||||
{
|
||||
const char *p = d->entries;
|
||||
for (int k = 0; k < d->count && *p; k++) {
|
||||
while (*p) p++;
|
||||
p++;
|
||||
}
|
||||
return (int)(p - d->entries);
|
||||
}
|
||||
|
||||
/* 处理一条入队帧:HELLO→ACK、ACT_QUERY→ACT_RESULT/ERR、ACT_DECLARE→ACK、其他→ERR。 */
|
||||
static void ipc_dispatch(struct meow_ipc_msg *m)
|
||||
{
|
||||
@@ -1125,6 +1151,18 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
|
||||
a.status = (uint32_t)g_naccts;
|
||||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||||
logmsg("ipc: ACT_DECLARE app=%s n=%u -> ACK", d.app_id, d.count);
|
||||
/* AMSD_DESIGN §6.2 二期:运行时 declare 转发 amsd(权威表增量;amsd 不在→静默丢)。
|
||||
* entries 载荷 hex 编码(含 NUL 分隔),amsd 解码后并集覆盖。 */
|
||||
{
|
||||
int elen = declare_entries_len(&d);
|
||||
if (elen > 0) {
|
||||
char hex[2 * MEOW_IPC_CARGO + 8];
|
||||
(void)ams_hex_encode((const unsigned char *)d.entries, elen, hex, sizeof hex);
|
||||
char fwd[2 * MEOW_IPC_CARGO + 80];
|
||||
int fl = snprintf(fwd, sizeof fwd, "DECLARE %s %d %s\n", d.app_id, (int)d.count, hex);
|
||||
if (fl > 0) meow_ams_send(fwd, fl);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MSG_ACT_QUERY: {
|
||||
@@ -1214,12 +1252,21 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
|
||||
memcpy(&s, m->payload, m->plen < (uint16_t)sizeof s ? m->plen : (uint16_t)sizeof s);
|
||||
s.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||
s.activity[MEOW_ACT_MAX - 1] = 0;
|
||||
int rc = meow_ams_pending_start_set(s.app_id, s.activity);
|
||||
s.picker_action[23] = 0;
|
||||
s.picker_caller[MEOW_APP_ID_MAX - 1] = 0;
|
||||
/* §6.2 二期:Picker 上下文(picker=1 且 action 非空)→ 置位武装位,launch_app_chain 注入 env */
|
||||
int rc;
|
||||
if (s.picker)
|
||||
rc = meow_ams_pending_start_set_ex(s.app_id, s.activity,
|
||||
s.picker_action, s.picker_req_id, s.picker_caller);
|
||||
else
|
||||
rc = meow_ams_pending_start_set(s.app_id, s.activity);
|
||||
m_ack_t a;
|
||||
a.code = (uint32_t)(rc < 0 ? -rc : MEOW_OK); /* 0=OK;MEOW_ERR_BUSY=有前台 App */
|
||||
a.status = 0;
|
||||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||||
logmsg("ipc: AMS_START app=%s act=\"%s\" -> ACK code=%u", s.app_id, s.activity, a.code);
|
||||
logmsg("ipc: AMS_START app=%s act=\"%s\" picker=%u req=%u -> ACK code=%u",
|
||||
s.app_id, s.activity, s.picker, s.picker_req_id, a.code);
|
||||
break;
|
||||
}
|
||||
case MSG_AMS_KILL: {
|
||||
@@ -1239,6 +1286,47 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
|
||||
logmsg("ipc: AMS_KILL app=%s -> ACK code=%u", k.app_id, a.code);
|
||||
break;
|
||||
}
|
||||
/* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。
|
||||
* launcher 仅转发 amsd + ACK 发起方(best-effort;结果经 MSG_PICKER_RESULT 回路记录)。
|
||||
* 诚实出入:A 发送后即断开,amsd 结果暂时落记录(picker-status 可查),push 回 A 需
|
||||
* 将来 MSG_REGISTER 会话通道(M4)。 */
|
||||
case MSG_PICKER_CALL: {
|
||||
m_picker_call_t p;
|
||||
memset(&p, 0, sizeof p);
|
||||
memcpy(&p, m->payload, m->plen < (uint16_t)sizeof p ? m->plen : (uint16_t)sizeof p);
|
||||
p.caller[MEOW_APP_ID_MAX - 1] = 0;
|
||||
p.action[23] = 0;
|
||||
m_ack_t a;
|
||||
a.code = MEOW_OK;
|
||||
a.status = 0;
|
||||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||||
char fwd[96];
|
||||
int fl = snprintf(fwd, sizeof fwd, "PICKER_CALL %s %u %s\n", p.caller, p.req_id, p.action);
|
||||
if (fl > 0) meow_ams_send(fwd, fl);
|
||||
logmsg("ipc: PICKER_CALL caller=%s req=%u act=%s -> ACK", p.caller, p.req_id, p.action);
|
||||
break;
|
||||
}
|
||||
/* AMSD_DESIGN §6.2 二期:picker 结果回传(handler App → launcher → amsd 记录)。
|
||||
* value 可能含空格 → hex 编码,amsd 解码。 */
|
||||
case MSG_PICKER_RESULT: {
|
||||
m_picker_result_t r;
|
||||
memset(&r, 0, sizeof r);
|
||||
memcpy(&r, m->payload, m->plen < (uint16_t)sizeof r ? m->plen : (uint16_t)sizeof r);
|
||||
r.caller[MEOW_APP_ID_MAX - 1] = 0;
|
||||
r.action[23] = 0;
|
||||
m_ack_t a;
|
||||
a.code = MEOW_OK;
|
||||
a.status = 0;
|
||||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||||
char hex[512 + 8];
|
||||
(void)ams_hex_encode((const unsigned char *)r.value, (int)strlen(r.value), hex, sizeof hex);
|
||||
char fwd[96 + 512 + 8];
|
||||
int fl = snprintf(fwd, sizeof fwd, "PICKER_RESULT %u %s %u %s\n",
|
||||
r.req_id, r.action, r.code, hex);
|
||||
if (fl > 0) meow_ams_send(fwd, fl);
|
||||
logmsg("ipc: PICKER_RESULT req=%u act=%s code=%u value=\"%s\"", r.req_id, r.action, r.code, r.value);
|
||||
break;
|
||||
}
|
||||
/* M2a:通知存储(NOTIFICATION_SYSTEM.md §4/§7/§8)。IPC 线程只入队,此处在主循环
|
||||
* 单写者落存储 + 回 ACK(与 HELLO/QUERY 同一线程模型,绝不在 socket IO 阻塞)。 */
|
||||
case MSG_NOTIFY: {
|
||||
|
||||
@@ -50,6 +50,20 @@
|
||||
#include <linux/input.h> /* KEY_* / EV_KEY / EV_SYN / struct input_event(按键注入) */
|
||||
|
||||
#include "meow_ipc.h" /* 帧格式/消息号 + m_perf_query_t/m_perf_result_t + POWER_* 枚举 */
|
||||
#include "mp_jpeg.h" /* 实时画面:RGB565 → 最小 baseline JPEG(自包含,零依赖) */
|
||||
|
||||
/* 实时画面 / 触摸注入(阶段 C)。fb ioctl 常量按 launcher fb_compositor 同款手工定义
|
||||
* (避免 libc/kernel struct ABI 差异,读入裸字节缓冲再按偏移取字段)。 */
|
||||
#ifndef FBIOGET_VSCREENINFO
|
||||
#define FBIOGET_VSCREENINFO 0x4600
|
||||
#define FBIOGET_FSCREENINFO 0x4602
|
||||
#endif
|
||||
#ifndef FB_DEV
|
||||
#define FB_DEV "/dev/fb0" /* 16bpp RGB565,480x720,双层虚拟缓冲 */
|
||||
#endif
|
||||
#ifndef TOUCH_DEV
|
||||
#define TOUCH_DEV "/dev/input/event1" /* goodix-ts:接收 write() 注入(本机实测可投递) */
|
||||
#endif
|
||||
|
||||
/* ---------------- 常量 ---------------- */
|
||||
#define PANEL_VERSION "0.1"
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
/* mp_jpeg.c — 最小 baseline JPEG 编码器(顺序 DCT,4:2:0,标准 Huffman 表 Annex K)。
|
||||
*
|
||||
* 自包含:零第三方依赖,无 libm(DCT 用预计算余弦常数表),仅 malloc/realloc。
|
||||
* 输入 16bpp RGB565 → YCbCr(4:2:0) → DCT → 量化 → zigzag → RLE/Huffman → entropy 字节填充。
|
||||
* 供 MeowPanel 实时画面(GET /api/screen.jpeg)编码 fb0 帧。
|
||||
*/
|
||||
#include "mp_jpeg.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- 输出缓冲(带字节填充) ---- */
|
||||
typedef struct { uint8_t *d; size_t len, cap; } mj_out;
|
||||
static int mj_reserve(mj_out *o, size_t need) {
|
||||
if (o->len + need <= o->cap) return 0;
|
||||
size_t nc = o->cap ? o->cap * 2 : 8192;
|
||||
while (nc < o->len + need) nc *= 2;
|
||||
uint8_t *nd = (uint8_t *)realloc(o->d, nc);
|
||||
if (!nd) return -1;
|
||||
o->d = nd; o->cap = nc;
|
||||
return 0;
|
||||
}
|
||||
static int mj_byte(mj_out *o, uint8_t b) {
|
||||
if (mj_reserve(o, 1)) return -1;
|
||||
o->d[o->len++] = b;
|
||||
return 0;
|
||||
}
|
||||
static int mj_bytes(mj_out *o, const uint8_t *b, size_t n) {
|
||||
if (mj_reserve(o, n)) return -1;
|
||||
memcpy(o->d + o->len, b, n);
|
||||
o->len += n;
|
||||
return 0;
|
||||
}
|
||||
static int mj_u16(mj_out *o, uint16_t v) {
|
||||
if (mj_reserve(o, 2)) return -1;
|
||||
o->d[o->len++] = (uint8_t)(v >> 8);
|
||||
o->d[o->len++] = (uint8_t)v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 标准量化表(Annex K,quality 50) ---- */
|
||||
static const uint8_t MQ_LUMA[64] = {
|
||||
16,11,10,16,24,40,51,61, 12,12,14,19,26,58,60,55,
|
||||
14,13,16,24,40,57,69,56, 14,17,22,29,51,87,80,62,
|
||||
18,22,37,56,68,109,103,77, 24,35,55,64,81,104,113,92,
|
||||
49,64,78,87,103,121,120,101, 72,92,95,98,112,100,103,99
|
||||
};
|
||||
static const uint8_t MQ_CHROMA[64] = {
|
||||
17,18,24,47,99,99,99,99, 18,21,26,66,99,99,99,99,
|
||||
24,26,56,99,99,99,99,99, 47,66,99,99,99,99,99,99,
|
||||
99,99,99,99,99,99,99,99, 99,99,99,99,99,99,99,99,
|
||||
99,99,99,99,99,99,99,99, 99,99,99,99,99,99,99,99
|
||||
};
|
||||
|
||||
/* ---- 标准 Huffman 表 ---- */
|
||||
static const uint8_t HB_DC_L[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 };
|
||||
static const uint8_t HV_DC_L[12] = { 0,1,2,3,4,5,6,7,8,9,10,11 };
|
||||
static const uint8_t HB_AC_L[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 };
|
||||
static const uint8_t HV_AC_L[162] = {
|
||||
0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,
|
||||
0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08,0x23,0x42,0xB1,0xC1,0x15,0x52,
|
||||
0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,
|
||||
0x26,0x27,0x28,0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,
|
||||
0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x63,0x64,
|
||||
0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,
|
||||
0x84,0x85,0x86,0x87,0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,
|
||||
0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6,
|
||||
0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,
|
||||
0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,
|
||||
0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA
|
||||
};
|
||||
static const uint8_t HB_DC_C[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 };
|
||||
static const uint8_t HV_DC_C[12] = { 0,1,2,3,4,5,6,7,8,9,10,11 };
|
||||
static const uint8_t HB_AC_C[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 };
|
||||
static const uint8_t HV_AC_C[162] = {
|
||||
0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,
|
||||
0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91,0xA1,0xB1,0xC1,0x09,0x23,0x33,
|
||||
0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,
|
||||
0x19,0x1A,0x26,0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,
|
||||
0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x63,
|
||||
0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,
|
||||
0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,
|
||||
0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,
|
||||
0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,
|
||||
0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,
|
||||
0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA
|
||||
};
|
||||
|
||||
/* ---- 预计算 FDCT 余弦系数(cosp[u][x] = C(u)/2 * cos((2x+1)u*pi/16)) ---- */
|
||||
static const double MP_COSP[8][8] = {
|
||||
{0.35355339059327373,0.35355339059327373,0.35355339059327373,0.35355339059327373,0.35355339059327373,0.35355339059327373,0.35355339059327373,0.35355339059327373},
|
||||
{0.49039264020161522,0.41573480615127262,0.27778511650980114,0.097545161008064166,-0.097545161008064096,-0.27778511650980098,-0.41573480615127267,-0.49039264020161522},
|
||||
{0.46193976625564337,0.19134171618254492,-0.19134171618254486,-0.46193976625564337,-0.46193976625564342,-0.19134171618254517,0.191341716182545,0.46193976625564326},
|
||||
{0.41573480615127262,-0.097545161008064096,-0.49039264020161522,-0.27778511650980109,0.27778511650980092,0.49039264020161522,0.097545161008064388,-0.41573480615127256},
|
||||
{0.35355339059327379,-0.35355339059327373,-0.35355339059327384,0.35355339059327368,0.35355339059327384,-0.35355339059327334,-0.35355339059327356,0.35355339059327329},
|
||||
{0.27778511650980114,-0.49039264020161522,0.097545161008064152,0.41573480615127273,-0.41573480615127256,-0.097545161008064013,0.49039264020161533,-0.27778511650980076},
|
||||
{0.19134171618254492,-0.46193976625564342,0.46193976625564326,-0.19134171618254495,-0.19134171618254528,0.46193976625564337,-0.4619397662556432,0.19134171618254478},
|
||||
{0.097545161008064166,-0.27778511650980109,0.41573480615127273,-0.49039264020161533,0.49039264020161522,-0.41573480615127251,0.27778511650980076,-0.097545161008064291}
|
||||
};
|
||||
|
||||
/* ---- zigzag 顺序(自然索引 = v*8+u) ---- */
|
||||
static const uint8_t MP_ZZ[64] = {
|
||||
0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,
|
||||
12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,
|
||||
35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,
|
||||
58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63
|
||||
};
|
||||
|
||||
/* ---- Huffman 码表 ---- */
|
||||
typedef struct { uint16_t code; uint8_t size; } mj_hc;
|
||||
static void huff_build(const uint8_t *bits, const uint8_t *vals, int nval, mj_hc *out) {
|
||||
int code = 0, k = 0;
|
||||
for (int len = 1; len <= 16; len++) {
|
||||
for (int i = 0; i < bits[len - 1]; i++) {
|
||||
if (k >= nval) return;
|
||||
out[vals[k]].code = (uint16_t)code;
|
||||
out[vals[k]].size = (uint8_t)len;
|
||||
k++; code++;
|
||||
}
|
||||
code <<= 1;
|
||||
}
|
||||
}
|
||||
static int cat_of(int v) {
|
||||
int a = (v < 0) ? -v : v, s = 0;
|
||||
while (a) { a >>= 1; s++; }
|
||||
return s;
|
||||
}
|
||||
static uint32_t cat_bits(int v, int s) {
|
||||
return (uint32_t)((v >= 0) ? v : (v + ((1 << s) - 1)));
|
||||
}
|
||||
|
||||
/* ---- bit writer(MSB 先行 + entropy 0xFF 字节填充) ---- */
|
||||
typedef struct { mj_out *o; uint32_t acc; int nbits; } mj_bw;
|
||||
static int bw_bits(mj_bw *w, uint32_t code, int size) {
|
||||
if (size > 0) {
|
||||
uint32_t m = (size >= 31) ? 0xffffffffu : ((1u << size) - 1u);
|
||||
w->acc = (w->acc << size) | (code & m);
|
||||
w->nbits += size;
|
||||
}
|
||||
while (w->nbits >= 8) {
|
||||
w->nbits -= 8;
|
||||
uint8_t b = (uint8_t)((w->acc >> w->nbits) & 0xff);
|
||||
if (mj_byte(w->o, b)) return -1;
|
||||
if (b == 0xff) { if (mj_byte(w->o, 0x00)) return -1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int bw_flush(mj_bw *w) {
|
||||
if (w->nbits > 0) {
|
||||
uint8_t b = (uint8_t)((w->acc << (8 - w->nbits)) & 0xff);
|
||||
if (mj_byte(w->o, b)) return -1;
|
||||
if (b == 0xff) { if (mj_byte(w->o, 0x00)) return -1; }
|
||||
w->acc = 0; w->nbits = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 量化表(按 quality 缩放) ---- */
|
||||
static void build_q(const uint8_t *base, int quality, uint8_t *out) {
|
||||
int s = (quality < 50) ? (5000 / quality) : (200 - quality * 2);
|
||||
if (s < 1) s = 1;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
int v = (base[i] * s + 50) / 100;
|
||||
if (v < 1) v = 1;
|
||||
if (v > 255) v = 255;
|
||||
out[i] = (uint8_t)v;
|
||||
}
|
||||
}
|
||||
|
||||
static int clamp255(int v) { if (v < 0) v = 0; if (v > 255) v = 255; return v; }
|
||||
|
||||
/* ---- 抓 8x8 样本(越界 clamp 到边缘) ---- */
|
||||
static void grab8(const uint8_t *plane, int pstride, int pw, int ph,
|
||||
int x0, int y0, double s[8][8]) {
|
||||
for (int y = 0; y < 8; y++) {
|
||||
int py = y0 + y; if (py >= ph) py = ph - 1;
|
||||
const uint8_t *row = plane + (size_t)py * (size_t)pstride;
|
||||
for (int x = 0; x < 8; x++) {
|
||||
int px = x0 + x; if (px >= pw) px = pw - 1;
|
||||
s[y][x] = (double)row[px] - 128.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 编码一个 8x8 块(DCT→量化→zigzag→RLE/Huffman) ---- */
|
||||
static int encode_block(mj_bw *w, const double samp[8][8], const uint8_t *q,
|
||||
const mj_hc *dc, const mj_hc *ac, int *prev_dc) {
|
||||
int coef[64];
|
||||
double R[8][8], F[8][8];
|
||||
for (int y = 0; y < 8; y++)
|
||||
for (int u = 0; u < 8; u++) {
|
||||
double acc = 0;
|
||||
for (int x = 0; x < 8; x++) acc += MP_COSP[u][x] * samp[y][x];
|
||||
R[u][y] = acc;
|
||||
}
|
||||
for (int u = 0; u < 8; u++)
|
||||
for (int v = 0; v < 8; v++) {
|
||||
double acc = 0;
|
||||
for (int y = 0; y < 8; y++) acc += MP_COSP[v][y] * R[u][y];
|
||||
F[u][v] = acc;
|
||||
}
|
||||
for (int v = 0; v < 8; v++)
|
||||
for (int u = 0; u < 8; u++) {
|
||||
double c = F[u][v] / (double)q[v * 8 + u];
|
||||
coef[v * 8 + u] = (int)(c + (c >= 0 ? 0.5 : -0.5));
|
||||
}
|
||||
|
||||
int dcv = coef[0];
|
||||
int diff = dcv - *prev_dc;
|
||||
*prev_dc = dcv;
|
||||
int ds = cat_of(diff);
|
||||
if (bw_bits(w, dc[ds].code, dc[ds].size)) return -1;
|
||||
if (ds && bw_bits(w, cat_bits(diff, ds), ds)) return -1;
|
||||
|
||||
int run = 0;
|
||||
for (int p = 1; p < 64; p++) {
|
||||
int val = coef[MP_ZZ[p]];
|
||||
if (!val) { run++; continue; }
|
||||
while (run >= 16) {
|
||||
if (bw_bits(w, ac[0xF0].code, ac[0xF0].size)) return -1;
|
||||
run -= 16;
|
||||
}
|
||||
int sc = cat_of(val);
|
||||
int sym = (run << 4) | sc;
|
||||
if (bw_bits(w, ac[sym].code, ac[sym].size)) return -1;
|
||||
if (bw_bits(w, cat_bits(val, sc), sc)) return -1;
|
||||
run = 0;
|
||||
}
|
||||
if (run > 0) {
|
||||
if (bw_bits(w, ac[0x00].code, ac[0x00].size)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- DHT 段 ---- */
|
||||
static int emit_dht(mj_out *o, uint8_t spec, const uint8_t *bits,
|
||||
const uint8_t *vals, int nval) {
|
||||
if (mj_byte(o, 0xFF) || mj_byte(o, 0xC4)) return -1;
|
||||
if (mj_u16(o, (uint16_t)(2 + 1 + 16 + nval))) return -1;
|
||||
if (mj_byte(o, spec)) return -1;
|
||||
if (mj_bytes(o, bits, 16)) return -1;
|
||||
if (mj_bytes(o, vals, (size_t)nval)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality,
|
||||
uint8_t **out, size_t *outlen) {
|
||||
if (!rgb565 || w <= 0 || h <= 0 || !out || !outlen) return -1;
|
||||
if (quality < 1) quality = 1;
|
||||
if (quality > 100) quality = 100;
|
||||
|
||||
int wc = (w + 1) / 2, hc = (h + 1) / 2;
|
||||
uint8_t *Y = (uint8_t *)malloc((size_t)w * (size_t)h);
|
||||
uint8_t *Cb = (uint8_t *)malloc((size_t)wc * (size_t)hc);
|
||||
uint8_t *Cr = (uint8_t *)malloc((size_t)wc * (size_t)hc);
|
||||
uint8_t ql[64], qc[64];
|
||||
if (!Y || !Cb || !Cr) { free(Y); free(Cb); free(Cr); return -1; }
|
||||
build_q(MQ_LUMA, quality, ql);
|
||||
build_q(MQ_CHROMA, quality, qc);
|
||||
|
||||
/* RGB565 → Y 平面(全分辨率) + Cb/Cr 平面(2x2 平均) */
|
||||
for (int cy = 0; cy < h; cy += 2)
|
||||
for (int cx = 0; cx < w; cx += 2) {
|
||||
double cb = 0, cr = 0;
|
||||
int n = 0;
|
||||
for (int dy = 0; dy < 2; dy++)
|
||||
for (int dx = 0; dx < 2; dx++) {
|
||||
int py = cy + dy, px = cx + dx;
|
||||
if (py >= h || px >= w) continue;
|
||||
uint16_t v = (uint16_t)(rgb565[((size_t)py * (size_t)w + (size_t)px) * 2]
|
||||
| (rgb565[((size_t)py * (size_t)w + (size_t)px) * 2 + 1] << 8));
|
||||
int r5 = (v >> 11) & 0x1f, g6 = (v >> 5) & 0x3f, b5 = v & 0x1f;
|
||||
int R = (r5 << 3) | (r5 >> 2);
|
||||
int G = (g6 << 2) | (g6 >> 4);
|
||||
int B = (b5 << 3) | (b5 >> 2);
|
||||
Y[(size_t)py * (size_t)w + (size_t)px] = (uint8_t)clamp255((int)(0.299 * R + 0.587 * G + 0.114 * B + 0.5));
|
||||
cb += -0.168736 * R - 0.331264 * G + 0.5 * B;
|
||||
cr += 0.5 * R - 0.418688 * G - 0.081312 * B;
|
||||
n++;
|
||||
}
|
||||
size_t k = (size_t)(cy / 2) * (size_t)wc + (size_t)(cx / 2);
|
||||
Cb[k] = (uint8_t)clamp255((int)(128.0 + cb / (n ? n : 1) + 0.5));
|
||||
Cr[k] = (uint8_t)clamp255((int)(128.0 + cr / (n ? n : 1) + 0.5));
|
||||
}
|
||||
|
||||
mj_out o = {0};
|
||||
mj_hc hdc_l[16], hac_l[256], hdc_c[16], hac_c[256];
|
||||
memset(hdc_l, 0, sizeof hdc_l); memset(hac_l, 0, sizeof hac_l);
|
||||
memset(hdc_c, 0, sizeof hdc_c); memset(hac_c, 0, sizeof hac_c);
|
||||
huff_build(HB_DC_L, HV_DC_L, 12, hdc_l);
|
||||
huff_build(HB_AC_L, HV_AC_L, 162, hac_l);
|
||||
huff_build(HB_DC_C, HV_DC_C, 12, hdc_c);
|
||||
huff_build(HB_AC_C, HV_AC_C, 162, hac_c);
|
||||
|
||||
int rc = -1;
|
||||
/* SOI */
|
||||
if (mj_byte(&o, 0xFF) || mj_byte(&o, 0xD8)) goto done;
|
||||
/* APP0 JIF */
|
||||
static const uint8_t jfif[14] = { 'J','F','I','F',0,1,1,0,0,1,0,1,0,0 };
|
||||
if (mj_byte(&o, 0xFF) || mj_byte(&o, 0xE0) || mj_u16(&o, 0x0010)) goto done;
|
||||
if (mj_bytes(&o, jfif, 14)) goto done;
|
||||
/* DQT(2 张表) */
|
||||
if (mj_byte(&o, 0xFF) || mj_byte(&o, 0xDB) || mj_u16(&o, 0x0084)) goto done;
|
||||
if (mj_byte(&o, 0x00) || mj_bytes(&o, ql, 64)) goto done;
|
||||
if (mj_byte(&o, 0x01) || mj_bytes(&o, qc, 64)) goto done;
|
||||
/* SOF0 baseline, 8-bit, 3 分量, 4:2:0 */
|
||||
if (mj_byte(&o, 0xFF) || mj_byte(&o, 0xC0) || mj_u16(&o, 0x0011)) goto done;
|
||||
if (mj_byte(&o, 0x08)) goto done;
|
||||
if (mj_u16(&o, (uint16_t)h) || mj_u16(&o, (uint16_t)w)) goto done;
|
||||
if (mj_byte(&o, 0x03)) goto done;
|
||||
if (mj_byte(&o, 0x01) || mj_byte(&o, 0x22) || mj_byte(&o, 0x00)) goto done; /* Y */
|
||||
if (mj_byte(&o, 0x02) || mj_byte(&o, 0x11) || mj_byte(&o, 0x01)) goto done; /* Cb */
|
||||
if (mj_byte(&o, 0x03) || mj_byte(&o, 0x11) || mj_byte(&o, 0x01)) goto done; /* Cr */
|
||||
/* DHT(4 张表) */
|
||||
if (emit_dht(&o, 0x00, HB_DC_L, HV_DC_L, 12)) goto done;
|
||||
if (emit_dht(&o, 0x10, HB_AC_L, HV_AC_L, 162)) goto done;
|
||||
if (emit_dht(&o, 0x01, HB_DC_C, HV_DC_C, 12)) goto done;
|
||||
if (emit_dht(&o, 0x11, HB_AC_C, HV_AC_C, 162)) goto done;
|
||||
/* SOS */
|
||||
if (mj_byte(&o, 0xFF) || mj_byte(&o, 0xDA) || mj_u16(&o, 0x000C)) goto done;
|
||||
if (mj_byte(&o, 0x03)) goto done;
|
||||
if (mj_byte(&o, 0x01) || mj_byte(&o, 0x00)) goto done; /* Y: DC0 AC0 */
|
||||
if (mj_byte(&o, 0x02) || mj_byte(&o, 0x11)) goto done; /* Cb: DC1 AC1 */
|
||||
if (mj_byte(&o, 0x03) || mj_byte(&o, 0x11)) goto done; /* Cr: DC1 AC1 */
|
||||
if (mj_byte(&o, 0x00) || mj_byte(&o, 0x3F) || mj_byte(&o, 0x00)) goto done;
|
||||
|
||||
{
|
||||
mj_bw bb = { &o, 0, 0 };
|
||||
int pd_y = 0, pd_cb = 0, pd_cr = 0; /* 每分量独立 DC 预测器(JPEG 规范) */
|
||||
int mx_count = (w + 15) / 16, my_count = (h + 15) / 16;
|
||||
for (int my = 0; my < my_count; my++)
|
||||
for (int mx = 0; mx < mx_count; mx++) {
|
||||
for (int by = 0; by < 2; by++)
|
||||
for (int bx = 0; bx < 2; bx++) {
|
||||
double s[8][8];
|
||||
grab8(Y, w, w, h, mx * 16 + bx * 8, my * 16 + by * 8, s);
|
||||
if (encode_block(&bb, s, ql, hdc_l, hac_l, &pd_y)) goto done;
|
||||
}
|
||||
{
|
||||
double s[8][8];
|
||||
grab8(Cb, wc, wc, hc, mx * 8, my * 8, s);
|
||||
if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cb)) goto done;
|
||||
}
|
||||
{
|
||||
double s[8][8];
|
||||
grab8(Cr, wc, wc, hc, mx * 8, my * 8, s);
|
||||
if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cr)) goto done;
|
||||
}
|
||||
}
|
||||
if (bw_flush(&bb)) goto done;
|
||||
}
|
||||
if (mj_byte(&o, 0xFF) || mj_byte(&o, 0xD9)) goto done; /* EOI */
|
||||
rc = 0;
|
||||
|
||||
done:
|
||||
free(Y); free(Cb); free(Cr);
|
||||
if (rc == 0) { *out = o.d; *outlen = o.len; }
|
||||
else { free(o.d); *out = NULL; *outlen = 0; }
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* mp_jpeg.h — 最小 baseline JPEG 编码器(自包含,零第三方依赖,无 libm)。
|
||||
*
|
||||
* 供 MeowPanel 实时画面端点(GET /api/screen.jpeg)把 fb0 的 RGB565 帧编码为 JPEG。
|
||||
* 仅实现顺序 DCT + 4:2:0 色度抽样 + 标准 Huffman 表(Annex K),8-bit,输出到内存。
|
||||
*/
|
||||
#ifndef MP_JPEG_H
|
||||
#define MP_JPEG_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* 把 16bpp RGB565 帧(row-major,每行 w*2 字节)编码为内存中的 baseline JPEG。
|
||||
* quality ∈ [1,100](近似 libjpeg 语义:50 ≈ 标准质量;<50 更小更糊,>50 更大更好)。
|
||||
* 成功返回 0,*out 置为 malloc 的 JPEG 缓冲(调用方 free),*outlen 为其字节数。
|
||||
* 失败(参数非法 / 分配失败)返回 -1,*out=NULL。 */
|
||||
int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality,
|
||||
uint8_t **out, size_t *outlen);
|
||||
|
||||
#endif
|
||||
@@ -1 +1 @@
|
||||
{"name":"网易云音乐","exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm","package":"dev.player.ncm","version":"0.1","version_code":1,"framework":1,"immersive":1,"fonts":"gbk","author":"MeowOS"}
|
||||
{"name":"网易云音乐","exec":"/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm","package":"dev.player.ncm","version":"0.1","version_code":1,"framework":1,"immersive":1,"fonts":"common","author":"MeowOS"}
|
||||
@@ -117,35 +117,53 @@ int appstate_load_playlists(void)
|
||||
return rc;
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs(int idx)
|
||||
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
|
||||
/* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
|
||||
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
|
||||
const ncm_playlist_t *pl = &g_st.playlists[idx];
|
||||
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
|
||||
if (offset < 0) offset = 0;
|
||||
|
||||
int idn = 0;
|
||||
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
|
||||
NULL, 0, NULL, 0, g_st.song_ids, NCM_SONG_MAX, &idn);
|
||||
if (rc != NCM_OK) { g_st.song_count = 0; return rc; }
|
||||
/* 二期:改走 /api/playlist/track/all 服务端分页(REPORT §11)。
|
||||
* 相对旧 /v6/playlist/detail 的「全量 trackIds(5191 首 ≈900KB)」本端点直接按页返回歌曲
|
||||
* 对象(含 al.picUrl),无需 detail 瓶颈 + 二次 song_detail。 */
|
||||
ncm_song_t tmp[NCM_SONG_BATCH];
|
||||
int n = 0; int64_t total = 0;
|
||||
int rc = ncm_api_playlist_track_all(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
|
||||
offset, limit, tmp, NCM_SONG_BATCH, &n, &total);
|
||||
if (rc != NCM_OK) { if (offset == 0) g_st.song_count = 0; return rc; }
|
||||
|
||||
memset(g_st.songs, 0, sizeof g_st.songs);
|
||||
int outn = 0;
|
||||
for (int off = 0; off < idn; off += NCM_SONG_BATCH) {
|
||||
int batch = idn - off;
|
||||
if (batch > NCM_SONG_BATCH) batch = NCM_SONG_BATCH;
|
||||
int bn = 0;
|
||||
rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[off],
|
||||
batch, &g_st.songs[off], NCM_SONG_MAX - off, &bn);
|
||||
if (rc != NCM_OK) break;
|
||||
outn += bn;
|
||||
if (bn < batch) break;
|
||||
}
|
||||
g_st.song_count = outn;
|
||||
if (offset == 0) g_st.song_count = 0; /* 重头:清空旧歌单(含此前打开的另一歌单) */
|
||||
|
||||
/* 追加到 g_st.songs[offset..],封顶 NCM_SONG_MAX(内存纪律) */
|
||||
int cap = NCM_SONG_MAX - offset;
|
||||
if (cap <= 0) return NCM_OK;
|
||||
if (n > cap) n = cap;
|
||||
memcpy(&g_st.songs[offset], tmp, sizeof(ncm_song_t) * (size_t)n);
|
||||
int end = offset + n;
|
||||
if (end > NCM_SONG_MAX) end = NCM_SONG_MAX;
|
||||
g_st.song_count = end;
|
||||
appstate_apply_playlist_cover(idx);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
int appstate_load_playlist_songs(int idx)
|
||||
{
|
||||
/* 兼容入口:整批载入(内部按页循环到 NCM_SONG_MAX 或歌单尾部)。 */
|
||||
int rc, loaded = 0;
|
||||
do {
|
||||
rc = appstate_load_playlist_songs_page(idx, loaded, NCM_PLAYLIST_PAGE);
|
||||
if (rc != NCM_OK) return rc;
|
||||
int before = loaded;
|
||||
loaded = g_st.song_count;
|
||||
if (loaded - before < NCM_PLAYLIST_PAGE) break; /* 尾部:已载完 */
|
||||
if (loaded >= NCM_SONG_MAX) break;
|
||||
} while (1);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
void appstate_apply_playlist_cover(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.playlist_count) return;
|
||||
|
||||
@@ -16,6 +16,22 @@ static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||||
static int s_n;
|
||||
|
||||
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
|
||||
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带容忍 \r\n。有界 ≤max。 */
|
||||
static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
|
||||
{
|
||||
size_t oi = 0, si = 0;
|
||||
while (si < n && oi < max - 1) {
|
||||
if (src[si] == '\\' && si + 1 < n && (src[si + 1] == 'n' || src[si + 1] == 'r')) {
|
||||
dst[oi++] = ' ';
|
||||
si += 2;
|
||||
} else {
|
||||
dst[oi++] = src[si++];
|
||||
}
|
||||
}
|
||||
dst[oi] = 0;
|
||||
}
|
||||
|
||||
static void lrc_parse(const char *lrc, int is_trans)
|
||||
{
|
||||
if (!lrc) return;
|
||||
@@ -55,14 +71,14 @@ static void lrc_parse(const char *lrc, int is_trans)
|
||||
/* 翻译行:按 first_ms 匹配到已有原文行 */
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_t[i] == first_ms) {
|
||||
memcpy(s_tl[i], p, len); s_tl[i][len] = 0;
|
||||
copy_lrc_text(s_tl[i], p, len, LRC_MAX_TEXT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (s_n < NCM_LYRIC_LINES) {
|
||||
s_t[s_n] = first_ms;
|
||||
memcpy(s_txt[s_n], p, len); s_txt[s_n][len] = 0;
|
||||
copy_lrc_text(s_txt[s_n], p, len, LRC_MAX_TEXT);
|
||||
s_tl[s_n][0] = 0;
|
||||
s_n++;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
|
||||
#define NCM_SONG_MAX 256 /* 单个歌单曲目上限(大歌单截断前 256 条) */
|
||||
#define NCM_SONG_BATCH 64 /* song_detail 分批每批条数 */
|
||||
#define NCM_PLAYLIST_PAGE 50 /* 歌单曲目服务端分页每页条数(/api/playlist/track/all) */
|
||||
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
|
||||
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
|
||||
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
|
||||
@@ -246,7 +247,10 @@ void appstate_set_logged_in(const char *uid, const char *nickname);
|
||||
void appstate_logout(void);
|
||||
int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取回;0=成功 */
|
||||
int appstate_load_playlists(void); /* 拉我的歌单 */
|
||||
int appstate_load_playlist_songs(int idx); /* 拉歌单详情+曲目详情 */
|
||||
int appstate_load_playlist_songs(int idx); /* 兼容:整批载入(内部走分页循环) */
|
||||
int appstate_load_playlist_songs_page(int idx, int offset, int limit);
|
||||
/* 服务端分页载入第 offset 页(offset=0 重头填,
|
||||
* offset>0 追加到 g_st.songs[offset..]);写 g_st.song_count */
|
||||
void appstate_apply_playlist_cover(int idx);
|
||||
void appstate_set_playing_song(int idx, const char *cover_url);
|
||||
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
|
||||
@@ -258,6 +262,7 @@ const char *lyric_line_text(int i);
|
||||
const char *lyric_line_tl(int i);
|
||||
int lyric_count(void);
|
||||
void lyric_clear(void);
|
||||
int lyric_state(void); /* 0=空闲/未拉 1=加载中 2=已拉完(可能 0 行=无词) */
|
||||
|
||||
/* cover */
|
||||
void cover_request(const char *url); /* 提交目标封面 URL(后台线程下载+解码) */
|
||||
|
||||
@@ -280,21 +280,89 @@ static void *player_thread(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 为当前曲目拉歌词并解析(UI 线程调):buf 有界,解析后存 lyric 模块。 */
|
||||
/* ---- 歌词后台加载(二期):UI 不阻塞 —— 持久 worker 监视当前播放歌曲,首播/切歌/自动顺延
|
||||
* 时在独立线程拉取并解析,UI 经 lyric_state() 判「加载中 / 无词」。 ----
|
||||
* 现存 race(§10.9 FM4,UI 读 lyrics 与 worker 写)为良性:数组定长、越界已防护,
|
||||
* 最坏显示一帧旧/空行,下一 tick 自愈;与 g_st.songs 写读竞态同接受。 */
|
||||
static pthread_mutex_t g_lyrmu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_t g_lyr_thr;
|
||||
static int g_lyr_thr_on;
|
||||
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
|
||||
static char g_lyr_song_id[32]; /* 歌词归属歌曲 id */
|
||||
|
||||
static void *lyric_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char last_id[32] = "";
|
||||
while (!g_app_quit) {
|
||||
usleep(300 * 1000);
|
||||
int idx;
|
||||
pthread_mutex_lock(&g_play.mu); idx = g_play.cur_song; pthread_mutex_unlock(&g_play.mu);
|
||||
if (idx < 0 || idx >= g_st.song_count) {
|
||||
lyric_clear();
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
g_lyr_song_id[0] = 0;
|
||||
g_lyr_state = 0;
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
last_id[0] = 0;
|
||||
continue;
|
||||
}
|
||||
const char *id = g_st.songs[idx].id;
|
||||
if (!id[0]) continue;
|
||||
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
|
||||
|
||||
ncm_strlcpy(last_id, id, sizeof last_id);
|
||||
lyric_clear();
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
g_lyr_state = 1; /* 加载中 */
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
|
||||
char *lrc = (char *)malloc(64 * 1024);
|
||||
char *tl = (char *)malloc(32 * 1024);
|
||||
if (!lrc || !tl) {
|
||||
free(lrc); free(tl);
|
||||
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
||||
continue;
|
||||
}
|
||||
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) lyric_parse(lrc, tl);
|
||||
free(lrc); free(tl);
|
||||
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
|
||||
g_lyr_state = 2; /* 拉完(可能 0 行=无词) */
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void lyric_ensure_worker(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
if (!g_lyr_thr_on) {
|
||||
g_lyr_thr_on = 1;
|
||||
if (pthread_create(&g_lyr_thr, NULL, lyric_worker, NULL) == 0) pthread_detach(g_lyr_thr);
|
||||
else g_lyr_thr_on = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
}
|
||||
|
||||
/* 为当前曲目「异步」拉歌词:确保后台 worker 在跑即返回(UI 不阻塞网络/解析)。
|
||||
* 实际拉取+解析在 worker 线程(约 0.3s 内开始),结束置 lyric_state()=2。 */
|
||||
int player_manager_load_lyric(void)
|
||||
{
|
||||
lyric_clear();
|
||||
int idx = player_manager_playing_song();
|
||||
if (idx < 0 || idx >= g_st.song_count) return NCM_ERR_ARG;
|
||||
const ncm_song_t *s = &g_st.songs[idx];
|
||||
char *lrc = (char *)malloc(64 * 1024);
|
||||
char *tl = (char *)malloc(32 * 1024);
|
||||
if (!lrc || !tl) { free(lrc); free(tl); return NCM_ERR_OOM; }
|
||||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, s->id, lrc, 64 * 1024, tl, 32 * 1024);
|
||||
if (rc == NCM_OK) lyric_parse(lrc, tl);
|
||||
free(lrc);
|
||||
free(tl);
|
||||
return rc;
|
||||
lyric_ensure_worker();
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
/* 歌词加载状态(UI 判「loading / no lyric」)。 */
|
||||
int lyric_state(void)
|
||||
{
|
||||
int s;
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
s = g_lyr_state;
|
||||
pthread_mutex_unlock(&g_lyrmu);
|
||||
return s;
|
||||
}
|
||||
|
||||
int player_manager_start(void)
|
||||
|
||||
@@ -231,7 +231,12 @@ static void plr_draw_dyn(void *ctx)
|
||||
int line_h = (g_landscape) ? 24 : 26;
|
||||
int center_y = g_lyr.y + g_lyr.h / 2;
|
||||
int lcx = g_lyr.x + g_lyr.w / 2;
|
||||
if (n == 0) { meow_ui_text_center(lcx, center_y - 10, "no lyric", 20, th->onSurfaceVariant); }
|
||||
if (n == 0) {
|
||||
/* 二期:歌词后台加载,状态区分「加载中 / 真的无词」 */
|
||||
meow_ui_text_center(lcx, center_y - 10,
|
||||
lyric_state() == 2 ? "no lyric" : "loading lyric...",
|
||||
20, th->onSurfaceVariant);
|
||||
}
|
||||
else {
|
||||
int64_t ps = player_manager_pos_ms();
|
||||
int cur2 = lyric_line_at(ps);
|
||||
@@ -249,9 +254,10 @@ static void plr_draw_dyn(void *ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/* 页脚提示 */
|
||||
/* 页脚提示(用「・」U+30FB 而非 U+00B7「·」:common 字库按 GB2312 解码 A1A4 得 U+30FB,
|
||||
* 不含 U+00B7 → 后者在设备上是豆腐块。REPORT §11 二期。) */
|
||||
char foot[64];
|
||||
snprintf(foot, sizeof foot, "左右滑切歌 · 点顶部切横屏");
|
||||
snprintf(foot, sizeof foot, "左右滑切歌 ・ 点顶部切横屏");
|
||||
meow_ui_text_center(r_cont.x + r_cont.w / 2, r_cont.y + r_cont.h - APP_FOOTER_H,
|
||||
foot, 20, th->onSurfaceVariant);
|
||||
}
|
||||
@@ -297,9 +303,13 @@ void player_on_tap(int x, int y)
|
||||
void player_play_song(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= g_st.song_count) return;
|
||||
/* 二期:黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅),
|
||||
* 无 al.picUrl(如无版权占位)时回落歌单封面。 */
|
||||
const ncm_song_t *s = &g_st.songs[idx];
|
||||
const char *cover = (s && s->al_pic[0]) ? s->al_pic : g_st.cover_url;
|
||||
player_manager_play(idx);
|
||||
appstate_set_playing_song(idx, g_st.cover_url);
|
||||
cover_request(g_st.cover_url);
|
||||
appstate_set_playing_song(idx, cover);
|
||||
cover_request(cover);
|
||||
meow_view_page_push(ACT_PLAYER);
|
||||
}
|
||||
|
||||
@@ -328,6 +338,11 @@ void player_tick(uint64_t now)
|
||||
|
||||
if (sc != g_last_song) {
|
||||
g_last_song = sc;
|
||||
/* 切歌(next/prev/自动顺延)→ 请求新曲专辑封面(黑胶盘换图) */
|
||||
if (sc >= 0) {
|
||||
const ncm_song_t *s2 = &g_st.songs[sc];
|
||||
cover_request(s2->al_pic[0] ? s2->al_pic : g_st.cover_url);
|
||||
}
|
||||
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌名/歌手变 */
|
||||
}
|
||||
if (cover_changed()) {
|
||||
|
||||
@@ -33,6 +33,8 @@ static volatile int g_plgen;
|
||||
static int g_placked;
|
||||
static volatile int g_plloading;
|
||||
static volatile int g_plloaded_idx;
|
||||
static volatile int g_ploffset; /* 本页起始下标(0=首屏重头;>0=触底追加) */
|
||||
static volatile int g_pl_more; /* 1=尚有下一页可加载(页满且未到 NCM_SONG_MAX) */
|
||||
|
||||
static void pl_drag_reset(void)
|
||||
{
|
||||
@@ -171,12 +173,12 @@ static void pl_draw_footer(void *ctx)
|
||||
ncm_draw_footer(r_cont, foot);
|
||||
}
|
||||
|
||||
static void pl_apply(void)
|
||||
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(触底追加,防跳顶)。 */
|
||||
static void pl_apply(int reset_scroll)
|
||||
{
|
||||
if (g_list) {
|
||||
meow_listview_set_count(g_list, g_st.song_count);
|
||||
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); }
|
||||
}
|
||||
if (g_tb && g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count) {
|
||||
snprintf(g_title, sizeof g_title, "%s", g_st.playlists[g_st.active_playlist].name);
|
||||
@@ -191,12 +193,17 @@ static void *playlist_worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
int my_idx = g_st.active_playlist;
|
||||
int off = g_ploffset;
|
||||
int rc;
|
||||
if (g_host_mock) rc = 0;
|
||||
else if (my_idx < 0) rc = NCM_ERR_ARG;
|
||||
else rc = appstate_load_playlist_songs(my_idx);
|
||||
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_PLAYLIST_PAGE);
|
||||
pthread_mutex_lock(&g_plmu);
|
||||
g_load_err = rc;
|
||||
/* 页满(==NCM_PLAYLIST_PAGE)且未到记忆上限 → 还有下一页;否则到屏尾 */
|
||||
int loaded = (rc == NCM_OK) ? (g_st.song_count - off) : 0;
|
||||
g_pl_more = (rc == NCM_OK) && (loaded >= NCM_PLAYLIST_PAGE) &&
|
||||
(g_st.song_count < NCM_SONG_MAX);
|
||||
g_plloading = 0;
|
||||
g_plloaded_idx = my_idx;
|
||||
g_plgen++;
|
||||
@@ -204,18 +211,39 @@ static void *playlist_worker(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void playlist_refresh(void)
|
||||
/* 触底加载下一页:仅当仍有下一页、未在载、且未到 NCM_SONG_MAX 时发。 */
|
||||
static void pl_maybe_load_more(void)
|
||||
{
|
||||
if (g_host_mock) { g_load_err = 0; pl_apply(); return; }
|
||||
if (g_plthr_on) return;
|
||||
g_load_err = 0;
|
||||
g_st.song_count = 0;
|
||||
if (g_plthr_on || g_load_err != 0 || g_host_mock) return;
|
||||
if (!g_pl_more) return;
|
||||
if (g_st.song_count >= NCM_SONG_MAX) return;
|
||||
/* 视口底接近列表底(最后一行即将可见即预取) */
|
||||
int list_px = g_st.song_count * APP_ROW_H;
|
||||
int max_scroll = list_px - r_list.h;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (meow_listview_scroll(g_list) < max_scroll - APP_ROW_H * 2) return;
|
||||
g_ploffset = g_st.song_count;
|
||||
g_plloading = 1;
|
||||
g_plthr_on = 1;
|
||||
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
|
||||
g_plthr_on = 0; g_plloading = 0; g_load_err = NCM_ERR_IO;
|
||||
}
|
||||
pl_apply();
|
||||
}
|
||||
|
||||
void playlist_refresh(void)
|
||||
{
|
||||
if (g_host_mock) { g_load_err = 0; g_pl_more = 0; pl_apply(1); return; }
|
||||
if (g_plthr_on) return;
|
||||
g_load_err = 0;
|
||||
g_st.song_count = 0;
|
||||
g_ploffset = 0;
|
||||
g_pl_more = 0;
|
||||
g_plloading = 1;
|
||||
g_plthr_on = 1;
|
||||
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
|
||||
g_plthr_on = 0; g_plloading = 0; g_load_err = NCM_ERR_IO;
|
||||
}
|
||||
pl_apply(1);
|
||||
}
|
||||
|
||||
void playlist_tick(uint64_t now)
|
||||
@@ -227,8 +255,11 @@ void playlist_tick(uint64_t now)
|
||||
g_plthr_on = 0;
|
||||
g_plloading = 0;
|
||||
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
|
||||
if (g_load_err != 0) g_st.song_count = 0;
|
||||
pl_apply();
|
||||
if (g_load_err != 0) {
|
||||
if (g_ploffset == 0) g_st.song_count = 0; /* 首屏失败 → 整页空态 */
|
||||
else { ncm_snack("加载失败"); g_load_err = 0; } /* 追加失败 → 保留已载歌曲 */
|
||||
}
|
||||
pl_apply(g_ploffset == 0); /* 首屏回顶;触底追加保持滚动 */
|
||||
}
|
||||
|
||||
void playlist_open(int idx)
|
||||
@@ -294,8 +325,10 @@ void playlist_touch_move(int x, int y)
|
||||
if (dy == 0) return;
|
||||
int before = meow_listview_scroll(g_list);
|
||||
meow_listview_scroll_by(g_list, -dy);
|
||||
if (meow_listview_scroll(g_list) != before)
|
||||
if (meow_listview_scroll(g_list) != before) {
|
||||
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
pl_maybe_load_more();
|
||||
}
|
||||
}
|
||||
|
||||
void playlist_swipe(int dir, int dist)
|
||||
@@ -305,8 +338,10 @@ void playlist_swipe(int dir, int dist)
|
||||
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
|
||||
int before = meow_listview_scroll(g_list);
|
||||
meow_listview_scroll_by(g_list, del);
|
||||
if (meow_listview_scroll(g_list) != before)
|
||||
if (meow_listview_scroll(g_list) != before) {
|
||||
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
pl_maybe_load_more();
|
||||
}
|
||||
}
|
||||
|
||||
static void pl_page_create(meow_view *v, void *ctx)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/* bench/offline_track_all.c — 二期大歌单分页 离线样本重放(不打网络)。
|
||||
*
|
||||
* 用与 App 完全相同的 core 解析函数 ncm_api_parse_track_all 解析一段合成的
|
||||
* /api/playlist/track/all 响应(songs[] 全量对象含 al.picUrl + total)。纯解析,
|
||||
* 验证:① 数组路径 songs 命中;② id >2^31 不溢出;③ 新字段 al.picUrl 解析。
|
||||
* 风控纪律:验证期 0 次真实 NCM 请求(离线样本重放)。
|
||||
* 编译(宿主):$CC_HOST -O2 -DJSMN_HEADER -Icore -I../vendor/jsmn \
|
||||
* offline_track_all.c ../out/core-host/*.o \
|
||||
* -L../out/mbedtls/host -lmbedtls -lmbedx509 -lmbedcrypto -lz -lpthread -lm -o offline_track_all
|
||||
*/
|
||||
#include "ncm.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
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)
|
||||
|
||||
int main(void) {
|
||||
/* 合成真实形态 track/all 响应:songs[](全 song 对象,含 al.picUrl)+ total。
|
||||
* id 用 >2^31 数字(2746301825),验 int64 不溢出;al.picUrl 验专辑封面。 */
|
||||
const char *json =
|
||||
"{\"code\":200,\"songs\":["
|
||||
"{\"id\":2746301825,\"name\":\"Chrono Rewind\",\"dt\":253000,\"fee\":1,"
|
||||
" \"ar\":[{\"name\":\"CVRCH\"}],"
|
||||
" \"al\":{\"name\":\"Rewrite\",\"picUrl\":\"http://p1.music.126.net/ab/cover1.jpg\"}},"
|
||||
"{\"id\":2048962657,\"name\":\"Verve\",\"dt\":210000,\"fee\":0,"
|
||||
" \"ar\":[{\"name\":\"Ryan Taubert\"}],"
|
||||
" \"al\":{\"name\":\"Rebuild\",\"picUrl\":\"https://p2.music.126.net/cd/cover2.jpg\"}}"
|
||||
"],\"total\":5191}";
|
||||
|
||||
ncm_song_t songs[8];
|
||||
int n = 0;
|
||||
int64_t total = 0;
|
||||
int rc = ncm_api_parse_track_all(json, strlen(json), songs, 8, &n, &total);
|
||||
CHECK(rc == NCM_OK, "parse_track_all rc=%d", rc);
|
||||
CHECK(n == 2, "parsed 2 songs (n=%d)", n);
|
||||
CHECK(total == 5191, "total=%lld", (long long)total);
|
||||
CHECK(strcmp(songs[0].name, "Chrono Rewind") == 0, "song0 name='%s'", songs[0].name);
|
||||
CHECK(strcmp(songs[0].art_name, "CVRCH") == 0, "song0 artist='%s'", songs[0].art_name);
|
||||
CHECK(strcmp(songs[0].album_name, "Rewrite") == 0, "song0 album='%s'", songs[0].album_name);
|
||||
CHECK(strcmp(songs[0].al_pic, "http://p1.music.126.net/ab/cover1.jpg") == 0,
|
||||
"song0 al_pic='%s'", songs[0].al_pic);
|
||||
CHECK(strcmp(songs[1].id, "2048962657") == 0, "song1 id='%s'(numeric->text)", songs[1].id);
|
||||
CHECK(strcmp(songs[1].al_pic, "https://p2.music.126.net/cd/cover2.jpg") == 0,
|
||||
"song1 al_pic='%s'", songs[1].al_pic);
|
||||
CHECK(songs[0].duration_ms == 253000 && songs[0].fee == 1, "song0 dt=%ld fee=%d",
|
||||
songs[0].duration_ms, songs[0].fee);
|
||||
|
||||
/* 数组路径容错:缺 songs 但嵌套 playlist.tracks 也应命中(防御真实端点变体)。 */
|
||||
const char *json2 =
|
||||
"{\"code\":200,\"playlist\":{\"trackCount\":3,\"tracks\":["
|
||||
"{\"id\":1,\"name\":\"A\",\"dt\":1000,\"fee\":0,\"ar\":[{\"name\":\"X\"}],"
|
||||
"\"al\":{\"name\":\"AL\",\"picUrl\":\"http://x/y.jpg\"}}]}}";
|
||||
int n2 = 0; int64_t t2 = 0;
|
||||
rc = ncm_api_parse_track_all(json2, strlen(json2), songs, 8, &n2, &t2);
|
||||
CHECK(rc == NCM_OK && n2 == 1, "parse variant playlist.tracks rc=%d n=%d", rc, n2);
|
||||
CHECK(t2 == 3, "variant total(trackCount)=%lld", (long long)t2);
|
||||
|
||||
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -100,6 +100,38 @@ static int obj_bool(const ncm_json_t *j, int tok, const char *key) {
|
||||
return (strcmp(s, "true") == 0);
|
||||
}
|
||||
|
||||
/* 解析一处「歌曲对象数组」(song_detail / cloudsearch / playlist_track_all 共用)。
|
||||
* 元素字段:id/name/ar[0].name/al.name/al.picUrl/dt/fee。id 可为数字或字符串(均以文本拷贝,
|
||||
* 兼容 >2^31 的歌曲 id)。返回 NCM_OK,*out_n=写入数。 */
|
||||
static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int max, int *out_n)
|
||||
{
|
||||
*out_n = 0;
|
||||
if (arr < 0 || ncm_json_type(j, arr) != JSMN_ARRAY) return NCM_ERR_NOTFOUND;
|
||||
int cnt = ncm_json_arr_len(j, arr);
|
||||
if (cnt > max) cnt = max;
|
||||
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
int e = ncm_json_arr_get(j, arr, i);
|
||||
if (e < 0) break;
|
||||
obj_str(j, e, "id", out[i].id, sizeof out[i].id);
|
||||
obj_str(j, e, "name", out[i].name, sizeof out[i].name);
|
||||
out[i].duration_ms = obj_int(j, e, "dt");
|
||||
out[i].fee = (int)obj_int(j, e, "fee");
|
||||
int ar = ncm_json_obj_get(j, e, "ar");
|
||||
if (ar >= 0 && ncm_json_type(j, ar) == JSMN_ARRAY) {
|
||||
int a0 = ncm_json_arr_get(j, ar, 0);
|
||||
if (a0 >= 0) obj_str(j, a0, "name", out[i].art_name, sizeof out[i].art_name);
|
||||
}
|
||||
int al = ncm_json_obj_get(j, e, "al");
|
||||
if (al >= 0) {
|
||||
obj_str(j, al, "name", out[i].album_name, sizeof out[i].album_name);
|
||||
obj_str(j, al, "picUrl", out[i].al_pic, sizeof out[i].al_pic); /* 专辑封面(黑胶) */
|
||||
}
|
||||
}
|
||||
*out_n = cnt;
|
||||
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) {
|
||||
if (!out_n) return NCM_ERR_ARG;
|
||||
@@ -195,28 +227,9 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
int arr = ncm_json_path(&c.j, "songs");
|
||||
if (arr < 0 || ncm_json_type(&c.j, arr) != JSMN_ARRAY) { api_free(&c); return NCM_ERR_NOTFOUND; }
|
||||
int cnt = ncm_json_arr_len(&c.j, arr);
|
||||
if (cnt > max) cnt = max;
|
||||
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
int e = ncm_json_arr_get(&c.j, arr, i);
|
||||
if (e < 0) break;
|
||||
obj_str(&c.j, e, "id", out[i].id, sizeof out[i].id);
|
||||
obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name);
|
||||
out[i].duration_ms = obj_int(&c.j, e, "dt");
|
||||
out[i].fee = (int)obj_int(&c.j, e, "fee");
|
||||
int ar = ncm_json_obj_get(&c.j, e, "ar");
|
||||
if (ar >= 0 && ncm_json_type(&c.j, ar) == JSMN_ARRAY) {
|
||||
int a0 = ncm_json_arr_get(&c.j, ar, 0);
|
||||
if (a0 >= 0) obj_str(&c.j, a0, "name", out[i].art_name, sizeof out[i].art_name);
|
||||
}
|
||||
int al = ncm_json_obj_get(&c.j, e, "al");
|
||||
if (al >= 0) obj_str(&c.j, al, "name", out[i].album_name, sizeof out[i].album_name);
|
||||
}
|
||||
*out_n = cnt;
|
||||
int rc2 = parse_songs_array(&c.j, arr, out, max, out_n);
|
||||
api_free(&c);
|
||||
return NCM_OK;
|
||||
return rc2;
|
||||
}
|
||||
|
||||
int ncm_api_song_url(const char *device_id, const char *cookie, const char *id,
|
||||
@@ -305,30 +318,69 @@ int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *k
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
int arr = ncm_json_path(&c.j, "result.songs");
|
||||
if (arr < 0 || ncm_json_type(&c.j, arr) != JSMN_ARRAY) { api_free(&c); return NCM_ERR_NOTFOUND; }
|
||||
int cnt = ncm_json_arr_len(&c.j, arr);
|
||||
if (cnt > max) cnt = max;
|
||||
memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0));
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
int e = ncm_json_arr_get(&c.j, arr, i);
|
||||
if (e < 0) break;
|
||||
obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name);
|
||||
out[i].duration_ms = obj_int(&c.j, e, "dt");
|
||||
out[i].fee = (int)obj_int(&c.j, e, "fee");
|
||||
/* id 用 i64:网易云歌曲 id 可 >2^31 */
|
||||
int vt = ncm_json_obj_get(&c.j, e, "id");
|
||||
int rc2;
|
||||
int64_t idv = ncm_json_get_i64_at(&c.j, vt, &rc2);
|
||||
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)idv);
|
||||
int ar = ncm_json_obj_get(&c.j, e, "ar");
|
||||
if (ar >= 0 && ncm_json_type(&c.j, ar) == JSMN_ARRAY) {
|
||||
int a0 = ncm_json_arr_get(&c.j, ar, 0);
|
||||
if (a0 >= 0) obj_str(&c.j, a0, "name", out[i].art_name, sizeof out[i].art_name);
|
||||
}
|
||||
int al = ncm_json_obj_get(&c.j, e, "al");
|
||||
if (al >= 0) obj_str(&c.j, al, "name", out[i].album_name, sizeof out[i].album_name);
|
||||
}
|
||||
*out_n = cnt;
|
||||
int rc2 = parse_songs_array(&c.j, arr, out, max, out_n);
|
||||
api_free(&c);
|
||||
return NCM_OK;
|
||||
return rc2;
|
||||
}
|
||||
|
||||
/* 从一段 track/all 响应 JSON 串解析歌曲数组 + total(离线样本/单元测试用,不打网络)。
|
||||
* 数组取「songs → playlist.tracks → data.songs」首个命中;total 取「total → count →
|
||||
* playlist.trackCount → musicCount」,全缺则 0。 */
|
||||
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
||||
int *out_n, int64_t *total) {
|
||||
if (out_n) *out_n = 0;
|
||||
if (total) *total = 0;
|
||||
if (!json || len == 0 || !out || max <= 0) return NCM_ERR_ARG;
|
||||
|
||||
jsmn_parser p; jsmn_init(&p);
|
||||
int n = jsmn_parse(&p, json, len, NULL, 0);
|
||||
if (n <= 0) return NCM_ERR_JSON;
|
||||
jsmntok_t *toks = (jsmntok_t *)malloc(sizeof(jsmntok_t) * (size_t)n);
|
||||
int *parent = (int *)malloc(sizeof(int) * (size_t)n);
|
||||
if (!toks || !parent) { free(toks); free(parent); return NCM_ERR_OOM; }
|
||||
ncm_json_t j;
|
||||
int rc = ncm_json_parse(&j, json, len, toks, n, parent);
|
||||
if (rc < 0) { free(toks); free(parent); return NCM_ERR_JSON; }
|
||||
|
||||
/* 定位歌曲数组(多路径容错)。 */
|
||||
int arr = ncm_json_path(&j, "songs");
|
||||
if (arr < 0) arr = ncm_json_path(&j, "playlist.tracks");
|
||||
if (arr < 0) arr = ncm_json_path(&j, "data.songs");
|
||||
if (arr < 0 || ncm_json_type(&j, arr) != JSMN_ARRAY) { free(toks); free(parent); return NCM_ERR_NOTFOUND; }
|
||||
|
||||
long v = 0;
|
||||
if (ncm_json_path_int(&j, "total", &v) != NCM_OK && ncm_json_path_int(&j, "count", &v) != NCM_OK &&
|
||||
ncm_json_path_int(&j, "playlist.trackCount", &v) != NCM_OK && ncm_json_path_int(&j, "musicCount", &v) != NCM_OK)
|
||||
v = 0;
|
||||
if (total) *total = (int64_t)v;
|
||||
|
||||
int on = 0;
|
||||
rc = parse_songs_array(&j, arr, out, max, &on);
|
||||
if (out_n) *out_n = on;
|
||||
free(toks); free(parent);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int ncm_api_playlist_track_all(const char *device_id, const char *cookie, const char *playlist_id,
|
||||
int offset, int limit, ncm_song_t *out, int max, int *out_n,
|
||||
int64_t *total) {
|
||||
if (out_n) *out_n = 0;
|
||||
if (limit <= 0) limit = 50;
|
||||
if (limit > max) limit = max;
|
||||
if (offset < 0) offset = 0;
|
||||
|
||||
char json[256];
|
||||
snprintf(json, sizeof json, "{\"id\":\"%s\",\"offset\":%d,\"limit\":%d,\"n\":%d,\"s\":8}",
|
||||
playlist_id, offset, limit, limit);
|
||||
|
||||
api_json_t c; memset(&c, 0, sizeof c);
|
||||
int rc = api_post(device_id, cookie, "/api/playlist/track/all", json, NCM_CRYPT_EAPI,
|
||||
NCM_RESP_MAX, &c);
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
int64_t tot = 0;
|
||||
rc = ncm_api_parse_track_all(c.resp.body, c.resp.body_len, out, max, out_n, &tot);
|
||||
if (rc == NCM_OK && total) *total = tot;
|
||||
api_free(&c);
|
||||
return rc;
|
||||
}
|
||||
@@ -38,6 +38,7 @@ typedef struct ncm_song {
|
||||
char name[256];
|
||||
char art_name[256]; /* 首个歌手名 */
|
||||
char album_name[256];
|
||||
char al_pic[512]; /* 专辑封面 picUrl(song_detail/cloudsearch/playlist_track_all 解析;播放页黑胶封面) */
|
||||
long duration_ms;
|
||||
int fee;
|
||||
} ncm_song_t;
|
||||
@@ -83,6 +84,20 @@ int ncm_api_lyric(const char *device_id, const char *cookie, const char *id,
|
||||
int ncm_api_cloudsearch(const char *device_id, const char *cookie, const char *keyword,
|
||||
ncm_song_t *out, int max, int *out_n);
|
||||
|
||||
/* 歌单曲目服务端分页(eapi /api/playlist/track/all,REPORT §11 二期)。
|
||||
* 相对 /v6/playlist/detail 的「全量 trackIds(5191 首 ≈900KB)」改进:本端点按
|
||||
* offset/limit 直接返回该页的歌曲对象(含 al.picUrl,无需再 song_detail 二次请求)。
|
||||
* 首屏 offset=0 limit=50;滚动到底继续 offset+=limit。out 填 ncm_song_t。
|
||||
* *total 返回歌单总曲数(0=未知;调用方可退化为 playlist.trackCount)。 */
|
||||
int ncm_api_playlist_track_all(const char *device_id, const char *cookie, const char *playlist_id,
|
||||
int offset, int limit, ncm_song_t *out, int max, int *out_n,
|
||||
int64_t *total);
|
||||
|
||||
/* 离线解析:从一段 track/all 响应 JSON 串解析歌曲数组(含 al.picUrl)+ total。
|
||||
* 网络版内部同一解析器;供「宿主离线样本重放」与单元测试用,不打网络。 */
|
||||
int ncm_api_parse_track_all(const char *json, size_t len, ncm_song_t *out, int max,
|
||||
int *out_n, int64_t *total);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -222,6 +222,36 @@ static void test_network(void) {
|
||||
if (!net_ok) printf(" SKIP network live checks (host unreachable or TLS CA missing)\n");
|
||||
}
|
||||
|
||||
/* ---------- 二期:/api/playlist/track/all 服务端分页 离线解析 ---------- */
|
||||
static void test_track_all_parse(void) {
|
||||
printf("== track_all 离线解析(二期)==\n");
|
||||
/* 合成一个真实形态的 track/all 响应:songs[](全 song 对象,含 al.picUrl)+ total。
|
||||
* id 为 >2^31 数字,验证 int64 不溢出;al.picUrl 验证专辑封面字段。 */
|
||||
const char *json =
|
||||
"{\"code\":200,\"songs\":["
|
||||
"{\"id\":2746301825,\"name\":\"Chrono Rewind\",\"dt\":253000,\"fee\":1,"
|
||||
" \"ar\":[{\"name\":\"CVRCH\"}],"
|
||||
" \"al\":{\"name\":\"Rewrite\",\"picUrl\":\"http://p1.music.126.net/ab/cover1.jpg\"}},"
|
||||
"{\"id\":2048962657,\"name\":\"Verve\",\"dt\":210000,\"fee\":0,"
|
||||
" \"ar\":[{\"name\":\"Ryan Taubert\"}],"
|
||||
" \"al\":{\"name\":\"Rebuild\",\"picUrl\":\"https://p2.music.126.net/cd/cover2.jpg\"}}"
|
||||
"],\"total\":5191}";
|
||||
ncm_song_t songs[8];
|
||||
int n = 0;
|
||||
int64_t total = 0;
|
||||
int rc = ncm_api_parse_track_all(json, strlen(json), songs, 8, &n, &total);
|
||||
CHECKRC(rc, "parse_track_all rc");
|
||||
CHECK(n == 2, "parsed 2 songs (n=%d)", n);
|
||||
CHECK(total == 5191, "total=%lld", (long long)total);
|
||||
CHECK(strcmp(songs[0].name, "Chrono Rewind") == 0, "song0 name");
|
||||
CHECK(strcmp(songs[0].art_name, "CVRCH") == 0, "song0 artist");
|
||||
CHECK(strcmp(songs[0].album_name, "Rewrite") == 0, "song0 album");
|
||||
CHECK(strcmp(songs[0].al_pic, "http://p1.music.126.net/ab/cover1.jpg") == 0, "song0 al_pic");
|
||||
CHECK(strcmp(songs[1].id, "2048962657") == 0, "song1 id(numeric->text)");
|
||||
CHECK(strcmp(songs[1].al_pic, "https://p2.music.126.net/cd/cover2.jpg") == 0, "song1 al_pic");
|
||||
CHECK(songs[0].duration_ms == 253000 && songs[0].fee == 1, "song0 dt/fee");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *data_dir = (argc > 1) ? argv[1] : "/tmp/ncm_test";
|
||||
printf("ncm_core self-test, data_dir=%s\n", data_dir);
|
||||
@@ -236,6 +266,7 @@ int main(int argc, char **argv) {
|
||||
test_cache();
|
||||
test_qr();
|
||||
test_network();
|
||||
test_track_all_parse();
|
||||
|
||||
printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS ==");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Binary file not shown.
@@ -59,19 +59,23 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
|
||||
unsigned char *out = (unsigned char *)malloc((size_t)dw * dh * 4);
|
||||
if (!out) return NULL;
|
||||
float s = 1.0f;
|
||||
float ox = 0.0f, oy = 0.0f;
|
||||
if (cover) {
|
||||
/* 真正铺满(cover):每目标像素对应 s 个源像素,s = min(W/dw, H/dh)——缩放后图像
|
||||
* 宽/高两维都不小于目标(可能外溢),居中裁方。居中裁剪偏移(源像素)= (W-dw*s)/2
|
||||
* 恒 ≥ 0。曾误作 `s = max(sx,sy)` 且采样 `(dx+ox)/s`(除以缩放比):把整块输出映射到
|
||||
* 源左上角 ~dw/s 像素的透明角——图标(透明角 logo)整块透明、不绘制,即
|
||||
* 「icon: ... OK 但渲染不出(只显首字母占位)」根因。正确:源索引 = dx*s + 裁剪偏移
|
||||
* (与下方非 cover 分支 `dx*im->w/dw` 同一乘向)。 */
|
||||
float sx = (float)im->w / dw, sy = (float)im->h / dh;
|
||||
s = sx > sy ? sx : sy; /* 铺满 → 可能外溢,居中裁 */
|
||||
float sw = im->w / s, sh = im->h / s; /* 目标显示寸(sw×sh) */
|
||||
ox = (float)(dw - sw) * 0.5f;
|
||||
oy = (float)(dh - sh) * 0.5f;
|
||||
if (ox < 0) ox = 0; if (oy < 0) oy = 0;
|
||||
s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */
|
||||
float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */
|
||||
float oys = ((float)im->h - (float)dh * s) * 0.5f;
|
||||
if (oxs < 0) oxs = 0; if (oys < 0) oys = 0;
|
||||
for (int dy = 0; dy < dh; dy++) {
|
||||
int syi = (int)((dy + oy) / s);
|
||||
int syi = (int)((float)dy * s + oys);
|
||||
if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1;
|
||||
for (int dx = 0; dx < dw; dx++) {
|
||||
int sxi = (int)((dx + ox) / s);
|
||||
int sxi = (int)((float)dx * s + oxs);
|
||||
if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1;
|
||||
memcpy(out + ((size_t)dy * dw + dx) * 4,
|
||||
im->rgba + ((size_t)syi * im->w + sxi) * 4, 4);
|
||||
|
||||
@@ -6,12 +6,20 @@
|
||||
* meow_ui_run / IME 循环弹出分发。应用不再自读 event*。
|
||||
*
|
||||
* 触摸:BTN_TOUCH 按下记录原点,抬起时位移 < 阈值 → TAP,否则按主导轴 → SWIPE。
|
||||
* 物理键:EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.24 全面按键接管:电源/音量/播放/
|
||||
* 上下曲全部由系统(watchdog/launcher)消费,SDK 吞噬不发给 APP(应用只收 tap/swipe/tick)。
|
||||
* 物理键:EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.55 起系统按键语义在 SDK 输入层消化:
|
||||
* 电源 116 → 维持现状(息屏/亮屏/长按菜单由内核/sys_server/watchdog),SDK 不动、不下发 APP;
|
||||
* 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,volume_step/volume_max
|
||||
* 约束,与 launcher 设置页/watchdog 同源);
|
||||
* 播放/暂停 164、上一曲 165/19、下一曲 163 → 转发到当前媒体会话(App 是媒体源→本地回调,
|
||||
* 否则经 IPC 发 launcher 转发)。
|
||||
* 「App 收到的是语义事件」:上述音量/媒体键在 SDK 消化后**仍下发 MEOW_EV_KEY**(on_key),但
|
||||
* App 不得把它们映射成自定义行为(APPS.md §3.4.1 语义不变)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_sys.h"
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_ui_media.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -81,6 +89,45 @@ static uint64_t now_ms(void)
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */
|
||||
static const char *meow_settings_path(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_SETTINGS_FILE");
|
||||
return (e && e[0]) ? e : "/usr/data/meow/settings.conf";
|
||||
}
|
||||
|
||||
/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)----
|
||||
* 物理键是系统资源(APPS.md §3.4.1),语义全局统一。本函数在输入线程执行系统动作:
|
||||
* 电源 116 → 息屏/亮屏/长按菜单(内核/sys_server/watchdog),SDK 不动(维持现状);
|
||||
* 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,与 launcher
|
||||
* 设置页/watchdog 同源:volume_step/volume_max 约束),仅按下边沿(1)一步;
|
||||
* 播放/上次曲 164/165/19/163 → 转发到当前媒体会话(App 是媒体源→本地回调;否则 IPC→launcher)。
|
||||
* 仅处理按下边沿(val==1)触发系统动作;重复(2)/抬起(0)不重复执行。on_key 的下发由输入线程
|
||||
* 在调用本函数后另行 push(音量/媒体键仍通知 App,App 不得映射)。 */
|
||||
static void meow_input_handle_sys_key(int code, int val)
|
||||
{
|
||||
switch (code) {
|
||||
case MEOW_KEY_POWER: /* 电源:维持现状,SDK 不动 */
|
||||
break;
|
||||
case MEOW_KEY_VOLUMEDOWN:
|
||||
case MEOW_KEY_VOLUMEUP:
|
||||
if (val == 1) {
|
||||
int dir = (code == MEOW_KEY_VOLUMEUP) ? +1 : -1;
|
||||
int nv = meow_ui_vol_step(meow_settings_path(), dir); /* settings 语义+持久化 */
|
||||
meow_ui_vol_dac_write(nv); /* 写真实 CS43198 DAC */
|
||||
}
|
||||
break;
|
||||
case MEOW_KEY_PLAYPAUSE:
|
||||
case MEOW_KEY_PREVIOUS:
|
||||
case MEOW_KEY_NEXT:
|
||||
case 19: /* KEY_PREVIOUSSONG 旧码 */
|
||||
if (val == 1) meow_ui_media_sys_key(code);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void meow_input_drain(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_qm);
|
||||
@@ -335,14 +382,25 @@ static void *input_thread(void *arg)
|
||||
if (code == BTN_TOUCH) {
|
||||
if (val && !t_down) touch_press(t_cx, t_cy);
|
||||
else if (!val && t_down) touch_release();
|
||||
} else if (code == MEOW_KEY_POWER || code == MEOW_KEY_VOLUMEDOWN ||
|
||||
code == MEOW_KEY_VOLUMEUP || code == MEOW_KEY_PLAYPAUSE ||
|
||||
code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 19) {
|
||||
/* M2.22(A)/M2.24 纪律:物理键(电源 116 / 音量 114,115 / 播放 164 /
|
||||
* 上一曲 165(MEOW_KEY_PREVIOUS)/19(旧码) / 下一曲 163)全部是系统语义 —— watchdog/launcher 统一消费,
|
||||
* 绝不下发 APP。应用不碰物理键、不把任何物理键映射成确认/exit/返回/翻页
|
||||
* (KEY_CONTROL_DESIGN §1.3 / APPS.md §3.4.1);APP 只接收 tap/swipe/tick。
|
||||
* 忽略该键,不发 MEOW_EV_KEY。 */
|
||||
} else if (code == MEOW_KEY_POWER) {
|
||||
/* M2.55(接 M2.24 纪律):电源=息屏/亮屏/长按菜单,由内核/sys_server/
|
||||
* watchdog 统一处理,SDK 不动、也不下发 APP。维持现状。 */
|
||||
} else if (code == MEOW_KEY_VOLUMEDOWN || code == MEOW_KEY_VOLUMEUP) {
|
||||
/* 音量键=系统音量(M2.55:SDK 输入层消化,App 前台也生效)。
|
||||
* 语义动作在 meow_input_handle_sys_key(仅按下边沿一步);随后仍下发
|
||||
* MEOW_EV_KEY(App on_key 收到语义事件,但不得映射成自定义行为)。 */
|
||||
if (val == 1 || val == 0) {
|
||||
meow_input_handle_sys_key(code, val);
|
||||
push_ev(MEOW_EV_KEY, code, (val == 1) ? 1 : 0);
|
||||
}
|
||||
} else if (code == MEOW_KEY_PLAYPAUSE || code == MEOW_KEY_PREVIOUS ||
|
||||
code == MEOW_KEY_NEXT || code == 19) {
|
||||
/* 媒体键=转发到当前媒体会话(M2.55:SDK 输入层消化)。语义动作在
|
||||
* meow_input_handle_sys_key;随后仍下发 MEOW_EV_KEY(App 不得映射)。 */
|
||||
if (val == 1 || val == 0) {
|
||||
meow_input_handle_sys_key(code, val);
|
||||
push_ev(MEOW_EV_KEY, code, (val == 1) ? 1 : 0);
|
||||
}
|
||||
} else if (val == 1) { /* 按下边沿(忽略重复=2)——仅未知键(本设备无) */
|
||||
push_ev(MEOW_EV_KEY, code, 1);
|
||||
} else if (val == 0) { /* 抬起 */
|
||||
|
||||
@@ -30,6 +30,11 @@ static pthread_t g_thr;
|
||||
static int g_thr_on = 0;
|
||||
static volatile int g_quit = 0;
|
||||
static meow_media_playctl_fn g_cb = NULL;
|
||||
/* 本进程最后一次经 meow_ui_media_meta 上报的 state(0=停 1=播放 2=暂停 3=缓冲)。
|
||||
* 供「物理播放/暂停键」做切换判据:播放中→暂停(2),否则→播放(1)。与 launcher
|
||||
* now_playing.c 的同款判定(state==PLAYING ? 2 : 1)对齐。 */
|
||||
static int g_last_state = 0;
|
||||
#define MEOW_UI_MEDIA_STATE_PLAYING 1
|
||||
|
||||
/* 后台线程主体(定义在文件后段;on_playctl 懒起时引用)。 */
|
||||
static void *media_thread_main(void *arg);
|
||||
@@ -177,6 +182,7 @@ void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms,
|
||||
memset(&mm, 0, sizeof mm);
|
||||
pthread_mutex_lock(&g_mu);
|
||||
media_app_id();
|
||||
g_last_state = (int)state; /* 记录最近上报 state:供物理播放/暂停键切换判据(见 sys_key) */
|
||||
snprintf(mm.app_id, sizeof mm.app_id, "%s", g_app);
|
||||
mm.state = state;
|
||||
mm.pos_ms = pos_ms;
|
||||
@@ -207,6 +213,64 @@ void meow_ui_media_stop(void)
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
/* ---- 物理媒体键 → 系统语义(M2.xx:SDK 输入层接管,App 前台也生效)----
|
||||
* 物理键是系统资源(APPS.md §3.4.1):播放/暂停/上一曲/下一曲 → 转发到当前媒体会话。
|
||||
* 本进程注册过 playctl 回调(= 本 App 是媒体源)→ 直接本地回调(最简,无 IPC 往返);
|
||||
* 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到「最近活跃媒体会话」。best-effort 绝不崩。 */
|
||||
|
||||
/* 物理媒体键 keycode → MSG_PLAYCTL cmd(1=播放 2=暂停 3=上一首 4=下一首 5=seek)。
|
||||
* 播放/暂停(164)按最近上报 state 切换(播放中→暂停,否则→播放);上一曲 165/19→3,下一曲 163→4。
|
||||
* 非媒体键返回 -1。last_state 由调用方在持锁下传入(读 g_last_state 快照)。 */
|
||||
static int media_key_cmd(int code, int last_state)
|
||||
{
|
||||
switch (code) {
|
||||
case 164: /* KEY_PLAYPAUSE:切换(依最近上报 state) */
|
||||
return (last_state == MEOW_UI_MEDIA_STATE_PLAYING) ? 2 : 1;
|
||||
case 165: case 19: /* KEY_PREVIOUSSONG / 旧码 */
|
||||
return 3;
|
||||
case 163: /* KEY_NEXTSONG */
|
||||
return 4;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 控制器路径:发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话(launcher ipc_dispatch
|
||||
* MSG_PLAYCTL → meow_media_playctl → 下发到当前源 fd)。
|
||||
* 专门开一条临时连接(不复用媒体源持久连接 g_fd,避免与后台线程 poll/读竞争);
|
||||
* 无 launcher/断线 → 静默降级(绝不崩)。 */
|
||||
static void send_playctl_upstream(int cmd, int64_t seek_ms)
|
||||
{
|
||||
int fd = connect_path(sock_path());
|
||||
if (fd < 0) return;
|
||||
m_playctl_t c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.cmd = (uint32_t)cmd;
|
||||
c.seek_ms = seek_ms;
|
||||
if (send_frame(fd, MSG_PLAYCTL, 1, &c, sizeof c) != 0) { close(fd); return; }
|
||||
/* 读 ACK(best-effort):带 200ms poll,防 launcher 挂死导致输入线程阻塞。 */
|
||||
struct pollfd p;
|
||||
p.fd = fd; p.events = POLLIN; p.revents = 0;
|
||||
if (poll(&p, 1, 200) > 0 && (p.revents & POLLIN)) {
|
||||
uint8_t rbuf[128];
|
||||
uint16_t rt;
|
||||
(void)read_frame(fd, &rt, rbuf, sizeof rbuf);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* 物理媒体键按下边沿(val==1 时由输入线程调用;重复/抬起不重复触发)。 */
|
||||
void meow_ui_media_sys_key(int code)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
meow_media_playctl_fn cb = g_cb;
|
||||
int cmd = media_key_cmd(code, g_last_state);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (cmd <= 0) return;
|
||||
if (cb) { cb(cmd, 0); return; } /* 本 App 是媒体源(注册过回调)→ 本地回调 */
|
||||
send_playctl_upstream(cmd, 0); /* 否则经 IPC 发 launcher 转发到最近活跃会话 */
|
||||
}
|
||||
|
||||
/* 后台线程主体:懒连 + poll 读 launcher 下发 MSG_PLAYCTL → 调 cb。 */
|
||||
static void *media_thread_main(void *arg)
|
||||
{
|
||||
|
||||
@@ -39,6 +39,13 @@ void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms,
|
||||
/* 停止后台接收线程并断开连接(App 退出时调;幂等)。 */
|
||||
void meow_ui_media_stop(void);
|
||||
|
||||
/* 物理媒体键 → 系统语义(SDK 输入层调用;App 前台也生效)。
|
||||
* code:164=播放/暂停、165/19=上一曲、163=下一曲。
|
||||
* 本 App 注册过 playctl 回调(媒体源)→ 本地直接回调 cb(cmd,seek_ms);
|
||||
* 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话。非媒体键/无源静默。
|
||||
* best-effort:无 launcher/socket → 优雅降级,绝不崩。 */
|
||||
void meow_ui_media_sys_key(int code);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -50,3 +50,72 @@ int meow_ui_vol_read(const char *path, const char *key, int def)
|
||||
while (*found == ' ' || *found == '\t') found++; /* value 左去空白;atoi 天然停于非数字 */
|
||||
return atoi(found);
|
||||
}
|
||||
|
||||
/* 改 settings.conf 一个 int 键:流式拷贝原文件到 <path>.tmp,命中键行改写为
|
||||
* "key = value"(launcher 落盘格式,带空格),键缺失则末尾追加;rename 原子替换。
|
||||
* 保留其余行(注释/其它设置)——绝不 O_TRUNC 整文件。 */
|
||||
int meow_ui_vol_set(const char *path, const char *key, int val)
|
||||
{
|
||||
if (!path || !path[0] || !key || !key[0]) return -1;
|
||||
FILE *in = fopen(path, "r");
|
||||
char tmp[512];
|
||||
int kl = (int)strlen(key);
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||
FILE *out = fopen(tmp, "w");
|
||||
if (!out) { if (in) fclose(in); return -1; }
|
||||
|
||||
int found = 0;
|
||||
char line[256];
|
||||
while (in && fgets(line, sizeof line, in)) {
|
||||
char *eq = strchr(line, '=');
|
||||
char *k = line;
|
||||
while (*k == ' ' || *k == '\t') k++;
|
||||
int n = eq ? (int)(eq - k) : 0;
|
||||
while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--;
|
||||
if (eq && n == kl && strncmp(k, key, (size_t)kl) == 0) {
|
||||
fprintf(out, "%s = %d\n", key, val); /* 命中:改写为规范格式 */
|
||||
found = 1;
|
||||
} else {
|
||||
fputs(line, out); /* 其它行原样保留 */
|
||||
}
|
||||
}
|
||||
if (in) fclose(in);
|
||||
if (!found) fprintf(out, "%s = %d\n", key, val);
|
||||
fclose(out);
|
||||
|
||||
if (rename(tmp, path) != 0) { remove(tmp); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 音量步进(系统语义,与 launcher 设置页/watchdog 同源:volume_step/volume_max 约束)。
|
||||
* 只做 settings 逻辑 + 持久化;DAC 写入由调用方(meow_ui_vol_dac_write)负责。 */
|
||||
int meow_ui_vol_step(const char *path, int dir)
|
||||
{
|
||||
int vol = meow_ui_vol_read(path, "audio.volume", MEOW_UI_VOL_DEFAULT);
|
||||
int vol_max= meow_ui_vol_read(path, "audio.volume_max", MEOW_UI_VOL_MAX_DEF);
|
||||
int step = meow_ui_vol_read(path, "audio.volume_step", 1);
|
||||
if (step < 1) step = 1;
|
||||
if (step > 5) step = 5; /* 与 watchdog 读取口径一致(1..5) */
|
||||
if (dir < 0) dir = -1;
|
||||
else if (dir > 0) dir = 1;
|
||||
else return vol;
|
||||
|
||||
int v = meow_ui_vol_clamp(vol + dir * step, vol_max);
|
||||
if (v == vol) return vol; /* 已在上下限:无进展,不写 */
|
||||
meow_ui_vol_set(path, "audio.volume", v);
|
||||
return v;
|
||||
}
|
||||
|
||||
/* 真实 DAC 音量(CS43198, numid 1/2, 0-255),pct→255 映射直写两声道。
|
||||
* 与 launcher meow_dac_vol_write / watchdog wd_vol_write 完全同路径(语义同源)。 */
|
||||
void meow_ui_vol_dac_write(int pct)
|
||||
{
|
||||
int dac = pct * 255 / 100;
|
||||
if (dac < 0) dac = 0;
|
||||
if (dac > 255) dac = 255;
|
||||
char cmd[160];
|
||||
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",
|
||||
dac, dac);
|
||||
system(cmd);
|
||||
}
|
||||
@@ -29,4 +29,22 @@ int meow_ui_vol_clamp(int vol, int vol_max);
|
||||
* path 由调用方传入(设备传导 MEOW_SETTINGS_FILE;宿主单测指向临时文件)。 */
|
||||
int meow_ui_vol_read(const char *path, const char *key, int def);
|
||||
|
||||
/* 改 settings.conf 里一个 int 键(地址更新/新增;**保留其余行**——与 launcher 落盘格式
|
||||
* "key = value"(带空格)一致;键缺失则追加)。用临时文件 + rename 原子替换,失败返回 -1。
|
||||
* 用途:SDK 输入层调系统音量时写 audio.volume(绝不像 watchdog wd_vol_save 那样整文件
|
||||
* O_TRUNC,避免删掉 display.* 等其它设置)。 */
|
||||
int meow_ui_vol_set(const char *path, const char *key, int val);
|
||||
|
||||
/* 读 audio.volume + audio.volume_max + audio.volume_step,按 dir(±1)迈一步(绝对跨度
|
||||
* = volume_step),clamp 到 [0, MEOW_UI_VOL_MAX] 再 clamp 到 volume_max(同 meow_ui_vol_clamp
|
||||
* 语义,与 launcher 设置页/watchdog 一致),经 meow_ui_vol_set 持久化到 audio.volume。
|
||||
* 返回写入 DAC 的映射值(= 夹后新卷值);已在上下限未变则返回当前值(不写)。
|
||||
* 仅做 settings 逻辑(不改 DAC);DAC 写入由调用方用 meow_ui_vol_dac_write 做。 */
|
||||
int meow_ui_vol_step(const char *path, int dir);
|
||||
|
||||
/* 写真实 DAC(CS43198, numid 1/2, pct→0-255),与 launcher meow_dac_vol_write / watchdog
|
||||
* wd_vol_write 同路径(amixer cset = SNDRV_CTL_IOCTL_ELEM_WRITE)。无副作用前提:宿主上
|
||||
* amixer 不存在 → system() 静默失败(拼音量的宿主单测只测 meow_ui_vol_step,不测写 DAC)。 */
|
||||
void meow_ui_vol_dac_write(int pct);
|
||||
|
||||
#endif /* MEOW_UI_VOL_H */
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* test_sdk_input_keys.c — M2.55 宿主验证:SDK 输入层接管系统按键语义(App 前台也生效)。
|
||||
*
|
||||
* 覆盖(直接调输入层语义函数,等效"模拟按键事件"):
|
||||
* K1 音量 +/step:audio.volume 迈一步(volume_step 约束),settings.conf 落到值改变;
|
||||
* K2 音量 ± clamp:volume_max 软上限、0 下限、硬上限 100;到限不再写;
|
||||
* K3 保留其它键:改 audio.volume 不整文件 O_TRUNC(display.brightness/注释保留);
|
||||
* K4 媒体键本地回调:注册 playctl 回调后,sys_key(上一曲/下一曲/播放暂停) → 回调 cmd 正确
|
||||
* (播放/暂停按最近上报 state 切换:播放中→暂停(2),暂停/停止→播放(1));
|
||||
* K5 媒体键 IPC 兜底:未注册回调(g_cb==NULL)→ 走 IPC 发 launcher(无 socket → 静默不崩)。
|
||||
*
|
||||
* 编译(宿主 gcc,仅链 vol/media 两个 TU;无需 meow_ui 其它子系统):
|
||||
* gcc -Wall -Wextra -pthread -o test_sdk_input_keys test_sdk_input_keys.c \
|
||||
* meow_ui_vol.c meow_ui_media.c -I. -I../meow_launcherd
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_ui_media.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static int failures = 0;
|
||||
static void check(const char *name, int cond)
|
||||
{
|
||||
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (!cond) failures++;
|
||||
}
|
||||
|
||||
static char g_path[64];
|
||||
static void wfile(const char *content)
|
||||
{
|
||||
FILE *f = fopen(g_path, "w");
|
||||
if (!f) { perror("fopen"); exit(2); }
|
||||
fputs(content, f);
|
||||
fclose(f);
|
||||
}
|
||||
static int vol_in_file(void)
|
||||
{
|
||||
return meow_ui_vol_read(g_path, "audio.volume", -1);
|
||||
}
|
||||
|
||||
/* —— 媒体回调捕获 —— */
|
||||
static int cb_calls = 0, cb_cmd = 0;
|
||||
static int64_t cb_seek = 0;
|
||||
static void playctl_cb(int cmd, int64_t seek_ms)
|
||||
{ cb_calls++; cb_cmd = cmd; cb_seek = seek_ms; }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
snprintf(g_path, sizeof g_path, "/tmp/meow_kb_%d.conf", (int)getpid());
|
||||
char sock[64]; /* 指向不存在路径:让 IPC 兜底路径确定性静默失败 */
|
||||
snprintf(sock, sizeof sock, "/tmp/meow_kb_%d.sock", (int)getpid());
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
|
||||
/* ---- K1 音量迈一步(volume_step=1 默认)---- */
|
||||
wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 1\n");
|
||||
check("K1 vol 60 +1 -> 61", meow_ui_vol_step(g_path, +1) == 61);
|
||||
check("K1 settings.conf 落到 61", vol_in_file() == 61);
|
||||
check("K1 vol 61 -1 -> 60", meow_ui_vol_step(g_path, -1) == 60);
|
||||
check("K1 settings.conf 回到 60", vol_in_file() == 60);
|
||||
|
||||
/* ---- K2 步进 volume_step=4 + clamp ---- */
|
||||
wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 4\n");
|
||||
check("K2 step=4: 60 +1 -> 64", meow_ui_vol_step(g_path, +1) == 64);
|
||||
wfile("audio.volume = 99\naudio.volume_max = 100\naudio.volume_step = 4\n");
|
||||
check("K2 step=4: 99 +1 -> 100 (硬上限)", meow_ui_vol_step(g_path, +1) == 100);
|
||||
|
||||
/* volume_max 软上限 */
|
||||
wfile("audio.volume = 60\naudio.volume_max = 55\naudio.volume_step = 1\n");
|
||||
meow_ui_vol_step(g_path, +1);
|
||||
check("K2 volume_max=55: vol -> 55 (clamp 上限)", vol_in_file() == 55);
|
||||
|
||||
/* 到上限不再写 */
|
||||
wfile("audio.volume = 100\naudio.volume_max = 100\naudio.volume_step = 1\n");
|
||||
check("K2 at hard max: +1 -> 100 (no write)", meow_ui_vol_step(g_path, +1) == 100);
|
||||
wfile("audio.volume = 0\naudio.volume_max = 100\naudio.volume_step = 1\n");
|
||||
check("K2 at min: -1 -> 0 (no write)", meow_ui_vol_step(g_path, -1) == 0);
|
||||
check("K2 at min settings 保持 0", vol_in_file() == 0);
|
||||
|
||||
/* ---- K3 保留其它键(不整文件 O_TRUNC)---- */
|
||||
wfile("# meow-settings 1\ndisplay.brightness = 100\naudio.volume = 60\n");
|
||||
meow_ui_vol_step(g_path, +1);
|
||||
check("K3 vol 60 -> 61", vol_in_file() == 61);
|
||||
check("K3 保留其它键 display.brightness=100",
|
||||
meow_ui_vol_read(g_path, "display.brightness", -1) == 100);
|
||||
|
||||
/* ---- K5 媒体键 IPC 兜底:未注册回调 → 走 IPC(无 socket 静默不崩)---- */
|
||||
cb_calls = 0;
|
||||
meow_ui_media_sys_key(165); /* 上一曲:g_cb==NULL 走 IPC 兜底,不崩 */
|
||||
check("K5 未注册回调不崩", cb_calls == 0);
|
||||
|
||||
/* ---- K4 媒体键本地回调:注册后按 cmd 分发 ---- */
|
||||
meow_ui_media_on_playctl(playctl_cb);
|
||||
meow_ui_media_sys_key(165); /* 上一曲 */
|
||||
check("K4 上一曲(165) -> cb(3)", cb_calls == 1 && cb_cmd == 3 && cb_seek == 0);
|
||||
meow_ui_media_sys_key(163); /* 下一曲 */
|
||||
check("K4 下一曲(163) -> cb(4)", cb_calls == 2 && cb_cmd == 4);
|
||||
meow_ui_media_sys_key(19); /* 旧码上一曲 */
|
||||
check("K4 旧码(19) -> cb(3)", cb_calls == 3 && cb_cmd == 3);
|
||||
|
||||
/* 播放/暂停按最近上报 state 切换 */
|
||||
meow_ui_media_meta(1, 0, 0, "t", "a", "al", NULL, NULL); /* state=PLAYING */
|
||||
meow_ui_media_sys_key(164);
|
||||
check("K4 播放中按播放/暂停 -> cb(2) 暂停", cb_calls == 4 && cb_cmd == 2);
|
||||
meow_ui_media_meta(2, 0, 0, "t", "a", "al", NULL, NULL); /* state=PAUSED */
|
||||
meow_ui_media_sys_key(164);
|
||||
check("K4 暂停中按播放/暂停 -> cb(1) 播放", cb_calls == 5 && cb_cmd == 1);
|
||||
meow_ui_media_meta(0, 0, 0, "t", "a", "al", NULL, NULL); /* state=STOPPED */
|
||||
meow_ui_media_sys_key(164);
|
||||
check("K4 停止中按播放/暂停 -> cb(1) 播放", cb_calls == 6 && cb_cmd == 1);
|
||||
|
||||
meow_ui_media_stop();
|
||||
|
||||
unlink(g_path);
|
||||
unlink(sock);
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user