王智刚

反射屏 + 板上 web server

Misc 4 分钟
目录

上一篇解决了”能编出 bin”。这篇说两件互相绑定的事——怎么把 300×400 单色屏排满有用信息,以及 怎么让板子自己当 web server,浏览器直接看屏 / 改配置 / 听录音。

反射屏:不是墨水屏,但接近

板子上是 ST7305 反射式 LCD,4.2 寸、300×400、双稳态:

  • 无背光——靠环境光反射成像,桌面 / 户外都能看,黑暗里完全黑屏(这反而是优点,桌面摆件晚上不晃眼)
  • 类墨水屏观感,但刷新比 EPD 快得多(10ms vs 700ms)
  • 1-bit per pixel:要么黑要么白,没有灰阶

物理是竖屏 300×400,软件按 Waveshare 官方 landscape 公式映射成 400×300 横屏,GRAM 布局不变,15KB framebuffer 跑 MALLOC_CAP_DMA 直送 SPI DMA:

pub const WIDTH: u16 = 400;
pub const HEIGHT: u16 = 300;
pub const BUF_LEN: usize = (WIDTH as usize / 2) * (HEIGHT as usize / 4); // 15_000

信息密集仪表盘

400×300 不大,但 1-bit 单色屏密度可以拉满。主页 Dashboard 一屏塞了 12 项信息:

dashboard

布局思路:

  • 顶部 日期 + 星期 + 电源源(USB / Battery)一行带过
  • 中段大字钟logisoso58 字体,几乎占半屏,一眼就是它
  • 温湿度大数字 + sparkline 60 点历史曲线(左温右湿),瞥一眼能看趋势
  • 底部进度条 APP 分区使用率 / SRAM / PSRAM 三条横条
  • 页脚 uptime + reset reason + IDF 版本 + WiFi RSSI + IP

按 BOOT 或 KEY 切到第二页 GitHub 页,从 GraphQL contributionsCalendar 拉真热力图:

github

热力图按真实 weekday 对齐(凌晨 1 点 commit 别画到错位的日子里),最近 28 天压缩成 4 列;右侧 28-day snapshot 是 commits / active days / streak / PR / unread notifications。下面一行 LATEST 事件 + 相对时间(“10m ago”)+ commit hash,再下面是未读列表。

整套 UI 用 embedded-graphics + u8g2-fonts 画,单色屏不需要抗锯齿/混合,绘制逻辑就是一堆 Rectangle::new().fill_solid()Text::new(),~1300 行 ui/mod.rs 搞定两个页面 + 配网页 + splash 启屏。

板子自己当 web server

仪表盘搞完,发现一个痛点:调 UI 不可能凑过去看屏——尤其改字体大小、位置,每次烧完跑过去对一眼太低效。索性让板子自己起 HTTP server,把 framebuffer 推成图片,浏览器直接看。

graph LR A[屏渲染主循环] --> B[FrameBuffer 15KB] B --> C[ST7305 SPI DMA] B --> D[镜像到 SharedFb] D --> E["GET /screen.bmp"] E --> F[浏览器自刷新页] G[Settings 表单] --> H["POST /api/config"] H --> I[NVS 持久化] I --> J[SharedConfig RwLock] J --> A

实现就几十行:每次主循环 flush 完 framebuffer,把 15KB 字节 clone 到 Arc<Mutex<Vec<u8>>>;HTTP handler 读这块共享内存,编码成 1-bit BMP(带 14 字节文件头 + 12 字节 DIB 头 + 8 字节调色板)发出去。前端 HTML 内嵌一个 <img src=/screen.bmp> + setInterval 每秒 reload,肉眼看就是实时。

// GET / → 自刷新 HTML 页(内嵌 <img src=/screen.bmp>)
// GET /screen.bmp → 当前 framebuffer 编码为 1-bit BMP
// POST /next → 翻页触发(等同按 KEY)
// GET /settings → Tailwind 配置表单页
// GET /api/config → 当前 RuntimeConfig(JSON,token 已脱敏)
// POST /api/config → 更新字段,落 NVS
// GET /api/wifi → 已保存 WiFi 凭据 SSID 列表
// POST /api/wifi → 追加凭据 / 提升到 slot 0
// POST /api/reboot → esp_restart()

/system.html:硬件状态面板

主页镜屏只看得到 1-bit 的 LCD 内容,但板子内部状态远比这丰富——堆 / PSRAM / Flash / 录音存储 / 传感器 / 电池 / 时钟源。再起一个 /system.html,全用 Tailwind + Iconify 写成卡片式:

system desktop

8 张卡:Identity / Memory / Flash / Storage / Sensors / Power / WiFi / Clock。每秒 fetch /api/system 拿最新 snapshot。SRAM / PSRAM / App 分区都有横向进度条,颜色按使用率梯度(红/橙/绿)。

为啥要做手机端?因为板子放桌面摆件场景,懒得每次开 PC——掏手机扫 IP 直接看:

a315c6c96f1bedc8de21f7e71f84003e.jpg

/settings:运行时改配置

刷固件麻烦,能在线改的全做成运行时配置。15 个字段都进 NVS:GitHub user / token、refresh 周期、时区偏移、传感器校准、splash 动画次数、自动翻页周期……加 WiFi 凭据 CRUD。

桌面版:

settings desktop

手机版:

09ef73eec31d24702e972864f0523f0d.jpg

GitHub token “Discover” 按钮是个小巧思——粘进去原始 token 点一下,板子自己请求 https://api.github.com/user 反查 username 自动填表。

/logs:实时日志流

调错最实用的页。log_sinklog crate 的所有输出 hook 进一个环形 buffer,HTTP 端用 SSE 推过来:

logs

不用插 USB、不用开 espflash monitor,浏览器开着就行。手机也能看,半夜电池没电关机为啥能 grep 一下。

SoftAP + HTTP 配网:把 BLE 砍了

最初版本是 BLE 配网——esp-idf-svc 有现成的 BleProvisioner,跑通了,但发现两个问题:

  1. BLE 栈占内部 SRAM ~30KB,开了之后 heap_min_ever 直接逼 10KB
  2. 手机端要装专门 App(ESP BLE Provisioning),用户体验劝退

改成 SoftAP + HTTP 门户:板子开 CuriosityLab-Setup(open,无密码),手机连上浏览器打开 192.168.4.1 填表单,提交后切 STA。整个 BLE 栈砍掉,sdkconfig 一行 CONFIG_BT_ENABLED=n,SRAM 直接回血。

graph LR A[开机] --> B{NVS 有凭据?} B -->|有| C[扫 AP] C --> D[按 RSSI 排序] D --> E[逐个试连] E -->|成功| F[STA 模式] E -->|3 次失败| G[SoftAP 模式] B -->|无| G G --> H["HTTP 192.168.4.1"] H --> I[手机填表单] I --> J[提交凭据] J --> K[试连] K -->|成功| L[落 NVS] --> F K -->|失败| H

一句话总结

屏只有 400×300,但板子有 16MB Flash 和 8MB PSRAM——能跑 web server,能跑 HTTPS 客户端拉 GitHub 数据,能编码图像。桌面摆件不该只是显示,它本来就是一台塞在 4.2 寸壳子里的 Linux 机器(虽然跑的是 FreeRTOS)。

下一篇说 ES7210 双麦 + esp-sr:从手搓 VAD 到神经网络