diff --git a/patch_v2/meowpanel/README.md b/patch_v2/meowpanel/README.md index 0903d27..e75307d 100644 --- a/patch_v2/meowpanel/README.md +++ b/patch_v2/meowpanel/README.md @@ -28,7 +28,7 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel - 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。 - 产出(本次构建实测): - `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`, - 体积约 **218 KB**(固件阈值内)。md5:`51ffc183403aa20e0d19d0f811c300b7`(本次含字体库 tar.gz 导入/导出 + 排序修复)。 + 体积约 **218 KB**(固件阈值内)。md5:`015f9cf1c12e36d133b01cf6b6648edd`(本次含 `/api/audio` 音频状态端点 + 前端「音频」「字体」页)。 - `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **148 KB**。 - 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自 `meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。 @@ -112,6 +112,7 @@ http://.local:8080/ | GET | `/api/fonts` | 字体库列表(扫描 `MeowOS/fonts///` 三件套齐) | ✅(无卡→503) | | POST | `/api/fonts/import` | 上传字体库 `.tar.gz`(multipart `file`,可带 `?force=1` 覆盖) | ✅(缺件/坏json/穿越→400,冲突→409,超限→413) | | GET | `/api/fonts/export` | 导出 `//` 三件套为 `.tar.gz`(`?family=&weight=`) | ✅(缺件→404) | +| GET | `/api/audio` | 音频状态快照:meow_audiod IPC `MSG_AUDIO_QUERY→STATE` 透传(当前流/路由/音量/underrun,见 §5.12) | ✅(audiod 不在→`available:false`,不报错) | | GET | `/api/apps` | 扫描 `MeowAPPs/` 读 `app.json` | ✅ | | GET | `/api/apps/:pkg` | 应用详情 | ✅ | | POST | `/api/apps/install` | 经 `nyai install`(multipart 或 path) | ✅ | @@ -306,6 +307,27 @@ http://.local:8080/ 再导入→逐字节一致;GNU tar/`tarfile` 均能读;目录穿越 `../`/绝对路径/软链成员/缺件/坏 font.json/多出条目/ 子目录包裹/非 gzip——全部拒绝;打包文件名 `-.font.tar.gz`。见 §7 宿主测试备注。 +### 5.12 音频状态(`/api/audio`,新增,AUDIOD_DESIGN §8 面板对接) +- **数据源**:`meow_audiod` 独占音频守护(板载进固件,svcd 托管;HibyOS 模式按门控不启动)。 + 后端连 `/usr/data/meow_audio.sock`(可被 `--sock-audio` / env `MEOW_AUDIO_SOCK` 覆盖,宿主/qemu 测试用, + 与 audiod CLI 同 env),发 `MSG_AUDIO_QUERY`(0x00C7) 空负载,收 `MSG_AUDIO_STATE`(0x00C8) 文本帧 + (`active=.. app=.. sid=.. rate=.. ch=.. fmt=.. written=.. played=.. underrun=.. paused=.. route_mode=.. port=.. vol=..`, + 空格分隔 k=v、NUL 结尾)。**audiod 无 MSG_HELLO 握手**(见 meow_audio.c CLI),与 perf/power/launcher 路径不同。 +- **响应**:解析 k=v 为 JSON——`{available:true, active, app, sid, rate, ch, fmt, written_frames, played_frames, + underrun, paused, route_mode("auto"/"manual"), port, vol}`;`port` 为 amixer numid=9 路由口 + (0=空闲 1=lineout 2=3.5 3=4.4 4=Type-C SPDIF;宿主无 amixer 时为 -1)。 +- **audiod 不在**(socket 缺失/连接拒/超时/非预期应答)→ HTTP 200 `{"available":false}`,**不报错**(面板音频页显示 + 「audiod 不可用」)。只读端点;不放测试音(真机乱响风险,tone 留给 CLI `meow_audio tone`)。 +- **前端**:「音频」页(`web/#/audio`)5s 轮询展示——当前流(app/rate/ch/fmt/已写帧/已播放帧/underrun/暂停态)+ + 路由口 + 音量 + audiod 可用性;切页即停轮询。 + +### 5.13 字体管理页(前端 `web/#/fonts`,新增) +- 后端 `/api/fonts*` 三端点(§5.11,agent-243 落地)的专属页面:表格列出 family/weight/name/tier/glyph_count, + 行内「导出」(busy 防连点「打包中…」)+ 顶部「上传字体库 .tar.gz」(导入中禁用按钮=进度态)+「刷新」。 +- 上传遇 **409 CONFLICT**(同 family/weight 已存在)→ 弹确认框,确认后带 `?force=1` 重试;成功 toast 带 + `family/weight` 与 `warning`(sha256 不符仅提示)/`hint`。 +- 设置页原「字体库」卡保留(同一后端,互不影响)。 + --- ## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md) @@ -345,6 +367,10 @@ http://.local:8080/ 上限 32MB,`?force=1` 覆盖)/`/api/fonts/export`(导出,GET,`-.font.tar.gz`)。前两者由前端设置页「字体库」卡接入。 还修正了 `/api/files` 排序:**目录优先 + 名称自然排序**(数字感知 track2.local:8080/ ``` patch_v2/meowpanel/ -├── meowpanel.c # 后端源码(约 4560 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面+增量帧/触摸注入/通知 IPC 实时化/字体库 tar.gz 导入导出/自然排序) +├── meowpanel.c # 后端源码(约 4650 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面+增量帧/触摸注入/通知 IPC 实时化/字体库 tar.gz 导入导出/自然排序/audiod 状态快照) ├── 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.c + mp_jpeg.c) -├── meowpanel # 交叉产物(mipsel 静态,约 218 KB,md5 51ffc183…) +├── meowpanel # 交叉产物(mipsel 静态,约 218 KB,md5 015f9cf1…) ├── meowpanel_host # 宿主编译(本机 ld,调试用) ├── services.d/ │ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf ├── web/ # 前端(已存在;部署时拷到后端同目录) -│ ├── index.html # 含登录页 overlay + 设置密码对话框 +│ ├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航 │ ├── meowpanel.css # 含登录页样式 -│ └── meowpanel.js # API 客户端带 token/401 登录、设置页鉴权卡片 +│ └── meowpanel.js # API 客户端带 token/401 登录、设置页鉴权卡片、音频页(5s轮询)/字体页 └── README.md # 本文件 ``` diff --git a/patch_v2/meowpanel/meowpanel b/patch_v2/meowpanel/meowpanel index babe734..693a7c4 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 1a5cb38..9de2d58 100644 --- a/patch_v2/meowpanel/meowpanel.c +++ b/patch_v2/meowpanel/meowpanel.c @@ -92,6 +92,7 @@ static const char *g_notif_log = NULL; /* 通知转储日志:/notificat #define PERF_SOCK "/usr/data/meow_perf.sock" #define POWER_SOCK "/usr/data/meow_power.sock" #define LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */ +#define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */ #define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */ #define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */ #define BOOT_DEFAULT_FILE "/data/meow_boot_default" @@ -123,6 +124,7 @@ static const char *g_sensitive_names[] = { static const char *g_sock_perf = PERF_SOCK; static const char *g_sock_power = POWER_SOCK; static const char *g_sock_launcher = LAUNCHER_SOCK; +static const char *g_sock_audio = AUDIO_SOCK; /* ---------------- 增量帧缓存(GET /api/screen.jpeg?delta=1,阶段 C 增强) ---------------- * 服务端缓存上一帧紧凑 RGB565,按 8×8 块比对,只编码变化块(外接 bbox 子图)并以响应头回传偏移。 @@ -908,6 +910,72 @@ done: return rc; } +/* ---------------- 音频状态:meow_audiod IPC(MSG_AUDIO_QUERY→MSG_AUDIO_STATE) ---------------- + * audiod 无 MSG_HELLO 握手(见 patch_v2/meow_audiod/meow_audio.c CLI:直连发空负载 QUERY, + * 收 STATE 文本帧——"k=v" 空格分隔、NUL 结尾)。audiod 不在/超时/应答异常一律回 + * {"available":false}(HTTP 200,不报错)——面板音频页据此显示「audiod 不可用」。 + * socket 可被 env MEOW_AUDIO_SOCK / --sock-audio 覆盖(宿主/qemu 测试用,与 audiod CLI 同 env)。 */ +static const char *audio_kv(const char *txt, const char *key, char *out, size_t cap) +{ + size_t kl = strlen(key); + const char *p = txt; + while (p && *p) { + if (!strncmp(p, key, kl) && p[kl] == '=') { + p += kl + 1; + size_t n = strcspn(p, " \t\n"); + if (n >= cap) n = cap - 1; + memcpy(out, p, n); + out[n] = 0; + return out; + } + p = strchr(p, ' '); + if (p) p++; + } + out[0] = 0; + return NULL; +} +static long long audio_kv_ll(const char *txt, const char *key, long long defv) +{ + char b[64]; + if (!audio_kv(txt, key, b, sizeof b) || !b[0]) return defv; + return strtoll(b, NULL, 10); +} + +static int api_audio(int sock) +{ + int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS); + if (fd < 0) return send_json(sock, 200, "{\"available\":false}"); + uint8_t buf[MEOW_IPC_MAX]; + uint16_t type = 0; uint32_t rid = 0; + int n = -1; + if (send_frame(fd, MSG_AUDIO_QUERY, 5, NULL, 0) == 0) + n = recv_frame(fd, &type, &rid, buf, sizeof buf - 1); + close(fd); + if (n <= 0 || type != MSG_AUDIO_STATE) + return send_json(sock, 200, "{\"available\":false}"); + buf[n] = 0; + const char *txt = (const char *)buf; + char app[MEOW_APP_ID_MAX + 8], fmt[32], rmode[16]; + audio_kv(txt, "app", app, sizeof app); + audio_kv(txt, "fmt", fmt, sizeof fmt); + audio_kv(txt, "route_mode", rmode, sizeof rmode); + sbuf_t s = {0}; + sA(&s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0)); + sA_str(&s, app); + sA(&s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":", + audio_kv_ll(txt, "sid", 0), audio_kv_ll(txt, "rate", 0), audio_kv_ll(txt, "ch", 0)); + sA_str(&s, fmt); + sA(&s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":", + audio_kv_ll(txt, "written", 0), audio_kv_ll(txt, "played", 0), + audio_kv_ll(txt, "underrun", 0), audio_kv_ll(txt, "paused", 0)); + sA_str(&s, rmode); + sA(&s, ",\"port\":%lld,\"vol\":%lld}", + audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1)); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + /* ---------------- 路径安全 ---------------- */ static int tf_present(void) { @@ -4385,6 +4453,9 @@ static int handle(int sock, req_t *r) if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock); if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r); + /* ---- 音频状态(meow_audiod 快照,只读) ---- */ + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock); + /* ---- 实时画面(阶段 C) ---- */ if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg")) return api_screen_jpeg(sock, r); @@ -4513,10 +4584,11 @@ int main(int argc, char **argv) else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i]; else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i]; else if (!strcmp(argv[i], "--sock-launcher") && i + 1 < argc) g_sock_launcher = argv[++i]; + else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i]; else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { printf("MeowPanel backend %s\n", PANEL_VERSION); - printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P]\n"); + printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P]\n"); return 0; } } @@ -4527,6 +4599,8 @@ int main(int argc, char **argv) if (env && env[0]) g_sock_power = env; env = getenv("MEOW_IPC_LAUNCHER_SOCK"); if (env && env[0]) g_sock_launcher = env; + env = getenv("MEOW_AUDIO_SOCK"); + if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */ env = getenv("MEOW_SETTINGS_FILE"); if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */ diff --git a/patch_v2/meowpanel/meowpanel_host b/patch_v2/meowpanel/meowpanel_host index fadde84..62db04a 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 ebaec0e..ba903df 100644 --- a/patch_v2/meowpanel/web/index.html +++ b/patch_v2/meowpanel/web/index.html @@ -29,6 +29,10 @@ 亮度·音量 + + ' + + '' + + '' + + '

加载字体库列表…

' + + '

上传 tar.gz(内含 font.png + font.idx + font.json,平铺三件套)→ 校验后解压到 ' + + 'MeowOS/fonts/<family>/<weight>/;同 family/weight 冲突时会要求确认覆盖。' + + '导入后进设备「设置 → 字体」页,列表自动重扫生效。

'; + loadFontsPage(); + } + async function loadFontsPage() { + const box = $('#fpage-list'); if (!box) return; + let d; + try { d = await api('GET', '/api/fonts'); } + catch (e) { box.innerHTML = '

字体库列表加载失败:' + esc(errText(e)) + '

'; return; } + const fs = (d && d.fonts) || []; + if (!fs.length) { box.innerHTML = '
暂无字库(TF 卡 MeowOS/fonts 下无完整三件套)
'; return; } + let h = ''; + fs.forEach(f => { + h += '' + + '' + + '' + + ''; + }); + box.innerHTML = h + '
familyweight名称tierglyph 数操作
' + esc(f.family || '') + '' + esc(f.weight || '') + '' + esc(f.name || '—') + '' + esc(f.tier || '—') + '' + (f.glyph_count ? Number(f.glyph_count).toLocaleString() : '—') + '
'; + } + async function importFontPage(files) { + if (!files || !files.length) return; + const f = files[0]; + const btn = $('#fpage-import'); + if (btn && btn.disabled) return; // 上传中防连点(进度态) + const fd = new FormData(); fd.append('file', f); + if (btn) { btn.disabled = true; btn.textContent = '导入中…'; } + toast('正在导入 ' + f.name + '…'); + let res = null; + try { res = await api('POST', '/api/fonts/import', fd); } + catch (err) { + if (err.code === 'CONFLICT') { + const ok = await confirmDlg('覆盖字库', '目标字库(同 family/weight)已存在,覆盖导入吗?', true); + if (ok) { + try { res = await api('POST', '/api/fonts/import?force=1', fd); } + catch (e2) { toast('导入失败:' + errText(e2)); } + } + } else toast('导入失败:' + errText(err)); + } + if (btn) { btn.disabled = false; btn.textContent = '上传字体库 .tar.gz'; } + if (res) { + toast('已导入 ' + (res.family || '') + '/' + (res.weight || '')); + if (res.warning) toast(res.warning); + else if (res.hint) toast(res.hint); + loadFontsPage(); + } + } + function bindFonts() { + const c = $('#content'); + c.addEventListener('click', function (e) { + if (e.target.closest('#fpage-import')) { const inp = $('#fpage-file'); if (inp) { inp.value = ''; inp.click(); } return; } + if (e.target.closest('#fpage-refresh')) { loadFontsPage(); return; } + const fe = e.target.closest('[data-fpexport]'); + if (fe) { + if (fe.dataset.busy) return; // 打包期间防连点 + fe.dataset.busy = '1'; + const old = fe.textContent; fe.textContent = '打包中…'; + const f = fe.dataset.fpexport, w = fe.dataset.fpweight; + doDownload('/api/fonts/export?family=' + encodeURIComponent(f) + '&weight=' + encodeURIComponent(w), + (f || 'font') + '-' + (w || 'regular') + '.font.tar.gz') + .finally(() => { delete fe.dataset.busy; fe.textContent = old; }); + return; + } + }); + c.addEventListener('change', function (e) { + if (e.target.id === 'fpage-file') importFontPage(e.target.files); + }); + } + /* ---------------- 5. 设置镜像页 ---------------- */ function settingControl(it) { const key = it.key; @@ -1050,8 +1185,10 @@ const ROUTES = { perf: { title: '实时性能', render: renderPerf, bind: null }, bright: { title: '亮度 · 音量', render: renderBright, bind: null }, + audio: { title: '音频状态', render: renderAudio, bind: null }, files: { title: '文件管理', render: renderFiles, bind: bindFiles }, apps: { title: 'MeowAPPs 管理', render: renderApps, bind: bindApps }, + fonts: { title: '字体管理', render: renderFontsPage, bind: bindFonts }, settings: { title: '设置镜像', render: renderSettings, bind: bindSettings }, remote: { title: '远程控制', render: renderRemote, bind: bindRemote }, screen: { title: '实时屏幕', render: renderScreen, bind: bindScreen }, @@ -1070,6 +1207,7 @@ const r = ROUTES[id] || ROUTES.perf; state.route = id; stopPerf(); + stopAudio(); stopScreenPoll(); $$('.nav-item').forEach(b => b.classList.toggle('active', b.dataset.route === id)); $('#page-title').textContent = r.title; @@ -1089,7 +1227,7 @@ bindNav(); bindThemeUi(); bindSlider(); - bindFiles(); bindApps(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); + bindFiles(); bindApps(); bindFonts(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindLogin(); bindPwDlg(); // 首次进入探鉴权状态:启用且无有效会话 → 登录页;否则直接进应用