feat(meowpanel): 阶段C收尾——实时画面+远程遥控 (agent-224) — /api/screen.jpeg(fb0活动帧→自研最小baseline JPEG编码器,480x720≈24KB/帧,无libm零依赖) + /api/input/tap(坐标注入,BTN_TOUCH先行时序,本机触摸优先5s仲裁); 前端「屏幕」页(1fps可调,点击映射,息屏提示)
meowpanel→e98aec66 已部署; 真机验证: jpeg非全黑+注入tap拉起Files(日志+截图双证) 注: 注入走 write event1(实测本机可投递,未用uinput)
This commit is contained in:
1 parent
6c78a88b8c
commit
fbdd77a72e
8 files changed
+333
-7
No files matched your search
@@ -3427,6 +3427,187 @@ static int api_apps_remove(int sock, const char *pkg, const req_t *r)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---------------- 实时画面(GET /api/screen.jpeg) ---------------- */
|
||||
/* fb0 读当前显示帧(只读,不影响渲染):按 yoffset 读活动缓冲,16bpp RGB565。 */
|
||||
static int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
||||
{
|
||||
int fd = open(dev, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
unsigned char vinfo[160], finfo[68];
|
||||
memset(vinfo, 0, sizeof vinfo);
|
||||
memset(finfo, 0, sizeof finfo);
|
||||
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 ||
|
||||
ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) { close(fd); return -1; }
|
||||
uint32_t xres = *(uint32_t *)(vinfo + 0);
|
||||
uint32_t yres = *(uint32_t *)(vinfo + 4);
|
||||
uint32_t yoff = *(uint32_t *)(vinfo + 20);
|
||||
uint32_t bpp = *(uint32_t *)(vinfo + 24);
|
||||
uint32_t line = *(uint32_t *)(finfo + 44);
|
||||
if (!line) line = xres * (bpp / 8);
|
||||
if (bpp != 16 || xres == 0 || yres == 0 || line < xres * 2) { close(fd); return -1; }
|
||||
size_t framelen = (size_t)line * (size_t)yres;
|
||||
uint8_t *frame = (uint8_t *)malloc(framelen);
|
||||
if (!frame) { close(fd); return -1; }
|
||||
ssize_t got = pread(fd, frame, framelen, (off_t)line * (off_t)yoff);
|
||||
close(fd);
|
||||
if (got != (ssize_t)framelen) { free(frame); return -1; }
|
||||
/* 若 line != xres*2(行尾填充),压缩为紧凑 RGB565(xres*yres*2) */
|
||||
if (line != xres * 2) {
|
||||
size_t cplen = (size_t)xres * 2;
|
||||
for (uint32_t y = 0; y < yres; y++)
|
||||
memmove(frame + (size_t)y * cplen, frame + (size_t)y * line, cplen);
|
||||
}
|
||||
*out = frame; *w = (int)xres; *h = (int)yres;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 仅读 fb 几何(不抓帧),用于坐标 clamp 上限。 */
|
||||
static int fb_dims(const char *dev, int *w, int *h)
|
||||
{
|
||||
int fd = open(dev, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
unsigned char vinfo[160];
|
||||
memset(vinfo, 0, sizeof vinfo);
|
||||
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0) { close(fd); return -1; }
|
||||
uint32_t xres = *(uint32_t *)(vinfo + 0);
|
||||
uint32_t yres = *(uint32_t *)(vinfo + 4);
|
||||
close(fd);
|
||||
if (!xres || !yres) return -1;
|
||||
*w = (int)xres; *h = (int)yres;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int api_screen_jpeg(int sock, const req_t *r)
|
||||
{
|
||||
int q = 70; /* 默认质量(体积/清晰折中) */
|
||||
char qv[8];
|
||||
if (qparam(r->query, "q", qv, sizeof qv)) {
|
||||
int t = atoi(qv);
|
||||
if (t >= 1 && t <= 100) q = t;
|
||||
}
|
||||
const char *dev = getenv("MEOW_FB_DEV"); if (!dev || !dev[0]) dev = FB_DEV;
|
||||
uint8_t *frame = NULL; int w = 0, h = 0;
|
||||
if (screen_read_frame(dev, &frame, &w, &h) != 0)
|
||||
return send_err(sock, 500, "INTERNAL", "读取帧缓冲失败(/dev/fb0)");
|
||||
uint8_t *jpeg = NULL; size_t jlen = 0;
|
||||
int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen);
|
||||
free(frame);
|
||||
if (rc != 0) return send_err(sock, 500, "INTERNAL", "JPEG 编码失败");
|
||||
int ret = send_raw(sock, 200, "image/jpeg", jpeg, jlen);
|
||||
free(jpeg);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ---------------- 触摸注入(POST /api/input/tap) ----------------
|
||||
* 本机厂商内核 write 进 goodix(事件1) 会投递给读者(实测:写 tap 打开 App 成功),故复用
|
||||
* 「写 evdev」注入(同按键注入机制,但用 MT 协议 B + legacy ABS_X/Y + BTN_TOUCH,等于真实
|
||||
* goodix 事件序列)。仲裁:本机 event1 最近活动后 5s 内忽略注入(本机优先打断)。 */
|
||||
#define TOUCH_ACTIVE_MS 5000
|
||||
|
||||
static int64_t now_mono_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static int64_t g_last_self_inject_ms = 0; /* 本进程上次注入时刻(用于剔除自注入事件) */
|
||||
|
||||
/* 排空触摸设备,返回「最近一次本机真实触摸」的单调毫秒时间(0=无)。剔除自注入事件。
|
||||
* MIPS 的 struct input_event 无 time 成员(ABI),按 launcher 同款读裸 16 字节、
|
||||
* 取偏移 0(int32 tv_sec)/4(int32 tv_usec)。 */
|
||||
static int64_t touch_last_local_ms(const char *dev)
|
||||
{
|
||||
int fd = open(dev, O_RDONLY | O_NONBLOCK);
|
||||
if (fd < 0) return 0;
|
||||
int64_t last = 0;
|
||||
unsigned char ev[16];
|
||||
int64_t inject_deadline = g_last_self_inject_ms + 200; /* 注入事件时间 ≈ 注入时刻 */
|
||||
while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||||
int64_t sec = (int64_t)(*(int32_t *)(ev + 0));
|
||||
int64_t usec = (int64_t)(*(int32_t *)(ev + 4));
|
||||
int64_t t = sec * 1000 + usec / 1000;
|
||||
if (t <= inject_deadline) continue; /* 自注入,跳过 */
|
||||
if (t > last) last = t;
|
||||
}
|
||||
close(fd);
|
||||
return last;
|
||||
}
|
||||
|
||||
static int inject_tap(const char *dev, int x, int y)
|
||||
{
|
||||
int fd = open(dev, O_WRONLY);
|
||||
if (fd < 0) return -1;
|
||||
struct input_event ev;
|
||||
#define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \
|
||||
ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0)
|
||||
/* 事件顺序:先 BTN_TOUCH=1(按下边沿),后 ABS 坐标 —— launcher/meow_touch 在按下边沿
|
||||
* 会重置坐标 (-1),只在 down 之后到来的首个有效 ABS 才锁原点(goodix 时序免疫)。若先发
|
||||
* ABS 再发 BTN_TOUCH,原点永不锁定 → 抬手表被判为无效 tap(只高亮不启动)。 */
|
||||
__WE(EV_KEY, BTN_TOUCH, 1);
|
||||
__WE(EV_ABS, ABS_MT_SLOT, 0);
|
||||
__WE(EV_ABS, ABS_MT_TRACKING_ID, 7);
|
||||
__WE(EV_ABS, ABS_MT_POSITION_X, x);
|
||||
__WE(EV_ABS, ABS_MT_POSITION_Y, y);
|
||||
__WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6);
|
||||
__WE(EV_ABS, ABS_X, x);
|
||||
__WE(EV_ABS, ABS_Y, y);
|
||||
__WE(EV_SYN, SYN_REPORT, 0);
|
||||
usleep(100000); /* 按住 100ms 再抬起,抬起到按下的边沿被 launcher 视为一次 tap */
|
||||
__WE(EV_KEY, BTN_TOUCH, 0);
|
||||
__WE(EV_ABS, ABS_MT_TRACKING_ID, -1);
|
||||
__WE(EV_SYN, SYN_REPORT, 0);
|
||||
#undef __WE
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int api_input_tap(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jx = jv_obj_get(root, "x");
|
||||
jv_t *jy = jv_obj_get(root, "y");
|
||||
if (!jx || jx->type != J_NUM || !jy || jy->type != J_NUM) {
|
||||
jv_free(root);
|
||||
return send_err(sock, 400, "BAD_REQUEST", "缺 x/y(数值)");
|
||||
}
|
||||
int x = (int)jx->inum, y = (int)jy->inum;
|
||||
jv_free(root);
|
||||
|
||||
const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV;
|
||||
/* 坐标 clamp 到屏幕范围(前端已按 480x720 映射,此处再兜底) */
|
||||
int sw = 480, sh = 720;
|
||||
{ int fx = 0, fy = 0; const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV;
|
||||
if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } }
|
||||
if (x < 0) x = 0;
|
||||
if (x >= sw) x = sw - 1;
|
||||
if (y < 0) y = 0;
|
||||
if (y >= sh) y = sh - 1;
|
||||
|
||||
/* 仲裁:本机最近触摸后 5s 内忽略注入 */
|
||||
int64_t now = now_mono_ms();
|
||||
int64_t last = touch_last_local_ms(dev);
|
||||
if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) {
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}",
|
||||
(long long)(TOUCH_ACTIVE_MS - (now - last)));
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
g_last_self_inject_ms = now_mono_ms();
|
||||
if (inject_tap(dev, x, y) < 0)
|
||||
return send_err(sock, 500, "INTERNAL", "触摸注入失败(无法打开触摸设备)");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"dispatched\":true,\"x\":%d,\"y\":%d}", x, y);
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ---------------- 静态文件托管(web_root = 二进制所在目录) ---------------- */
|
||||
static char g_webroot[512];
|
||||
static const char *web_root(void)
|
||||
@@ -3503,6 +3684,10 @@ static int handle(int sock, req_t *r)
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system"))
|
||||
return api_system(sock);
|
||||
|
||||
/* ---- 实时画面(阶段 C) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg"))
|
||||
return api_screen_jpeg(sock, r);
|
||||
|
||||
/* ---- 系统动作 ---- */
|
||||
if (!strcmp(r->method, "POST")) {
|
||||
if (!strcmp(p, "/api/system/reboot")) return api_system_action(sock, "reboot", r);
|
||||
@@ -3517,6 +3702,7 @@ static int handle(int sock, req_t *r)
|
||||
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
|
||||
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
|
||||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||||
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
||||
char seg[256];
|
||||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||||
return api_key_single(sock, atoi(seg), r);
|
||||
|
||||
Reference in new issue
Block a user