# HiveBox
**Repository Path**: hwyhy/HiveBox
## Basic Information
- **Project Name**: HiveBox
- **Description**: 蜂巢盒子---ESP32 S3开发的多功能盒子
- **Primary Language**: Rust
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 1
- **Created**: 2026-10-13
- **Last Updated**: 2026-10-20
## Categories & Tags
**Categories**: hardware
**Tags**: None
## README
# 纯 Rust 开发的 UI 框架和 UI 系统(ESP32-S3)
> 当前版本:[`v0.11.2-20261020`](https://github.com/HWYWL/HiveBox/releases/latest) —— 版本号怎么读见下方「版本号规则」
## 📖 项目简介
这个项目是为 微雪 ESP32-S3-Touch-AMOLED-2.16 触屏开发板开发的纯 Rust UI 系统。
项目基于 **ESP-IDF v6.1** 构建。
- **测试平台**:目前固件仅在 **微雪 ESP32-S3-Touch-AMOLED-2.16** 上进行过测试。
- **可移植性**:应用与 UI 框架与硬件解耦,移植仅需适配屏幕刷新、触摸输入及 `pomelo-hal` 外设 Trait。
- **产品官方文档**:[微雪 ESP32-S3-Touch-AMOLED-2.16 文档](https://docs.waveshare.net/ESP32-S3-Touch-AMOLED-2.16)
### 基于 pomelo-ui 深度定制
本项目是 [**pomelo-ui**](https://github.com/pomelos-on-sale/pomelo-ui) 的深度定制版,两者的关系写在这里,以免读的人以为这套界面是从零写的:
- **上游 [pomelo-ui](https://github.com/pomelos-on-sale/pomelo-ui) 提供框架**:`ui-framework/` 下的 iced 移植、渲染后端(`iced-pomelo-gfx`)、软件光栅器(`pomelo-gfx`)、字体资产与烘焙工具(`pomelo-font`)、图标库(`pomelo-material-symbols`),以及它们各自的设计与文档。
- **本项目在框架之上做定制**:板级固件与板级驱动(`pomelo-firmware/`)、外设抽象与模拟后端(`pomelo-hal/`)、应用(`pomelo-apps/`)、应用共享控件(`pomelo-widgets/`),以及界面本身 —— 具体每一条见下方「版本时间线」。
- **上游的名字与目录都保留**:`ui-framework/` 的结构、各组件的 `README.md` 与 `LICENSE`、`pomelo-*` 的命名照旧,便于与上游逐项对照。上游组件按需做最小的修改(例如字体子集补字),这类改动同样在这里可见。
---
## 📷 界面预览
下面都是**真机屏幕**(480×480 AMOLED)的实拍,不是设计稿。每张都按 160×160 显示 —— 图都写死了宽度,不然表格会按每格说明文字的长短把列宽分得不一样,图片也就跟着一大一小;原图是 480×480,点开可以看细节:
| 桌面 | 设置 · 读数面板 | 设置 · 主列表 | 设置 · 系统信息 |
| :---: | :---: | :---: | :---: |
|
|
|
|
|
| 状态栏、四个应用与分页点(这张是两页七个应用那版的实拍,现在是两页十五个) | 三个环 + 芯片与固件卡,页脚是编译/系统时间与已启动 | 无线网络、内存、存储、电池、主题、日期与时间 | 规格与读数分开:操作系统一栏说明来源,固件版本是镜像自报 |
| 音乐播放器 | 网页管理 | 终端 | 计算器 |
| :---: | :---: | :---: | :---: |
|
|
|
|
|
| 读 TF 卡里的音乐,列表点击播放,进度与音量可直接拖动 | 板子自己起 HTTP 服务,同一网络里用浏览器打开 | 触屏键盘输入,板内命令行 | 用 iced 自带控件写的计算器 |
| 时钟 | 概览 | 电池(电量日志) | 文件管理 |
| :---: | :---: | :---: | :---: |
|
|
|
|
|
| 一枚表盘带秒针,下面是大号数字时间与日期;同一个磁贴里还有番茄钟与闹钟两页 | 五张卡一屏:电池、无线网络、存储(内部 + 存储卡),下面还有内存与服务器;右上角是「重新检测」 | 页头写着「电池」,磁贴上叫「电量日志」;顶部是过热警示条,下面电量卡 + 放电曲线,充电中不给剩余时间估算 | 内部 / 存储卡两个卷,路径与目录项,底部五个动作:新建文件夹、重命名、复制、粘贴、删除 |
| 天气 | 信道地图 | 游戏 |
| :---: | :---: | :---: |
|
|
|
|
| 打开先按 IP 定位到城市(页眉写着地名,这张是广州),再取**当地**的现在与未来几天;「数据来自 Open-Meteo」是服务名 —— 它不要密钥也不要账号 | 十三个信道的压力图,加一条建议信道(这里是 6);下面还有信号分布(每行 10 dBm)与网络列表,5 GHz 照列但不参与算术 | 一个磁贴打开的是**游戏列表**,点进去才是棋盘;加第二款游戏不用再占一个桌面磁贴,每局用开机时长当种子 |
---
## 🚀 快速上手
### 1. 拿到代码
```bash
git clone https://github.com/HWYWL/HiveBox.git
cd HiveBox
```
上游框架组件(`ui-framework/`)在本仓库里是普通目录,会跟着主仓库一起下来,不需要 `--recurse-submodules`。上游项目本身见 [pomelo-ui](https://github.com/pomelos-on-sale/pomelo-ui)。
### 2. 在 PC 上跑起来(不需要板子)
```bash
cd pomelo-apps
cargo run -p app-launcher # 整机界面:桌面、状态栏、应用切换
cargo run -p calculator # 单个应用
```
Q 键为返回键,W 键为退出键。读数来自桌面模拟后端(`pomelo-hal/src/sim/`),所以不接板子也能看到完整界面。
### 3. 刷到板子上
板子:**微雪 ESP32-S3-Touch-AMOLED-2.16**。
#### 3.1 直接刷现成固件(推荐:不用装 ESP-IDF 和 Rust 工具链)
到 [**Releases**](https://github.com/HWYWL/HiveBox/releases/latest) 下载 `hivebox-firmware-*.bin`,用 USB 连上板子,然后:
```bash
pip install esptool
esptool --chip esp32s3 --port COM3 --baud 921600 write_flash 0x0 hivebox-firmware-<版本>.bin
```
- **烧到 `0x0`**:这个 `.bin` 是**合并镜像**(bootloader + 分区表 + 应用),一条命令写完,不用管偏移。
- **同一页挂着的 `hivebox-ota-*.bin` 不要下载了往 `0x0` 刷**:它是**应用镜像**,一份槽里的东西,没有 bootloader 和分区表,写进 `0x0` 板子起不来。它和 `bin/manifest.json` 是给**板子自己下**的 —— 「更新」应用里点「固件」那一行的「安装」,它会把这个镜像写进另一个固件槽,重启就换过去了。
- **刷机本身不碰你的数据**:数据分区从 `0xC20000` 才开始,而合并镜像只覆盖 `0x0` 到约 `0x5C0000`(两个固件槽里也只写了 `ota_0`)。
- **但 `v0.9.0` 这一版要重新连 Wi-Fi**:内置存储从 `0xB00000` 挪到了 `0xC20000`(它前面多了一个 6 MB 的固件槽),新的文件系统是空的 —— Wi-Fi 密码、服务器列表、音乐设置、存在内置存储里的文件都要**重配一次**。它们没被删,只是换了位置之后不再被读,但对你的体感是一样的。**以后不会再有这种事**:分区表安定下来了,之后都是普通更新。SD 卡完全不受影响。
- 想回到出厂状态,先 `esptool --chip esp32s3 --port COM3 erase_flash`,再刷一次。
- `--port` 换成你自己的串口:Windows 是 `COM3` 这类(设备管理器 → 端口),Linux 是 `/dev/ttyACM0`,macOS 是 `/dev/tty.usbmodem*`。
- 正常情况下 esptool 会用 DTR/RTS 自动让板子进下载模式;连不上时按住 **BOOT** 键再插 USB。
#### 3.2 从源码编译烧录(改代码时用)
先装工具链:
- **ESP-IDF v6.1**(或兼容版本)
- **Rust Xtensa 工具链**:`cargo install espup && espup install`
然后是:
```bash
cd pomelo-firmware/firmware
./flash.sh # Linux/macOS:默认串口 /dev/ttyACM0,结尾会进 monitor(Ctrl+] 退出)
./flash.sh COM3 # Windows:把串口当第一个参数传进去
```
`flash.sh` 在找不到 `idf.py` 时会去 source `$IDF_PATH`(默认 `~/.espressif/v6.1/esp-idf`)的 export 脚本;ESP-IDF 装在别处就先 `export IDF_PATH=...`。不想用脚本也可以直接:
```bash
idf.py -p COM3 flash monitor # 编译 + 烧录 + 串口日志
idf.py -p COM3 app-flash # 只写应用区(0x10000),日常改代码够用
```
### 4. 第一次开机
按下 **PWR** 键开机。开机后进入桌面,左右滑动翻页,点击应用图标打开应用,左按钮最小化应用,右按钮退出应用,中键长按关机。
---
## ✨ 功能与硬件支持
### 1. UI 框架与图形特性
| 特性 | 状态 | 说明 |
| :--- | :---: | :--- |
| **Iced 组件** | ✅ 已支持 | 按钮、滑块、Canvas 自定义画布、布局容器等 |
| **中文字体** | ✅ 已支持 | 思源黑体烘焙子集,离线位图蒙版,零矢量排版开销 |
| **系统图标** | ✅ 已支持 | Material Symbols 图标字体封装,强类型 `const` 常量 |
| **软件光栅化** | ✅ 已支持 | 纯 Rust RGB565 引擎(`pomelo-gfx`),原生直写 16 位帧缓冲 |
| **脏区呈现** | ✅ 已支持 | 局部损坏区域追踪(Damage Tracking),静态界面 0 重绘 |
| **PC 端运行** | ✅ 已支持 | 桌面窗口模拟运行与调试(Q 返回,W 退出) |
### 2. 硬件封装与 UI 接入
> 底层驱动由 ESP-IDF 提供,本项目负责封装为 Rust `pomelo-hal` Trait 并接入 UI 呈现与交互。
| 模块 | 状态 | 说明 |
| :--- | :---: | :--- |
| **屏幕** | ✅ 已接入 | 脏区局部刷新,DMA 切片传输 |
| **触摸** | ✅ 已接入 | 点击、滑动交互,系统统一事件队列驱动 |
| **Wi-Fi** | ✅ 已接入 | 热点异步扫描与连接,状态栏/设置页显示 IP 与信号强度 |
| **电池** | ✅ 已接入 | 实时电量百分比、电压监测、充电状态与电源键事件 |
| **音频** | ✅ 已接入 | I2S PCM 流式播放,支持音量调节 |
| **按键** | ✅ 已接入 | 物理按键捕获,支持桌面翻页、退出应用与关机 |
| **RTC 时钟** | ✅ 已接入 | 关机继续走时,自动同步 RTC 时钟与 POSIX 时钟 |
| **IMU 传感器** | 🚧 待接入 | HAL Trait 及模拟器就绪,硬件驱动待接入 |
| **麦克风** | 🚧 待接入 | 音频采集接口与录音待开发 |
| **TF 卡** | ✅ 已接入 | SDMMC 1-bit 挂载 FATFS,设置页显示容量、已用与剩余空间 |
| **蓝牙** | 🚧 待接入 | 硬件具备,协议栈暂未启用 |
---
## 🏷️ 版本号规则
版本号形如 `v0.1.1-20261010`:
| 位置 | 含义 | 什么时候 +1 |
| :--- | :--- | :--- |
| 第一位 `0` | 大版本 | 重大更新,**慎之又慎**:换架构、换上游基线、破坏兼容的量级才动 |
| 第二位 `1` | 功能版本 | 新增了功能 |
| 第三位 `1` | 修复版本 | 修 bug、补漏洞、优化实现或文档 |
| `-` 之后 | 构建日期 | `YYYYMMDD`,这一版固件编译的日子 |
**设置里的「固件版本」就是这个 tag。** 镜像的版本号取自 `git describe`(ESP-IDF 的默认行为,本项目没有另外指定):在打过 tag 的提交上构建出来的固件,版本就是 tag 名;在 tag 之后又提交了几次再编译,`git describe` 会在后面缀上「距离该 tag 有几个提交」和提交短哈希,形如 `v0.1.1-20261010-3-g16414fb`,工作区有未提交改动时还会再带一个 `-dirty`。
**界面上只显示版本号**(`v0.1.1-20261010`)。中间那段提交信息属于*构建*,不属于版本:它留在启动日志和网页管理页里给开发看,而界面这一行旁边就是「编译时间」,已经回答了「这是什么时候编的」。`-dirty` 保留 —— 它不是「离 tag 多远」,而是「这份代码根本不是 tag 那份」,藏掉它等于替一个没人发布的镜像自称版本。想让自己编译的固件也显示成版本号,就在要发布的那次提交上打个 `v*` 的 tag。
**版本串本身必须装得进镜像**:`esp_app_desc_t.version` 只有 32 字节,装不下的部分是被悄悄剪掉的 —— 而被剪断的版本串界面认不出来(收窄靠的是结尾那几个标记),只好整串照画。所以写进镜像的写法由 `pomelo-firmware/firmware/CMakeLists.txt` 定:哈希取最短的四位,实在还长就只留 tag,`-dirty` 永不省略。
发布是自动的:[`.github/workflows/release-firmware.yml`](.github/workflows/release-firmware.yml) 在推 `v*` tag 时编译固件、生成合并镜像、把 `docs/releases/.md` 当发行说明挂到 Releases;它还会核对「镜像自报的版本 == tag」,不一致就让构建失败。
---
## 🕒 版本时间线
本项目按日期记录。每条是从 `git log` 里挑出来的一句话——「改了哪一页、哪条读数」这类细节以提交信息为准,这里只留对人有用的一行。新的一天加在最上面,**最近的一天直接展开,更早的日期默认折叠、点标题展开**;每次发布在 [**Releases**](https://github.com/HWYWL/HiveBox/releases) 里留一版带 tag 的固件(刷法见「快速上手」3.1,版本号读法见上一节)。
> 维护方式:改完一批东西,在最上面那一天的列表里加一行;跨天的改动另起一个日期。写「做了什么、对用的人意味着什么」,不写「改了哪个文件」。
### 2026-10-20
- **服务器能自己加机器了**:列表每行右边多了「编辑」与「删除」,右上角「添加」按下去弹出的是一张带键盘的表格(地址、端口、账号、密码),保存后写进板子自己的 `nas.conf`;编辑一台机器时密码留空就是不改它。键盘画在包自己的画布上 —— ABI 里没有「请给我一个输入框」这一条,能画的只有矩形与文字。顺带查清了上一版那颗「添加」按不动的原因:它摆在 y=22,而屏幕最上面 48 像素归外壳的「顶边下拉任务列表」,落在那里面的手指应用一个也收不到 —— 这一版的控件都让到了 56 以下。三件事各要一条新服务(`hb_nas_remember`、`hb_nas_forget`、`hb_nas_terminal`),所以 **ABI 1.5 → 1.6**,要刷固件;服务器包 v0.2.1 → v0.2.3。
- **详情页的「终端」交给了终端应用**:原来那一页自己开 shell,现在它只递一句「开在哪台机器上」,整块屏幕、键盘和回滚都归终端应用 —— 密码从板子那份清单里取,不经过应用的边界。
- **商店的图标方回来了**:`iced` 的 `center_x(填满)` 连宽度一起改了,于是 40×40 的方块被摊成160 宽的长条、字形跟着变形 —— 改用 `center_x(固定 40)`,现在跟桌面磁贴一样是四方的。
- **天气的温湿度与风速不是 0 了**:Open-Meteo 的 `current_units` 在 `current` 前面、键名一样(值是 `"°C"`、`"%"`、`"km/h"`),原来读到的正是那一个单位;现在先定位 `current` 那一段再读。
- **服务器恢复成三页**:列表(哪几台机器、状态点、右上角「添加」)→ 详情(六格读数、右上角「终端」)→ 终端(真开会话、四个快捷命令)。这要一条新服务:`hb_nas_devices` 把板子配置里的 `host` 与 `user` 递出来(密码不过边界),所以 **ABI 1.4 → 1.5**,要刷固件。
- - **十二个应用包把内置版的样子找回来了**:页头(各应用自己的图标与自己的主色)、带图标的列表行、空态、页脚说明 —— 沙箱里原来只有 `rect` 和 `text`,内置版那些控件一个都画不出来,所以先给 `app-sdk` 补了 `icons` 与十个控件,再改页面。
- - **功能也补回来了**:音乐能拖进度(`SEEK`)也能拖音量了,并且能翻页(原来第二屏之后的歌点不到);信道地图的柱子改成按 20 MHz 重叠加权,并给出建议信道(只在 1、6、11 里挑),列表改成完整的三十二个;天气按 WMO 码给图标、最高温橙最低温蓝、失败之后多一句「该怎么办」;电量日志多了「记一次」和「还能撑多久」;游戏可以在棋盘上划一下,最高分存进包自己的文件。
- - **包版本 v0.1.0 → v0.2.0**:商店的「更新」靠版本号认新包,不动版本号,装过的人永远看不到这些页面。
- - **还没回来的先在页面上写明**:文件管理的新建 / 重命名 / 复制、任务详情的逐任务表、电量日志的芯片温度、服务器的磁盘容量与读写速率 —— 这些要等 ABI 补上宿主服务,不是包能自己画的。
- **应用不再编译进固件,改成商店里装的包**:十二个 `.app` —— 游戏、电量日志、任务详情、信道地图、天气、服务器、云服务、文件管理、音乐、网页管理,加上之前的计算器与时钟。装完立刻出现在桌面,不用重启;卸载后磁贴与最近列表一起消失。
- **桌面只剩四个磁贴,商店排在第一位**:设置、终端、更新跟在它后面。一台没装应用的盒子,第一个问题是「应用从哪来」,而那个答案该在拇指不用找的地方。
- **固件瘦了**:应用镜像 6,150,144 → 5,781,552 字节,6 MB 的槽里空出 510 KB(原来只剩 141 KB);十个应用的 crate 从固件里删掉 —— 能装上去的就不该再带一份。
- **宿主补了两个服务**:列目录(`FILES`)与网页服务的开关(`WEB`),ABI 次版本 1.3 → 1.4。列目录拒绝一切带 `..` 的路径:那不是一个能改成可看样子的路径,而是一口回绝。
- **发布链接上了应用那一环**:`publish_bin.py --apps bin/apps.json` 在 CI 里也得带,否则仓库清单里没有 `apps` 数组,一台刚刷完的盒子在商店里什么也装不上。
- **发布 `v0.11.0-20261020`**:这一版把「应用是可装卸的包」这件事做完,动的是版本号第二位。刷机方式没变(合并镜像写 `0x0`);第一次开机只有四个磁贴,应用去商店装(要联网),或者把 `.app` 拷到 `/sdcard/apps`。已装的应用在 internal 分区,OTA 不会清掉。
### 2026-10-16
- **新应用「更新」**:启动器里第 15 个磁贴。一页两行(固件、字库),每一行把「**机器上装的**」和「**仓库里有的**」摆在一起;点「检查」就去把仓库里那份清单取回来。取回来的是人话而不是版本号:`已是最新` / `有新版本 v0.9.0-20261016` / `仓库里没有这一项` / `网络不通,过会儿再试`。
- **这一版还不能在板子上装**:两个「安装」按钮画的是「**这一步还没接通**」—— 下载还没有后端(把文件拉下来、写进另一个槽、让它启动,这一整套还没写)。所以这一页现在负责「**知道**」,不负责「**装**」,要装还是走数据线。
- **flash 重新分成两份固件槽**:原来只有一份 `factory`(10.94 MB),**装不下第二份** —— 而更新这件事的前提正是「写进另一份,写坏了还能退回原来那份」。现在是 `otadata` + `ota_0` / `ota_1` 各 **6 MB**:这份固件 5.63 MB,放进 6 MB 还剩 390 KB(6.2%)。`otadata` 空白时芯片启动 `ota_0`,所以第一次开机和以前没有区别。
- **内置存储 5 MB → 3.88 MB**:两个槽的每一字节都是从它身上让出来的。
- **内置字形表从 4013 字裁到 528 字**:24 px 那张表 **2.0 MB → 234 KiB**,而**源字体 `…-Subset.otf` 也从 1.78 MB 降到 195 KB** —— 镜像里两样都带着(源字体给 iced 排版,烘好的表给设备查表),按同一份字单裁。两样加起来省下约 **3.3 MB**,固件从 **8.93 MB** 降到 **5.75 MB**,这才塞进了 6 MB 的第二个槽。这 528 字不是随手挑的,是**扫整个源码树里真正写过的汉字**得出的(`tools/scan_charset.py`),所以界面本身一个字都不会缺。**代价**是源码之外的字 —— SD 卡里的中文文件名、Wi-Fi 名、服务器返回的中文 —— 会画成一个**方框**。以后补字有两条路:改了源码就重扫重烘重刷,或者把一张更全的表放到 SD 卡上(开机取最新那张,表越大开机占的内存越多)。
- **启动器两列改三列**:应用 14 → 15 个,排成三页(6 / 6 / 3),图标 160 → 144 px。
- **存储页的地图上现在有两份固件**,中间还夹着一小块 **56 KB 的空**:app 分区必须从 64 KB 边界开始,而分区表、NVS、`otadata`、PHY 结束在它下面 56 KB,中间那块既不能给固件也不能给文件系统 —— 空着也是事实,所以画出来。
- **发布 `v0.9.0-20261016`**:把上面这一批一起交出去。这一版**板子上的应用变了**(多了「更新」),所以动的是版本号第二位。**刷机方式变了一次**:合并镜像照旧写到 `0x0`,但**分区表换了位置,内置存储里的东西要重配**(见「快速上手」3.1)—— 这是最后一次。
- **卡上的字体包:屏幕上的文件名、Wi-Fi 名不再画方框**:开机从 SD 卡读**一对**文件 —— 字体(`/sdcard/fonts/hivebox-face-*.otf`)与照它烘的表(`/sdcard/fonts/hivebox-font-24px-*.bin`),两个都读到才拿它画字,缺一个就退回内置那份。这是上一版「内置字形表裁到 528 字」的代价的解药:内置那份只认识源码里写过的字,而卡里的文件名、路由器名字、服务器返回的中文都不在源码里。
- **表靠哈希认领自己的字体**:表头多了烘它那份字体的 SHA-256(前 16 字节),渲染器拿它与手里那份字体比 —— 以前判的是「这是不是镜像里那一份」(指针比较),卡上来的表在那个判断里没有归属。所以**换表救不了内置子集没有的字,得连字体一起换**:表的字形号是那份字体的字形号,不是通用的。
- **成套的包优先于更新的孤表**:孤表(只有表、没有配对字体)是照镜像里那份字体烘的,把它交给卡上的字体,等于把另一套字形号递过去,渲染器会拒收;被拒之后每个字都要现场光栅(约 1.6 ms/字形)。所以这是**正确性规则而不是偏好**。
- 字体与表**各 3 MiB 上限**,读进内存后一直留着(渲染器按引用持有)—— 8 MB 的 PSRAM 装得下这对 1.91 MB + 2.27 MB 的包。
- **更新页的「字库」那一行现在真能装了**:点「安装」先问卡在不在,再从仓库把表拉下来写进 `/sdcard/fonts`,带进度(百分比 + `84 KB / 228 KB` —— 数字回答的是「它到底动没动」,那是等人时唯一能看出没死的东西),下载中可以取消;写完核对大小,拿到网页或半个文件就删掉 —— 文件名就是开机认表的方式,留一个半截的在那儿,下次开机就会拿它去画字。
- **下载进度是新的 HAL 接口**:`HttpProgress` 与 `hal_http_get_progress()`,两边都只在请求在飞时才有意义(没有请求时读到的是上一次的余数,画出来就是一根已经走完的进度条)。
- **清单地址与文件地址从同一处推出来**(`Release::url`):写成两个字符串就会各自漂移,而漂移的样子是「清单指着一个不存在的文件」。
- **「固件」那一行还是「这一步还没接通」**:字库那行做的是「下载 + 写文件」,固件那行要的是「写进另一个槽 + 重启」,那是另一件事,这一版没做。
- **卡上的兜底字符集:GB2312 的全部可显字符 + 源码里画过的字**,生成它的过程也写下来了(`tools/gen_charset_gb2312.py`;以前是「文件生成了,但没说怎么生成」)。**补了「雲」**(U+96F2)—— GB2312 一级汉字里只有简体「云」,繁体不在里面,这类字没有批量来源,在脚本里显式列一行。
- **重烘的卡上字体包 v3**:24 px 表 4536 → **4537** 个字形、零缺字(字体 1.91 MB + 表 2.27 MB;烘出来在 `ui-framework/pomelo-font/dist/card/`,发布出去的那一份在 `bin/`,Releases 页上也挂着)。两个文件一起拷进 `/sdcard/fonts`,重启就用上了,不用重刷固件。
- **发布 `v0.10.0-20261016`**:把上面这一批一起交出去。这一版**板子上的应用变了**(更新页的字库那一行能在板子上装了),所以动的是版本号第二位。**刷机方式没变**:合并镜像写到 `0x0`,分区表没动 —— 你原来的 Wi-Fi、文件与服务器列表都还在。
- **卡上字体包也进发布面了**:固件、字库、这一对,由 `publish_bin.py --card-dir` 一次归置、与 Releases 一起发出去 —— 之前它只在源码树里(`dist/card/`),想用的人得自己在仓库里翻。
- **更新页的「字库」那一行现在能装下一个「包」**:清单里 `font` 的 `"file"` 是表、旁边的 `"face"` 是烘出这张表的那份字体(`publish_bin.py --card-dir` 写的就是这个形状)。点一次「安装」下**两个**文件,**先字体、后表** —— 表是开机扫的那一件,所以它是把这一对变成字体的那一下。中途断了、点了「取消」、或者第二件没下来,卡上都不会留下一对错的:先落下的那份字体没有表就什么都不是,开机照旧用原来那份。所以**不用再手动拷贝**了:板子自己就能把 v3 那一对下到卡上,重启就用上。
- **`bin/card/` 拆了,那一对平铺进 `bin/`**:清单里的名字既是下载地址也是卡上的文件名 —— 更新页把一个名字既当 URL 又当落盘路径,带目录的名字会被写进 `/sdcard/fonts/` 的子目录,而开机只扫这一层。发布脚本因此把这张表认成「清单里 `font` 那一项」的两种形态之一(单独一张表,或者包里的表),谁发出去就留谁。
- **不再发布那张烘自内置字体的表**:它和镜像里已经带着的那张一个字节都不差,装它等于什么都没装 —— 而更新页那一行会一直对着它喊「可更新到 v1」。这正是这次「把卡上的字体删了、检查更新却显示可更新到 v1」的根源:清单把镜像自带的那张表当成了更新。
- **装完会问一句「要不要重启」**:装好那一屏不再只报「已更新到 v3」,下面摆着两个按钮 —— 「**稍后**」与「**立即重启**」,点「立即重启」板子当场重启,不用去摸电源键。这是这一页**唯一**一个画两个按钮的地方,因为也只有这一句是真的在问:文件已经落在机器上、但还没被用上(界面画的是**开机**读到的那张表),所以「现在重启」和「回头再说」是两件不同的事。
- **发布 `v0.10.1-20261016`**:把上面这一批一起交出去。这一版是把已经有的那件事**接通**(字库那一行从「能装一个文件」变成「能装一整对」)并修掉一处骗人的显示,属于修修补补,所以动的是版本号**第三位**。**刷机方式没变**:合并镜像写到 `0x0`,分区表没动,Wi-Fi、文件与服务器列表都还在。刷完进「更新」→「检查」,「字库」那一行会指出 v3,点「安装」下到卡上,装完点「立即重启」(**这两步都不用刷机**)。
- **更新页的「固件」那一行现在也能装了**:点「安装」,板子自己从仓库把**应用镜像**拉下来、写进**另一个**固件槽,装完点「立即重启」,新固件就是它了。手感与字库那一行一致:有进度(百分比 + 已下字节)、可以取消,中断、拔电、半截的请求都不会让那个槽变成可启动的 —— 只有整个请求下完、写完、`esp_ota_end` 收下了,槽才算数。刷机走数据线那条路没变(见「快速上手」3.1)。
- **发布面一次出两份固件,清单指的是新那一份**:`bin/hivebox-firmware-*.bin` 是**合并镜像**(bootloader + 分区表 + `otadata` + 应用),给用数据线刷 `0x0` 的人;`bin/hivebox-ota-*.bin` 是**应用镜像**,也就是在线更新要写进一个槽的那份,`manifest.json` 的 `firmware.file` 指的是它。两份不能混:槽里放的从来不是一整片 flash 的开头,把合并镜像塞进去 `esp_ota_end` 会拒收,而那时槽已经被写过了。
- **写坏的固件会自己退回去**:v0.9.0 把 flash 分成两个槽时就说了「写进另一份,写坏了还能退回原来那份」,但那时回滚并没有开 —— 新固件起不来,板子就停在那儿,还是那句话,只有前半句是真的:现在两半都成了(`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y`)。新固件写完是**待验证**状态,只有它自己说一声「我起来了」才算站稳,而那一声在**第一帧画到屏上**那一刻说(`board_display.c` → `board_hal_confirm_boot()`)—— 到那一刻为止,一次更新能弄坏的东西都跑过一遍了:板子起来了、字库在它该在的地方、渲染器画得出、屏把这一帧收下了。在那之前崩了、起不来,下次复位 bootloader 就把**原来那一份**装回来。用数据线刷的那份不在此列:合并镜像里的 `otadata` 是空的,bootloader 自己就认它有效,所以刷完开机不会等任何人。
- **发布 `v0.10.2-20261016`**:把上面这一批一起交出去。这一版把更新页已经画出来的那一行**接通**、又给固件更新加了一道防线,没有新开一页、没有加磁贴,所以动的是版本号**第三位**。**刷机方式没变**:合并镜像照旧写到 `0x0`,分区表没动,Wi-Fi、文件与服务器列表、卡上的字体包都还在。**但 bootloader 换了**(回滚要它),所以这一版得**用数据线刷一次**,刷完就能在板子上把整套走一遍:「更新」→「检查」→「安装」→「立即重启」,**这三步之后都不用再插线**。
- **启动器一页八个磁贴**:三列两行改**四列两行**,图标 144 → 92 px。一页从六个位置变成八个,两个磁贴因此从第二页挪到了第一页 —— 分页点还是两个。
- **两个磁贴换了短名字**:「任务查看器」改成「**任务详情**」,「服务器监控」改成「**服务器**」。「服务器」这一页自己的标题也跟着改了。名字变短不是口味问题:磁贴上的名字只有**一行**,放不下就被截断,而四列之后一个磁贴只放得下**四个汉字** —— 这两个名字原来都是五个字,一直在屏幕上画成「任务查…」和「服务器…」,看不出来是因为以前三列宽得能藏住。以后往目录里加名字,得先数一数。
- **「文件管理」进了第一页,「计算器」让出了位置**:第一页现在收在「任务详情 / 服务器 / 文件管理」这三张上 —— 都是「关于这台盒子本身」的(它在跑什么、旁边那台机器、这两边的存储)。八个位置装不下九个想上第一页的应用,让位的是计算器:它是这一屏上**唯一一个和盒子无关**的应用,也是以前一页六个位置时排在末位的那一张。它现在开第二页。
- **设置页「固件版本」那张卡片不再把版本号画到卡片外面**:版本号是一串**没有空格**的字符(`v0.10.3-20261016` 十六个字符约 197 px),一行放不下就换行,而卡片的高度原来只按**两行**算(68 px 的内容装进 88 px 的卡)—— 多出来的那一行于是被顶出卡片:上半截把「固件版本」几个字顶上卡片上沿,下半截(`16`)画在卡片下边缘外面。卡片现在按**三行**算(118 px),芯片型号那张留着第三行空着 —— 两张一样高才是一排卡片,空的这一行是它该付的。
- **发布 `v0.10.3-20261016`**:把上面这一批一起交出去。这一版换的是磁贴的名字与所在页、又修好一处画到卡片外面的读数,属于修修补补,所以动的是版本号**第三位**。**刷机方式没变**:合并镜像照旧写到 `0x0`,分区表没动、bootloader 没换,Wi-Fi、文件与服务器列表、卡上的字体包都还在 —— 也可以不插线,在板子上「更新」→「检查」→「安装」→「立即重启」。
- **两页磁贴按用途重排了一遍**:第一页「音乐 / 网页管理 / 服务器 / 文件管理」与「天气 / 游戏 / 设置 / 更新」,第二页「终端 / 任务详情 / 时钟 / 电量日志」与「计算器 / 信道地图」。八个位置一页没变,分页点还是两个 —— 变的是同一排上谁挨着谁:管这台盒子的(它在跑什么、旁边那台机器、两边的存储、它自己的设置与更新)留在第一页,看板子外面的世界与板子自己的读数往后放。
- **「概览」这个应用删掉了**:它最后一版已经是五项里四项和「设置」页重复的读数,「任务详情」又把它最有用的一项做得更细。删掉之后第一页正好留出一个位置给「文件管理」。
- **安装固件时那十秒的空白没有了**:点下「安装」到进度条开始走之间原来要空十秒左右 —— 时间花在**擦除**上,而且花在下载之前:一个槽必须先擦干净才能写,镜像五点七 MB,`esp_ota_begin` 会一口气用 64 KB 的块擦掉九十来块,串口上什么都没有、进度条停在 0% 不动,看起来像死机。现在擦除交给**每一个将要写入它的 `esp_ota_write`** 去做(`OTA_WITH_SEQUENTIAL_WRITES`:`esp_ota_begin` 在这一版里什么都不擦),写入按 **64 KB 整块**走而不是按 socket 的 2 KB —— 因为 `esp_flash_erase_region` 只在拿到**对齐的一整块**时才用块擦除命令,而一次块擦除的开销约等于十六次扇区擦除。于是这十秒被摊进了那几十秒的下载里:网络的每一段在读,flash 在下一段之前擦好,第一块字节到得比原来早十秒,整个安装也没有变慢。镜像比槽还大这种错仍然在**第一个字节之前**报出来(`OTA_WITH_SEQUENTIAL_WRITES` 自己不查长度,所以这条判断挪到了我们这边)—— 「仓库发布的是一个给两倍 flash 的板子用的镜像」是人能处理的,写到分区尾端才失败则不是。
- **发布 `v0.10.4-20261016`**:把上面这一批一起交出去。这一版删了一个应用、重排了两页磁贴,又挪走安装固件前的那段空白,属于修修补补,所以动的是版本号**第三位**。**刷机方式没变**:合并镜像照旧写到 `0x0`,分区表没动、bootloader 没换、镜像仍然只覆盖到约 `0xAC0000` 而数据分区从 `0xB00000` 才开始,Wi-Fi、文件与服务器列表、卡上的字体包都还在 —— 也可以不插线,在板子上「更新」→「检查」→「安装」→「立即重启」。
2026-10-14
- **送显不再每帧拷贝整屏**:原来每次 `flush` 都先把画面整屏拷进一块 PSRAM 影子帧,再从影子帧切条带、换字节序、发给屏 —— 满屏一次 `flush` 是 **44.6 ms**,其中这次拷贝独占 **20.9 ms**。现在直接读合成器自己那块帧缓冲,影子帧与那半兆 PSRAM 一起去掉,满屏 `flush` 降到 **23.7 ms**。
- **字节交换内核改成一次读四个字再一起写回**:原来的循环「读一个字、换、写回」读写串行,同一时刻只有一个读在途,PSRAM 每次读那十几个周期全在空等;展开到四个字后同一个内核从 **31.8 MB/s 提到 38.8 MB/s**,满屏这一段从 18.76 ms 降到 **16.68 ms**。展开到八个字反而掉到 21.8 MB/s —— Xtensa 只有 16 个通用寄存器,八个字加两个指针放不下,值溢出到栈上,多出来的访存比省下的等待更贵,四个字是寄存器装得下的宽度。两件事都写进了注释。
- **量出来的一件事:跨步访问不花钱**。竖切条带时每行是从 480 宽的画面里抠 32 个像素、下一行跳 960 字节 —— 看着很伤缓存,实测与按地址顺序读只差 **7%**。慢不在访问顺序上。
- **QSPI 时钟 60 → 80 MHz**:整屏 460.8 KB 的传输从 **15.3 ms 降到 11.5 ms**。80 MHz 是 SPI 主机的上限;板间有差异,出现杂点或断列的板子把这一行注释掉、退回 60 MHz 重编。
- **任务查看器多了「占用率」一列**:每个任务显示自己占了多少 CPU(一位小数),数据来自 FreeRTOS 运行时间统计(`CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y`,代价是每次任务切换多读一次计数器)。**`IDLE0` / `IDLE1` 这两行就是两个核各自还剩多少** —— 空闲任务是核上没别的事可跑时才跑的那个,它的占比就是那个核的空闲;没有这个计数器的板子这一列画 `-` 而不是 0(0 会被读成「什么都没干」)。桌面模拟端同样有这一列,占比随时间增长、加权和为 100。
- **界面主任务固定到 CPU1**:IDF 默认把 `app_main` 放在 CPU0,而 Wi-Fi 与 TCP/IP 也在那儿 —— 一帧和一包网络数据排在同一个核上,网络突发会落在重画中间。搬到 CPU1 的依据是另一个核本来就空着(`IDLE1` 常年 99.9%),而这句话现在能在任务页里直接看到。电台留 CPU0,面板拿 CPU1。
- **字体补了一个字**:`雲`(U+96F2,`云` 的繁体)。字表 4012 → 4013,24 px 那张表大约多 506 B。
- **发布 `v0.6.0-20261014`**:把上面这一批一起交出去。这一版**板子上的应用变了**(任务页多了一列),所以动的是版本号第二位。**刷机方式没变**:合并镜像写到 `0x0`,镜像只覆盖到约 `0xABFF00`,而数据分区从 `0xB00000` 才开始 —— 你原来的 Wi-Fi 与文件都还在。
- **「服务器监控」的设备列表好按了**:每一行的编辑与删除原来是两个小图标,**字形多大点击区就多大**,手指压上去压不压得中要看运气;现在是两个文字按钮(`编辑` / `删除`),约 59 × 32 px,按下时底色填充、文字反过来。那一行也从一个「大按钮」拆成三个各管一段的按钮:左边那半点进监控页,右边两个分别是编辑与删除 —— 按钮套按钮时,一次点击归谁要看平台先走到哪一个,这不该由运气决定。
- **每一行都能直接编辑**:点某一行的「编辑」打开的是**那台机器**的表单(地址、账号、密码已填好),不用再点进监控页、再从右上角找铅笔。保存的规矩不变:同一地址再存一次是更正而不是新增。
- **删除变成「先问一句」**:点「删除」不再当场删,列表底部原来放「添加设备」的位置换成一排确认(`确定删除 192.168.1.10 ?` + `取消` + 红底 `删除`)。这一行放在一张用拇指滑动的列表上,一次没按准的滑动就可能删掉它,而被删掉的是一个地址、一个账号和一个**没有第二份的密码**。问句出现时那台机器**仍然留在屏幕上**(问句里的地址就是拿来和它对一眼的东西);「取消」也是画出来的按钮,而不是「点别处就算取消」—— 这一页的「别处」是另一个设备行,而那一行本身也是按钮。
- **那个问句记的是地址而不是行号**:列表会变 —— 在表单里删掉一台机器,下面每一行都会往上挪一格,问句若记的是行号,挪完之后被删的就是另一台。有一条单测盯着这件事:先问第二台,再从表单删掉第一台,最后确认,走掉的必须是第二台。
- **表单的输入框不再「超出边界」**:三个输入框是整页宽的,而 iced 把滚动条画在内容**上面**(不占布局),导轨正好压住输入框最右边约 10 px 的边框与圆角,看起来就像框子跑出了屏幕。这一页的滚动条改为不画(**拖动照旧有效**),输入框恢复满宽与两侧圆角;代价是少了「下面还有内容」那一点提示 —— 三个字段比键盘上方那点空间高,密码要往上拖一下才看得全,而这一点改之前也是如此。
- **发布 `v0.7.0-20261014`**:把上面这一批一起交出去。这一版**板子上的应用变了**(设备列表多了每一行的编辑与删除,且删除要先确认),所以动的是版本号第二位。**刷机方式没变**:合并镜像写到 `0x0`,镜像只覆盖到约 `0xABFF00`,而数据分区从 `0xB00000` 才开始 —— 你原来的 Wi-Fi、文件与服务器列表都还在。
2026-10-13
- **新应用「天气」**:打开先按 IP 定位到你在的城市(标题栏写着地名),再去取**当地**的预报,一屏三段(现在:温度、天气、湿度、风速;未来几天:今天 / 明天 + 后面三天)。两个服务都不要账号与密钥(ip-api 定位、Open-Meteo 预报),板子插在广州报的就是广州 —— 原来这里写死了北京,一块插在别处的板子报的是别的城市的天。**每一种失败都单独说清是哪一种**:没有连接到网络 / 时钟还没有校准 / 没有服务器回应 / 请求超时 / 安全连接失败 / 返回内容超出上限,各自配一句该怎么办(「请在设置里打开 Wi-Fi」「先校准时间,再试一次」)—— 报一句「加载失败」等于把排查的活儿推给了看屏幕的人。
- **点开天气就死机重启,是两个真 bug 撞在一起**:C 侧 HTTP 客户端在「连接打不开」的分支里把同一块内存**释放了两遍**(堆被写坏,之后跳到野函数指针,串口里是一句 Guru Meditation),同时 HTTPS 握手会向内部 SRAM 要缓冲区,而那里只剩约 33 KB(报 `MBEDTLS_ERR_SSL_ALLOC_FAILED`)。重复的那次释放删掉了,TLS 缓冲区改从 8 MB 的 PSRAM 拿;真机上连着开关五个应用、开合天气两次,没有再重启。
- **新应用「信道地图」**:2.4 GHz 十三个信道画成一张**压力图**,并给出一条**建议信道**。算法不是「数一数每个信道有几条网络」,而是每条网络按 20 MHz 掩码摊到相邻信道、再按信号强度加权 —— 只数精确匹配,会让「隔壁有一条大嗓门」的信道看起来是空的,而那正是让人搬错家的那个判断;建议在 1、6、11 里挑互不重叠的那条。下面还有信号分布图(-40 到 -100 dBm,每行 10 dBm)与网络列表,5 GHz 的网络照列但在算术之外(信道 37 上的一条网络占不了信道 1 一点地方)。扫描是**异步**的:打开问一次、按钮再问一次,扫描那两秒里页面照常画、写着「正在扫描…」。
- **新应用「游戏」**:一个磁贴打开一张游戏列表(第一款是 2048),滑动合并数字,拼出 2048 还能接着玩,分数、胜负、重开都在同一页;屏幕下方就是四个方向键 —— 触屏没有键盘可借,游戏就得把方向键画出来。每一局用**板子的开机时长**当种子,所以每次打开都是新的一局。
- **板子学会了「自己上网」**(HAL 第十个后端 `HttpBackend`):`start` → 轮询 `state` → `take_result` 三步,和 Wi-Fi 扫描同一个形状;请求是一次阻塞的 socket + TLS 握手,所以它跑在**自己的任务**里(自己的栈),界面永远不会被一条网络请求停住,同一时刻也只有一条请求在飞 —— 第二条拿到的是「忙」,不是悄悄插队。证书用镜像里的根证书包校验,响应体上限 16 KB,太大是一种明确的失败而不是读到一半崩掉。
- **板子会自己对时了(SNTP)**:连上 Wi-Fi、拿到 IP 的那一刻开始对时,时间落进**硬件 RTC**(`board_rtc.c` 一直拦着 `settimeofday`,断电再开也还在)。有个前提值得知道:HTTPS 证书的有效期是拿系统时钟比的,一块从没对过时的板子(时钟停在 1970)握手必然失败,而它看起来和断网一模一样 —— 这一种失败被单独拎出来叫「时钟还没有校准」,页面会直接说「先校准时间,再试一次」,而不是让人去查 Wi-Fi。
- **桌面从十一个应用变成十四个,三张桌面翻成四张**(4 / 4 / 4 / 2)。新磁贴的天空蓝、青绿、街机金是色相上剩下没被占的三处空档,游戏那个是唯一一个「不是读数」的颜色;信道地图收在第三页(它看的是板子周围的空气),天气与游戏在第四页(板子管不着的那两件事)。
- **电量日志、概览、信道地图右侧的方角修好了**:这三页的滚动条是画在内容**上面**的(iced 的浮动滚动条不占布局),轨道正好压住每张卡片最右侧约 10 像素,于是左边圆角、右边被切成方角、还一直顶到屏幕边缘。三页都改成不画滚动条(终端、设置、音乐播放器一直如此),卡片恢复满宽 + 两侧圆角;信道图第 13 信道与「20 MHz 重叠模型」的标注也完整了。
- **发布 `v0.4.0-20261013`**:把上面三个新应用(天气、信道地图、游戏)与 HAL 的 HTTP、SNTP 两个后端一起交出去。这一版是**功能版本**,所以动的是版本号第二位。
- **「游戏」里多了第二款:贪吃蛇**:16×16 的格子,吃到苹果长一节、也快一点 —— 每步从 220 ms 起,每吃一个少 7 ms,到 90 ms 为止(不加速就只是一局越玩越长的游戏,没有下限则还没看清就结束),撞墙或撞到自己这一局结束。它和 2048 用的是**同一张 304 px 的棋盘**(16 格 × 19 px,格子之间不留缝 —— 蛇是一具身体,不是一列方块),所以换游戏时换的是玩法、不是版面。开局种子照旧是**板子的开机时长**,打开就是新的一局。
- **蛇可以用「倾斜机身」来转向**:把哪一边压低就往哪边走(读的是加速度计里那点方向分量,阈值约 20°,也就是重力在那一轴上的 0.35)。**平放就是不转**,所以用方向键玩的人不会被「上一次的倾斜」带着走。这个读数是**先除以自己的模长**再看方向的,驱动报 g 还是 m/s² 都成立;它只在一局蛇开着的时候读,2048 不动它 —— 那个游戏是有人动它才动,为一张不变的棋盘每秒重画六十次没有意义。
- **界面字号从「四档可调」收敛成一档**:设置里原来能选 18 / 20 / 24 / 30 px,而每一页的每一个尺寸都是从**同一个基准**推出来的 —— 四档只是让同一张页面在四个大小之间缩放。现在基准就是一个数(`FONT_BASE`,24 px),设置里那一行也删了。跟着删掉的是 18 px 与 20 px 两套**预烘字形表**(在构建机上先把字形光栅成位图,设备查表命中就不用现场画):设备端查表是**拿字号逐位比对**的,同一时刻只有被选中的那一档会命中,另外两张表一直躺在 flash 里没人读。
- **省下来的空间给了「你能写的那一半」**:应用分区原来为了装下三张表开到 12 MB,现在收窄到 **10.94 MB**(固件本身 9.08 MB,还留约 1.9 MB 余量),加上芯片尾部原本闲置的 0.94 MB,一共 2 MB 全部并给数据分区:**`internal` 从 3 MB 变成 5 MB**。整张 16 MB 的 flash 现在一点不剩 —— 分区表最后一行刚好顶到芯片末尾,概览里那张存储图在桌面上也与板子上一致了。
- **分区表换掉之后,还留着一批旧数字**:设置页的模块文档、分段条的单测与 HAL 的 flash 布局单测里都还是 `0xC00000` / `0x300000` / `0xF0000` 那一套,存储页的集成测试则直接断言旧的「未分配」尾段 —— 它是红的,意味着这批改动之前从没被整套跑过。现在全按新表算;顺带的好处是那个「零字节的区段也必须拿到一份,否则布局引擎会把整条 bar 发给它」的规则,因为尾段真的变成了 0,第一次被真正测到。
- **发布 `v0.5.0-20261013`**:把第二款游戏(贪吃蛇与倾斜转向)和这次字号收敛一起交出去。这一版**加了功能**,所以动的是版本号第二位。**刷机方式没变**:合并镜像写到 `0x0` 即可,而镜像只覆盖到约 `0x8E0000`、数据分区从 `0xB00000` 才开始 —— 你原来的 Wi-Fi 与文件都还在。
2026-10-12
- **设备侧 SSH 真的接通了**:板子上那句「本版固件还没有 SSH 客户端」的占位换成了 Rust 写的 SSH 会话(`sunset`,替换原来的 libssh C 库),板子现在自己登录那台服务器,把 CPU / 内存 / 网络 / 磁盘读出来。移植路上撞到两个会真正让功能失灵的问题:会话线程 32 KB 的栈在内部内存里已经挤不出来(改从 PSRAM 拿),而这块芯片明令禁止**栈在 PSRAM 的任务碰 flash** —— 连读都不行,LittleFS 连 `fopen` 都会走到那条路,于是主机密钥的读写全部搬出会话线程:开机由主线程读一次进内存镜像,会话线程只查镜像,变了再由主线程落盘。
- **「服务器监控」变成多台设备**:进应用先看到**设备列表**(每行一个地址、一个账号,以及这台机器最后一次的状态),点某一行才进那台机器的监控页;底部可以**直接在板子上添加设备** —— 地址、账号、密码三个字段配面板自带键盘,同一地址再存一次是更正而不是新增;监控页右上角可以编辑,表单里可以删除。配置从「一台」变成「一份列表」,已经配好的旧文件仍然能读。
- **网页管理页的服务器表单改为追加**:填一台加一台,不再覆盖掉已有的那台;「停止监控」相应改成「清空监控列表」。
- **「NAS 监控」改名为「服务器监控」**:它看的是一台能 SSH 的机器,未必是 NAS。
- **运行时间不再被屏幕圆角切掉**:页眉是最上面一行,改为和桌面状态栏一样退到切角之外。
- **概览从四张卡变六张**:多了「存储」(容量合计 + 盘数)与「磁盘繁忙」(最忙的那块盘),卡里的内容改为垂直居中 —— 四张卡各占半屏、底下大半空着的样子没有了。
- **硬盘卡片的大号数字改为读写合计**:原来只画「读」,而那台机器的读全部由 page cache 消化、可以长时间是 0,于是这一格看起来像坏了;现在合计在上,`读取 / 写入` 分行在下。
- **「硬盘」选项卡点得动了**:它原来是个 `container`,没有 `on_press`,手指按上去什么都不会发生。
- **设备列表直接说「在线」还是「离线」**:每台机器右边一个点、一句话,绿的在线、红的离线。它不再写「多少秒前」—— 那回答不了「现在连得上吗」。板子自己去敲每台机器的 SSH 端口(只开一个 socket,不登录、不开会话),所以离线是现在时;列表开着时每十秒过一轮,点进某台机器立刻让位给它。
- **点开一台机器,CPU 与网络不再要等十几秒**:它们要两次采样才算得出速率,而原本第二次要等一整个轮询间隔、每次采样还都是一次完整的 SSH 会话(板上连接 + 登录约 3 秒),于是这两格长时间是空的。现在一台机器的**第一次**读取在同一个连接里连着采两次,间隔取那台机器自己的开机时长之差 —— 点开约五秒就有数。
- **硬盘表的盘名不再和容量挤在一起**:型号放不下就按列宽截断、没有型号就不放(eMMC 那种盘不再留一个孤零零的 `·`)。原来按「16 个字符」截,而那一列只有 138 px,于是文字换行、和右边一列互相压住。
- **读不到数时画的是短横,不再是方框**:面板字体的子集里没有 `—`(U+2014),画出来是空心方框;六张卡与硬盘表统一改用 `-`(U+FF0D,同宽同高)。
- **「固件版本」真的只剩版本号了**:镜像自己的版本字段只有 32 字节,`git describe` 的完整写法(`v0.1.1-20261010-16-gf7c258c-dirty` 是 33 字符)装不下,被剪成 `…-gf7c258c-dir`;而界面是靠结尾那几个标记才能把它收窄成 tag 的,被剪断的尾巴一个都认不出,只好整串照画。现在写进镜像的版本串自己就装得下,设置里显示的就是它基于的那次发布。
- **发布 `v0.2.0-20261012`**:设备侧 SSH 与多台设备一起交出去,这是「服务器监控」第一次在板子上真的能用;合并镜像挂在 Releases,一条 `esptool` 命令刷进去。
- **桌面背景改成纯黑,图标改成饱满的色块**:这块屏幕装在一个黑色边框里,原来那层近黑的蓝底会把边框的边线画在自己身上(屏幕的圆角尤其明显)—— 现在底色是纯黑,边框与屏幕连成一片,屏幕上只剩磁贴本身。磁贴也从「近黑的方块 + 白色字形」变成**饱和色块 + 白色字形**(音乐玫红、网页管理蓝、终端绿、设置灰、计算器琥珀、任务查看器青、服务器监控紫),一眼认得出哪一个;按下时改为混入一点白(原来的 ×1.35 会把饱和色顶成灰)。任务查看器、网页管理、音乐播放器、服务器监控的页面底色一起换成同一片纯黑,**设置应用里的卡片与导航栏也一并变黑**(分组交给那根细线,不再靠一层灰底 —— 一屏有一半是灰矩形,正是打开设置时看到的东西)。(设置里「壁纸」那一行照实改了 —— 它不再自称 macOS Sierra。)
- **发布 `v0.2.1-20261012`**:把上面这套外观交出去。`v0.2.0` 的 tag 是在改外观之前打的,所以那一版镜像里还是旧配色;这一版是修复与优化,只动第三位。
- **网页控制台修了几处「得先点一下」**:「网络」页一打开就自己扫描(并记住上次结果,按钮用于重扫),每信道驻留从驱动的默认值收到 60–90 ms,实测约 2.4 秒出 7 个网络;「控制」页一打开就取回屏幕画面,不用先点「截屏」;新增独立的**「服务器」页** —— 列出设备上已保存的每一台(地址、账号、端口,**不含密码**),可以单条移除,也可以继续在这里添加。列表就是设备「服务器监控」应用读的那一份,两边改的是同一个文件。
- **网页的画面镜像不再被裁掉**:480×480 的面板被按卡片宽度放大(约 688),而 `.stage` 的 `62vh` 上限把底部裁了 —— 屏幕上最下面一排按钮就藏在「截屏 / 熄屏 / 响一声」后面。现在镜像按「装得下」定尺寸:宽度与同一个 `62vh` 上限两者取小,任何窗口比例下都完整可见(实测 819×780、1440×900、390×844、1024×620 四种都不裁切)。触摸换算本来就是按信箱式算的(`scale` + 居中偏移),所以点击位置依然精确到像素。
- **网页控制台再修四处**:①「网络」页不再把同名接入点合并 —— 扫描到几条就画几行(实测 9 条记录、5 个不同名字,原来只画 5 行,看起来像「剩下的藏起来了」,而那一页其实是能滚的);②「控制」页那块「屏幕」卡不再永远卡在骨架屏 —— `/api/display` 的路由写成了 `HTTP_GET | HTTP_POST`,而在 ESP-IDF 里 `httpd_method_t` 是枚举(`1|3 = 3 = HTTP_POST`),页面用 GET 读状态永远拿到 405;③在画面镜像上点按或拖动之后会自己取一张新画面(终端已经执行了、网页还停在旧画面);④「服务器」页每行多了铅笔:改地址 / 账号 / 密码,密码留空就是不动它 —— 同一地址再存一次从此是**原地替换**,端口和密码都保留,不再往文件里堆第二份。
- **发布 `v0.2.2-20261012`**:把上面这批网页端的修复与优化交出去(镜像完整显示、点按后画面自动更新、网络列表一条不落、服务器可编辑)。这一版只动网页与它的 C 侧接口,板子上的应用没有变化。
- **新应用「时钟」**:一个磁贴里三页 —— 表盘(指针 + 日期)、番茄钟(一圈倒计时)、闹钟(列表)。切页不打断番茄钟;番茄钟按**板子的开机时长**计时,退出应用再进来剩下的时间还在走(按 `Instant` 算就做不到了)。板子**没对过时**的时候表盘不画指针、读数写 `--:--:--` —— 给一块不知道几点的板子画 12:00 是替它编一个事实。闹钟列表存在 `AppData/CLOCK/alarms.txt`,铃声开机合成一次。
- **新应用「概览」**:五张卡(电量、无线网络、存储、内存、服务器)一屏看完 —— 想不起是哪一项不对时打开这一页就够。每秒刷新,但**两个慢问题不是每秒问一次**:卡槽里有没有卡是一次 SDMMC 事务,隔壁那台机器是一次 SSH 登录,它们只在打开页面和点右上角「刷新」时发起,答案下一拍取回(按钮写「正在检查」,因为屏幕上那几个数此刻确实还是旧的)。内存分**内建堆**与 **PSRAM** 两个池子,不混着算。
- **新应用「文件管理」**:上面一排**卷**(内置分区 / TF 卡),下面当前目录。**一次点选、再点一次进入** —— 触屏没有双击可借,而「选中但没进入」正是重命名需要的那个状态。四件事:新建目录、重命名、删除、复制/粘贴。读走 `std::fs`(终端与音乐播放器一直这么读),写走后端 —— 见下一条。
- **存储层补上「写」这半边**:`StorageBackend` 新增 `write_file` / `append_file` / `remove_file` / `rename`。原因是这台板子上 **`std::fs` 能读、能 stat、能列目录、甚至能建目录,但建不了文件**(`File::create` 与每一种 `OpenOptions` 都返回 `ENOENT`,当初 Wi-Fi 凭据就是这么丢的),所以写入统一走后端的 `fopen`。**跨卷改名**(内置 ↔ TF 卡)是复制 + 删除:两个驱动之间没有 inode 可搬,复制没走完就把目标删掉,**目标的名字上永远不留半截文件**,原文件只在副本完整关闭后才删。网页控制台的「移动」也改用同一个实现 —— 同一件事写两遍,就是两个地方可以把这件事弄错。
- **新应用「电量日志」**:**放电曲线**(每分钟一个点,画最近 120 分钟)+ **预计剩余时间**(按当前放电速度,旁边同时写着那个速度)+ **芯片过热警示条**(盖在读数上面而不是塞进卡片里)。充电中、电量已空、曲线平或上升时**不给估算** —— 估算是对「正在放电」的陈述。**每小时往 `AppData/POWER/history.csv` 追加一行**(追加而不是重写:否则每加一行都要把它前面的每行读回来再写回去,成本随文件变老而涨),页面上另有「立即记录」按钮,因为等一小时才知道盘有没有写成太久了。写盘失败不静默:错在哪一步写在日志卡上,没写成的行也不计入行数。
- **桌面从七个应用变成十一个,两张桌面翻成三张**(4 / 4 / 3)。新磁贴各占色相上剩下的一处空档:时钟玫红、概览青柠、文件管理棕、电量日志紫;电量日志用趋势线而不是电池图标 —— 概览那个磁贴已经是电池了,两个电池并排就是要人读两遍才分得清。
- **发布 `v0.3.0-20261012`**:把上面四个新应用(时钟、概览、文件管理、电量日志)与存储层的写操作一起交出去。这一版是**功能版本**,所以动的是版本号第二位。
2026-10-10
- **电池改为自己画**:电量数字写进电池形状里面,不再跟在数字后面的那个 `%`;充电改用一支闪电,不再借绿色电池表示「正在充电」。
- **顶边下拉的后台面板**:从屏幕顶边往下拉,列出正在运行的应用(最近用过的排在前面),每张卡右侧一个 ⊗ 可以单个结束,标题栏右侧可以一次全部清除。它是盖在桌面上的一层,收起时不会结束任何应用。
- **固件版本带上编译时间**:读数面板的页脚多了「编译时间」,和「系统时间」「已启动」排在一起——三条都来自镜像自己,从编译到开机到现在。
- **系统信息的操作系统行**改为一句话说明来源:基于 pomelo-ui 深度定制;镜像自己的名字与版本移到了同一页的「固件版本」行。
- **网页管理页的「浏」不再是方框**:字体子集补入该字并重烘字形表,页面上「启动后即可在浏览器中打开」整句可以正常显示。
- **首次对外发布**:README 换成自己的仓库地址、补上真机预览图与「下载后怎么刷」的说明;固件以合并镜像的形式挂到 Releases,一条 `esptool` 命令即可刷进板子。
- **发布流程自动化**(`v0.1.1-20261010`):新增 GitHub Actions —— 推一个 `v*` 的 tag,CI 就用 ESP-IDF v6.1 与 Xtensa 工具链编译、生成合并镜像、把 `docs/releases/.md` 当发行说明挂到 Releases,并核对「镜像自报的版本 == tag」,不一致直接构建失败。README 补上「版本号规则」,同时去掉一个指向不存在文件的英文 README 链接。本版没有固件功能改动,属于修复与流程优化,所以只动第三位。
- **应用进出有了过渡动画**:点图标打开从右侧滑入,返回键或左边缘返回向右滑出,底部上滑回桌面向上滑出 —— 240 ms、`EaseOutCubic`,与翻页同一套手感。滑动期间应用画在桌面之上,而屏幕本身在「手势决定」的那一帧就切换,所以状态栏、后台列表与退出键的行为一点没变:动的只是画面。
- **固件版本只显示版本号**:读数面板的固件卡与系统信息页原来显示一整串 `v0.1.1-20261010-3-g16414fb`,现在只显示 `v0.1.1-20261010`(也就是它基于的那个发布),旁边的「编译时间」回答「这是什么时候编的」。`-dirty` 仍然显示,因为那表示这份代码不是 tag 那份;启动日志与网页管理页里的完整版本串不变。
- **返回与回桌面改成「跟手」**:从左边缘往右拖、从底部往上拖,应用跟着手指走;拖到一半松手时,按「够远或够快」决定它走还是回来 —— 够远约屏幕的三分之一,够快是 450 px/s(和翻页同一个速度阈值)。拖动期间不误伤列表滚动:只有从边缘那一圈开始的拖动才会被认领,页面中间的拖动仍然是页面的。
- **后台面板也跟着手**:从顶边往下拖,面板跟着手指下来;往回推它就回去,甩一下则判定 —— 判定与返回/回桌面共用同一套(够远 35%、够快 450 px/s)。面板已经打开时,从底部往上拖同样能把它推回去。点遮罩或按返回键收起面板也变成滑动,不再"啪"地消失。
- **手势变得好触发了**:判定门槛回到改造前的水平 —— 拖过屏幕 **96 px**(原先跟手版本要 168 px)或甩到 **250 px/s**(原先要 450 px/s)就算数。另外手势改按「手指从哪条边落下」判定,不再按起手那十几像素的方向:拇指从底边往上滑时带一点侧向弧线,以前会认不出来,现在照旧算「回桌面」。
- **边缘带从 24 px 放宽到 48 px**:这是手指**落点**必须在其中的范围。480 px 对应约 39 mm,所以 24 px 只有 1.9 mm —— 比指尖的接触面还窄,得刻意瞄准才落得进去,手势自然「难滑」。48 px 不到 4 mm,接近手机的边缘带。判定距离同时改为按「手指走过的距离」算(把起拖那 18 px 补回来),于是「一次 96 px 左右的滑动」和改造前一样好用。
- **新应用「任务查看器」**(桌面第二页):一眼 `htop` —— 顶部两条内存计量条(内建堆与 PSRAM),下面一张任务表,列出调度器正在跑的每个任务的**状态、优先级、剩余栈、所在核心**,正在画这张表的那一行带 `*`;每秒刷新一次。数据全部来自板子:`uxTaskGetSystemState` 读任务(为此在 `sdkconfig.defaults` 里打开 `CONFIG_FREERTOS_USE_TRACE_FACILITY`),PSRAM 是新加的一项独立读数(不与内建堆相加 —— 那两个池子混在一起会让「堆快满了」看起来像「还剩三分之二」)。
- **任务查看器:列对齐修好了,标签换成中文**。第一版的表头是**单独排版**的,其中「任务」那一格宽度写成了 0,于是表头全挤在左边、数字全靠在右边 —— 一眼就能看出错位。现在**表头与每一个数据行都由同一份列定义(`columns`)生成**,对齐在结构上不可能再漂移。
- **任务查看器的标签跟着系统语言走**:列头 `任务 / 状态 / 优先级 / 空闲栈 / 核心`(英文界面下是 `htop` 的 `TASK S PRI FREE CORE`),状态列一个汉字 `运 / 阻 / 挂 / 退`(英文下是 `R / S / T / Z`,正是 `htop` 打的那四个),计量条 `内存 / 外部内存`(英文 `MEM / PSRAM`)。页眉还加了 `htop` 顶端那行里最有用的一条:`任务 14 · 运行 3`。
- **「核心」被滚动条压住 → 给列表留出净空**:iced 把滚动条画在内容右边缘*之上*,而最后一列正落在那里 —— 板上「核心」的表头只剩一个「核」。列宽改成按「面板 − 页边距 − 滚动条」计算,并写进自检:一种字号(24 px)× 两种语言,每个表头与它下面最宽的值都要装得进自己的列,剩下的宽度还要够放最长的任务名(`board_pwr_mon`,13 字符)。这条自检的量法是**照着截图量的**(方块字 1 em、小写拉丁 0.5 em),不再只算加法。
- **删掉表底那一行**:`每秒刷新 · * 是正在画这张表的任务` 占一行,却没说表格说不出的东西(刷新看得见,`*` 就在那一行上)。
- **网页管理:开机即启用 HTTP 服务器**。原来要进应用按「启动服务器」才开始服务,而这个页面的用处正是**不用走到板子跟前**就能打开 —— 所以改为在启动流程里起动(`rust_main`,和 WiFi 自动连接同一处),应用从此只是那台服务器上的一个开关:进应用可以关掉它,重启前不会自己再开。**没有密码这件事没变**:从开机那一刻起,同一网络里任何人都能读写板子上的文件、改它的网络 —— 这句话现在写在启动处、模块文档和页面上。
- **新应用「NAS 监控」(第一段:数据层与界面)**:目标是把家里那台 NAS(飞牛)的状态放到这块屏上 —— CPU、内存、网络、磁盘读写与每块盘的容量/温度/繁忙度/用途。传输方式定为**板子用 SSH 登录 NAS 拉数据**(NAS 侧不需要装任何常驻服务)。这一段落地的是**不需要 NAS 的部分**:HAL 新增第九个后端 `NasBackend` + `NasReading` 类型 + 桌面模拟实现(会答、会拒、会保留上次读数),凭证文件 `/internal/AppData/NAS/nas.conf`(与 `wifi.conf` 同构,含主机密钥指纹 TOFU),以及一个完整的 iced 应用(概览四张卡 + 硬盘表,中英双语,桌面 `cargo run -p nas-monitor` 可看)。设备侧的 SSH 客户端(libssh2 + mbedTLS)与启动器磁贴是下一段。
- **一处纠正**:上一轮谈方案时我说「app 分区 12 MB、当前只用了 2.1 MB,加 SSH 毫无压力」—— 那是把 flash 日志里的*资产偏移*当成了镜像大小。真实数字是 **firmware.bin 11.2 MB / 12 MB,只剩 1.35 MB(11%)**。加 libssh2(约 150–250 KB)仍然放得下,但余量并不宽裕,字体与资产再长就要动分区表了。
- **NAS 监控的解析层(不需要 NAS 就能测穿的那一半)**:设备侧 SSH 之前先做最容易悄悄坏掉的部分。C 侧只负责「开会话、跑一条命令、交出纯文本」,解析在 Rust(`pomelo-hal/src/nas_sample.rs`)—— 因为解析出错时是「开始输出垃圾」而不是报错。一条命令一次往返(SSH 的开销在往返),命令用 `=== 名字` 标记每一段,缺段就是缺一项读数而不是解析失败;`Sampler` 把累计计数器变成速率(间隔取两次采样**到达**的时间差,不是请求间隔),**第一次采样没有速率**、**新插入的盘前后取同一份**(差为 0,而不是拿开机以来的总量除以间隔)。写这一层抓到两个真 bug:`lsblk` 的型号里有空格(改用 `-P` 成对输出 + 名字前缀判断分区归属)、首次采样把开机总量当成了速率。
- **NAS 监控接上桌面**:目录第七个磁贴(`NAS 监控`),桌面第二页因此有三个。**只在它作为当前页时才推进时钟** —— 一次查看是到另一台机器的网络往返,没人在看的页面没有理由去敲隔壁机器的门。板上现在是「能看到但还连不上」:设备侧后端仍是诚实占位,这一页显示「本版固件还没有 SSH 客户端」。
2026-10-09
- 设置页顶部新增**系统信息读数**:内存占用、芯片温度、存储占用三个环,芯片型号与固件版本两张卡,以及时间与运行时长。
- 系统信息页的固件行改为读**镜像自报**的名字与版本,不再写字面量(此前写的是 `Pomelo OS v0.2.0 (Build 2026.09)`,一个并不存在的镜像)。
- 桌面只留五个应用,删掉示例应用并重排两页。
- 音乐播放器:进度条与音量条可以直接拖动;列表点击才播放;音量与 Wi-Fi 一样记住。
- 网页管理端:板级 HTTP 服务(网络自检、遥控触控、日志、试听喇叭)与配套的 `web-manager` 应用(文件/网络/设备三页签)。
- 列表轻触不再被误判为滚动(新增 18px 触控阈值),行内按钮恢复可点。
- 字体子集补入「渲」,设置页「渲染器」不再画成方框;修复画布:开放路径不再被强行闭合(圆环曾被画成楔形)。
2026-10-08
- **起点**:以上游 [pomelo-ui](https://github.com/pomelos-on-sale/pomelo-ui) 提供的框架为基础,导入完整的 Rust ESP32-S3 UI 工程(`ui-framework/` 保留上游全部组件)。
- Wi-Fi 凭据改为经 C 侧真正落盘,无线开机默认开启,烧录不再覆盖数据分区。
- 手机式边缘手势:底部上滑回桌面,左右边缘滑动返回上一页。
- 设置页新增重启;电池页温度改为真实 PMIC 读数。
- 存储页展示整块 16 MB 闪存与分区;修复按钮点击后的残留高亮。
---
## 🎨 iced 框架移植(`ui-framework/`)
> 这一层来自上游 **pomelo-ui**,本项目按需做最小的改动(例如字体子集的补字)。改动同样记录在上面时间线的日期里。
- **`ui-framework/iced`**:针对嵌入式环境定制的 iced 分支,移除了 Xtensa 架构不支持的 64 位原子操作与 `mmap` 系统依赖。
- **`ui-framework/iced-pomelo-winit`**(包名 `iced_winit`):嵌入式平台适配层,负责事件循环派发、RGB565 帧缓冲呈现、脏区追踪以及字体加载。
- **`ui-framework/iced-pomelo-gfx`**:iced 渲染后端实现,负责将应用界面的绘制命令录制为渲染批次供平台层回放。
- **`ui-framework/pomelo-gfx`**:面向 16 位色彩原生面板的高性能纯 Rust 软件光栅化引擎,支持几何图形、抗锯齿描边与字形蒙版绘制。
- **`ui-framework/pomelo-font`**:嵌入式字体工具链与资产,提供中文字体子集化及预烘焙位图字形表。
- **`ui-framework/pomelo-material-symbols`**:Google Material Symbols 图标字体库封装,提供类型安全的图标常量定义与字形映射。
---
## 🙌 欢迎发起 Issue 和提交 Pull Request
欢迎为本项目贡献代码、扩充组件、优化性能或修复问题。提交步骤:
- 创建 Issue 描述问题或计划添加的新特性
- Fork 仓库并创建新分支
- 本地开发与测试(用项目自己的测试套件:`cd pomelo-apps && cargo test --workspace`)
- 提交 Pull Request 并关联对应 Issue
---
## 🤖 AI 编写声明
本项目所有代码(底层驱动、UI 框架与应用程序)全部由 **AI 编写生成**。受限于生成机制与验证深度,代码质量可能存在以下问题:
- **功能不全与边界异常**:部分功能实现可能不够完备,在边界情况或非预期输入下可能表现异常。
- **潜在 Bug 与稳定性隐患**:缺少系统性的自动化测试覆盖,可能潜藏未被发现的逻辑缺陷或崩溃风险。
- **性能与资源开销未调优**:代码未做深度的性能分析与优化,可能存在内存浪费或运行效率低下的问题。
- **可读性与可维护性不足**:多轮增量生成缺乏顶层长远规划,可能存在架构松散、逻辑冗余及规范不统一的问题。
> **免责声明**:本项目仅作为实验与技术探索,请自行承担用于生产环境的风险和后果。
---