diff --git a/patch_v2/meowpanel/README.md b/patch_v2/meowpanel/README.md index 76b057f..cdc9642 100644 --- a/patch_v2/meowpanel/README.md +++ b/patch_v2/meowpanel/README.md @@ -28,10 +28,12 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel - 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**。 - 产出(本次构建实测): - `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`, - 体积约 **183 KB**(固件阈值内)。md5:`f91856a44de0c8d12aba00cd40b44b91`。 - - `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **142 KB**。 + 体积约 **202 KB**(固件阈值内)。md5:`e98aec6682b7387a035ab5f63c6777e7`。 + - `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **146 KB**。 - 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自 `meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。 + 阶段 C 实时画面用**自研最小 baseline JPEG 编码器** `mp_jpeg.c`(顺序 DCT + 4:2:0 + 标准 Huffman 表 Annex K; + 无 libm、零依赖,仅 malloc/realloc),质量 70 下 480×720 帧约 24 KB。 - 头文件引用:`-I patch_v2/meow_launcherd`(`meow_ipc.h` 帧格式 / `m_perf_*`/`m_power_*`/`POWER_*`, **header 为权威源**,结构体字段以它为准)。 @@ -78,6 +80,8 @@ http://.local:8080/ | 方法 | 路径 | 说明 | 实现 | |---|---|---|---| | GET | `/`、`/meowpanel.css`、`/meowpanel.js`、`/assets/` | 静态托管 | ✅ | +| GET | `/api/screen.jpeg` | 实时画面:fb0 当前帧 → RGB565→JPEG(`?q=1-100` 质量,默认 70) | ✅(阶段 C) | +| POST | `/api/input/tap` | 触摸注入:`{x,y}`(480×720 原生坐标;write 进 goodix event1,本机实测可投递);本机触摸后 5s 内忽略注入 | ✅(阶段 C) | | GET | `/api/auth/status` | 鉴权状态(enabled/会话有效) | ✅(公开) | | POST | `/api/auth/login` | 密码登录发 token(sha256 带盐校验) | ✅(公开;错→401) | | POST | `/api/auth/logout` | 登出失效 token | ✅(公开;幂等) | @@ -233,6 +237,25 @@ http://.local:8080/ 首次进入以 `/api/auth/status` 决定是否弹登录。设置页「面板鉴权」卡片含开关(`panel.auth_enabled`)+「设置/修改密码」 按钮(`panel.auth_pass`,走 `passwordDlg` 输入)。 +### 5.10 实时画面(`/api/screen.jpeg`)+ 触摸注入(`/api/input/tap`)— 阶段 C +- **实时画面**:`GET /api/screen.jpeg` 读 `/dev/fb0` **当前显示帧**(`FBIOGET_VSCREENINFO` 取 `yoffset`/`xres`/`yres`/`bpp`, + `FBIOGET_FSCREENINFO` 取 `line_length`,`pread` 活动缓冲,**只读,不影响渲染**;16bpp RGB565)→ + `mp_jpeg_encode_rgb565()` 编码为内存 JPEG(自研最小 baseline 编码器,见 §1),返回 `Content-Type: image/jpeg`。 + `?q=1-100` 控制质量(默认 70)。恢复 `line_length != xres*2`(行尾填充)时先压缩为紧凑 RGB565。 +- **触摸注入**:`POST /api/input/tap`,body `{x,y}`(0..479 × 0..719 原生像素坐标,前端按 480×720 映射,后端再 clamp)。 + **机制 = write 进 goodix evdev(`/dev/input/event1`)**——**本机实测 write 注入可投递**(见 §7 修正历史结论), + 事件序列与真实 goodix 一致:先 `BTN_TOUCH=1`(按下边沿)**后** `ABS_MT_*`/`ABS_X/Y`(坐标),`SYN` → 按住 100ms → + `BTN_TOUCH=0` + `ABS_MT_TRACKING_ID=-1` + `SYN`。**关键**:launcher/`meow_touch` 在按下边沿会**重置坐标 -1**, + 只在 down 之后到来的首个有效 ABS 才锁原点(goodix 时序免疫)——故坐标必须**在 BTN_TOUCH=1 之后**,否则原点永不 + 锁定 → 抬手被判无效 tap(只高亮不启动)。注入前不写 uinput(本设备厂商内核 write 进 goodix 即可投递,逐 UB 少)。 +- **仲裁(本机优先打断)**:注入前 `touch_last_local_ms()` 排空 event1,取「最近一次本机真实触摸」的单调毫秒时间 + (剔除本进程自注入事件:事件时间 ≤ 上次注入时刻+200ms),若 `now-last < 5000` → **忽略注入**,返回 + `{"dispatched":false,"reason":"local_touch_active","cooldown_ms":N}`;否则注入并返回 `{"dispatched":true,"x","y"}`。 + MIPS 的 `struct input_event` 无 `time` 成员(ABI),按 launcher 同款读裸 16 字节取偏移 0/4 的 `tv_sec/tv_usec`(单调时钟)。 +- **前端「屏幕」页**(`web/#/screen`,导航「屏幕」):`` 轮询 `GET /api/screen.jpeg?t=…&q=70`(`fetch`→blob→objectURL, + 防重叠 + 旧 URL 回收;后端不可达提示「后端未连接」);默认 **1fps**,滑条 **0.2–2fps**(存 `screen.fps`,页面隐藏/切走即停刷, + `visibilitychange` 回前台续刷);点击画面按 480×720 比例换算坐标 → `POST /api/input/tap`,`dispatched:false` 时 toast「本机触摸中」。 + --- ## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md) @@ -259,6 +282,9 @@ http://.local:8080/ 恒 ENOTDIR 失败)。上传体上限 `MAX_UPLOAD_BODY`=8MB,超限 → 413 `PAYLOAD_TOO_LARGE`。 10. **TF 卡根可覆盖(宿主测试)**:`env MEOW_TF_ROOT` 覆盖卡根(设备上不设,恒用 `/usr/data/mnt/sd_0`);`env MEOW_NOTIF_LOG` 覆盖通知转储日志路径。设 `MEOW_TF_ROOT` 时 `tf_present()` 恒真(测试用)。 +11. **实时画面/触摸注入(阶段 C)**:协议 §6 待定案项称「实时画面(`/api/frame` 等,本文件暂未定义)」。本面板落地为 + **`GET /api/screen.jpeg` + `POST /api/input/tap`**(命名自定:`.jpeg` 表达二进制帧 + `input/tap` 表达触摸注入), + 见 §5.10。回改协议时可补这两端点(以及 `frames` 语义、`/api/input/tap` 的 `dispatched:false/reason` 软失败约定)。 --- @@ -282,6 +308,15 @@ http://.local:8080/ - **上传为「限长缓冲」非全流式**:multipart 请求体整体读入(上限 8MB,超限 413),下载为流式(8KB 分块)。严格「socket 逐字节流式上传 解析」未实现(需增量 multipart 解析器),列为后续;14MB 内存约束下 8MB 上限 + 流式下载已覆盖大文件读侧。 - **存储为 `meowpanel` 编译时刻写死**的 socket/路径会因 `--sock-perf/--sock-power`(qemu sim)可覆盖,但默认走 `/usr/data/meow_*.sock`。 +- **触摸注入在设备息屏时不生效**:launcher 主界面在 `meow_screen_on==0` 时**不处理触摸**(main_menu.c,息屏防误触);设备自动息屏 + (30s 空闲)后注入的 tap 会被 launcher 忽略——面板无法用触摸唤醒(需先电源键点亮)。前端「屏幕」页已提示「设备息屏请先点亮」; + 对「正在看」设备的正常使用场景(屏幕亮着)注入即有效。 +- **实时画面读取 fb0 当前帧(只读)**:双层缓冲下 `pread` 活动缓冲,存在翻页瞬间撕裂一行的可能(1fps 预览可接受); + `/dev/fb0` 不可读/非 16bpp → 500 `INTERNAL`(绝不静默)。 +- **JPEG 编码器为 baseline 4:2:0**:质量 70 下 480×720 ≈ 24KB;质量越高体积越大(`q=100` 约 60-80KB)。编码为单线程 + 串行(面板本就单线程),一帧编码约几十 ms(MIPS 单核),1fps 轮询无压力。 +- **触摸注入为即时性注入**:不维护长按/多指手势状态机(与按键注入一致);`dispatched:false`(本机触摸仲裁)属软失败, + 前端可按 `reason` 提示,不视为错误。 --- @@ -289,9 +324,11 @@ http://.local:8080/ ``` patch_v2/meowpanel/ -├── meowpanel.c # 后端源码(单文件,约 3667 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256) +├── meowpanel.c # 后端源码(约 3650 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面/触摸注入) +├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,无 libm,-Wall -Wextra 0 warning) +├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG) ├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning) -├── meowpanel # 交叉产物(mipsel 静态,约 183 KB,md5 f91856a4…) +├── meowpanel # 交叉产物(mipsel 静态,约 202 KB,md5 e98aec66…) ├── meowpanel_host # 宿主编译(本机 ld,调试用) ├── services.d/ │ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf diff --git a/patch_v2/meowpanel/build_meowpanel.sh b/patch_v2/meowpanel/build_meowpanel.sh index 645c60f..920bdd5 100755 --- a/patch_v2/meowpanel/build_meowpanel.sh +++ b/patch_v2/meowpanel/build_meowpanel.sh @@ -7,6 +7,7 @@ # 用法: # ./build_meowpanel.sh # 交叉编译 → patch_v2/meowpanel/meowpanel(mipsel 静态) # ./build_meowpanel.sh host # 宿主编译 → patch_v2/meowpanel/meowpanel_host +# 源码:meowpanel.c + mp_jpeg.c(最小 baseline JPEG 编码器,阶段 C 实时画面)。 set -e cd "$(dirname "$0")" ROOT="$HOME/Project/R3ProII" @@ -15,14 +16,14 @@ I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/m_perf_query_t/m_perf if [ "$1" = "host" ]; then HOST_CC="${CC_HOST:-cc}" - "$HOST_CC" -Os -Wall -Wextra -I"$I_HDR" meowpanel.c -o meowpanel_host + "$HOST_CC" -Os -Wall -Wextra -I"$I_HDR" meowpanel.c mp_jpeg.c -o meowpanel_host echo "== host build ==" ls -la meowpanel_host file meowpanel_host exit 0 fi -"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meowpanel.c -o meowpanel +"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meowpanel.c mp_jpeg.c -o meowpanel echo "== meowpanel (mipsel static) ==" ls -la meowpanel md5sum meowpanel diff --git a/patch_v2/meowpanel/meowpanel b/patch_v2/meowpanel/meowpanel index e9a8058..880fc60 100755 Binary files a/patch_v2/meowpanel/meowpanel and b/patch_v2/meowpanel/meowpanel differ diff --git a/patch_v2/meowpanel/meowpanel.c b/patch_v2/meowpanel/meowpanel.c index b69d5a3..63135b2 100644 --- a/patch_v2/meowpanel/meowpanel.c +++ b/patch_v2/meowpanel/meowpanel.c @@ -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); diff --git a/patch_v2/meowpanel/meowpanel_host b/patch_v2/meowpanel/meowpanel_host index 8962f61..f919430 100755 Binary files a/patch_v2/meowpanel/meowpanel_host and b/patch_v2/meowpanel/meowpanel_host differ diff --git a/patch_v2/meowpanel/web/index.html b/patch_v2/meowpanel/web/index.html index 95d2d78..ebaec0e 100644 --- a/patch_v2/meowpanel/web/index.html +++ b/patch_v2/meowpanel/web/index.html @@ -45,6 +45,10 @@ 远程 +