Files
matrix-screen-controller/整体开发需求/01_网页配置端需求.md
T

1273 lines
153 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 01 网页配置端需求
本文描述核桃派 ZeroW 在局域网内启动的网页配置端。这个网页是用户后续操作点阵屏幕的主要入口,当前阶段提供可叠加的像素与多文字内容编辑,以及纯色、诊断、清屏等独占设备测试能力。
移动端是独立控制入口,设备接入要求见 `03_移动端接入需求.md`,手机需求见 `../移动端相关内容/开发要求/移动端需求.md`。网页布局调整不应影响 BLE 协议;设备功能改动须登记移动端兼容影响,不自动更新 App。
项目默认一台核桃派只做这一件事:开机后自动启动网页服务和屏幕控制服务,同一局域网内的手机、电脑或平板通过 `http://核桃派IP:8080` 访问。
## 0. 需求编号索引
本文件使用 `WEB-*`、`CONFIG-*` 和 `DEPLOY-*` 编号描述网页端、接口、配置和运行入口。测试覆盖关系维护在 `测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md`,不要在本文件复制完整测试命令。
| 编号 | 范围 | 当前阶段要求 |
|---|---|---|
| `WEB-ENTRY` | 网页入口和整体页面 | 局域网设备访问移动端优先的控制面板,首页直接进入可操作界面。 |
| `WEB-WORKSPACES` | 可扩展工作区和导航 | 工作区通过注册信息生成单层侧栏和 URL hash;用户可在内存草稿中上移、下移并显式保存设备共享顺序。 |
| `WEB-PREVIEW` | 共享正向预览与设备监视器 | 普通编辑工作区共用页面顶部的逻辑 `0°` 组合画板;设备状态、系统设置和模板管理在同一位置显示当前设备逻辑帧的只读监视画面。 |
| `WEB-COMPOSITION` | 统一场景与图层合成 | 有序画笔和多文字图层依次覆盖;中央64×64输出,设备测试不得销毁草稿。 |
| `WEB-LAYERS` | 多图层编辑与复制 | 至少一个画笔层,文字层多元素;置顶复制、命名、排序及跨内容追加。 |
| `WEB-EXTENDED-CANVAS` | 可切换192画布与整层移动 | 扩展显示按浏览器记忆,偏移±64,视线和图层移动分离。 |
| `WEB-LAYER-OVERFLOW` | 越界确认与永久裁切 | 确认前不保存,取消恢复,背景不报警;文字裁切持久化且可撤销。 |
| `CONFIG-SCENE-V2` | 场景及内容迁移 | scene v2、模板v2、动图v3、配置v11;摘要校验和安全迁移。 |
| `WEB-BACKEND` | FastAPI、REST、WebSocket、静态文件托管 | 服务监听 `0.0.0.0:8080`,页面通过 API/WS 调用显示服务。 |
| `WEB-STARTUP-INDICATOR` | 开机运行指示兼容资源 | 旧开机笑脸不再作为独立启动覆盖层;其两态像素资源继续供只读演示案例和默认演示动图复用。 |
| `WEB-DEFAULT-DISPLAY` | 默认显示内容 | 模板管理可把演示案例、用户静态模板或非空用户动图设为默认;开机和启动提示结束后显示该内容,失效时回退演示动图。 |
| `WEB-CURRENT-DISPLAY` | 顶栏当前画面 | 顶栏持续显示当前实际可见内容并可打开实屏帧预览;活动动图还提供整段跳转、暂停/继续和固定倍速控制,控制状态只属于当前播放会话。 |
| `WEB-WIFI-SETTINGS` | WiFi、IPv4 与网络提示设置 | 系统设置可修改受管 WiFi、明文显示密码、DHCP/静态 IPv4,并分别选择立即切换或下次断电开机生效;网络提示延迟使用下方独立设置卡保存。 |
| `WEB-NETWORK-DIAGNOSTICS` | 分层网络检测 | 系统设置可检测接口/IP、默认路由、DNS、外网 TCP/TLS 和辅助 ping,以证据区分未连接、网关、DNS、时间/TLS 和目标站点异常。 |
| `WEB-FRP-MAINTENANCE` | FRP 远程维护 | 系统设置底部管理多个 FRP 原文配置,选择一个由独立 systemd 服务启停,保存前必须通过官方 `frpc verify`。 |
| `WEB-ASSET-VERSION` | 前端版本和缓存一致性 | 首页和动态 API 不缓存;同一次页面加载只使用同一内容摘要版本的静态资源,旧页面能发现服务升级。 |
| `WEB-MULTI-CLIENT` | 多标签和多设备一致性 | 浏览器草稿保持本地边界,共享设备状态及时刷新,草稿和模板冲突不得静默覆盖。 |
| `WEB-INTERACTION-FEEDBACK` | 控件可用性、处理中与失败反馈 | 所有操作区分可用、条件未满足、处理中、成功和失败;桌面、键盘与触屏都能获知不可用原因,任何网络操作都必须有界结束并恢复控件。 |
| `WEB-SYSTEM-RESOURCES` | 应用与整机资源概览 | 顶栏低频实时显示当前服务进程及整机的 CPU、内存占用,不记录历史、不提供配置开关。 |
| `WEB-OTA` | 软件版本与浏览器全量更新 | 系统设置显示正式软件版本和固定功能更新时间,并允许上传单个全量 `.ota` 包;页面仅在上传、安装或失败时显示临时进度,不展示最近 OTA 结果,切换服务期间允许短暂断线后自动恢复。 |
| `DEPLOY-SYSTEMD` | systemd 自启动 | 核桃派开机后自动启动服务,具体远端验证见测试文档。 |
| `DEPLOY-NETWORK-MANAGER` | 无线网络运行依赖 | 服务和镜像首启依赖 NetworkManager 而不依赖网络已在线;通过参数化 `nmcli` 管理 `wlan0` 的受管连接。 |
| `DEPLOY-UPDATE` | 手工部署与浏览器 OTA | 手工部署和全量 OTA 共用离线构建、原子切换、健康检查与失败回滚边界;拒绝同版和旧版,成功后删除旧程序与暂存。 |
| `DEPLOY-RELEASE-VERSION` | OTA 与镜像共用版本 | 只有明确导出成功才推进同一三段版本并登记发布记录;失败不占号;导出 IMG 可删除,OTA 长期保留(1.0.1 为历史例外)。 |
| `DEPLOY-FEATURE-TIMESTAMP` | 功能更新时间 | 每次可部署的功能新增或优化写入源码固定北京时间;单纯导出 OTA/镜像、重复部署和文档修改不得改写。 |
| `DEPLOY-IMAGE-EXPORT` | Rufus 可刷镜像 | 从未修改官方 IMG 复制生成;FAT16 只携带双槽配置、元数据和首启程序,Linux 根分区携带应用与已验证预编译内核离线载荷,元数据格式固定为 v3。 |
| `DEPLOY-IMAGE-FIRSTBOOT` | 无外设自动首启 | 只连接核桃派本体即可离线安装应用与双内核候选、配置身份和网络、清理秘密并自动重启;SSID、认证、DHCP 或默认路由不可用不阻断安装,真实安装失败恢复原 boot 文件且不循环重启。 |
| `DEPLOY-OFFLINE-DEPS` | 离线依赖生命周期 | Python wheel、Debian AArch64 包、预编译内核及固定内核源码均有摘要与用途;代码依赖增删必须同次同步。 |
| `DEPLOY-WORKSPACE-LAYOUT` | 工作区路径与独立性 | 适配分类目录,支持中文、空格及不同调用目录;清理不越界,包内路径与设备路径保持兼容。 |
| `DEPLOY-REPOSITORY-HYGIENE` | Git 仓库安全与可移植性 | 私有凭据与示例分离,阻止秘密和开发机路径提交,并以普通 Git 保留正式产物、离线依赖和历史归档。 |
| `DEPLOY-FRP` | FRP 系统组件 | 固定、校验并离线交付官方 Linux ARM64 `frpc`,安装到 `/usr/local/bin`,由维护账户运行且纳入首装、手工更新和 OTA 回滚。 |
| `DEPLOY-IMAGE-EDITOR` | 便携镜像编辑器 | Python/PySide6 跨平台源码读取、修改或另存账户、WiFi 和 IPv4;Windows 交付绿色单 EXE,编辑器版本独立于软件版本。 |
| `DEPLOY-FONTS` | 多语言字体运行依赖 | 部署环境提供 Fontconfig、Noto Core 和 Noto CJK,服务启动后能解析主流现代文字体系。 |
| `WEB-FILL` | 全屏纯色测试 | 支持黑、红、绿、蓝、白和持久自定义颜色;测试使用可恢复的临时亮度会话。 |
| `WEB-DEVICE-STORAGE` | 设备与软件容量概览 | 设备状态页显示设备总量、可用量、软件总占用和模板内容占用,使用有界缓存而非随状态轮询反复扫描。 |
| `WEB-DIAGNOSTIC` | 硬件诊断图案 | 支持发送四角横线、行带、地址检查和文字复现图案,用于排查重复/错位显示。 |
| `WEB-CANVAS` | 像素底层和 WebSocket 帧 | 维护 `64 x 64 x RGB` 像素底层,合成后通过 `WS /ws/canvas` 发送最终整帧。 |
| `WEB-CANVAS-EDIT` | 防误触全屏编辑与画布视角 | 像素画布默认只读,经明确入口进入全屏绘画;编辑期提供可移动悬浮菜单、会话撤销以及独立的平移缩放模式。 |
| `WEB-CANVAS-COORDINATES` | 自适应画布坐标系 | 全屏像素编辑可从悬浮菜单开关浏览器本地记忆的坐标标尺与贯穿辅助线,随平移和缩放自适应且不进入任何场景或输出。 |
| `WEB-COLOR-UI` | 统一颜色选择与绘画工具 | 使用软件自有的 RGB/HSV 取色面板、最近颜色、共享色板和触摸友好的画笔工具,不调用系统颜色选择器。 |
| `WEB-ORIENTATION` | 页面方向设置 | 支持 `0/90/180/270`,保存后重启仍生效。 |
| `WEB-BRIGHTNESS` | 页面实时亮度设置 | 拖动亮度条时持续应用并保存最新值,当前画面立即反馈且不需要再次发送。 |
| `WEB-MATRIX-REFRESH` | 屏幕扫描刷新率挡位 | 系统设置提供固定 HUB75 扫描上限挡位,选择后在网页服务不断线的情况下立即应用并保存。 |
| `WEB-PERFORMANCE-MODE` | 可选 CPU 性能模式 | 系统设置提供默认关闭的性能模式;只有实际 cpufreq policy 可切换时才允许开启,应用、持久化和回滚必须组成事务,服务停止时恢复启动前 governor。 |
| `WEB-SCREEN-VOLTAGE` | 屏幕输入电压状态与校准 | 顶栏显示当前屏幕输入电压,设置页提供每台设备独立的软件校准流程,传感器故障不得拖垮显示服务。 |
| `WEB-LOW-VOLTAGE-PROTECTION` | 低电压提示与保护开关 | 可选保护按校准后的屏幕输入电压限制实际亮度或覆盖低电图标,页面显示当前限制且不改写用户显示设置。 |
| `WEB-TEXT` | 多文字元素 | 每组文字是可独立选择、编辑、拖动、等比缩放、复制和删除的透明元素。 |
| `WEB-TEXT-I18N` | 多语言静态文字 | `default` 自动选择能覆盖全文的字体,支持常见现代语言且不以缺字方框代替失败。 |
| `WEB-TEXT-FONTS` | 字体选择与导入 | 文字编辑器使用可搜索的设备字体目录,允许导入受校验的字体文件且不要求用户输入服务器路径。 |
| `WEB-TEMPLATES` | 可编辑场景模板 | 内容工作区可保存或更新完整场景模板;模板管理统一提供播放、编辑、复制、重命名、删除和混合排序,动图卡片按完整帧序列动态预览,播放与编辑草稿互不影响;动态预览与动图管理共享设备配置的并发上限。 |
| `WEB-ANIMATIONS` | 动图文件夹与逐帧编辑 | 把有序 scene v2 帧按独立间隔循环播放;网页负责文件夹、帧顺序、逐帧编辑、复制目标和播放入口,卡片名称单行省略并提供非触摸悬停全名,缩略图按完整帧序列动态预览;动态预览与模板管理共享设备配置的并发上限。 |
| `WEB-MEDIA-IMPORT` | 媒体内容转换 | 流式导入常见图片、动图和视频,在设备后台统一裁切、缩放、透明合成和逐帧转换为静态模板或动图。 |
| `WEB-API` | REST/WS 契约 | 定义 `/api/status`、`/api/config`、`/api/display/*`、`/api/preview/*`、`/ws/canvas`,包括带会话防护的活动动图运行时控制。 |
| `CONFIG-DATA-LIFECYCLE` | 持久、可再生、运行期与更新暂存数据 | 生产持久根固定为 `/var/lib/matrix-screen-controller`,运行根固定为 `/run/matrix-screen-controller`;格式只向唯一当前 schema 迁移,失败时保留原件并拒绝启动。 |
| `CONFIG-OTA-STATE` | OTA 活动状态和最近结果 | 活动任务只写运行根;持久根只保存一条有 schema 的最近结果和未完成事务恢复信息,不保存包历史或更新日志索引。 |
| `CONFIG-BASE` | 配置文件和默认值 | 配置保存在持久根的 `config.json`;仅文件缺失时创建默认值,现有文件损坏或无法迁移时不得静默回落。 |
| `CONFIG-WIFI` | WiFi 连接身份与开机提示延迟 | 持久根的 `wifi_config.json` v1 保存受管连接 UUID 和 `prompt_delay_seconds`;SSID、密码与 IPv4 仍由 NetworkManager 保存。 |
| `CONFIG-FRP` | FRP 配置目录 | 持久根 `frp/` 以严格 v1 目录保存多个 UUID 配置和唯一选中项;默认无配置、服务关闭,损坏时保留原件并只关闭 FRP 子系统。 |
| `CONFIG-PREVIEW-REFRESH` | 当前帧监视刷新间隔 | 按设备保存 `preview_refresh_interval_ms`,默认 `1000ms`;允许 `1..60000ms`,低于 `100ms` 时提示负载风险但不阻止保存。 |
| `CONFIG-UI-STATE` | 浏览器场景草稿与编辑工具状态 | 场景使用稳定键 `matrixController:scene`;编辑工具使用独立带版本键,并兼容迁移已有画布、文字和工具状态。 |
| `CONFIG-TEMPLATES` | 核桃派模板持久化 | 完整 scene v2 和有效后端缩略图保存在持久根的 `templates/`;部署、重启和一般清理不得删除。 |
| `CONFIG-ANIMATIONS` | 核桃派动图持久化 | 动图 v3 元数据、独立 scene v2 帧和有效缩略图保存在持久根的 `animations/`;播放按不可变快照懒加载,升级不得删除。 |
| `CONFIG-MEDIA-JOBS` | 媒体转换任务 | 持久根保存活动任务、源文件、预览和暂存输出;成功或取消立即清理,失败与待设置任务最多保留 24 小时。 |
| `CONFIG-FONTS` | 用户导入字体持久化 | 导入字体按内容摘要保存在持久根的 `fonts/`,重启、更新、回滚和一般清理不得删除。 |
| `CONFIG-LIBRARY-ORDER` | 模板管理混合顺序 | 用户静态模板和用户动图的统一顺序保存在持久根的 `library/order.json` v1;演示案例不进入记录。 |
| `CONFIG-COLOR-PALETTE` | 设备共享收藏色 | 按 `CONFIG-DATA-LIFECYCLE` 在设备配置中持久化最多 24 个规范化的 `#RRGGBB` 收藏色,供所有浏览器共享。 |
| `CONFIG-SCREEN-VOLTAGE` | 电压传感器校准 | 按 `CONFIG-DATA-LIFECYCLE` 为每台设备独立保存校准系数、参考值、未校准值和校准时间;新设备默认使用标称系数 `1.0`。 |
| `CONFIG-LOW-VOLTAGE-PROTECTION` | 低电压保护开关 | 配置 v3 严格保存布尔字段 `low_voltage_protection_enabled`,新建和升级设备均默认关闭。 |
| `CONFIG-MATRIX-REFRESH` | 屏幕扫描刷新率上限 | 配置 v4 严格保存 `matrix_refresh_rate_limit_hz`,只允许 15、20、30、45、60、80、100,默认 100。 |
| `CONFIG-TEST-COLOR` | 自定义纯色测试颜色 | 配置 v5 严格保存规范 `#RRGGBB` 字段 `custom_test_color`,v4 升级默认 `#40A0FF`。 |
| `CONFIG-DEFAULT-CONTENT` | 默认显示内容引用 | 配置 v6 严格保存 `{type,id}`,只引用静态模板或非空动图;v5 升级默认引用只读“演示动图”。 |
| `CONFIG-WORKSPACE-ORDER` | 设备共享工作区顺序 | 配置 v7 严格保存唯一工作区 ID 数组;保存后跨浏览器和重启生效,未保存编辑不持久化。 |
| `CONFIG-PERFORMANCE-MODE` | CPU 性能模式请求 | 配置 v9 严格保存布尔字段 `performance_mode_enabled`,v8 升级与新设备均默认关闭;请求状态与实际 governor 分开报告。 |
| `CONFIG-ANIMATION-PREVIEW-CONCURRENCY` | 动态缩略图并发上限 | 配置 v11严格保存整数(v10引入) `animation_preview_max_concurrent`,v9 升级与新设备均默认 `2`,只允许 `1..50`;系统设置在性能模式下方保存该值,并明确提示高并发会造成严重性能问题。 |
| `DEPLOY-KERNEL-ROLLBACK` | AXP313A 候选内核与自动回滚 | 已验证候选以带摘要的预编译离线载荷交付;原内核、DTB、模块和 boot 脚本保持可启动,未通过内核、cpufreq、服务和 GPIO 健康检查时下一次自动回到原内核。 |
| `DEPLOY-OTA-KERNEL-RECOVERY` | OTA 内核能力守护与轻量恢复 | OTA 前后比较内核、cpufreq、性能模式和候选启动标记;正常能力退化时回滚应用。已回退设备可更新应用,只有本机候选内核、DTB、模块及回滚设施通过固定摘要校验时,更新完成后才独立重启尝试恢复一次;失败不得自动重试,缺失或损坏材料只报告需要专用修复包,普通 OTA 不携带内核归档。 |
| `DEPLOY-MEDIA-DECODERS` | 媒体解码依赖与隔离 | Debian 提供 FFmpeg/ffprobe/libheif;独立转换 unit 限制 CPU、内存、IO、设备和网络访问。 |
| `WEB-SECURITY` | 局域网安全边界 | 第一阶段默认可信局域网,保留访问密码和上传限制扩展入口。 |
| `WEB-ERRORS` | 日志和错误展示 | 后端记录关键事件,前端显示屏幕可用性和最近操作结果。 |
| `WEB-UI-COPY-CATALOG` | 网页程序文案目录 | 为固定文字、属性文字和动态状态模板分配稳定文案键,动态值使用受保护占位符。 |
| `WEB-UI-COPY-EDITOR` | 特殊构建文案编辑器 | 仅后端构建标识开启时提供全局可视化文案编辑、字号预览、隐藏和响应式新增提示。 |
| `CONFIG-UI-COPY-DRAFT` | 文案编辑草稿 | 特殊构建把结构化变更集原子保存到持久数据根,并用目录摘要和修订号阻止错误覆盖。 |
| `DEPLOY-UI-COPY-EDITOR` | 文案编辑特殊版本 | 编辑能力只能由后端源码构建标识启用;正式版本不注册接口、不挂载资源且不生成前端入口。 |
## 1. 技术路线
### 1.1 后端服务(`WEB-BACKEND`)
- 使用 `FastAPI + Python` 做常驻服务。
- 服务监听 `0.0.0.0:8080`,允许局域网其他设备访问。
- 静态前端文件由 FastAPI 托管。
- 普通配置和按钮操作走 REST API。
- 透明文字层预览走 REST API,统一画板最终帧走 WebSocket。
- 页面在 `DisplayService` 上层维护无 DOM 场景模型与合成控制器,底层只接收最终 RGB 帧。
- 服务内部调用屏幕底层控制接口,不让网页前端直接接触 GPIO 或 HUB75 细节。
- 服务启动时按静态目录中排序后的相对路径和文件内容计算稳定摘要版本;任一前端文件变化都必须产生新的版本路径。
- 首页和动态 `/api/*` 返回 `Cache-Control: no-store`;带内容摘要查询参数的模板缩略图继续使用长期缓存。当前版本静态资源使用 `/static/<version>/...` 和长期 `immutable` 缓存;旧 `/static/...` 路径继续提供但不得缓存,用于兼容升级前页面。
推荐进程形态:
```text
浏览器页面
-> REST API / WebSocket
-> FastAPI 常驻服务
-> DisplayService 屏幕抽象接口
-> walnutpi-h618-hub75
-> HUB75 点阵屏
```
### 1.2 前端页面(`WEB-ENTRY`)
第一阶段不需要复杂前端框架,可以先用原生 HTML/CSS/JavaScript 实现。后续如果界面变复杂,再迁移到 React/Vue 等框架。
前端必须围绕 `64 x 64` 像素屏幕设计,不要把它当普通高分辨率显示器:
- 页面只有一个共享画板,用浏览器 Canvas 放大显示最终 `64 x 64` 组合场景。
- 前端预览和后端实际显示都以同一套坐标为准。
- 所有发送到屏幕的画面最终都要变成 `64x64 RGB` 帧。
- 像素底层和文字等元素由场景模型管理,不能依靠工作区 DOM 的显示/隐藏状态决定合成结果。
- JavaScript 模块使用相对导入并从同一版本目录加载,禁止新 HTML、旧 CSS 或不同版本 JavaScript 模块混装。
### 1.2.1 控件状态与操作反馈(`WEB-INTERACTION-FEEDBACK`、`WEB-ERRORS`)
- 每个可触发操作的按钮、开关或等价控件必须明确处于“可用、条件未满足、处理中”之一;成功和失败是操作结束结果,不得与处理中共用视觉状态。
- 条件未满足不等于处理中。此类控件使用 `aria-disabled="true"` 保留键盘焦点和解释入口,鼠标使用不可用光标,不得显示忙碌圆环、进度光标或无法结束的加载动画。
- 条件未满足必须提供具体、可执行的中文原因。桌面端在鼠标悬停或键盘聚焦时显示锚定说明;触屏点击时显示页面内固定提示条并同步无障碍播报。提示不得只依赖 hover,也不得使用阻塞式系统对话框。
- 真正的异步处理中才允许原生禁用控件,并同时设置 `aria-busy="true"`、明确的“正在……”文案或邻近状态;成功、失败、超时和异常都必须在 `finally` 等确定路径恢复控件。
- JSON、Blob 等普通 HTTP 请求默认 `15s` 超时,超时统一提示“请求超时,请检查核桃派网络或服务状态后重试”。画板 WebSocket 保持独立 `10s` 超时。用户主动打开的对话框和持续编辑不属于网络等待,不自动超时。
- 页面初始化、工作区进入、列表刷新和操作回调的异常必须显示在当前页面或顶栏状态区;不得只写浏览器控制台、静默吞掉异常或留下永久禁用控件。
- 新增控件时必须通过共享状态模块声明不可用原因和忙碌状态,并补充 `TEST-INTERACTION-FEEDBACK` 映射;禁止直接新增无解释的静态 `disabled` 操作按钮。
- 可拖动卡片只能从非交互区域开始拖动。按钮、输入框、选择框、链接、可编辑元素及其外围 `8 CSS px` 均为起拖保护区;从保护区按下不得阻止控件本身的选择、点击或键盘行为。拖动已经开始后经过或落到保护区不取消拖动。
- 单像素编辑模式开关属于始终可用的同步切换控件,使用 `aria-pressed` 表达状态;切换完成后同时更新邻近持久说明和顶栏无障碍播报,不伪装成异步忙碌操作。
### 1.2.1.1 程序文案目录与特殊编辑器(`WEB-UI-COPY-CATALOG`、`WEB-UI-COPY-EDITOR`)
- 网页中由程序编写的标题、按钮、标签、说明、占位符、不可用原因、状态和错误模板必须显式登记稳定 `copy_id`;不得按当前 DOM 文字猜测或全页扫描文案。实时数值、功能数值、用户输入或命名、SSID/IP、日期、容量以及后端返回的原始错误内容不属于可编辑文案,也不得显示选择轮廓。
- 动态程序文案只能通过统一文案渲染入口绑定目标;使用 `{name}` 占位符时,编辑后的占位符名称和出现次数必须与基线完全相同。变量值只读,文字始终按纯文本渲染,不接受 HTML。
- 特殊构建在任意工作区右下角提供固定呼出按钮。编辑器支持浏览、选择文字和添加提示三种模式;选择模式不得触发被选中的原操作,浏览模式保持应用正常可用。
- 现有文案可修改内容、设置统一适用于全部视口的 `8..64 CSS px` 字号或通过醒目的“删除这行文字”二次确认仅隐藏视觉文字。隐藏按钮或字段文字时必须保留可靠的无障碍名称,不得删除关联功能;新增提示提供独立的二次确认删除入口。
- 新提示只能相对已登记的页面锚点在上方或下方插入,进入正常响应式文档流;不得使用绝对坐标遮挡页面控件。
- 文字和字号输入只修改编辑面板内的暂存值,不得同时修改正文或发送请求。每个项目由用户点击“确定修改”后先以当前 `If-Match` 保存完整草稿,保存成功才一次性注入正文;失败或冲突时正文保持原样并保留表单内容。输入框清空后确认必须显示行内错误、恢复本次编辑前文字且不发送请求。
- 恢复默认、当前会话撤销、新增提示和二次确认删除同样遵守“先保存、成功后注入”;界面必须区分未确认、保存中、已保存、失败和冲突。编辑器自身的目录或状态更新不得触发应用区域的 MutationObserver 循环。
- 构建标识关闭时,首页不包含编辑器引导信息或按钮,编辑 API 与编辑资源均不存在;任何前端操作都不能改变构建标识。
`WEB-UI-COPY-CATALOG` 当前目录 schema 为 v2,每项明确属于静态文字、属性文字或显式动态模板;功能数值和数据值不得进入目录。正式合并后的隐藏文案继续以 `default_hidden` 和原始纯文本保留在目录中,普通构建不渲染可见文字但保留可靠无障碍名称;以后再次开启特殊构建时仍可搜索并恢复显示,不能因定稿隐藏而丢失文案键。`CONFIG-UI-COPY-DRAFT` 继续使用独立 schema v1,至少包含目录摘要、修订号、更新时间、按 `copy_id` 的覆盖项和响应式插入项。文件只在特殊构建中读取和写入,使用严格字段校验和原子替换;损坏、未来版本、未知键、非法字号、非法锚点或目录摘要不一致时保留原件并拒绝编辑,不得静默重建。目录升级必须先导出旧草稿,再用显式迁移报告记录保留和排除项并原子更新摘要;失败时同时回滚程序和草稿。
### 1.2.2 页面版本和多客户端状态(`WEB-ASSET-VERSION`、`WEB-MULTI-CLIENT`)
- `GET /api/status` 提供应用版本、当前服务实例标识和设备状态修订号;成功改变显示帧、方向或亮度时修订号递增,只读与预览请求不得递增。
- 页面可见时每 5 秒读取一次状态,重新获得焦点时立即读取;检测到另一页面改变实屏状态时更新模式、方向和亮度展示并提示,但不得自动覆盖或发送本地画板。
- 检测到应用版本变化后持续提示刷新;本地画板编辑和导出仍可用,但旧页面不得继续提交设备、配置、色板、模板或 WebSocket 写操作。
- 不同浏览器或设备的 scene v2 草稿继续相互独立。相同来源的多个标签页写入同一草稿时,收到 `storage` 变化的标签页必须暂停继续持久化,并让用户明确选择载入外部草稿或以当前草稿覆盖。
- 共享物理屏幕仍采用最后一次成功命令生效,不引入用户账户、编辑锁或实时协作画板。
`WEB-CURRENT-DISPLAY` 把顶栏当前画面改为可聚焦按钮。点击后使用原生对话框读取 `GET /api/display/current-frame`:静态模板、未保存内容和系统覆盖只显示当前逻辑帧,点击预览任意位置、遮罩或按 `Escape` 关闭;活动动图显示同一预览、整段循环进度、播放/暂停以及 `0.5x/1x/1.5x/2x` 四档倍速。动图对话框只允许遮罩、关闭按钮或 `Escape` 关闭,操作控件不得误触关闭。
- 动图进度按全部帧 `duration_ms` 之和计算,播放期间网页平滑外推位置,并按已有 `preview_refresh_interval_ms` 单请求递归刷新当前逻辑帧。拖动期间不得被状态轮询抢回旧位置;网页最多每 `100ms` 提交一次跳转、同一时刻最多一个控制请求,始终保留并最终提交最新位置。
- 暂停、跳转和倍速直接控制实屏后台播放;跳转保持原暂停状态,关闭对话框不得自动继续。播放任何静态内容、新动图或重新播放同一动图都创建新的运行时会话,位置归零、恢复播放和 `1x`,并使旧页面尚未完成的控制请求失效。
- 播放会话、位置、暂停和倍速只保存在显示服务内存,不写入设备配置或用户资源。当前内容或播放会话改变时,已打开页面立即关闭旧对话框并丢弃局部控制状态;其他浏览器最迟在既有 `5s` 状态轮询内同步。
### 1.2.2 应用与整机资源概览(`WEB-SYSTEM-RESOURCES`)
- 顶栏按 `CPU、RAM、低电压保护警告、电压` 排列;保护未活动时警告元素隐藏。CPU 和内存仍是两个可点击的紧凑按钮,外层固定显示 `CPU 0.7核 · 16%` 和 `RAM 60M · 48%` 这种“应用值 · 整机值”格式,不在顶栏重复“应用”“总计”等说明文字;首次采样显示 `CPU -`、`RAM -`,异常显示 `CPU !`、`RAM !`。
- CPU 的应用值表示当前 FastAPI 服务进程折合占用的逻辑核心数,可在 `0..逻辑核心总数` 间变化;整机值表示全部逻辑核心综合后的非空闲百分比。内存应用值显示当前服务进程常驻内存(RSS)的整数 MiB 简写,整机值表示按 `MemTotal - MemAvailable` 计算的整机内存百分比。
- 点击 CPU 或 RAM 均打开同一个“资源占用详情”原生对话框,不能依赖鼠标悬停。弹层显示应用 CPU 的核心数、折合单核百分比、占整机百分比、整机总占用,以及从 `CPU 1` 开始编号的每核心进度条;内存区显示应用 RSS 的准确 MiB、应用百分比和整机总占用。弹层提供明确关闭按钮,支持触控、键盘焦点和 Escape。
- 常驻后端监测组件每 `5s` 读取一次 Linux `/proc` 并缓存最新快照;API 请求只复制缓存,不能为每个浏览器、每次状态请求重复计算或启动采样。首次 CPU 样本允许只显示占位符,取得时间差分后再显示百分比。
- 顶栏沿用页面已有的 `5s` 状态轮询,不新增前端定时器或接口;页面隐藏时停止状态请求,重新可见时读取最新缓存。窄屏允许 CPU、RAM、保护警告和电压自然换行,但不得产生横向滚动或把完整说明重新塞回顶栏。
- `GET /api/status` 返回独立的 `resources` 对象,继续保留 `cpu.application_percent`、`cpu.total_percent`、`memory.application_bytes`、`memory.application_percent` 和 `memory.total_percent`,并增加 `cpu.logical_cpu_count`、`cpu.application_core_equivalent` 和按 `/proc/stat` 的 `cpu0..cpuN` 顺序排列的 `cpu.cores_percent`。首次差分样本的 CPU 百分比和核心数允许为空;采样失败时返回稳定错误状态和空值,弹层解释读取失败,不得影响屏幕、电压、配置或其他 API。
- 资源快照只存在当前进程内存中,不写入配置、持久数据根、运行数据根或日志历史;当前阶段不提供开关、刷新频率设置、告警阈值、历史曲线或导出功能。
### 1.2.3 屏幕输入电压状态(`WEB-SCREEN-VOLTAGE`)
- FastAPI 进程内只有一个电压监测组件持续访问 ADS1110,其他组件和请求只读取其线程安全缓存;`DisplayService` 不得直接访问 ADC,监测组件只在自身锁和 I²C 锁外发布不可变保护指令。
- 每组固定取 `5` 个 15 SPS 新样本的中位数,保护判定使用校准后、未舍入的组中位数。组起始间隔在保护关闭或稳定高于 `4.8V` 时为 `5s`;保护开启且高于 `4.8V` 但持续下降时,按预计到达 `4.8V` 的时间缩短到 `1..5s`;`4.5..4.8V` 为 `1s`;首次低于 `4.5V` 为 `0.5s`,之后使用 `clamp(0.5, 2.0, 0.02 / max(|dV/dt|, 0.01))`,放慢每轮最多增加 `0.5s`、加速立即生效。连续读取错误按 `1s、2s、5s` 退避。
- 每次 I²C 尝试分配单调序号,晚返回的旧结果不得覆盖新结果。传感器不可用、读取失败、保护回调失败或重连不得阻塞网页和其他 API;回调失败与 ADC 读取状态分开记录并在后续周期重试。
- 顶栏标题右侧显示校准后的当前电压并固定保留两位小数;首次读取、断开和异常分别显示“正在读取电压…”“电压传感器断开”“电压读取异常”。
- `GET /api/status` 返回独立的 `power.screen_input_voltage` 对象,包含 `status`、有效时的 `volts` 和 `sampled_at`、`calibrated` 以及稳定的 `error_code`。失败不得伪装成 `0V`。
- 状态缓存内部保留未舍入电压、raw 中位数、采样时间和错误类别,供低电压保护使用;保护只做软件降载与提示,不是 BMS、物理断电、充电管理或核桃派关机保护。
### 1.2.4 电压传感器软件校准(`WEB-SCREEN-VOLTAGE`、`CONFIG-SCREEN-VOLTAGE`)
- 系统设置页显示当前电压、传感器状态、是否已校准、校准时间,并提供“开始校准”和“恢复标称值”。
- 校准时整机保持正常约 `5V`,用户把可靠万用表并联到 ADC 相同的屏幕输入 `5V/GND` 测量点并输入 `4.500..5.500V` 参考值;页面不得引导用户调高或调低整机电源。
- 预览操作通过唯一监测组件取得 `15` 个新样本的中位数,按 `参考电压 / 未校准电压` 计算建议系数;不得与旧系数连乘。建议系数必须在 `0.8..1.2`,否则拒绝生成可保存提案。
- 预览只返回参考值、未校准值、当前系数、建议系数和预计值,不修改配置。确认接口只接受同一服务实例最近生成且尚未过期的提案;传感器状态变化或提案超过 `120s` 时拒绝。
- 确认后持久化本机校准记录并触发一次新采样;恢复标称值将系数设回 `1.0` 并清空校准元数据。两者都不得改变屏幕帧、模式、方向、亮度或设备修订号。
- 校准记录只属于当前核桃派、当前 ADC 和当前测量点。正常部署、更新包和未来 OTA 必须按 `CONFIG-DATA-LIFECYCLE` 保留持久根的 `config.json`;更换模块或测量接线后必须恢复标称值并重新校准,不得自动套用其他机器的系数。
### 1.2.5 低电压亮度保护(`WEB-LOW-VOLTAGE-PROTECTION`、`CONFIG-LOW-VOLTAGE-PROTECTION`)
- “供电电压”设置卡在电压详情与校准按钮下方提供保护开关;默认关闭。开启后立即请求一组新样本,关闭后立即撤销限制和覆盖,不等待恢复确认,也不改变用户亮度、方向、画面或 `last_mode`。
- 保护模式固定为 `disabled / unavailable / inactive / limiting / critical`。开启但本进程尚无成功样本时为 `unavailable` 且不凭空锁屏;取得过成功样本后 ADC 故障保持最后一次保护并标记 `reading_stale=true`。
- 电压严格大于 `4.8V` 为 `inactive`;`4.5V..4.8V` 为 `limiting`,亮度上限为 `floor(50 + 50 × (V - 4.5) / 0.3)`;严格低于 `4.5V` 为 `critical`。因此 `4.8V` 仍警告且上限 `100%`,`4.5V` 上限 `50%`。
- 更严格的模式或更低上限由一组成功样本立即应用;放宽上限、退出临界或完全解除需要连续两组成功样本确认。读取失败取消待确认的放宽并保持最后保护,不另加电压滞回。
- `limiting` 时实际亮度为 `min(用户亮度或开机笑脸的 50%, 亮度上限)`。顶栏显示“电压过低,屏幕亮度限制”,亮度设置下方始终显示“当前允许的最高亮度:N%”;即使用户值已更低,该提示仍保留到保护恢复。
- `critical` 时用户内容仍可更新和保存,但输出由黑底红色低电图标覆盖并固定为 `35%`;图标继承用户方向。顶栏显示“电压过低,屏幕已经禁用,请充电”,亮度区说明用户画面已禁用且图标固定 `35%`。恢复后自动显示临界期间保存的最新用户内容。该亮度来自实屏保护膜下红色可视性复测,不能回写用户亮度。
- 页面仍以 `1..100` 保存用户亮度,不修改滑块 `max`。保护状态渲染必须先于亮度请求的重复值提前返回;告警只在模式或文案变化时进行一次无障碍播报,不在每轮状态轮询重复朗读。
- 临时纯色测试活动时,系统设置页仍允许保存用户亮度,但成功反馈必须明确显示“用户亮度已保存,但当前由测试亮度覆盖”;不得把临时测试亮度回写用户配置,也不得用看似普通的“已生效”文案造成设置失效的误解。
### 1.3 自启动(`DEPLOY-SYSTEMD`)
核桃派部署时用 systemd 启动服务:
- 服务名建议:`matrix-screen-controller.service`
- 启动目标:`multi-user.target`
- 失败策略:`Restart=on-failure`
- 启动命令调用项目虚拟环境里的 Python 或 uvicorn。
服务启动时在 HTTP 服务进入可用状态前解析并显示 `CONFIG-DEFAULT-CONTENT`。默认静态模板按当前 scene 渲染,默认动图以当前记录快照无限播放;两者继承用户方向和亮度,不算用户改屏,也不取消开机 WiFi 提示。旧开机笑脸不再作为独立覆盖层启动;未配置的 v5 设备迁移后默认引用内容完全相同的“演示动图”。同一 boot ID 内 systemd 重启也必须重新恢复默认内容,因为动画播放只属于进程内快照。
服务与 `NetworkManager.service` 排序但不得等待 `network-online.target`。`ExecStartPre` 的 `check --service` 只要求 NetworkManager 已启动,不得因为冷启动时默认路由尚未生成而失败或依赖 systemd 重试;人工专用主机检查和服务就绪后的远端健康检查仍必须要求默认路由存在。HTTP 服务启动后在后台尝试连接受管 WiFi,开机提示截止时间和“本次开机已取消”状态保存到运行根;systemd 重启沿用同一 boot ID 的原截止时间,断电重启才创建新会话。成功访问首页或成功改屏会取消本次开机的 WiFi 提示,之后断网也不得重新弹出。WiFi 提示只覆盖默认或用户内容,不停止其动图线程;首页仅在该提示活动时撤销覆盖并恢复底层内容,不得把其他客户端刚设置的画面强制重置为默认。
示例结构仅作为后续实现参考:
```ini
[Unit]
Description=Matrix Screen Controller
After=NetworkManager.service
Wants=NetworkManager.service
[Service]
Type=simple
WorkingDirectory=/opt/matrix-screen-controller
ExecStart=/opt/matrix-screen-controller/.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8080
StateDirectory=matrix-screen-controller
StateDirectoryMode=0711
RuntimeDirectory=matrix-screen-controller
RuntimeDirectoryMode=0750
Environment=MATRIX_DATA_DIR=/var/lib/matrix-screen-controller
Environment=MATRIX_RUNTIME_DIR=/run/matrix-screen-controller
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
```
由于 `walnutpi-h618-hub75` 需要访问 `/dev/mem`、实时调度与内存锁定,实际部署必须验证 systemd 的 root 运行边界、设备白名单、capability 限制和状态目录写权限。权限优化不得破坏异常禁用 OE 与安全清屏。
### 1.3.1 专用核桃派运行模式(`DEPLOY-DEDICATED-HOST`)
生产核桃派只运行本控制器及其必需的网络、监测和系统服务,必须使用可重复、可检查的专用主机配置:
- 默认启动目标为 `multi-user.target`;LightDM、`display-manager`、Xorg 和桌面会话必须停用并屏蔽。移动控制器接入后允许必要的 BlueZ 与板载蓝牙初始化服务,按 `DEPLOY-MOBILE-BLE` 同步修改专用检查与部署;未完成该实施步骤前不得只手动解除屏蔽交付。
- 板载音频必须通过 boot 配置关闭,`snd_bcm2835` 必须加入模块黑名单且在控制器启动前确认未加载;WirePlumber、PipeWire 及其用户级 socket 必须全局屏蔽,SSH 登录不得重新拉起音频会话。不得通过关闭矩阵硬件脉冲来兼容板载音频。
- Wi-Fi、SSH、Avahi、`/dev/i2c-1`、Unit ADC、电压保护和 swap 必须保留;专用化不得修改无线网络、SSH 凭据、持久数据根或用户配置。
- `scripts/dedicated_host.py check` 只读报告全部专用条件;`apply` 以幂等方式更新 boot 配置、模块黑名单和 systemd 启用状态,但不得自动重启。boot 配置更新必须原子替换、保留无关内容,不创建多代备份。
- systemd 在启动应用前执行只读专用主机检查。任一硬件脉冲前置条件不满足时服务必须明确失败并记录原因,不能静默进入画面不稳定的软件脉冲模式。
- 未来需要声音时优先使用不加载 `snd_bcm2835` 的 USB 音频;恢复桌面或增加蓝牙必须同步需求、专用脚本,并检查刷新率、资源负载和输出帧。普通前端/BLE 入口改动不强制实屏视觉验收;仅驱动、扫描时序、底层输出改变或具体异常确需视觉证据时,先暂停询问用户如何启动,获明确指示后检查。不得自动启动摄像头、DroidCam、采集或视觉程序,也不得主动切换实屏测试图案。
多语言文字部署还必须满足 `DEPLOY-FONTS`:
- Debian/核桃派安装 `fontconfig`、`fonts-noto-core` 和 `fonts-noto-cjk`;默认字体目录排除彩色 Emoji 与未登记的符号字体,并对允许的 Unicode 范围做确定性校验。
- Python 环境安装与 `requirements.txt` 一致的 FontTools,用于读取字体 cmap 覆盖范围。
- 部署、systemd 权限收紧和清理不得破坏 `/usr/share/fonts` 的读取或删除系统字体包。
- 服务代码或字体依赖更新后必须重启服务,并在远端用一个多语言透明层请求确认 Pillow、Raqm、FontTools 和 Fontconfig 链路可用。
### 1.3.1 更新与数据根迁移(`DEPLOY-UPDATE`、`CONFIG-DATA-LIFECYCLE`)
- `/opt/matrix-screen-controller` 只保存可以由同版本更新包完整替换的程序、虚拟环境和部署文件;生产配置、校准、模板、上传图片、预设和未来用户资源只能位于 `/var/lib/matrix-screen-controller`。
- 手工部署和浏览器 OTA 使用同一保护边界:载荷不得包含持久根,也不得以包内默认文件覆盖设备现有数据。更新器先克隆当前持久根,在副本上执行新版本登记的单向迁移;只有迁移、构建和测试全部成功后才切换。失败时恢复更新前原件。
- 从旧 `/opt/matrix-screen-controller/data` 首次迁移时,必须先停服并运行 `scripts/migrate_state_root.py`。工具把除已登记运行期产物和共享写入器精确命名的事务临时文件之外的全部文件复制到目标旁暂存目录,按相对路径、大小和 SHA-256 复核后再切换到目标;未知持久文件即使名称含 `.tmp` 也必须保留。工具永不删除旧源目录,旧源与目标都没有可核验持久文件时必须报错,不能把空目录当成迁移成功。
- 旧数据根和新数据根同时包含数据时,仅当两边清单完全相同才视为已经准备完成;任一差异都必须中止,禁止静默合并、覆盖或选择较新文件。
- 部署固定顺序为:远端健康检查和持久数据基线 → 停止服务 → 准备并核验新数据根 → 部署程序与 systemd → 启动并完成 schema 迁移 → 冒烟和业务数据逐项比对 → 再次重启验证 → 删除已确认无用的旧数据根、迁移暂存和单份事务备份。
- 任一步失败都要停止新服务,保持旧源和未修改的持久原件可用于回退;不能通过删除损坏文件、生成默认配置或减少模板数量让服务勉强启动。
- `scripts/update_walnutpi.sh` 必须保留为日常开发的 SSH 手工更新入口;它与 OTA 工作服务共用版本化 release、离线 wheel、原生编译、自动化测试、数据迁移副本、原子 current 指针和健康检查实现。普通源码更新载荷不得为运行与测试重新携带预编译内核、固定内核源码或镜像;更新器明确标记源码轻量测试且这两类材料均不存在时,只跳过已登记大型镜像材料的完整性用例,本机完整项目仍必须执行该用例,材料只缺一类时仍失败。
- 新版本启动后必须轮询真实 `/api/status`,确认服务、硬件映射和生产 OE 后端均就绪;启动或健康检查失败时自动停止新版本、恢复旧 `/opt` 与旧 unit、重新启动旧服务并报告回滚结果。成功后只清理本次明确创建的 staging 和旧程序备份,不清理持久根。
### 1.3.2 浏览器全量 OTA(`WEB-OTA`、`CONFIG-OTA-STATE`、`DEPLOY-RELEASE-VERSION`)
- `DEPLOY-OTA-RUNTIME-LIFECYCLE`:主服务必须使用 `RuntimeDirectoryPreserve=yes`;`restart` 不能保护 OTA 的单独 stop。旧更新器升级时,候选迁移入口必须在停旧服务前安装、重载并核验运行期保护 drop-in,保护请求、日志和进度直到成功或回滚结束。运行数据仍由整机重启清空;临时 drop-in 按事务登记清理,不覆盖用户配置。必须使用真实 systemd unit 复现和验证生命周期,mock 不能代替验收。
- `TEST-OTA-LOG-RESILIENCE`:诊断目录意外丢失时尝试恢复日志;写入失败保留有限缓冲并输出不含配置或凭据的降级提示,不能阻止回滚或覆盖原始异常。恢复完成不代表升级成功,失败状态与原因必须保留。
- OTA 当前版本修复必须经用户明确授权,归档旧产物和记录;实际验收的候选包按摘要原样晋升,原子替换当前产物与记录,失败不改写原件、不推进版本。本次 1.1.0 必经节点修复适用此流程。
- `DEPLOY-OTA-OFFLINE-CLOSURE`:导出 OTA 前必须核对设备端直接 Python 依赖的离线 wheel、固定版本及 SHA-256 清单;候选包的实际载荷还必须通过 Debian 12/AArch64/Python 3.11 无网络依赖解析和隔离安装。电脑端测试工具依赖不得误入设备端 `requirements-dev.txt`;板端完整 pytest 所需夹具必须随设备源码携带,不能引用 OTA 载荷外的移动工程。缺件或解析失败时不得发布或推进版本。
- `DEPLOY-OTA-CANDIDATE`:新 OTA 先导出到正式目录外,候选阶段不修改 `VERSION` 或发布记录。真实设备试装报告绑定候选 SHA-256、目标版本、安装健康与恢复结果;晋升时逐字节复核候选、当前源码、离线依赖和报告,再把同一字节包原子发布。任何失败不得占用版本或留下半成品;已登记的坏包保留原字节并在其 README 明确警示。
- `DEPLOY-OTA-CHECKPOINT`:从 1.1.0 起,新增或变更系统软件依赖须增加 minor 并归零 patch;每个 `.0` 是软件安装包且逐个必经。同系列补丁允许跳跃;跨多个系列必须先安装下一个 `.0`,即使调试设备已装依赖也不能跳过版本节点。升级策略与依赖摘要保存在源码,不能引用历史导出目录。
- `DEPLOY-OTA-COMPONENT-TRANSACTION`:1.1.0 兼容旧 v1 包协议,依赖放在 software 内;后续包用 v2 阻止正式旧更新器越级。旧 worker 的候选迁移入口在实际 pytest 通过后执行组件事务,独立监护与开机恢复处理失败/中断。frpc 完整匹配则跳过,否则离线修复;保留配置、选择项、权限和启停状态,首次默认关闭。普通补丁不携带系统二进制,缺失依赖时拒绝更新并提示修复。维护账户必须可靠确定,不依赖 OTA 环境的 SUDO_USER。
- 正式版本使用源码根 `VERSION` 中严格的三段数字版本;最新功能更新时间使用源码根 `FEATURE_UPDATED_AT` 中精确到分钟且固定为 `+08:00` 的时间。`GET /api/status` 的 `service` 同时返回 `software_version`、`feature_updated_at` 与前端内容摘要 `app_version`,`GET /api/ota/status` 也返回相同的 `feature_updated_at`。目标 OTA 版本必须严格大于当前版本,自动失败回滚不算用户降级。
- 普通功能开发、修复以及通过 SSH 进行的开发部署不得改动 `VERSION`;每次可部署的功能新增或优化必须把 `FEATURE_UPDATED_AT` 更新为固定北京时间,同一份源码部署到不同设备时值必须相同。只有用户明确要求导出 OTA 或可刷镜像时才推进两者共用的正式软件版本;单纯导出、重复部署和文档修改不得改写功能时间。OTA 继续遵守必经 minor `.0` 软件安装节点;IMG 自带完整系统软件与离线依赖,不应用 OTA 跳转门禁,但新版本仍须严格大于当前版本且默认只增加补丁位。新 IMG 必须先构建到正式目录外的候选目录,静态校验和摘要绑定的真实 TF 卡正向验收全部成功后才允许把同一字节候选原子晋升、登记并推进 `VERSION`;候选失败不占号。已经登记但经真实首启证明不可用的当前版本镜像,只有在用户明确授权同版本修复时才能从未修改官方基线完整重建;修复必须复用原配置区,完整校验后原子替换目录和原发布记录,不追加重复版本、不修改 `VERSION`,失败时旧目录、记录和版本逐字节不变。
- 更新包是 `matrix-screen-controller-<version>.ota`,只含 `manifest.json` 与完整载荷。载荷包含程序、部署文件、测试和 AArch64 离线 wheelhouse;包不联网、不做增量、不加密、不签名。SHA-256 只用于传输和内容完整性校验。
- 包必须校验产品标识、格式版本、版本、文件名、摘要、压缩前后大小和归档路径;上传上限 `256 MiB`,载荷展开上限 `1 GiB`。路径穿越、未知条目、错误产品、摘要不符、同版、旧版或空间不足均在停服前拒绝。
- `POST /api/ota/update?filename=...` 只接受 `application/octet-stream` 流式上传并返回 `202` 任务;`GET /api/ota/status` 返回当前版本、固定功能更新时间、上限、活动任务阶段/百分比和最近结果。一次只允许一个上传或更新任务。
- 包完成校验后立即冻结普通写接口且不允许取消;独立 systemd oneshot 工作服务继续处理,浏览器关闭不影响更新。页面分别显示上传百分比和安装阶段,服务切换期间的请求失败按预期断线处理,恢复后自动刷新到新静态资源。设置页常态只显示正式版本、格式化为 `YYYY-MM-DD HH:MM(北京时间)` 的功能更新时间、导入按钮和断电警告;最近结果继续由后端保存但不在页面展示。更新成功后隐藏完成进度条和完成提示,失败进度与错误原因继续显示。
- OTA 与 SSH 手工更新运行 Python 门槛时必须把应用数据、运行数据、pytest `tmp_path` 和通用临时文件全部限制在本次事务目录,显式设置 `MATRIX_TEST_ROOT`、`TMPDIR` 与 pytest `--basetemp`,并禁用 pytest cache;不得依赖或污染系统默认 `/tmp`。新源码的 conftest 还必须兼容旧更新器只传入 `MATRIX_DATA_DIR`、`MATRIX_RUNTIME_DIR` 和 `MATRIX_SOURCE_ONLY_UPDATE_TESTS=1` 的调用方式。除发布记录中已经固化且不得覆盖的 1.0.6 一次性诊断引导包外,所有当前源码和后续 OTA 都必须实际执行完整 pytest,任一失败继续阻止切换并触发原子回滚;不得保留诊断标记、成功早退或其他测试绕过入口。
- OTA worker 必须把包校验、离线依赖、venv、原生编译、pytest、专用主机检查、迁移、切换、健康检查和回滚的阶段时间、安全主机摘要、命令、退出码及 stdout/stderr 写入单个运行期日志。日志不得读取或记录账户密码、IP、SSID、配置内容、用户资源内容或完整环境变量。失败时以耐久原子写入只保留最近一份、最多 `1 MiB` 的日志,超限时保留头部和最新尾部并标明截断;下一次成功更新删除旧失败日志。
- `GET /api/ota/status` 返回最近失败日志是否可用及字节数;`GET /api/ota/failure-log` 只在最近结果为失败且日志有效时以无缓存 UTF-8 纯文本返回,否则 `404`。设置页检测到带日志的失败后自动打开“OTA 更新失败日志”弹窗,以纯文本等宽滚动区显示,支持 Clipboard API 和非安全局域网 HTTP 下的 textarea 回退复制;关闭后仍保留查看和复制入口,日志读取失败时继续显示原简短错误且不得打开空弹窗。
- 活动状态原子写入 `/run/matrix-screen-controller/ota-status.json`;持久根 `ota/state.json` 只保留 schema v1 的最近结果和未完成事务恢复信息。不得积累历史包或索引工作区 `发布更新相关/OTA数据包/`。
- 新配置字段必须通过逐级迁移增加预设值。删除功能时,迁移只能删除已登记归属该功能的字段或路径;其余配置、字体、模板、动图和未知文件完整复制。成功后删除旧 release、上传包、数据备份和 staging;失败或中途重启按事务记录恢复旧程序、旧 unit 与旧数据。
### 1.3.3 可刷镜像和离线材料(`DEPLOY-IMAGE-EXPORT`、`DEPLOY-IMAGE-FIRSTBOOT`、`DEPLOY-OFFLINE-DEPS`、`DEPLOY-IMAGE-EDITOR`)
- `发布更新相关/核桃派镜像` 只保存未修改官方镜像。导出器复制后向 FAT16 启动分区只写入摘要元数据、一次性 root 首启程序和固定大小双槽配置区;应用源码、AArch64 wheel 与 Debian 包组成的 `MSCBOOT.TGZ` 由 Linux bootstrap 写入复制镜像根分区 `/opt/matrix-image-bootstrap/app/MSCBOOT.TGZ`,预编译内核写入相邻的 `/opt/matrix-image-bootstrap/axp313a`。固定内核源码只留在电脑端,禁止进入 IMG 或 OTA。
- 镜像元数据格式固定为 v3,记录应用载荷位置、压缩字节数和 SHA-256,以及候选内核 release、压缩/展开字节数、SHA-256、FAT 安装预算和安全余量。IMG 导出必须同时验证预编译载荷及固定源码归档;根分区注入前至少保留“应用压缩载荷 + 内核压缩载荷 + 256 MiB”,注入后重新只读挂载并逐字节核对 FAT v3 元数据、根分区两类载荷和离线依赖元数据。FAT 必须明确不存在 `MSCBOOT.TGZ`。旧 v1/v2 镜像不得原地刷新或迁移,必须从官方基线重建。
- 构建器必须在最终 FAT 布局上按簇取整计算首次安装新增的候选 Image、双份 DTB、System.map、kernel config、受管启动脚本、原始启动脚本回滚副本、临时文件和状态文件,并在这些实际预算之外保留固定 `32 MiB` 安全余量;不满足时在发布前拒绝。内核安装器在写入第一个候选 boot 文件前使用同一算法再次检查当前 `/boot`,不足时保留 rootfs 离线载荷并失败,禁止依靠清除应用载荷换取不可恢复的空间。
- 首启不依赖显示器、键盘、HUB75、ADC 或当前网络在线。它校验并离线安装载荷、编译真实驱动、配置账户和 NetworkManager、生成唯一 machine-id 与 SSH 主机密钥、部署服务,最后安装预编译双内核候选并自动重启一次。镜像配置仍必须包含有效的 WiFi 与 IPv4 字段;首启只写入启用自动连接的受管配置并重启 NetworkManager,不等待 SSID、认证、DHCP 或默认路由,以上任一未就绪都继续离线安装,安装完成后由正常运行服务继续连接并按既有逻辑显示断网提示。NetworkManager 未启动、配置写入失败以及载荷、硬件、权限、SSH 或内核错误仍必须令首启失败。安装内核前根分区至少保留“两倍展开字节数 + 256 MiB”,解包只进入明确的 `/var/tmp/matrix-axp313a-image-install`;成功后删除压缩载荷和展开目录,失败由安装器恢复原 boot 文件、清除部分候选并保留无秘密状态,不联网补包或循环重启。候选启动继续使用一次性健康标记,内核、AXP313A、cpufreq、应用或 GPIO 健康失败时下一次自动回原内核。首启完成时若受管连接或默认路由尚未就绪,FAT 状态仍写成功,但必须明确说明 WiFi 尚未连接且运行系统会继续处理,不得包含 SSID、密码、用户名或地址。
- SSH 是每个正式导出镜像的必需功能:复制镜像的 rootfs 必须已启用 `ssh.service`,首启期间受 `Before=ssh.service` 阻止而不得提前监听,成功重启后使用配置区的账户和密码登录。该账户必须属于 `sudo` 组并通过经 `visudo` 校验的 `ALL=(ALL:ALL) ALL` 规则获得需再次输入同一账户密码的完整权限;必须原子移除基础镜像的 `Defaults rootpw` 和旧 `pi` 免密规则,不得保留或配置 `NOPASSWD`。主机名变更必须同步 `/etc/hosts`,避免 sudo 因本机名解析失败而延迟。受管 sshd 策略固定为允许密码认证、关闭键盘交互和空密码、禁止 root 直接登录;直接运行 `sshd -t/-T` 前必须创建并校验模式为 `0755` 的易失目录 `/run/sshd`,不能依赖尚未启动的 `ssh.service` 代为创建。`sshd -t/-T`、sudoers 或服务启用校验失败都必须令首启失败。首启 unit 排序在控制服务之前,因此部署只能启用控制服务、不得在同一 oneshot 内同步等待其启动;该 unit 不得使用有限启动超时。控制服务和 SSH 必须在首启清理完成后的重启中启动;成功状态只能在秘密、内核载荷和临时文件清理、禁用首启 unit 及全盘同步完成后写入,随后必须成功请求重启。同版本首启若在完整安装移动到 `/opt` 后中断,只能在版本、venv、原生文件、unit 和专用主机检查全部通过时恢复。成功后覆盖并删除卡上明文配置。
- 镜像不得包含当前设备持久数据、网络状态、用户资源、校准、运行数据、主机密钥或项目凭据文件。未修改正式 IMG 故意包含一组可直接使用、通过 schema 校验的默认账户和 DHCP Wi-Fi 配置;正式发布目录 README 必须逐字写明这四项公开默认值、SHA-256 和修改警告,其他 manifest、发布记录、日志及归档不得记录这些值。当前设备和开发机凭据始终不得进入镜像或发布目录。
- 配置 schema v1 固定包含产品、软件版本、账户、WiFi 和 DHCP/静态 IPv4。双槽分别带单调代数、长度和 SHA-256;读取选择最高有效代,保存先完整写入非活动槽并刷新,写入中断必须仍能读取旧槽。
- 编辑器使用不依赖项目外部目录的 Python/PySide6 跨平台源码;Windows 10/11 x64 必须交付内置 Python、Qt 和运行库的唯一绿色单 EXE,其他电脑不得要求安装 Python、.NET、Qt 或补充 DLL。同一源码允许用户在 macOS 本地构建,Windows 发布不得宣称已交付或验收 macOS 产物。编辑器显示镜像版本和当前配置,密码默认遮挡;“修改原镜像”必须二次确认,“另存为”必须使用不同且尚不存在的宿主系统合法 `.img` 文件名,并提示按设备或地点命名。中文、空格和长路径必须受支持;保留名、错误扩展名、同路径、既有目标或无法创建同名摘要时必须在复制或修改前拒绝。未知产品、版本、损坏槽或摘要失败时禁止保存;每次保存以无 BOM UTF-8 生成规范的 `<64 位小写 SHA-256><两个空格><完整文件名><LF>` 侧车文件,按固定字段边界解析并严格复核文件名和镜像内容。摘要必须通过同目录临时文件原子替换,另存失败不得留下 IMG、摘要或临时摘要。
- 编辑器必须声明 Per-Monitor V2 DPI 感知;主窗口、输入区域以及应用内错误、警告、确认和成功提示均按当前显示器 DPI 与工作区自适应缩放,分辨率或显示器变化后保持完整可达。密码输入框与同列普通输入框等宽;低分辨率时允许纵向滚动但不得裁切字段、正文或操作按钮。Windows 原生打开和保存选择器继续使用系统界面。
- `发布更新相关/其他依赖` 是当前软件所需的唯一离线材料清单。新增依赖必须同时提供文件、架构、来源、用途和摘要;停用时删除二进制并记录停用版本与原因。AXP313A 运行载荷必须来自已经通过真实 GPIO 验收的 `6.1.31-matrix-axp313a1`,使用确定性 `tar.gz`,拒绝路径穿越、符号链接、特殊文件、错误 release/commit 和未登记文件;模块的 `build`、`source` 链接不进入载荷,由目标 `depmod` 重建索引。固定源码归档必须与 vendor commit 和补丁摘要同时可离线校验。Debian 包必须相对于登记的官方基础镜像包清单形成可递归解析的完整闭包,镜像构建前自动验证,缺少直接依赖、传递依赖、根包或基准清单时必须拒绝导出。首启 Debian 安装失败时,FAT 状态文件指向不含配置凭据的包诊断与基准包清单,失败后不得继续网络和部署阶段。
- `发布更新相关/导出包/<版本>` 只保存可删除 IMG。历史导出 IMG 缺失默认视为用户主动清理空间,不属于故障,不追补、不自动重建、不影响开发、常规测试或后续导出;任何源码、测试或后续导出不得引用其中内容。工作区 OTA 今后长期保留,不作为缓存清理;已缺失的 1.0.1 是用户接受的历史例外,不再追补、提醒或阻塞使用,原发布记录保留。
### 工作区路径约定(`DEPLOY-WORKSPACE-LAYOUT`)
- 测试入口位于 `测试相关资料/如何测试/本机测试环境/`,发布工具从 `发布更新相关/` 定位离线材料和产物;源码仍位于根目录 `核桃派软件源代码/`。路径须由脚本位置或显式参数解析,支持中文、空格及不同调用目录。
- 工作区搬迁不改变设备安装路径、镜像内部载荷目录或历史包协议;已有发布记录仅同步工作区产物路径,不改版本、摘要及历史验收结果。
- 本机测试与清理必须在隔离工作区验证:能够定位本地源码,保留 `.venv` 和人工持久数据,并拒绝清理越界路径。覆盖见 `TEST-WORKSPACE-LAYOUT`。
- 项目专用 SSH/编辑器 venv、测试 SDK/AVD、临时脚本与私有状态统一保存在本机测试环境 `.local/` 并精确忽略;可复建入口、完整依赖版本/哈希、官方镜像包登记和搭建经验跟踪维护。通用工具可复用,项目专用材料不得污染全局工具目录。复制后重建 venv、刷新 AVD 路径;Android 必需的临时 ASCII 联接只提供入口,数据仍在项目内,清理不得遍历链接目标。覆盖见 `TEST-LOCAL-ENV`。
### Git 仓库安全约定(`DEPLOY-REPOSITORY-HYGIENE`)
- 根 `.gitignore` 必须精确排除真实设备凭据、私钥、本机环境、运行数据和可再生缓存;凭据示例、源码、需求、测试、正式 IMG、OTA、离线依赖、编辑器程序和历史归档仍属于仓库内容。IMG、OTA、离线依赖、编辑器程序及其他已登记的大体积二进制允许通过 Git LFS 保存;LFS 不得改变工作区文件内容或扩大忽略范围。
- 真实调试文件固定为 `测试相关资料/核桃派的用户名和密码和ip/用户名密码ip.txt`,可提交示例为同目录 `用户名密码ip.example.txt`。私有文件缺失时只允许从示例创建副本并停止;字段缺失、为空或仍是占位符时,不得继续 SSH、部署或镜像定制。
- 仓库文本不得固化开发电脑盘符、用户目录、用户名、主机名或旧工作区根;改用仓库相对路径或文字占位符。设备固定部署路径、镜像内部协议路径和测试显式构造的安全夹具不受影响。
- 提交前必须按 Git 实际候选集合检查:真实凭据未入列、示例已入列、无私钥、无高可信明文秘密、无开发机路径。Git LFS 属性属于允许的仓库配置;二进制仍须按 ASCII、UTF-8 和 UTF-16LE 检查开发机标识,命中时从中性路径重建并同步摘要,不得直接改写有摘要保护的产物。
- 项目自有源码使用 GPLv3;第三方镜像、软件包、字体、工具和资料继续服从各自许可与来源条款。仓库根 README 必须说明完整仓库及 Git LFS 对象的克隆成本、客户端要求和第三方许可边界。
## 2. 页面功能
### 2.1 首页布局(`WEB-ENTRY`)
首页直接是控制面板,不做营销式首页。当前提供设备状态和测试、系统设置、像素画布、文字显示、动图管理、模板管理六个工作区,但实现不得假定工作区数量固定。
- 每个工作区声明稳定 `id`、标题、默认排序、持久化策略和进入/退出钩子;侧栏和 URL hash 由注册信息生成。侧栏固定为不带分类标题的单层列表。
- 手机、平板和电脑默认都只显示当前工作区;功能侧栏默认收起,展开时以带遮罩的抽屉覆盖在内容上,不挤压或移动主内容。
- 页面左上角始终保留同一个菜单图标按钮:收起时用于展开侧栏,展开后视觉上位于侧栏左上角并用于关闭;页面刷新或重新进入时仍默认收起,不持久化开合状态。
- 叉号正下方提供紧凑的“自定义项目位置”按钮。点击后按钮变为“保存”,每个项目显示“上移/下移”,首项不能上移、末项不能下移;移动只修改当前页面内存草稿,不发送请求。
- 排序编辑期间工作区项目不得导航,叉号、遮罩和 `Escape` 都不得关闭侧栏,只提示“请先保存项目位置”。不得注册关闭页面自动保存;刷新、关闭浏览器或崩溃直接丢弃本次未保存草稿。
- 保存按钮一次提交完整当前工作区顺序;成功后退出编辑态、保持侧栏打开并显示已保存顺序,失败或页面版本过期时保留草稿和编辑态并允许重试。非编辑态在任意屏幕尺寸选择工作区后侧栏自动关闭,点击遮罩或按 `Escape` 也能关闭。
- 默认工作区为“设备状态和测试”;无效 hash 回落默认工作区,不能导致白屏。
- 已保存顺序中不存在于当前版本的 ID 在渲染时忽略;后续新注册的工作区按默认注册顺序追加,下一次保存时用当前完整序列整理记录。后续新增图片、时钟等功能时,只注册新工作区,不改写现有导航分支。
- 手机控件的主要点击区域建议不小于 `48x48 CSS px`,相邻控件间距不小于 `8px`;页面不得出现非必要横向滚动。
- 抽屉必须维护 `aria-expanded`、`aria-hidden`、当前项状态和键盘焦点;关闭时侧栏内容不可聚焦并把焦点归还菜单按钮,展开时键盘焦点限制在菜单按钮和侧栏可操作项之间。
- 共享画板始终位于当前工作区最上方,脱离左右分栏、带内边距卡片和设置控件;其左右不得放置会压缩或遮挡画板的元素,所有工具和字段排列在画板下方。
- 画板在不引起页面横向滚动的前提下,按内容可用宽度和当前视口可用高度取最大正方形;约 `320px`、手机、平板和桌面视口都必须完整可见且保持 `1:1`。
页面使用移动端单列作为默认布局,较宽屏幕只增强画板下方的设置区域;共享画板仍单独占据一行并保持正方形。
### 2.1.1 正向预览(`WEB-PREVIEW`)
- 普通工作区始终预览完整组合场景,固定使用逻辑方向 `0°`,左上角为 `(0,0)`,不应用配置中的物理方向和底层行映射。
- 编辑变化只更新网页预览;用户点击“应用统一画板”后才把完整场景提交到真实屏幕。
- 像素底层在浏览器本地精确预览;每个文字元素通过 `POST /api/preview/text-layer` 获取透明 RGBA 图层,再由浏览器按场景顺序合成。
- 设备状态、系统设置和模板管理工作区把同一画板切换为只读监视器,通过 `GET /api/display/current-frame` 显示当前设备经过输出仲裁后的逻辑 RGB 帧;临界低电压时返回低电图标而不是被覆盖的用户帧。监视器不得显示或提交浏览器编辑草稿,固定使用逻辑 `0°`,不应用物理方向和底层行映射,最大边长为 `160 CSS px`,并隐藏应用、导出、模板和画板交互操作。设备状态工作区只在画面正上方居中显示小标题“当前屏幕画面”;系统设置和模板管理保留完整监视器上下文文案。
- 设备状态工作区内部不重复显示“设备 / 设备状态和测试”标题;刷新状态位于监视器正下方。屏幕状态只展示方向和亮度,其中亮度区分用户保存值与实际有效值。
- 进入设备状态、系统设置或模板管理时立即读取当前帧,并按 `preview_refresh_interval_ms` 递归刷新;同一页面最多存在一个当前帧请求,较晚返回的旧请求不得覆盖新工作区或较新的画面。页面隐藏或离开全部监视工作区时停止定时器,重新可见时立即刷新。
- 当前帧读取成功后不显示额外状态文字;加载或读取失败时可以临时显示对应状态。离开监视工作区时恢复其他工作区原有预览。
- 预览请求不得更新当前模式、当前逻辑帧、驱动或配置。
- 动图管理工作区把页面顶部切换为两个只读小预览:左侧“当前动图”按当前已打开动图的已保存帧顺序和各帧 `duration_ms` 在浏览器内循环,右侧“当前画板”显示当前统一场景。两者最大边长均为 `160 CSS px`,宽屏并排、窄屏上下排列,保持 `1:1` 且不得产生横向滚动。
- 动图管理预览不得调用播放或改屏接口;未保存的动图帧修改只反映在右侧当前画板,保存并刷新后才进入左侧循环。未打开动图、空动图或缩略图失败时显示黑色占位和明确说明;切换、刷新、删除、离开工作区或页面隐藏时必须终止旧预览计时器。
- 动图管理顶部不显示“应用统一画板”“导出合成 PNG”、保存模板或画板交互层;离开该工作区后按目标工作区恢复原有画板能力。
### 2.1.2 统一场景合成(`WEB-COMPOSITION`)
`WEB-LAYERS`:scene v2 使用有序画笔/文字图层,至少一个画笔层;每个文字层包含多个文字元素。画图页清空按钮下提供新建、删除、复制、命名、上移和下移,移除独立文字导航。默认图层名称依次编号,新建及复制置顶。画笔空白透出下层,黑色笔迹遮挡;各画笔层可独立开启192×192背景,橡皮和清空仅移除笔迹,文字层无背景。
`WEB-EXTENDED-CANVAS`:素材和编辑边界为192×192(-64..127),输出仅中央64×64(0..63)。编辑小圆球提供默认关闭、本浏览器记忆的扩展显示开关及移动当前层入口;移动提供X/Y输入、各轴±1/±10步进和拖动,偏移限于-64..64,保留视线平移缩放。显示扩展区或移动时标注实际屏幕边框。
`WEB-LAYERS` UI验收:画笔/背景颜色使用对齐的标题行和颜色按钮,桌面并列、窄屏上下排列;背景开关及复制目标帧复选框不得继承普通文本输入框的全宽与48px高度。标签整体可点选,长帧名称换行。复制及脏目标确认弹窗使用不透明表面背景,列表独立滚动,窄屏无横向溢出且操作按钮可达。
`WEB-EXTENDED-CANVAS` 模式退出:偏移面板顶部提供“退出图层移动”,切换到视线模式后仍可操作。退出仅返回当前画笔/文字编辑,保留全屏、扩展显示及视线位置,隐藏偏移面板;已提交位移保留,未提交拖动预览取消,不增减撤销历史。图层移动时“继续编辑”必须可用,不能误提示已经绘画。
`WEB-LAYER-OVERFLOW`:移动整层或文字后,有非背景可见笔迹超出192边界则在提交前确认;取消恢复,确定清除越界笔迹,部分文字累计裁切、整段越界删除。裁切随对象移动/缩放并持久化,移回不恢复;操作可作为一次会话撤销,最多50步,提交成功清空。背景及仅超出实际64屏幕不触发警告。
`CONFIG-SCENE-V2`:旧场景迁移为一个画笔层,有文字时额外一个文字层,中央输出保持一致。模板schema v2、动图schema v3、配置v11移除text导航;坏数据/未知字段/未来版本保全。画笔RGBA以PNG压缩持久化。导入静态一个画笔层,动图/视频逐帧一个画笔层。复制当前画板或已有模板/动图选定帧,副本独立并置顶;跨内容追加使用If-Match,批量动图索引一次提交,失败原帧不变。文字预览可选192尺寸,默认64;驱动RGB888及手机协议不变。
- 场景模型不得依赖 DOM,固定包含一个 `64x64 RGB888` 像素底层和一个有序 `elements` 数组;数组从前到后绘制,后面的元素覆盖前面的元素。
- 画笔运行时使用192×192 RGBA,持久化为压缩PNG;文字渲染使用192×192透明层,并在最终合成时取中央区域。透明区域露出下层,黑色笔迹遮挡下层。
- 新建或复制图层追加到末尾、置顶并选中;图层可上下排序,重排不改名称。默认名称取当前未占用的最小图层编号。
- 每个元素具有稳定 `id`,运行时另有递增修订号。透明层缓存以元素 `id + revision` 识别,旧请求晚于新请求返回时必须丢弃,不能覆盖当前元素;全场景元素ID唯一,所属层和裁切参与签名;修订号不持久化。
- 元素存在待渲染或渲染失败状态时,页面必须明确提示并禁用“应用统一画板”,不得提交缺少该元素的不完整帧。
- 场景最终合成为单张 `64x64 RGB` 帧后再交给 `DisplayService`;显示底层不保存、选择或编辑单个元素。
- 后续动画、图片、时钟、天气等普通内容功能必须注册为新的元素或图层提供者,并参与同一有序合成;不得新增直接覆盖屏幕且无法与现有内容叠加的普通显示路径。
- 纯色、诊断图案和清屏属于设备测试例外:它们独占覆盖完整帧,但不得修改或清空浏览器场景草稿。
### 2.1.3 模板管理(`WEB-TEMPLATES`、`WEB-DEMO-TEMPLATES`)
- 工作区注册信息可以声明“支持保存场景模板”;画图工作区声明该能力,后续内容工作区声明后自动出现同一个“保存为模板”入口。
- 保存内容必须是完整、可编辑的 scene v2,保留图层、偏移、PNG笔迹、背景、文字裁切和稳定ID;不得只保存合成 PNG,也不得保存运行时 revision、选择、请求状态或图层缓存。
- 保存时输入 `1..80` 个字符的模板名称;忽略首尾空白并按 Unicode 大小写不敏感规则保持唯一,重名不得覆盖已有模板。
- 从动图帧编辑上下文点击“保存为模板”时,名称弹窗必须在“保存为模板”标题右侧显示“当前帧将被保存为一个静态图模板”;普通场景保存以及共享弹窗用于新建、重命名时不得显示或残留该提示。
- 左侧栏新增“模板管理”。模板使用购物软件式响应式卡片网格:正方形缩略图在上、名称在下,并显示占用空间;手机默认两列,宽屏自动增加列数。静态模板和动图统一使用两列四行操作:第一行“播放、编辑”,第二行“复制、重命名”,第三行横跨两列显示“删除”,第四行“上移、下移”。默认内容选择模式可以在上述常态操作后额外显示单列“设为默认”。
- 静态模板缩略图必须由核桃派后端按场景顺序生成原生 `64x64 PNG`;动图卡片复用各帧同规格 PNG,按完整帧顺序和原始 `duration_ms` 在普通 `<img>` 中切换,不得抽帧、截断或在浏览器重新合成场景。动态缩略图只在当前可见工作区和视口内运行,页面隐藏、离开视口、启用减少动态效果或加载失败时必须安全暂停或回退,不得影响真实屏幕输出。
- 静态模板“播放”直接显示该模板并结束活动动图,但不得替换当前浏览器草稿或修改默认显示引用;动图“播放”保持相同的编辑解耦语义。静态模板“编辑”才读取完整 scene 并进入像素画布,且不得自动更新真实屏幕。
- 普通画布记录当前绑定模板、修订值和最近保存的 scene 基线。未绑定的新草稿,或绑定模板后相对基线发生修改,均视为未保存;静态编辑将替换该画布时必须提示“当前内容将会丢失”,取消不得读取或切换目标。干净的已保存模板可以直接切换。动图帧编辑继续使用独立的未保存修改守卫,并完整保存、恢复普通画布及其模板编辑状态。
- `WEB-DEFAULT-DISPLAY`、`WEB-TEMPLATES`:浏览器识别新的设备开机会话后载入默认静态模板及其编辑绑定;同次开机刷新恢复本地草稿和对应绑定,保存可以覆盖当前模板或另存。两个演示案例均提供保存选择,“保存到当前模板”为黑色受限态,只能另存;演示动图以第一帧作为可另存的静态编辑画面,来源仍为只读动图,不能误写静态模板 API。用户动图沿用现有逐帧编辑保存流程。绑定信息独立于 scene v2 和设备配置,保留原修订与基线以识别未保存修改和并发冲突;读取失败、草稿损坏或初始化期间发生编辑不得覆盖现有草稿。
- 编辑普通静态模板时,页面顶部提供单个“保存模板”入口,点击后选择“保存到当前模板”“另存为新模板”或取消。保存当前模板保留 ID、名称和创建时间并更新修改时间、内容摘要、缩略图及修订值;另存生成独立 ID 和唯一名称并把编辑上下文绑定到新模板。普通未绑定草稿沿用“保存为模板”;演示静态图的当前模板保存受限,只能另存。保存成功更新基线,失败或冲突保留当前画布和未保存状态。
- 删除需要确认,并删除模板及其所有有效、过期缩略图。页面同时显示单模板占用、模板总占用和模板目录所在文件系统的总/已用/可用空间。
- 模板管理固定把两个随程序发布的只读演示案例放在所有用户内容之前:第一个为“演示静态图”,内容与开机笑脸睁眼帧完全相同;第二个为“演示动图”,按开机笑脸的睁眼 `1300ms`、闭眼 `200ms` 两帧无限循环。二者来自可替换程序资源,不写入、不迁移也不计入用户模板持久空间。
- 两个演示案例永久锁定为模板管理第 1、2 项,不可作为拖动来源或放置目标,也不进入用户顺序记录,但仍显示黑色受限态的“上移、下移”以保持卡片布局一致。其后的用户静态模板和用户动图组成一个可跨类型混排的序列,支持拖动及“上移/下移”;第一张用户卡不得移动到演示案例之前。新建或复制的用户内容追加到已有自定义顺序末尾;没有自定义顺序时保持“用户静态模板、用户动图”及各自当前 API 顺序。
- 演示静态图允许播放、进入像素画布编辑和复制,但不得保存回演示原件;演示动图允许播放和深复制,但编辑不可用。两者的重命名、删除和移动均不可用,也不得把演示动图作为追加帧的目标。上述不可用按钮保持可聚焦/可点击并显示黑色受限态,桌面悬停或键盘聚焦展示原因,手机点击及桌面点击统一提示“演示案例不得编辑,请复制”;移动按钮使用固定顺序原因。前端隐藏或限制不是安全边界,相关写 API 也必须拒绝修改演示案例。
- 从任一演示案例复制出的静态模板或动图必须取得新的普通 UUID、独立 scene/帧记录和唯一副本名称;复制品与用户新建内容完全相同,可以编辑、重命名、再次复制和删除,后续开机笑脸变化也不得改写既有复制品。
- 标题右侧把“设置默认显示内容”放在“刷新”左边。按钮以 `aria-pressed` 进入选择模式;所有演示案例、用户静态模板和非空用户动图提供“设为默认”入口,空动图保留可触发的不可用说明。当前默认卡始终有醒目标记;保存时使用明确忙碌状态,成功后立即显示所选内容、退出选择模式并刷新标记,失败时保留原默认和原画面。
- 默认引用按稳定 ID 保存。重命名不改变引用,内容更新后下一次默认激活读取最新版。删除当前默认模板或把当前默认动图删空时,必须先切换并持久保存“演示动图”再完成删除;删除失败恢复原默认。服务启动发现引用不存在、类型不匹配或动图为空时同样原子修复为演示动图。
### 2.1.4 动图管理(`WEB-ANIMATIONS`、`CONFIG-ANIMATIONS`)
- 单层侧栏中动图管理的默认位置在媒体转换之后、模板管理之前;保存自定义工作区顺序后允许改变该位置。一个动图是带稳定 UUID、名称和修订值的文件夹,内部保存按顺序排列的完整 scene v2 帧。
- 动图管理页面内容顺序固定为“新建动图及从当前画板新建帧操作行、当前打开动图的帧卡片和单帧时长区、已有动图列表”。用户界面统一使用“动图”称呼;持久实现仍可把一个动图保存为带稳定 UUID、名称和修订值的文件夹。
- 已有动图卡片在名称上方显示正方形动态缩略图。名称固定单行尾部省略,任何中文、英文或无空格长名称都不得撑宽网格;完整名称只在重命名输入框以及确实发生截断时的细指针鼠标悬停提示中显示,触摸、点击和长按不得展开名称。
- 服务启动时对动图元数据、scene 与缩略图完成严格校验后,必须复用同一批已校验记录建立首次列表缓存;首次 `GET /api/animations` 不得再次遍历和校验全部帧文件。任一动图写入后缓存失效并按当前持久数据重建,REST 响应字段和错误语义不变。
- 新建帧复制当前统一画板;每帧保存稳定 UUID、可选自定义名称和 `50..604800000ms` 播放时长,默认 `500ms`,最长七天。名称为空时页面按当前顺序派生“动图名称-第N帧”,因此动图重命名或帧排序后自动名称随之更新;用户设置的 `1..80` 字符自定义名称保持不变,清空名称恢复自动名称。
- 帧卡片从上到下按“双色当前/总帧数与多选框、有效名称、重命名、显示时间与复制到已选帧、时长输入与单位切换、正方形缩略图、编辑此帧、次级操作”排列。每张卡独立在当前页面会话内切换秒或毫秒;毫秒只接受 `50..604800000` 的整数,秒只接受 `0.050..604800` 且最多三位小数,单位切换只精确换算显示值、不提交或持久化。缩略图固定 `1:1`,不能拉伸卡片或遮蔽操作。
- 时长输入在用户键入过程中不校验、不提交,只在失焦、按 Enter 或 `change` 时校验;非法值保留并显示字段错误,不调用 API。时长数字必须能用鼠标或触摸长按选择且不得触发拖动,全部交互控件及其 `8 CSS px` 保护区遵守 `WEB-INTERACTION-FEEDBACK`。
- 帧列表上方固定提供批量复制、批量移动和批量删除。勾选任意帧即进入选择模式:复选框、单位、时长、复制到已选帧和三个批量入口保持可用,拖动、编辑、重命名、上下移、单帧复制/删除以及切换动图或上下文的操作显示明确不可用原因。修改任意时长或复制时长到已选帧前必须二次确认;取消不发送请求并恢复发起输入,确认后原子应用到全部已选帧。批量时长成功后保留选择;复制、移动或删除成功后清空选择。
- 批量移动仅限当前动图,保持所选帧当前相对顺序,只能在弹窗中选择“插入到最前”或某个未选帧之后;已选帧在目标下拉中以黑色不可用项显示。批量复制先选择当前或其他普通动图,再复用相同位置选择,但复制目标不因来源已选而禁用。普通单帧复制为动图时也必须选择插入位置,复制为静态模板时保持现有流程。批量删除二次确认后允许留下空动图。
- 点击“编辑此帧”进入独立编辑上下文,普通浏览器 scene 草稿保持不变。画图工作区内的画笔和文字图层共同编辑该帧,并在“合成预览”右侧只显示一处编辑注释和已保存/尚未保存状态;自动帧为“动图名-第N帧”,自定义帧为“动图名-自定义帧名”,不得重复动图名。该注释不得出现在其他工作区;正在编辑的帧卡片同步高亮,顶部主按钮改为“保存当前动图帧”,保存不改变实屏,保存后继续编辑并刷新缩略图。
- 切换帧、打开其他动图、退出动图编辑、载入模板或外部浏览器草稿以及其他会替换当前画板的操作,必须先经过同一个未保存修改守卫。存在修改时提供“应用修改、否并放弃修改、取消”;保存失败或修订冲突必须取消后续操作并保留本页画面。刷新或关闭页面时仍有未保存帧修改则触发浏览器离开提示。
- 模板管理同时显示带“静态”或“动图”标记的卡片。动图卡片先显示第一帧,再在完整时间表就绪后按全部帧和原时长循环,并显示帧数、总时长和空间;空动图显示明确占位且不可播放。静态模板和单帧复制必须弹出目标选择,可深复制为静态模板或追加到指定动图。
- 动图卡片提供整项深复制;普通动图与演示动图复制后都生成包含全体帧、顺序、名称和时长的独立普通动图。
- `POST /api/animations/{id}/play` 成功后由核桃派后台无限循环,关闭网页不停止。播放使用请求时的不可变快照,后续保存需重新播放才生效;页面显示正在播放及旧版本提示。不提供独立停止按钮,其他成功改屏内容或另一动图负责替换播放。
- 删除正在播放的动图返回冲突;动图重命名和编辑不热更新现有播放快照。动图名称只在动图类型内按 Unicode 大小写不敏感唯一。
### 2.2 全屏纯色测试(`WEB-FILL`)
目标:快速确认屏幕供电、颜色通道、方向和底层刷新是否正常。
页面功能:
- 固定按钮为全黑、全红、全绿、全蓝、全白,另有“自定义纯色”;页面初始不选中任何颜色。
- 点击任意颜色立即启动设备级测试会话;新会话名义亮度固定为 `50%`,同一会话切换颜色保留用户刚调节的测试亮度,退出后再次进入才重置为 `50%`。
- 固定色同步更新颜色框;颜色框可打开 RGB/HSV/HEX 只读详情,但不得修改、收藏或确认新颜色。自定义纯色打开完整颜色面板,确认后立即应用并把规范颜色保存到 `custom_test_color`,取消时继续使用原保存值。
- 测试活动时显示独立亮度条、低电压保护说明和“退出测试”;测试亮度限制 `1..100`,仅影响当前测试,不写入用户亮度。临界低电压图标和限亮策略始终高于测试覆盖层。
- 测试会话跨页面切换、刷新和重新打开网页保持;手动退出恢复底层用户内容、动画、方向和保存亮度。其他成功改屏操作自动结束测试,避免新内容被覆盖。
- 纯色测试不得修改像素底层、场景元素、用户逻辑帧、`last_mode` 或用户保存亮度。诊断后端继续保留,但设备状态网页不展示诊断、运行所选测试或清屏入口。
后端行为:
- 接收颜色。
- 校验颜色格式必须是 `#RRGGBB` 或 `{r,g,b}`。
- 生成 `64x64 RGB` 纯色帧。
- 经过当前方向变换后提交给屏幕底层。
- 更新 `last_mode = "fill"`。
验收标准:
- 点击红绿蓝时,屏幕颜色通道和按钮含义一致。
- 全白不能默认长时间满亮度压力测试,第一版可以限制亮度或提示只短时间测试。
- 如果底层屏幕不可用,接口返回明确错误,前端显示失败状态。
### 2.3 像素画布(`WEB-CANVAS`)
目标:允许用户像画画一样编辑统一场景最底部的 `64 x 64` 像素层。
前端功能:
- 使用页面顶部唯一的共享 `64 x 64` Canvas,不在像素工作区另建预览。
- 每个逻辑像素对应屏幕一个 LED 像素。
- 支持画笔、橡皮、一次性吸管、清空像素层、填充像素层背景色;这些操作只改变像素层,不改变文字或其他元素。
- 普通模式下,画笔和橡皮共用 `1..16` 个逻辑像素的圆形笔尖,提供连续调节、`1/2/4/8` 快捷档和圆形笔尖预览。
- “笔尖粗细”下提供“单像素编辑”同步切换按钮。开启后画笔和橡皮改为 `1x1..4x4` 逻辑像素的正方形笔尖,连续调节范围和四个快捷档均为 `1/2/3/4`,预览与数值按 `N × N` 显示;关闭后恢复普通模式上次使用的尺寸。
- 单像素编辑模式开启时,共享画板上方显示精确 `64x64` 的低透明度对比网格。网格是 `pointer-events:none` 的网页辅助层,仅在像素画布工作区显示,不得进入场景像素、模板、导出 PNG 或 WebSocket 帧;进入其他工作区时隐藏,返回像素画布时按已保存模式恢复。
- 模式开关使用 `aria-pressed`,进入后在按钮附近持续提示“画布已按 64×64 方形像素分格,当前笔尖为 N×N”,并通过全局状态区播报进入或退出结果。
- 支持颜色选择。
- 支持鼠标拖拽绘制。
- 支持触摸绘制。
- 快速触摸移动时连接相邻采样点,避免手指移动过快产生断线。
- 共享画板在像素工作区默认只读,触摸不会落笔或阻止页面滚动;颜色和画笔工具区提供明确的“开始编辑”按钮,点击后使用现有共享画板进入 CSS 全视口专注编辑,不调用浏览器 Fullscreen API。
- 专注编辑默认进入绘画模式并锁定页面滚动;退出时完成已改变的当前笔画、复位视角并回到颜色和画笔工具区。离开像素工作区也必须先安全退出。文字工作区的画板选择、拖动和缩放行为不受该状态影响。
- 专注编辑内持续显示类似 AssistiveTouch 的圆形悬浮按钮。按钮初始位于左侧中部,可在安全区内拖动且不得产生画笔输入;点击展开当前模式提示、撤销、退出编辑、移动画布视线和继续编辑,菜单按剩余空间调整展开方向并维护键盘焦点、`aria-expanded` 和不可用原因。
- 悬浮菜单提供独立的“显示坐标系/隐藏坐标系”同步开关并使用 `aria-pressed`。坐标系默认关闭,选择按浏览器本地工具状态记忆;它只在像素画布的全屏专注编辑中显示,退出编辑或离开工作区时隐藏但不清除选择。
- 坐标系是 `pointer-events:none` 的高 DPI 网页辅助层。在画板可见区域内侧的顶部和左侧显示约 `24 CSS px` 的半透明标尺、`0..63` 坐标数字和刻度,主刻度以低透明度细线贯穿画板;坐标始终以左上角为 `(0,0)`,不得改变绘画命中区域或阻止任何画布手势。
- 坐标主刻度根据变换后单个逻辑像素的 CSS 尺寸,从 `1/2/4/8/16/32` 逻辑像素间隔中选取最接近约 `48 CSS px` 的档位。平移、捏合、滚轮缩放、视口或设备像素比变化后必须重新对齐;单像素模式的精确 `64x64` 网格保持原样并可与坐标主线同时显示。
- 坐标系不得写入场景、模板、动图、设备配置或显示帧,也不得出现在导出 PNG 和 WebSocket RGB888 数据中。开关只写入当前浏览器的带版本工具状态,从旧版本迁移时默认关闭。
- 移动画布视线时禁止落笔,单指平移、双指以中点为锚在 `1x..8x` 内缩放,桌面滚轮按指针位置缩放;切回继续编辑后保留视角并按变换后的画板矩形换算逻辑坐标。视口变化后重新约束位置,画布不得完全移出可见区。
- 撤销历史按每一笔画笔/橡皮、填充背景和清空像素层分组,只在像素确实变化时记录操作前的 RGB 快照;最多 50 步,只存在当前页面内存。撤销仅恢复像素层并保留文字。退出再进入专注编辑不清空历史;页面刷新、场景替换,以及成功应用统一画板、保存或更新模板、保存或新建动图帧后清空,失败提交不得清空。
- 支持“应用统一画板”和“保存 PNG”;PNG 保存导出最终组合场景,不只导出像素底层。
- 背景填充只属于像素工作区;文字和未来透明元素不得各自提供全帧背景填充。
- 后续可增加撤销、重做和导入图片。
画布坐标规则:
- 前端场景模型维护一个 `64 x 64 x RGB` 像素数组作为固定底层。
- 左上角是 `(0, 0)`。
- 右下角是 `(63, 63)`。
- 前端预览不直接旋转物理屏幕方向,方向统一由后端显示层处理。
实时预览策略:
- 编辑过程只更新网页组合预览;点击“应用统一画板”时使用 `WS /ws/canvas` 发送最终合成帧。
- WebSocket 发送全帧时,载荷为 `64 * 64 * 3 = 12288` 字节 RGB 数据的 base64,并固定声明 `"source": "composition"`。
- 服务端只保留最新帧,避免网络或浏览器发送过快导致堆积。
WebSocket 消息示例:
```json
{
"type": "frame_rgb",
"width": 64,
"height": 64,
"encoding": "base64_rgb888",
"source": "composition",
"data": "<base64>"
}
```
服务端响应示例:
```json
{
"type": "ack",
"mode": "composition",
"applied": true
}
```
验收标准:
- 像素层点亮 `(0,0)` 时,按当前方向规则显示在对应屏幕角落,已存在文字仍保留并叠加。
- 清空像素层后只有像素层变黑,文字和其他元素仍保留;只有场景没有其他可见元素时最终画面才全黑。
- 颜色不会因为前后端 RGB 顺序问题错乱。
- 画布快速拖动时服务不崩溃,最多丢弃旧帧。
- 单像素模式的 `1x1/2x2/3x3/4x4` 笔尖均为完整正方形,快速拖动不留采样断线,边缘按 `0..63` 精确裁剪;网格在深浅像素上可辨认且不改变导出或发送的 RGB。
- 约 `320px` 和常见手机视口下,未进入编辑时在画板上滑动页面不会改变像素;进入专注编辑后悬浮按钮拖动不落笔,移动模式可平移缩放,继续编辑后的落点仍与视觉像素一致。
- 坐标系在约 `320px`、`390px`、平板和桌面视口以及 `1x/2x/4x/8x` 视角下保持刻度、数字和贯穿线对齐,缩放时自动调整密度;普通和单像素绘画、吸管、撤销、平移、缩放及最终 PNG/RGB888 在开关前后保持一致。
- 撤销可连续恢复所有未提交像素操作;成功提交当前场景后撤销立即不可用,失败提交仍可撤销。历史不写入浏览器持久存储、设备配置、模板或动图记录。
### 2.3.1 统一颜色选择器(`WEB-COLOR-UI`、`CONFIG-COLOR-PALETTE`)
- 纯色测试、画笔、画布背景和文字四个颜色入口共用同一个软件自有颜色面板;页面不得包含或动态创建 `input[type="color"]`。
- 小于 `40rem` 的设备使用底部抽屉,宽屏使用居中对话框。打开时记录原颜色,面板内拖动只更新新颜色预览,点击“使用此颜色”才提交;取消、遮罩点击或 `Escape` 不修改目标值。
- HSV 模式提供色相轨道、饱和度/明度二维区域及数值输入;RGB 模式提供 R/G/B 自绘渐变轨道及数值输入;两种模式始终与大写 `#RRGGBB` 输入同步。
- 颜色轨道使用 Pointer Events 并禁用轨道区域的浏览器默认触摸手势,同时支持键盘方向键和 `aria-valuenow`。主要触控区域不小于 `48x48 CSS px`。
- 当前浏览器保存最近 10 个已提交或吸取的颜色,去重并按最近使用排序;面板模式和最近颜色不写入设备配置。
- 设备共享收藏色最多 24 个,允许删除至空。首次或旧配置缺少字段时使用:`#000000`、`#FFFFFF`、`#FF0000`、`#FF8000`、`#FFFF00`、`#00FF00`、`#00FFFF`、`#0000FF`、`#8000FF`、`#FF00FF`、`#808080`、`#C0C0C0`。
- 收藏色接口为 `GET /api/colors/palette`、`POST /api/colors/palette` 和 `DELETE /api/colors/palette/{RRGGBB}`。添加重复颜色幂等;达到 24 色后添加新颜色返回 `409`;删除不存在颜色幂等。
- 吸管点击画布后读取该逻辑像素的精确 RGB,更新画笔色和最近颜色,然后恢复吸管之前的画笔或橡皮。
验收标准:
- Windows 与 Android 打开四个颜色入口时显示同一套自有 UI,不出现系统颜色选择器,连续开关和拖动不导致页面卡死。
- HSV、RGB 和 HEX 往返转换在舍入到 8 位 RGB 后一致;取消操作保持目标颜色不变。
- 不同浏览器读取同一设备收藏色,最近颜色仍各自独立;配置重启后收藏色保留。
- `1/2/4/8/16` 像素笔尖快速绘制无断线,画布边缘正确裁剪。
### 2.4 屏幕方向保存(`WEB-ORIENTATION`、`CONFIG-BASE`)
目标:屏幕实际安装方向不确定,因此软件必须能锁定显示方向。
支持方向:
- `0`
- `90`
- `180`
- `270`
页面功能:
- 使用四个方向按钮或下拉框。
- 点击方向后立即应用到当前屏幕内容并写入配置文件,不设置单独保存按钮。
- 修改方向时重新提交当前逻辑帧,不能先清屏或替换显示内容,当前模式保持不变。
- 页面重新打开时读取当前保存方向。
后端行为:
- `PUT /api/config` 保存方向。
- 所有后续显示内容统一经过方向变换。
- 方向变换属于底层显示接口的一部分,不要求每个业务功能自己旋转图像。
验收标准:
- 保存 `180` 后重启服务,方向仍为 `180`。
- 纯色测试不受方向影响。
- 画布和文字都受同一方向规则影响。
- 后续新增任何显示功能,都不需要重新实现方向逻辑。
### 2.5 文字图层(`WEB-TEXT`、`WEB-TEXT-I18N`、`WEB-TEXT-FONTS`)
目标:允许用户在统一场景中创建多组互相独立、可叠加的静态文字元素。
页面功能:
- 选中文字图层时,上方工具区提供该层的元素列表,以及“新建文字”“复制”“删除”按钮;点击画板文字或列表项后切换当前选中元素。
- 现有文本、字体、字号、文字颜色、`x` / `y` 坐标和左/中/右对齐编辑器始终绑定当前选中元素,选中变化时立即显示该元素的值。
- “新建文字”创建内容为小写 `ok` 的元素,使用配置中的默认字体和字号、白色文字并默认位于画板中央;新元素追加到当前文字层末尾并自动选中。
- “复制”创建新 `id`,复制当前元素属性,将 `x` 和 `y` 各偏移 `2` 个逻辑像素,追加到顶层并选中新副本;复制保留裁切约束。
- “删除”只删除当前元素,不影响其他图层或元素;没有选中元素时复制和删除按钮禁用。
- 每个文字元素只包含文字本身,不提供背景颜色或全帧背景字段。
- 画板上的文字可用 Pointer Events 拖动;选中框角点提供类似演示文稿文本框的等比缩放,缩放只改变字号,不增加自由宽高或自动换行。
- 拖动和缩放过程中使用已缓存的透明层做连续视觉变换;交互结束后提交整数 `x`、`y` 和 `1..64` 的整数字号,再调用后端 Pillow 刷新精确图层。
- 数值字段必须保留,作为键盘操作和精确定位的替代入口;画板选择、拖动、缩放均不得阻止触摸页面的必要操作。
- 参数变化后防抖请求与实屏一致的透明文字图层;当前元素渲染失败时保留错误状态并禁止应用不完整场景。
- 字体字段必须是可搜索的 ARIA combobox,不再提供服务器字体路径的自由文本输入。目录按“自动字体、已导入字体、系统字体”分组并显示字体家族与样式;输入只筛选目录,只有确认选项才修改当前元素。
- combobox 支持鼠标、触摸和 `ArrowUp`/`ArrowDown`、`Home`/`End`、`Enter`、`Escape`;旧场景中的路径或已失效字体 ID 必须原样保留并显示“当前字体不可用,将自动回退”,不得因目录读取失败把场景改成 `default`。
- 字体控件最右侧提供始终可用的“导入字体”按钮。导入成功且存在选中文字时自动选择返回的第一个字体面;没有选中文字时只更新目录,不隐式创建或修改文字元素。
- 导入按钮使用共享控件状态区分真正上传、成功、重复文件、格式错误、超限、超时与网络失败;下拉框尚未加载或没有选中文字时也必须给出明确原因,不能留下永久忙碌状态。
第一版先实现静态文字,不要求滚动动画。滚动文字后续作为动画模式扩展。
后端行为:
- `POST /api/preview/text-layer` 用 Pillow 生成透明图层,`edit_size` 可选64或192(默认64),不调用显示驱动。
- 使用 `ImageDraw.text()` 绘制文字,透明区域 alpha 为 `0`,文字颜色区域保留有效 alpha。
- 使用 `ImageFont.truetype()` 加载字体;没有指定字体时使用默认字体。
- `font:"default"` 表示自动多语言字体,不再表示某一个固定英文字体。渲染器必须选择一张能覆盖当前完整文本的字体,以保留复杂文字塑形和双向排版。
- 用户指定的字体路径仍兼容;该字体不存在或不能覆盖全文时自动使用多语言后备字体。
- 当前范围覆盖中日韩、拉丁/西里尔/希腊、阿拉伯/希伯来、常见南亚文字和泰文;Emoji、历史文字和 Noto Extra 中的罕见文字不属于当前保证范围。
- 如果没有任何候选字体覆盖文本中的有效字符,接口返回明确的 Unicode 码位错误,不得静默画出 `.notdef` 方框。
- 字号、颜色、坐标和对齐由请求参数控制;最终由浏览器与像素底层及其他元素合成,再把 RGB 帧交给 `DisplayService`。
请求示例:
```json
{
"text": "ok",
"font": "default",
"size": 12,
"x": 4,
"y": 24,
"align": "left",
"color": "#FFFFFF"
}
```
验收标准:
- 两组以上文字能同时叠加在像素画上,各自内容、字体、字号、颜色、坐标和对齐互不影响。
- 通过元素列表或画板选择后,编辑器只修改选中元素;拖动与缩放结束后的精确渲染位置和字号与数值字段一致。
- 新建、复制和删除只改变目标元素,层叠顺序符合 `elements` 数组顺序。
- 默认字体可直接显示上述常见语言;中文、日文、韩文、阿拉伯文、希伯来文、印地语、泰文、俄文和希腊文必须分别进入自动化覆盖。
- 超出屏幕边界的文字允许被裁切,不应导致接口报错。
## 3. API 设计(`WEB-API`)
### 3.1 `GET /api/status`
用途:获取服务和屏幕状态。
响应示例:
```json
{
"ok": true,
"service": {
"app_version": "<static-content-digest>",
"instance_id": "<service-start-uuid>"
},
"screen": {
"width": 64,
"height": 64,
"driver": "walnutpi-h618-hub75",
"hardware_mapping": "walnutpi-pi-bank-pwm-oe-v2",
"driver_options": {
"rows": 64,
"cols": 64,
"scan_rows": 32,
"row_address_bits": 5,
"pwm_bits": 7,
"brightness": 40,
"limit_refresh_rate_hz": 100,
"pio_base": "0x0300B000",
"pi_bank_offset": "0x120"
},
"driver_status": {
"actual_refresh_rate_hz": 99.7,
"panel_scan_rate_hz": 199.4,
"completed_frames": 123456,
"completed_scans": 246912,
"scans_per_frame": 2,
"deadline_misses": 0,
"buffer_swaps": 42,
"oe_timing_backend": "h618-pwm4",
"oe_pulse_faults": 0,
"oe_forced_blanks": 0,
"max_programmed_oe_ns": 8000,
"oe_timing_error": null,
"cpu_affinity": 3,
"realtime_priority_active": true,
"memory_locked": true,
"governor": "performance",
"last_error": null
}
},
"state": {
"mode": "animation",
"orientation": 0,
"brightness": 40,
"effective_brightness": 40,
"startup_indicator_active": false,
"current_content": {
"category": "animation",
"id": "00000000-0000-4000-8000-000000000102",
"name": "演示动图"
},
"animation_playback": {
"active": true,
"session_id": "4dd167e8-7924-49f7-af08-df1541af9f2d",
"animation_id": "00000000-0000-4000-8000-000000000102",
"animation_revision": "demo-v1",
"frame_id": "frame-1",
"frame_index": 1,
"frame_count": 2,
"position_ms": 120,
"total_duration_ms": 1000,
"paused": false,
"speed": 1.0,
"supported_speeds": [0.5, 1.0, 1.5, 2.0]
},
"revision": 12
}
}
```
`brightness` 始终表示用户保存的亮度。默认内容使用该亮度;低电压限亮活动时 `effective_brightness` 再取保护上限。自动动画帧和自动保护变化都不增加用户 `revision`,也不覆盖用户最后模式;用户成功暂停、继续、跳转或改变倍速时 `revision` 只增加一次。`current_content` 表示实际可见画面的来源:静态模板和动图分别使用 `template`、`animation`,WiFi、低电压和纯色测试等覆盖使用 `system`,无法关联已保存内容时使用 `unsaved` 和固定名称“未保存内容”。`animation_playback` 始终存在;无活动动图时 `active=false`、运行期字段为 `null`,`supported_speeds` 仍返回固定四档。
真实核桃派驱动必须返回 `driver_status`。`actual_refresh_rate_hz` 是最近完整测量窗口的逻辑帧边界实际值而非配置回显;`panel_scan_rate_hz` 是同一窗口内完整 32 行 × 7 bitplane 物理扫描的实际值。`completed_frames`、`completed_scans` 和 `deadline_misses` 为单调累计计数;所有亮度每逻辑帧只执行一次完整扫描,`scans_per_frame` 固定为 1,完成帧和完成扫描保持 1:1。生产 `oe_timing_backend` 固定为 `h618-pwm4`,OE fault、强制黑屏、最大脉宽和时序错误必须真实报告。CPU 绑定、实时优先级、内存锁定和 governor 分项报告实际状态。mock 驱动使用相同字段但值为 `null` 或明确的 `false`,不得伪造硬件优化。
`WEB-PERFORMANCE-MODE` 通过 `/api/status.system.performance_mode` 报告 `requested`、`available`、`effective`、`current_governors`、`restore_governors` 和 `last_error`。设置页标题固定为“性能模式”,提示固定为“如果使用出现卡顿,可以打开此选项,但会降低续航。”;不可用时保留可聚焦控件并给出原因。开启、配置保存和失败回滚必须是同一事务;关闭或服务停止恢复启动前 governor,最终部署默认保持关闭。
响应还包含:
```json
{
"resources": {
"status": "ok",
"sampled_at": "2026-07-21T08:00:00.000Z",
"cpu": {
"application_percent": 1.2,
"total_percent": 18.4,
"logical_cpu_count": 4,
"application_core_equivalent": 0.048,
"cores_percent": [12.1, 18.5, 21.0, 22.0]
},
"memory": {
"application_bytes": 52428800,
"application_percent": 4.9,
"total_percent": 36.7
},
"error_code": null
},
"power": {
"screen_input_voltage": {
"status": "ok",
"volts": 5.01,
"sampled_at": "2026-07-20T12:34:56.000Z",
"calibrated": true,
"error_code": null
},
"low_voltage_protection": {
"enabled": true,
"mode": "limiting",
"brightness_limit_percent": 75,
"reading_stale": false,
"revision": 3,
"sampling_interval_seconds": 1.0,
"enforcement_error_code": null
}
}
}
```
`brightness` 始终是用户保存值,`effective_brightness` 是当前驱动实际值。临界保护时 `state.mode="low_voltage_indicator"`、`effective_brightness=35`,但用户 `brightness`、持久 `last_mode` 和缓存用户帧不变;限亮及临界自动切换不增加用户 `state.revision`,只增加独立的保护 `revision`。`enforcement_error_code` 只报告保护指令应用错误,不得冒充 ADC 读取错误。
### 3.1.1 电压校准 API
- `POST /api/power/voltage/calibration/preview` 接收 `{ "reference_volts": 5.000 }`,同步等待唯一监测组件完成一组专用采样,成功时返回 `proposal_id`、`expires_at`、参考值、未校准值、当前系数、建议系数和预计校准后值。
- `POST /api/power/voltage/calibration/confirm` 接收 `{ "proposal_id": "..." }`。只保存当前服务实例内仍有效且采样状态未变化的提案;不存在或过期返回 `409`,传感器不可用返回 `503`。
- `POST /api/power/voltage/calibration/reset` 不接收校准值,将本机恢复到标称系数 `1.0` 并清空校准元数据。
- 非法参考值或建议系数返回 `400`;预览、确认和恢复均不得修改显示状态修订号。
### 3.2 `GET /api/config`
用途:读取当前保存配置。
响应示例:
```json
{
"orientation": 0,
"brightness": 40,
"default_font": "default",
"default_text_size": 12,
"last_mode": "clear",
"low_voltage_protection_enabled": false,
"voltage_calibration_factor": 1.0,
"voltage_calibrated_at": null,
"voltage_calibration_reference": null,
"voltage_calibration_uncalibrated": null,
"workspace_order": ["device", "settings", "canvas", "media-import", "animations", "templates"],
"performance_mode_enabled": false,
"animation_preview_max_concurrent": 2
}
```
### 3.3 `PUT /api/config`
用途:保存配置。
请求示例:
```json
{
"orientation": 90,
"brightness": 35,
"default_font": "default",
"default_text_size": 12,
"low_voltage_protection_enabled": true,
"workspace_order": ["device", "settings", "canvas", "media-import", "animations", "templates"],
"performance_mode_enabled": false,
"animation_preview_max_concurrent": 2
}
```
规则:
- `orientation` 只能是 `0/90/180/270`。
- `brightness` 第一阶段限制在 `1..100`,但建议页面默认使用 `30..60`。
- `low_voltage_protection_enabled` 必须是真正的 JSON 布尔值,数值、字符串和 `null` 均拒绝;开启后立即触发一组新采样,关闭后立即撤销保护。
- `workspace_order` 必须提交完整的唯一合法工作区 ID 数组;保存使用现有原子配置写入,失败时不得改变原顺序。
- 保存成功后立即影响后续显示。
### 3.4 `POST /api/display/fill` 与临时纯色测试
用途:独占显示纯色设备测试帧。该接口不修改浏览器统一场景草稿。
请求示例:
```json
{
"color": "#00FF00"
}
```
响应示例:
```json
{
"ok": true,
"mode": "fill"
}
```
旧 `POST /api/display/fill` 继续作为兼容独占改屏接口,并会结束活动测试会话。新网页使用以下设备测试接口:
- `POST /api/display/test/fill` 接收 `{ "color": "#RRGGBB" }`。无活动会话时以 `50%` 测试亮度启动;已有会话时只换色并保留测试亮度。
- `PUT /api/display/test/brightness` 接收 `{ "brightness": 1..100 }`,只更新临时亮度;无活动测试返回 `409`。
- `DELETE /api/display/test` 幂等退出测试并恢复底层用户输出。
- `GET /api/status.state.display_test` 返回 `active`、`color` 和 `brightness`;`state.effective_brightness` 继续表示低电压仲裁后的真实驱动亮度。
### 3.5 `POST /api/display/text`
用途:兼容旧客户端的整帧文字显示。请求继续包含 `background` 并生成 `64x64 RGB` 帧;新网页不得用它显示场景文字,而应请求透明文字层并提交组合帧。
兼容请求示例:
```json
{
"text": "OK",
"font": "default",
"size": 12,
"x": 4,
"y": 24,
"align": "left",
"color": "#FFFFFF",
"background": "#000000"
}
```
响应示例:
```json
{
"ok": true,
"mode": "text"
}
```
### 3.6 `POST /api/display/clear`
用途:独占清屏。该接口不等同于“清空像素层”,也不得修改浏览器统一场景草稿。
响应示例:
```json
{
"ok": true,
"mode": "clear"
}
```
### 3.7 `POST /api/display/diagnostic`
用途:独占发送硬件诊断图案,用于排查屏幕重复、错位、扫描参数或接触不良问题。该接口只生成标准 `64x64 RGB` 帧,仍然通过底层显示服务提交,且不得修改浏览器统一场景草稿。
请求示例:
```json
{
"mode": "corners_lines"
}
```
允许的 `mode`:
- `corners_lines`
- `row_bands`
- `address_check`
- `text_ok123`
- `clear`
响应示例:
```json
{
"ok": true,
"mode": "diagnostic:corners_lines"
}
```
页面要求:
- 诊断按钮放在快速测试区域。
- 发送后只更新最近操作结果和屏幕状态,不自动连续执行多个需要目测的图案。
- 需要目测的诊断步骤按测试文档暂停等待用户反馈。
- 是否启用摄像头辅助由测试流程和用户确认决定;页面不默认要求摄像头,也不自动触发摄像头相关操作。
### 3.8 `POST /api/preview/text`、`POST /api/preview/diagnostic`
用途:生成网页正向预览,不操作真实或模拟驱动。
- 文字预览复用 `POST /api/display/text` 的请求结构和 Pillow 渲染路径。
- 诊断预览复用 `POST /api/display/diagnostic` 的请求结构和诊断渲染器。
- 成功响应为 `image/png`,尺寸固定 `64x64`;参数错误仍返回明确的 `400` JSON 错误。
- 预览始终是逻辑 `0°`,不得应用物理方向或行位映射,也不得修改 `last_mode`、当前逻辑帧和配置文件。
`POST /api/preview/text` 及 `POST /api/display/text` 的 `background` 字段为旧客户端兼容契约,继续保留;新网页不再调用这两个整帧文字接口。
### 3.9 `POST /api/preview/text-layer`
用途:为统一场景中的单个文字元素生成无副作用的透明图层。
请求示例:
```json
{
"text": "ok",
"font": "default",
"size": 12,
"x": 4,
"y": 24,
"align": "left",
"color": "#FFFFFF"
}
```
规则:
- 成功响应为 `image/png`,图像模式为 `RGBA`,尺寸固定 `64x64`;没有文字的区域必须透明,不能用黑色或其他背景填满。
- 请求不接受场景背景语义;背景只由像素底层决定。
- 字体、字号、坐标、对齐、颜色和边界裁切必须复用旧文字接口的 Pillow 渲染逻辑,避免网页预览与最终实屏分叉。
- 参数错误返回明确的 `400` JSON 错误。
- 调用前后 `last_mode`、当前逻辑帧、方向、亮度、真实或模拟驱动和配置文件必须保持不变。
### 3.10 `WS /ws/canvas`
用途:接收已扁平化的统一场景最终帧,同时兼容旧画布客户端。
规则:
- 继续支持整帧 `frame_rgb`。新网页额外发送固定字段 `"source":"composition"`,服务端成功后返回 `{"type":"ack","mode":"composition","applied":true}` 并把当前模式更新为 `composition`。
- 旧客户端省略 `source` 时仍按原画布消息处理,成功响应保持 `mode:"canvas"`,不能因新增字段要求而失效。
- 每次收到合法帧,后端立即提交最新帧。
- 如果上一帧还没处理完,新帧可以覆盖旧帧。
- 错误消息返回 JSON,不能让连接无声断开。
- 客户端正常 close、网络断开或页面刷新必须由断开分支直接结束循环并记录普通信息日志;不得被显示异常分支捕获,不得在 WebSocket 已 close 后再次发送错误消息。
错误响应示例:
```json
{
"type": "error",
"message": "frame size must be 64x64 RGB888"
}
```
### 3.11 模板 API(`WEB-TEMPLATES`、`WEB-DEMO-TEMPLATES`、`CONFIG-TEMPLATES`)
- `GET /api/templates` 返回按更新时间倒序的模板元数据、记录 `revision`、带内容摘要查询参数的缩略图 URL、单模板字节数、模板总字节数及模板目录所在分区的总/已用/可用字节数。`revision` 由完整记录计算,不写入模板 JSON。
- `GET /api/templates?include_demo=true` 在普通结果外返回“演示静态图”,并以 `is_demo=true`、`demo_order=1`、`read_only=true` 明确其展示和权限;不带参数时只列出用户持久模板,便于普通选择器避免把演示项当作写入目标。
- `POST /api/templates` 接收 `{ "name": string, "scene": sceneV1 }` 并创建模板。
- `GET /api/templates/{id}` 返回完整模板;`GET /api/templates/{id}/thumbnail?v=<digest>` 返回后端缓存的 `image/png`。
- `PUT /api/templates/{id}` 接收 `{ "scene": sceneV1 }` 并更新内容;`PATCH /api/templates/{id}` 接收 `{ "name": string }` 并只重命名;两者必须使用 `If-Match: "<revision>"` 提交读取时的记录修订值。
- `POST /api/templates/{id}/copy` 复制完整场景;名称依次使用“原名 - 副本”“原名 - 副本 2”等第一个可用名称。
- `POST /api/templates/{id}/play` 使用 `If-Match` 临时显示该修订的完整场景并结束活动动图,成功返回 `ok`、`template_id` 和 `revision`;演示静态图同样允许播放。该接口不得修改默认显示引用、模板记录或浏览器草稿。
- `DELETE /api/templates/{id}` 使用 `If-Match: "<revision>"` 删除模板及其缩略图。
- 演示静态图的详情和缩略图沿用模板读取接口;更新、重命名和删除统一返回 `409`,复制返回独立普通模板。
- 缺少 `If-Match` 返回 `428`;修订值已过期或名称冲突返回 `409`;非法 scene 返回 `422`,不存在或非法 UUID 返回 `404`,磁盘空间不足返回 `507`。任何失败都不得改写或删除当前模板。
### 3.11.1 默认内容 API(`WEB-DEFAULT-DISPLAY`、`CONFIG-DEFAULT-CONTENT`)
- `GET /api/display/default-content` 返回当前有效引用的 `type`、`id`、`name`、`revision` 和 `is_demo`。读取时发现持久引用失效必须先原子修复到演示动图再返回,不得留下悬空配置。
- `PUT /api/display/default-content` 接收 `{ "type": "template" | "animation", "id": UUID }`。目标必须是存在的演示或用户静态模板,或至少含一帧的演示或用户动图;成功必须同时完成配置原子写入和即时显示,任一步失败都恢复旧引用及旧画面。
- 删除当前默认内容或删除默认动图最后一帧时先切换演示动图。当前默认且正在播放的动图因此允许删除;其他正在播放的非默认动图仍返回 `409`。
- `/ws/canvas` 的 `source="composition"` 消息可选携带 `content_source:{type:"template",id,revision}`。前端只在模板载入后未发生任何编辑时发送;合法但过期或已失效的来源不阻止帧显示,只把 `current_content` 降级为“未保存内容”,格式非法仍返回协议错误。
### 3.12 动图与内容库 API(`WEB-ANIMATIONS`、`CONFIG-ANIMATIONS`、`CONFIG-LIBRARY-ORDER`)
- `GET/POST /api/animations` 列出或创建动图;`GET/PATCH/DELETE /api/animations/{id}` 读取、重命名或删除。删除活动动图返回 `409`。
- `GET /api/animations?include_demo=true` 在普通结果外返回“演示动图”,并以 `is_demo=true`、`demo_order=2`、`read_only=true` 声明其展示和权限;不带参数时不把演示动图暴露给动图编辑器或复制目标选择器。
- `/api/animations/{id}/frames` 及其 `/{frame_id}` 子资源提供帧创建、读取、场景覆盖、名称/时长修改和删除;摘要缩略图使用 `/{frame_id}/thumbnail?v=<digest>`。
- `PUT /api/animations/{id}/frame-order` 只接受全部现有帧 UUID 的准确排列。`PUT /api/animations/{id}/frame-durations` 接受时长和可选 `frame_ids`;省略 ID 时兼容原有全部帧更新,提供时要求非空、无重复且全部属于当前动图,并原子更新所选帧。
- `POST /api/animations/{source_id}/frames/copy` 使用来源 `If-Match`,接收非空无重复的 `frame_ids`、目标动图及其修订和可空 `insert_after_frame_id`;空位置表示插入最前。接口支持同动图或跨普通动图复制,保持来源顺序、名称、时长和 scene,生成独立 UUID 与缩略图,任何校验、缩略图或写入失败都不得留下部分结果。
- `POST /api/animations/{id}/frames/batch-delete` 使用 `If-Match` 原子删除非空无重复的现有帧集合,成功后只清理对应缩略图;删除全部帧合法。演示动图不得作为上述写接口的目标。
- `POST /api/library/copy` 明确提交静态模板或动图帧来源、静态或动图目标及相关修订值,生成完全独立的 scene 副本。
- `GET /api/library/order` 返回全部当前用户内容的统一顺序 `items: [{ "type": "template" | "animation", "id": UUID }]` 和计算修订值。演示案例不在结果中;不存在顺序文件时按模板列表后接动图列表派生默认顺序且不落盘,已有记录中的已删除引用在响应中被过滤,新内容追加在有效记录末尾。
- `PUT /api/library/order` 接收当前全部用户内容的准确排列并要求 `If-Match`。缺少修订值返回 `428`,过期修订或请求集合与当前用户内容不一致返回 `409`,格式错误、重复引用或包含演示 UUID 返回 `422`;成功后原子写入并返回新顺序和修订值。
- `POST /api/animations/{id}/copy` 深复制完整动图并生成唯一副本名称;演示动图也只通过该接口复制为普通动图。演示动图的帧、排序、时长、名称和删除写接口统一返回 `409`。
- `POST /api/animations/{id}/play` 使用 `If-Match` 启动该修订的预渲染快照;空动图和过期修订返回 `409`。所有写接口沿用模板 API 的 `428/409/422/404/507` 语义。
- `PATCH /api/display/animation-playback` 只控制当前活动快照,必须携带 `/api/status.state.animation_playback.session_id`,并至少提供 `position_ms`、`paused`、`speed` 之一。`position_ms` 是严格整数且范围为 `0..total_duration_ms-1`;`paused` 是严格布尔值;`speed` 只接受 `0.5/1/1.5/2`。成功返回最新 `animation_playback`,无活动动图或会话不匹配返回 `409`,空更新、错误类型、非法档位或越界位置返回 `422`。同一次请求可以原子修改多个字段,跳转立即更新当前逻辑帧。
### 3.12.1 媒体内容转换(`WEB-MEDIA-IMPORT`、`CONFIG-MEDIA-JOBS`)
- 侧栏新增稳定工作区 ID `media-import`。配置 v7→v8 在保持已有项目相对顺序的前提下,把它插入 `text` 之后、`animations` 之前。
- 用户上传时只选择本地文件,不提前填写输出名称。服务以 `application/octet-stream` 流式接收,要求可验证的 `Content-Length`,单文件上限 `4 GiB`;每次上传都创建独立任务 UUID 和独立 `source.bin`,相同文件名或相同字节内容不得复用、覆盖或拒绝。用户文件名只作显示元数据,并在未显式提供兼容 `name` 参数时去掉最后一个扩展名、截取至 80 个字符,作为分析完成后“保存名称”的可编辑预填值;无有效文件名时使用“未命名媒体”。
- 上传完成后后台分析媒体类型、尺寸、时长、方向、透明能力和最多五张代表帧。页面提供 `crop`、`contain`、`stretch` 三种模式;`crop` 在界面中显示为“自由取景”,以固定 1:1 输出视窗覆盖完整原图,不得先按短边居中裁切。`zoom=1` 时方向及像素宽高比归一后的完整素材居中可见,用户再缩放、拖动素材;整段媒体共用同一组 `center_x`、`center_y` 和 `zoom`,不得逐帧保存裁切。
- `crop` 的 `center_x`、`center_y` 表示映射到输出视窗中心的归一化素材坐标,允许超出 `0..1` 以把素材拖出视窗,但最终至少有一个 64x64 输出逻辑像素与素材相交。透明像素填充色与几何留白色是两个独立 `#RRGGBB` 值:素材自身透明像素使用透明填充色,`contain` 留白及 `crop` 视窗落在素材外的虚空像素使用留白色。输出先完成方向和像素宽高比归一,再缩放完整 RGBA 素材、定位并合成两种颜色,最终固定为 `64x64 RGB888`。
- 动态输入以 `20 fps` 为上限采样;最终 RGB 字节完全相同的相邻帧合并并累加持续时间。图片输出静态模板;视频或多帧图片输出动图,即使合并后只剩一帧也保持动图类型。
- 设备只允许一个分析或转换工作进程。任务状态固定为 `uploading`、`analyzing`、`awaiting_settings`、`queued`、`converting`、`failed`;队列跨服务和整机重启恢复,运行中断任务重新排队。
- 成功原子提交内容库后立即删除任务源文件、预览与暂存目录,转换卡片消失并立即刷新对应的静态模板或动图内容库;独立转换进程提交的动图必须在主服务不重启的情况下可见,不自动改变当前屏幕。取消同样立即清理;失败和等待设置任务保留 24 小时供重试,过期后清理。
- 任务列表每 `2s` 轮询时必须按任务 ID 增量更新。处于 `awaiting_settings` 的卡片在服务端状态未改变时复用原 DOM,自动轮询和手动刷新均不得使名称输入失焦,也不得覆盖尚未提交的适配模式、裁切焦点、缩放、颜色或代表帧选择;任务进入排队、转换、失败、完成、取消或过期状态时才按服务端状态替换或移除卡片。未提交设置只保留在当前页面内存中,整页刷新后允许恢复服务端值。
- 保存名称去除首尾空白后按 Unicode 大小写不敏感规则在静态模板、动图及其他 `queued`/`converting` 媒体任务之间保持唯一。两个 `awaiting_settings` 任务可暂时同名,先排队者原子占用名称;后提交者必须在保存设置或排队时收到 `409` 并保持任务、源文件和本地草稿可继续修改。失败、取消、完成或删除任务释放活动名称占用;转换开始前再次检查内容库,任何冲突不得留下部分内容。
- 护栏固定为媒体时长不超过 2 小时、合并后不超过 30,000 帧,并始终为设备保留至少 2 GiB 可用空间;异常像素尺寸、无进展超时、总处理超时、磁盘不足或解码失败均不得留下部分模板。
- Pillow 优先解码其支持的常见图片和多帧图片,HEIC/HEIF 使用 `heif-convert`,视频及其他可探测动态媒体使用 FFmpeg/ffprobe。音频流忽略,音频文件、网络 URL、播放列表、SVG、PDF、文档、压缩包和相机 RAW 拒绝。
- `POST /api/media-imports?filename=<name>` 创建流式任务,并继续兼容旧客户端可选的 `name=<output>`;`GET /api/media-imports` 与 `GET /api/media-imports/{id}` 返回公开状态;`GET /api/media-imports/{id}/previews/{index}` 返回预览;`PUT /api/media-imports/{id}/settings` 保存名称和转换参数;`POST .../convert`、`POST .../retry`、`DELETE .../{id}` 分别排队、重试和取消。
- API 不返回持久根、源文件、预览或暂存的绝对路径。输入按内容探测;FFmpeg 子进程只允许本地 `file` 与 `pipe` 协议,转换 unit 禁止网络和设备访问。
### 3.13 WiFi 与 IPv4 API(`WEB-WIFI-SETTINGS`、`CONFIG-WIFI`)
- `GET /api/network/wifi` 返回 `saved`、`active`、`prompt_delay_seconds` 和最近 `operation`;仅该专用设置接口在 `saved.password` 中返回 NetworkManager 保存的明文密码或 `null`,同时保留 `password_configured`。`/api/status.network`、WiFi 写接口响应和业务日志继续脱敏。
- `PUT /api/network/wifi` 接收 `ssid`、可空 `password`、`ipv4_mode`、静态地址参数和 `activation`。`password` 为空表示保留当前密码,SSID 改变时必须提交新密码;旧调用方可继续携带可选 `prompt_delay_seconds`,新页面不得通过该入口保存提示延迟。
- `PUT /api/network/wifi/prompt-delay` 只接收 `prompt_delay_seconds`,原子保存 `1..3600s` 的网络提示延迟并返回当前值。它不读取或修改 NetworkManager、不触发网络激活,也不重算当前 boot ID 已建立的运行期提示截止时间。
- `ipv4_mode="dhcp"` 清除静态地址;`manual` 必须提供 IPv4 地址和网关,子网前缀空值使用 `/24`,DNS 空值使用网关。
- `activation="next_boot"` 只更新持久 NetworkManager 配置并返回 `200`;`immediate` 先保存、返回 `202`,再异步激活连接,避免浏览器在响应发出前被切断。
- SSID 按 UTF-8 字节限制为 `1..32`,个人热点密码限制为 `8..63` UTF-8 字节;IPv4、前缀 `1..32` 和 DNS 必须严格校验。除 `GET /api/network/wifi` 的 `saved.password` 外,任何响应、错误和日志不得回显密码。
- `/api/status.network` 暴露同一脱敏活动状态与提示状态;读取状态、静态资源和预览不取消开机提示,成功首页访问、成功改屏或手机成功建立加密控制会话均取消本次开机的 WiFi 提示;手机断开、网络变化和服务重启不恢复,整机重启重新计时。
### 3.13.1 FRP 与网络诊断 API(`WEB-FRP-MAINTENANCE`、`CONFIG-FRP`、`WEB-NETWORK-DIAGNOSTICS`)
- `GET /api/frp` 只返回安装版本、目录元数据、唯一选中项、systemd 启用/运行状态和脱敏错误,不返回配置正文、服务器地址或令牌。
- `POST /api/frp/configs`、`GET|PUT|DELETE /api/frp/configs/{id}` 以不超过 `1 MiB` 的 UTF-8 原文管理 TOML/YAML/JSON/INI;名称不超过 80 字符,路径只由服务生成 UUID。
- `POST /api/frp/configs/{id}/select` 切换唯一活动配置;`POST /api/frp/service/start|stop` 分别对独立 unit 执行 `enable --now` 与 `disable --now`。没有选中配置时拒绝启动,活动运行配置在停止前拒绝删除。
- 新建和保存都先在同目录临时文件执行官方 `frpc verify -c`;活动配置保存成功后自动重启,重启失败恢复旧字节和旧进程。不检查或改写任意 FRP 服务器、端口、代理类型、插件、`includes`、证书、令牌源或环境变量模板。
- `POST /api/network/diagnostics` 依次返回接口/IP、默认路由、DNS、中国大陆多站点 TCP/TLS 及辅助 ICMP 证据。TLS 已通过时 ping 失败只作提示,不误判断网。
### 3.14 设备容量 API(`WEB-DEVICE-STORAGE`)
- `GET /api/device-storage` 返回 `sampled_at`、`device_total_bytes`、`device_free_bytes`、`software_bytes` 和 `templates_bytes`。
- 设备总量与可用量取程序根所在文件系统;`software_bytes` 为程序根与持久数据根去重后的文件占用总和,不包含运行根;`templates_bytes` 为静态模板、有效缩略图和动图内容占用。
- 服务使用线程安全进程内缓存,最多保留 `300s`;配置、WiFi、模板、动图或内容库成功写入后立即使缓存失效。目录统计不得跟随符号链接,也不得扫描持久根、程序根以外的位置。
- 模板页不再展示容量卡;设备页进入时读取容量,停留时每 `300s` 刷新,同页内容变更后立即刷新。读取失败只影响容量卡并给出明确提示,不阻塞设备状态和显示控制。
### 3.15 字体目录与导入 API(`WEB-TEXT-FONTS`、`CONFIG-FONTS`、`WEB-SECURITY`)
- `GET /api/fonts` 返回 `items`、`accepted_extensions`、`max_upload_bytes` 和 `warnings`。首项固定为 `id:"default"`;每个目录项只暴露 `id`、`label`、`family`、`style` 和 `source`,不得暴露服务器文件路径。
- `source` 只允许 `automatic`、`imported`、`system`。系统字体 ID 由规范路径与 Fontconfig face index 的 SHA-256 生成;导入字体 ID 为 `user:<文件SHA-256>:<face-index>`,服务重启后保持稳定。
- `POST /api/fonts/import?filename=<UTF-8文件名>` 接收 `application/octet-stream` 原始请求体,只接受 `.ttf`、`.otf`、`.ttc`、`.otc`,最大 `32 MiB`。服务必须同时检查扩展名、流式字节上限、实际字体结构和至少一个带 Unicode cmap 的可用字体面。
- 新内容返回 `201` 和 `created:true`;重复内容不重复落盘,返回 `200` 和 `created:false`。两种响应都返回文件中的全部可用字体面。超限、类型不支持、内容无效和空间不足分别返回 `413`、`415`、`422`、`507`。
- 上传先写入 `fonts/` 内明确命名的事务临时文件,完成校验、文件同步、原子替换和目录同步后才成功;失败只清理本次临时文件。不得把用户字体安装到系统目录或运行 `fc-cache`。
## 4. 配置和数据(`CONFIG-DATA-LIFECYCLE`、`CONFIG-BASE`)
生产环境的数据边界固定如下:
| 类别 | 位置 | 内容与处理 |
|---|---|---|
| 持久数据 | `/var/lib/matrix-screen-controller` | `config.json`、`wifi_config.json`、`frp/catalog.json` 与各 UUID 原文配置、模板 JSON、动图记录、`library/order.json`、`fonts/<SHA-256>.font`、用户导入图片、预设及未来用户资源;更新、回滚和清理不得删除、覆盖或以默认值重建。NetworkManager 自身的连接配置由其系统目录管理。 |
| 可再生数据 | 持久根下各模块的专用缓存目录 | 当前包括模板和动图帧缩略图;升级时保留,只有模块能按严格命名和引用关系删除已证明无效的临时、孤立或过期项。 |
| 运行期数据 | `/run/matrix-screen-controller` | 开机标记、WiFi 提示会话与操作结果、mock 最新帧、锁和进程级临时状态;整机重启可丢失,systemd restart 可按登记语义保留,不进入迁移清单或更新备份。 |
| 更新暂存与事务备份 | 更新工具声明的专用暂存位置 | 成功后删除,不长期累计多代 `.bak`、旧包或解压目录;失败时不得牵连原持久文件。 |
数据根和运行根分别按以下优先级解析:测试构造时显式传入的目录 → `MATRIX_DATA_DIR`/`MATRIX_RUNTIME_DIR` → systemd 的 `STATE_DIRECTORY`/`RUNTIME_DIRECTORY` → 本机源码隔离目录 `data/` 与 `data/runtime/`。生产 systemd 必须显式使用 `/var/lib/matrix-screen-controller` 和 `/run/matrix-screen-controller`,不得回落到 `/opt`。
设备配置位于持久根的 `config.json`。落盘的当前格式含仅供存储层使用的 `schema_version`:
```json
{
"schema_version": 11,
"orientation": 0,
"brightness": 40,
"default_font": "default",
"default_text_size": 12,
"last_mode": "clear",
"low_voltage_protection_enabled": false,
"color_palette": ["#000000", "#FFFFFF"],
"voltage_calibration_factor": 1.0,
"voltage_calibrated_at": null,
"voltage_calibration_reference": null,
"voltage_calibration_uncalibrated": null,
"preview_refresh_interval_ms": 1000,
"matrix_refresh_rate_limit_hz": 100,
"custom_test_color": "#40A0FF",
"default_display": {
"type": "animation",
"id": "00000000-0000-4000-8000-000000000102"
},
"workspace_order": ["device", "settings", "canvas", "media-import", "animations", "templates"],
"performance_mode_enabled": false,
"animation_preview_max_concurrent": 2
}
```
保存要求:
- 写入 JSON 时使用 UTF-8。
- 配置文件确实不存在时才创建当前 schema 的默认配置;已存在文件不能因读取、迁移或校验失败被默认值替换。
- 缺少 `schema_version` 的现有格式只按 legacy v0 处理;存储层通过登记的 `vN → vN+1` 函数逐级迁移,全部步骤和当前 schema 校验成功后才写回。业务层只接收当前格式,不保留多版本分支。
- 配置字段集必须按版本显式维护:v1 是原有方向、亮度、字体、模式、色板与四个电压校准字段;v2 增加 `preview_refresh_interval_ms`;v3 增加严格布尔字段 `low_voltage_protection_enabled`;v4 增加 `matrix_refresh_rate_limit_hz`;v5 增加严格大写 `#RRGGBB` 字段 `custom_test_color`;v6 增加 `default_display`;v7增加 `workspace_order`,v8插入媒体转换,v9增加性能模式,v10增加动图预览并发,v11仅移除导航中的 `text` 并保留其余顺序;旧 `#text` 入口跳转画图页。模板当前schema v2,动图v3。
- 损坏 JSON、布尔值冒充数值、未知字段、未知更高版本或无法安全迁移时,保持原文件字节不变、记录明确错误并阻止服务启动,不得跳过字段、猜测修复或自动另建配置。
- 写入前校验当前 schema 的完整字段集合和范围。电压校准系数必须是 `0.8..1.2` 的有限数值;四个校准字段作为一组校验,缺失或不一致时属于无效现有配置,不能静默回落为未校准状态。
- 所有配置和模板写入共用耐久原子写入:在目标同目录创建临时文件,写完后刷新并 `fsync` 文件,使用 `os.replace` 替换,再同步父目录;任一步失败都清理本次临时文件并保留旧文件,不累计滚动备份。
- `schema_version` 只存在于落盘记录,不出现在 `/api/config`、模板列表或模板详情响应中;`/api/config` 暴露当前业务字段(包括 `workspace_order`),其他 REST、WebSocket 和模板 API 的请求响应形状不因存储迁移改变。
系统设置页面按“屏幕亮度”“屏幕方向”“屏幕扫描刷新率”“供电电压”“预览刷新间隔”排列。亮度保留“拖动时自动应用,松手后立即保存”的说明,并按保护状态显示允许上限或临界覆盖提示;方向说明固定为“设置屏幕方向后会一直保存在软件内部,请使用带有方向性的文字进行设置。”;供电电压说明固定为“正常情况下,输入电压应该是5V左右,如果差距较大,请充电或者降低亮度。”,保护开关位于电压详情与校准按钮下方。页面不显示概括性的自动保存说明,也不显示更换 ADC 或测量接线后的提示;电压校准与恢复标称值功能保持不变。
“WiFi设置”位于系统设置末尾区域,页面提供 SSID、始终明文显示且读取后不清空的密码、默认开启的 DHCP 开关;关闭 DHCP 后显示必填 IPv4/网关及选填前缀/DNS。SSID、密码、DHCP、IPv4、网关、前缀或 DNS 发生过实际输入、删除或切换后,对应控件显示醒目边框和“对应内容已经修改”;只聚焦不算修改,改回服务端原值仍保持已修改。提示持续到网络配置成功应用或离开系统设置,失败时保留。
“立即生效”和“下次开机生效”在没有网络字段草稿时不点亮,并以 `aria-disabled`、桌面悬停/键盘聚焦说明和触屏提示解释“请先修改 WiFi 或 DHCP 设置”。任一网络字段被修改后两按钮同时点亮;处理中使用共享忙碌状态,成功后清除全部网络字段提示并恢复未点亮状态,失败或校验不通过时恢复为可点击且继续点亮。立即切换前明确告知当前页面可能断开。
“网络提示显示”是紧接在 WiFi 设置下方的独立设置卡,提供 `1..3600s` 输入和右侧“保存”按钮。该输入实际编辑后只点亮自己的保存按钮,不显示“对应内容已经修改”,也不点亮或禁用两个网络生效按钮;网络生效操作同样不得保存或清除该草稿。成功保存后按钮恢复未点亮,失败时保留草稿和可重试状态。
进入系统设置时读取服务端当前值作为 WiFi 与网络提示的共同基线;离开工作区时立即恢复该基线、清除所有未应用提示和按钮状态,并忽略尚未返回的旧读取结果。已经点击并发出的保存请求不因离开而取消;浏览器不在本地持久化这些草稿,关闭或重新打开后重新读取设备值。
`wifi_config.json` 当前 schema 为 v1,只包含 `managed_connection_uuid`(可空 UUID)和 `prompt_delay_seconds`。文件不存在时才创建;损坏、未知字段、未来版本或不安全迁移保持原件并阻止服务启动。NetworkManager 保存的密码不得复制到该 JSON 或业务日志;只允许物理断网提示和 `GET /api/network/wifi` 在进程内按需读取。
`matrix_refresh_rate_limit_hz` 是持久设备配置和 HUB75 逻辑帧边界的最高频率目标,不是保证最低或精确频率;面板物理扫描由当前有效亮度自适应为该值的三倍(`1..20%`)或两倍(`21..100%`),并由 `panel_scan_rate_hz` 和 `scans_per_frame` 单独报告。设置页只提供 `15、20、30、45、60、80、100 Hz` 七个按钮,不提供数字输入或“不限速”;点击不同挡位后整组按钮进入可感知的处理中状态,C 刷新线程在帧边界应用新上限并保持当前画面或当前动图位置,网页服务不得重启。成功反馈“<n>Hz 上限已生效并保存”;失败时恢复原选中挡位、原驱动参数和原画面并显示原因,不得把真实驱动创建失败的 mock 回退报告为成功。页面固定说明该值是逻辑帧上限、低于 `80Hz` 可能明显闪烁、降低后可能降低屏幕功耗并使画面变暗,但不承诺核桃派 CPU 或整机功耗按比例下降。
`preview_refresh_interval_ms` 是持久设备配置,当前 schema 默认值为 `1000`,只允许 `1..60000` 的整数。设置页使用毫秒数字输入;编辑过程不提交、不校验负载提示,只有输入完成并触发 `change`、失焦或按 Enter 后才保存。最终值小于 `100` 时在输入下方显示“刷新间隔过短,会增加核桃派负载,可能影响系统运行。”,但仍保存并立即应用;`100` 不显示该提示。无效、空值、非整数或超范围值不保存,并恢复最近一次有效值。
当前帧定时器只控制活动监视器的 PNG 请求,不改变固定的设备状态轮询、电压采样或 `matrix_refresh_rate_limit_hz`。定时器使用单请求递归调度,请求耗时超过配置间隔时允许实际刷新变慢,不得并发堆积。`GET /api/status` 通过 `ui.preview_refresh_interval_ms` 暴露当前值,使其他已打开页面在既有状态轮询内采用新间隔;屏幕扫描上限通过完整 `screen.driver_options.limit_refresh_rate_hz` 暴露,不虚构实时测量值。
亮度条的 `input` 变化必须立即进入设备更新队列,队列同一时间最多发送一个请求并在连续拖动时只保留尚未发送的最新值;松手后必须保证最终值已应用并保存。实屏应在拖动过程中持续反馈亮度变化,不得等待下一次画面更新。设置反馈默认隐藏,只在读取失败、提交中、成功或失败时显示。`default_font` 和 `default_text_size` 为兼容旧配置继续保留,文字工作区可以将它们作为首次打开时的回落值,但不在系统设置中展示。
浏览器编辑草稿(`CONFIG-UI-STATE`)使用稳定 `localStorage` 键 `matrixController:scene`,版本只保存在 JSON 内容中:
```json
{
"version": 2, "width": 64, "height": 64,
"layers": [
{"id": "layer-1", "name": "图层1", "type": "brush", "x": 0, "y": 0,
"background": null, "backgroundColor": "#000000", "pixelPng": "<base64 compressed 192×192 RGBA PNG>"},
{"id": "layer-2", "name": "图层2", "type": "text", "x": 0, "y": 0,
"elements": [{"id": "text-1", "type": "text", "text": "ok", "font": "default",
"size": 12, "x": 32, "y": 26, "align": "center", "color": "#FFFFFF", "clip": null}]}
]
}
```
- `layers` 从底到顶,ID唯一,至少一个画笔层;层偏移为−64..64整数。文字元素ID全场景唯一,坐标、大小沿用原范围。`clip` 为null或相对文字锚点、以字号为单位的 `[left, top, right, bottom]` 矩形;累计交集永久保留,随锚点和字号变换。图层和文字只保存已列字段。旧v1先严格校验,再把中央RGB写入一个画笔层,全部旧文字写入一个上方文字层,外围为空白。
- 整层拖动松手、单段位置拖动松手、位置输入回车/失焦和每次步进均为一次提交;候选越界先确认,确认前不得写草稿、保存或应用。背景不计入越界;确认裁切和位置变化是一个撤销步骤,最多50步。
- 选中元素、渲染中/失败状态、透明 PNG 缓存、状态消息、设备状态、测试选择和错误不持久化。
- 稳定键不存在时,先读取并验证所有实际存在的旧键;任一旧键损坏或无法识别都必须保留全部原值并停止迁移,不能跳过高优先级损坏值后拿低优先级草稿覆盖。全部验证通过后,先迁移现有 `matrixController:scene:v1`;没有该键时再按 `matrixController:canvas:v3`、`matrixController:canvas:v2`、旧 `matrixCanvasRgb` 的优先顺序选择像素草稿,同时独立读取 `matrixController:text:v1` 并合并为文字元素。迁移保留能验证的旧像素、画笔/背景颜色、画笔粗细及文字属性,丢弃旧文字背景。
- 旧文字草稿迁移为一个稳定 `id` 的 `type:"text"` 元素;损坏文字元素单独跳过,不能连带清空有效像素或其他元素。缺少画笔粗细时回落为 `1`。
- 只有新内容已完整校验并成功写入稳定键后,才精确删除本次已迁移的旧键;写入失败时旧键和值必须保留。
- 稳定键内容损坏、出现未知更高版本或迁移失败时,页面保留原值和内存草稿、停止自动写回并明确提示用户;不得用空白场景覆盖,不得自动上传到核桃派。场景只保存在当前浏览器,不写入设备 `config.json`,不要求跨浏览器共享,也不保证核桃派重启后自动恢复实屏。
- 同一来源的其他标签页修改稳定场景键时不得直接替换当前内存草稿;冲突解决前当前标签页的后续编辑继续保留在内存但暂停写回 `localStorage`。用户选择“载入其他标签草稿”后采用最后收到的有效外部值,选择“保留当前草稿”后才显式覆盖共享键。
- 画笔颜色、像素层背景颜色、画笔模式、两种模式各自的笔尖粗细、当前画笔工具和坐标系开关使用独立带版本号的本地 UI 状态;它们不是场景元素,扩展显示开关同样仅属于浏览器工具状态。当前键为 `matrixController:canvas-tools:v3`:刷新后恢复模式、两套尺寸和坐标系选择;首次从 v2/v1 读取时保留已有工具值、把旧 `brushSize` 迁入普通模式,像素模式默认 `1x1` 且默认关闭,坐标系默认关闭,损坏或缺失值安全回落到默认值。
- 每个新工作区显式选择 `none`、`local` 或 `server`;普通显示功能必须说明怎样接入 scene v2,不得把未来场景大对象无条件塞入服务端配置。
核桃派模板(`CONFIG-TEMPLATES`)使用持久根下独立的 `templates/` 数据区:
- 每个模板 JSON 使用不可变 UUID 文件名,用户名称不参与路径拼接;落盘记录包含仅供存储层使用的 `schema_version: 2`,API 中的模板 ID、名称、创建/修改时间、场景摘要、内容和修订语义保持不变。
- 缺少版本的现有模板按 legacy v0 逐级迁移到唯一当前 schema。服务启动必须先迁移并校验全部模板记录,再执行任何缩略图协调或清理;任一模板损坏、含未知字段、版本过高或迁移失败时,所有原件保持不变并阻止服务启动。
- 缩略图文件名包含模板 UUID 和规范场景内容摘要。更新时先生成并验证新图,再提交模板 JSON,最后删除旧摘要图片;重命名不重复生成内容未变化的缩略图。
- 服务启动和每次模板变更后只在专用缩略图目录内按严格 UUID/摘要规则清理临时文件、孤立图片和过期图片;读取或迁移阶段存在任何不可读记录时不得开始清理,也不得扫描或删除配置、运行期最新帧、模板 JSON 或其他用户数据。
- 场景缩略图渲染按元素类型注册;新增元素类型时必须同时提供后端缩略图渲染器和模板往返测试。
- 单模板空间等于其 JSON 与当前有效缩略图的实际文件大小;模板总空间为所有有效模板之和。磁盘空间取模板目录所在文件系统,不使用浏览器估算。
- 模板更新、重命名和删除在存储锁内比较请求修订值;过期客户端只能收到冲突并重新读取,不得以最后提交者静默覆盖。
设备默认内容引用(`CONFIG-DEFAULT-CONTENT`)保存在同一持久 `config.json` 的 v6 字段 `default_display` 中,格式固定为 `{ "type": "template" | "animation", "id": UUID }`。v5→v6 只增加演示动图引用并保留所有业务值;配置层只校验结构,应用层负责确认目标存在和动图非空。引用失效时应用层必须原子写回演示动图,不能只在内存中临时回落。
工作区顺序(`CONFIG-WORKSPACE-ORDER`)保存在同一持久 `config.json` 的 v8 字段 `workspace_order` 中。v6→v7 增加原六个稳定工作区 ID;v7→v8 只把 `media-import` 插入 `text` 之后、`animations` 之前,已有项目相对顺序不变。字段必须是 `1..128` 个唯一、合法 ID 的数组;前端读取后过滤当前版本不存在的 ID,并按注册默认顺序追加缺失工作区。只有用户点击“保存”才通过 `PUT /api/config` 原子替换该字段,编辑中的顺序不得写入本地存储、运行根或服务端。
媒体转换任务(`CONFIG-MEDIA-JOBS`)使用持久根下的 `media-import/jobs/<UUID>/`。每个 `job.json` 使用严格 schema v1,源文件固定命名,预览和暂存只允许位于同一任务目录。服务启动逐个验证任务;损坏或未来版本任务保持原件、禁用媒体转换子系统并报告错误,但不得阻止显示、配置和已有内容库启动。上传中断只删除严格匹配的 `.part` 文件;活动任务以原子状态写入恢复,成功内容提交后才删除源文件。
模板管理顺序(`CONFIG-LIBRARY-ORDER`)使用持久根下的 `library/order.json`:
- 当前格式为 v1,只持久化 `schema_version` 和用户静态模板/用户动图的类型与 UUID,不复制名称、scene、帧、演示案例或缩略图。文件不存在表示尚未自定义排序,读取不得因此创建文件。
- 顺序修订值由过滤已删除引用并追加当前缺失内容后的完整用户序列计算。首次排序及后续更新使用同目录临时文件、文件和目录同步及原子替换;资源新增或删除不要求联动改写顺序文件。
- 文件损坏、字段不严格、重复引用或未知版本时保留原件并阻止服务启动;记录中的已删除 UUID 不是损坏,读取时过滤,下一次成功排序用当前完整序列替换旧记录。
## 5. 安全和局域网边界(`WEB-SECURITY`)
第一阶段默认只在可信局域网使用,不实现登录。字体上传仍必须使用严格的扩展名白名单、`32 MiB` 服务端流式上限、FontTools 内容校验、内容摘要文件名和持久根路径隔离;媒体上传按 `WEB-MEDIA-IMPORT` 使用 `4 GiB` 流式上限、内容探测、协议白名单和独立资源隔离。查询与响应不得暴露系统或持久根绝对路径。
必须在文档和页面中保留后续安全升级位置:
- 简单访问密码。
- 只允许局域网地址访问。
- 禁止公网端口转发。
- 图片、动图和视频上传已按 `WEB-MEDIA-IMPORT` 定义大小、类型、任务生命周期和失败提示;后续新增文档、矢量或网络媒体必须另行扩展白名单和隔离要求。
## 6. 日志和错误处理(`WEB-ERRORS`)
服务至少记录:
- 服务启动和监听地址。
- 配置读取和保存。
- 屏幕驱动初始化成功或失败。
- API 参数错误。
- WebSocket 连接、断开和帧格式错误。
前端至少显示:
- 当前屏幕是否可用。
- 最近一次操作是否成功。
- 如果屏幕驱动初始化失败,说明网页仍可打开但无法实际点亮屏幕。
## 7. 后续实现验收清单
- 核桃派开机后服务自动启动。
- 开机笑脸按用户保存方向持续眨眼,独立使用 50% 有效亮度;首个成功改屏指令后同一开机内不再出现。
- 开机延迟到期且未被用户操作时,成功连接滚动实际 SSID 与控制 URL;断网显示静态 WiFi 图标并滚动保存的 SSID/密码,恢复连接后自动切换。提示固定 50% 名义亮度且不污染用户状态。
- WiFi 设置能明文读取并编辑保存的密码,保存 DHCP 或完整静态 IPv4,立即切换先返回响应,下次开机模式不改变当前活动连接;只有专用 WiFi 设置 GET 返回密码,状态、写响应和日志不泄露密码。网络提示延迟由下方独立按钮保存且不触发网络配置。
- 局域网设备可以访问 `http://核桃派IP:8080`。
- 纯色测试能显示黑、红、绿、蓝、白。
- 共享画板在所有工作区顶部无遮挡显示,像素底层与多组文字能同时叠加、独立编辑并作为一个组合帧应用到屏幕。
- 清空或填充像素层不影响文字;文字新建、复制、删除、拖动和等比缩放不影响像素层或其他文字。
- 设备测试独占覆盖实屏但保留场景草稿,离开测试页只恢复网页预览,点击“恢复/应用统一画板”后才恢复实屏组合场景。
- 保存方向后重启服务仍生效。
- 透明文字层预览无显示副作用,渲染待定或失败时不能应用不完整场景。
- WebSocket 快速发送画布帧时服务不崩溃。
- 屏幕底层不可用时网页仍能打开并给出明确错误。
- 低电压保护默认关闭且不改写用户设置;开启后按 `4.8V/4.5V` 精确边界限亮或覆盖 35% 低电图标,电压恢复后自动回到最新用户画面。
## 8. 未来扩展入口
以下内容只定义扩展落点和边界,不代表当前阶段必须实现。新增功能时先在本节或对应章节补充需求编号,再更新测试矩阵。
- 动画、滚动文字、时钟、天气、传感器数据展示:归入新的 `WEB-MODE-*` 或扩展 `WEB-TEXT`,必须说明页面入口和元素/图层提供者,并复用 `WEB-COMPOSITION` 与最终 `64x64 RGB` 帧边界。
- 把媒体作为可移动场景元素、逐帧独立裁切或网络媒体导入:在现有 `WEB-MEDIA-IMPORT` 基础上另行扩展;当前转换结果只生成像素底层 scene,不新增图片元素。
- 字体删除、重命名、全局默认和系统级安装:继续扩展 `WEB-TEXT-FONTS` 与 `CONFIG-FONTS`;当前只提供目录、选择和持久导入。
- 预设场景、启动默认画面、收藏画布:扩展 `CONFIG-BASE` 和 `CONFIG-DATA-LIFECYCLE`,必须说明持久位置、当前 schema、单向迁移和失败保全。
- 访问密码、局域网白名单、禁止公网转发提示:扩展 `WEB-SECURITY`,并在测试文档增加允许/拒绝场景。
- 多屏、新尺寸屏幕或新硬件映射:网页端只能消费底层暴露的尺寸和能力,不直接写死 GPIO 或 HUB75 细节;先更新硬件资料和 `02_屏幕底层控制接口需求.md`。
## 9. 参考资料
- 本地接线资料:`硬件相关资料和硬件的连接/点阵屏幕相关资料/手动接线说明/核桃派ZeroW_HUB75_接线与首次上电检查.md`
- 核桃派官方 `gpioc`(MIT):https://github.com/walnutpi/gpioc
- FastAPI WebSockets:https://fastapi.tiangolo.com/advanced/websockets/
- FastAPI StaticFiles:https://fastapi.tiangolo.com/tutorial/static-files/
- MDN Canvas API:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
- Pillow ImageDraw:https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html
- systemd service:https://manpages.debian.org/bookworm/systemd/systemd.service.5.en.html
### 图层追加接口(`WEB-LAYERS`)
- `POST /api/templates/{id}/layers`:`{layer: <完整图层>}`,要求 `If-Match`,副本独立ID并置顶。
- `POST /api/animations/{id}/frames/layers`:`{layer: <完整图层>, frame_ids: [<稳定帧ID>]}`,要求 `If-Match`;单帧、多选或全部帧共用此接口。全部新场景和缩略图成功后才一次提交索引;失败不改变任何原帧。时长及帧顺序不变。
- 正在编辑的目标有修改时,先保存/放弃/取消;保存失败和冲突停止复制。演示目标只读。
- `/api/preview/text-layer` 新增 `edit_size: 64|192`,默认64;192模式坐标为图像坐标(逻辑坐标加64)。输出仍64×64 RGB888,手机内容库与播放契约不变。
画笔层 `background` 为当前启用的颜色字符串或null(关闭);`backgroundColor` 保存该层在关闭状态下的颜色选择。此已登记可选字段缺省时读取为现有背景颜色或黑色,兼容早期scene v2;新网页保存时包含该值,切层、复制及重载不得借用其他图层颜色。关闭背景不绘制该颜色,擦除/清空/裁切不改变它。
迁移边界:旧导航只有 `text` 时,移除后使用当前默认导航顺序;其余非空顺序保持不变。旧浏览器草稿同样先严格检查字段、整数坐标及范围;未知字段、损坏或未来格式必须保留旧键并禁用覆盖保存,不能由旧宽松读取器丢弃字段后继续迁移。