把墨水屏便利贴改成 PC 和服务器的状态面板
手边有一块极趣实验室(ZecTrix)的 4.2 英寸墨水屏便利贴,主控是 ESP32-S3。我把它改成了一块状态面板,显示我的台式机和 VPS 的运行状态。整个过程由 AI 助手执行命令、读源码、写代码,我负责定需求、做决定和验收。下面是过程记录。
最终效果
- 两页内容:PC 页(CPU、GPU、温度等)和 VPS 页(资源占用和服务检查)。
- 默认页:PC 正在推送数据时显示 PC 页,否则显示 VPS 页。
- 按一下 OK 键切到另一页,10 分钟后自动回到默认页;再按一下可以提前切回。
- 刷新间隔:PC 在线时 2 分钟,离线时 15 分钟。图片内容没变时,设备不重刷屏幕。
硬件与工具链
- 插上 USB,设备是一个 COM 口,
VID_303A&PID_1001,即 ESP32-S3 原生的 USB-Serial/JTAG,不需要按键进下载模式就能连上。 - esptool 的
flash-id和get-security-info显示:ESP32-S3 rev v0.2,16 MB flash,内置 8 MB PSRAM,Secure Boot 和 Flash Encryption 都没有启用。也就是说可以自由刷写,ROM 中的下载模式可用,刷错了也能重来。 - ESP-IDF 的一个坑:PATH 里的 python 是 3.14,而本机只有用 3.11 建的 IDF 虚拟环境,
export.ps1因此失败。解决办法是设置IDF_PYTHON_ENV_PATH指向那个 venv,并把它放到 PATH 最前面。
方案:设备只负责显示
需求有四个:显示 PC 状态、监控 VPS、用上 Tailscale、用上麦克风和喇叭。取舍如下:
- 墨水屏的工作节奏是“深睡 → 醒来取一次数据 → 再睡”,最适合做纯显示终端,所有逻辑都放在服务器上。TRMNL 固件正是这个模式:设备定时拉取一张图片,我要写的只是服务端。
- Tailscale:ESP32 上有社区移植,内存也够;但设备每次醒来都要重新接入 tailnet,又慢又费电。所以设备走公网 HTTPS,Tailscale 只用在服务器之间和管理通道上。
- PC 数据不走 USB 串口,而是由 PC 通过网络推给服务端;USB 只负责供电。
- 麦克风和喇叭只有 xiaozhi 固件能用上,留作第二阶段。
- 社区固件中有一部分只支持 NOTE4C 版本,先行排除。
先读源码,再刷机
本机没有 git,就直接下载 TRMNL 固件 zectrix 分支的源码压缩包,先读源码,确认了几件事:
- 配网页面有 Custom Server 选项,保存为
api_url,替换默认的 trmnl.app。 - HTTPS 客户端调用了
setInsecure(),自签证书和纯 HTTP 都能用。代价是设备不验证服务器身份。 - 设备协议:
/api/setup、/api/display、/api/log,请求头带ID、Access-Token等。 - 只要返回的
filename不变,设备就不重新下载图片。所以服务端用图片哈希作为filename,内容不变就不刷屏。 - 更换服务器地址后,设备会自动清除旧的配对信息。
不需要改固件,于是直接用 release 中的预编译镜像,SHA-256 与发布说明一致。
备份出厂固件失败了:读到一半连接就断(esptool 报 “Packet content transfer stopped” 或 “No more data”)。改成 1 MB 分块加重试,16 块中也只成功了 11 块,缺的正好是 bootloader 和分区表所在的几块。我本来就不打算恢复出厂固件,所以没有继续;代价是出厂固件已经无法恢复。
刷机本身很顺利:erase-flash 用时 9.4 秒;在 0x0 写入合并镜像,校验通过(Hash of data verified)。
服务端和 PC 推送脚本
服务端 trmnl_server.py 只依赖 Python 标准库、Pillow 和 psutil:
- 访问控制:设备 MAC 白名单、设备 API key、PC 推送用的 ingest token。
- 把 PC 页和 VPS 页渲染成 PNG,文件名用内容哈希。
- 服务检查支持 http、tcp、systemd、docker 四种类型。
- 可选的内置 TLS。
PC 端的 pc_agent.py 从 psutil、nvidia-smi 和(可选的)LibreHardwareMonitor 读取数据,推送给服务端。
本地联调时遇到的问题:
- PowerShell 写出的
checks.json带 BOM;时区默认值不对。两处都已修正。 - 设备配网后显示 “not registered”。读源码才发现:配网页面要先点 Custom Server、再点 Yes,输入框才会出现。没点的话会提交空地址,设备回退到 trmnl.app,那边不认识这个 MAC,返回 404。重新配网后正常。
- 一个容易混淆的地方:服务端在本机跑的时候,“VPS 页”采集的其实是本机的数据。服务端采集的永远是它所在那台机器的状态。
部署到 VPS
我的 VPS 有一套运维约定:服务定义、密钥、反向代理配置各有固定位置,日常运维由服务器上的 agent 负责。所以流程是先读约定,再写任务书交给 agent 执行。
- 入口:一个子域名,由 Caddy 反向代理到服务端。泛域名 DNS 已经覆盖这个子域名,证书由 Caddy 自动签发,防火墙不用改,也不需要自签证书。
- 上传:先把换行统一为 LF,用 scp 传上去,再逐个比对 SHA-256,6 个文件全部一致。
- agent 按任务书执行,9 项验证全部通过。它有三处偏离任务书,都合理:任务书误以为 python3-venv 已经装好,它补装了;上传时目录权限设得太严,它把相关目录放宽到 755,而不是把服务用户加进别的组;服务器上没有
file命令,它改用 Pillow 检查图片格式。 - 之后又在本机独立复核了一遍。PC 推送用的 token 经 SSH 直接写入本机配置,不经过任何聊天记录。
OK 键:一段弯路
先从固件源码确认按键的分类(只在设备深睡时按下才生效):
- 短按:ShortPress。
- 0.8 秒内按两下,或按住 1–5 秒:DoubleClick。
- 按住 5–15 秒:清除 Wi-Fi,进入配网。
- 按住 15 秒以上:清除全部配对。
按键唤醒的请求带 Update-Source: EXT0 请求头;双击还会额外带一个 special_function: true 请求头。
第一版设计是短按刷新、双击切换页面,借用固件的 guest_mode 特殊功能实现。本地测试通过,部署后经公网测试,双击却不生效。
原因:直连服务端端口正常,经过 Caddy 就失效。Caddy 从 2.11.4 起在服务器层丢弃名字中带下划线的请求头,special_function 正好带下划线。这是一项安全改动:CGI 会把连字符映射成下划线,客户端发来的 Foo_Bar 可以冒充 Foo-Bar(FrankenPHP #2460)。2.11.6 增加了 expected_underscore_headers 选项,可以指定要保留的请求头(Caddy v2.11.6 发布说明)。
排查中出过一次错:AI 助手用一个临时 Caddy 实例测试 header_up 改名转发,测试只打印了请求头的名字,没有打印值,于是错误地得出“改名可行”的结论,并据此改了线上 Caddy,结果无效。后来改成直接回显请求头的值,才确认:经 HTTPS 访问时,无论 HTTP/1.1 还是 HTTP/2,后端都拿不到这个请求头。线上 Caddy 随后从备份恢复:先确认备份与 git 中提交的版本逐字节一致,再 validate、reload,最后检查各站点都正常。
最终回到我最初提的方案:按一下就切换。顺带做了两处改进:
- 切回默认页时清除倒计时。
- 响应中不再下发
special_function,以清掉设备上存着的旧值;同时总是带action: guest_mode,兼容仍存有旧值的设备。
本地和公网各测 7 项,全部通过。
Windows 端的长期运行
- 推送脚本和一个独立的 venv(只装了 psutil)放在用户目录下。
- 计划任务在我登录时以普通权限、用 pythonw 在后台运行,不保存密码;退出后 1 分钟自动重启,不会同时运行两份。
- Windows 上 venv 的 python 会出现两个进程(启动器和真正的解释器)。判断是否重复运行要看可执行文件路径,不能只看命令行,第一次就在这里判断错了。
- LibreHardwareMonitor 开启 Web 服务后,CPU 温度取 Core (Tctl/Tdie),主板温度取 Super I/O 芯片的一个温度通道。Windows 防火墙没有为它放行入站,所以它的 Web 服务只有本机能访问。
- 电脑重启后,Tailscale 服务停在 NoState,原因是托盘程序没有启动,SSH 因此不通。手动启动
tailscale-ipn.exe后恢复。
经验
- 先读源码,再动手。配网字段、filename 缓存、按键分类、特殊功能的处理路径,都是从源码里确认的。
- 代理会改写请求。直连测试通过,不代表经过反向代理也能通过,验证必须走真实路径。
- 测试要检查值,不能只检查“有没有”。这次就是因为只看了请求头的名字,导致一次无效的线上改动。
- Windows PowerShell 5.1 和 ssh:远端命令中的双引号和
|会被吃掉或拆开,经管道传给 ssh 的文本会被加上 BOM。最稳的做法是先写成无 BOM、LF 换行的文件,scp 上去再执行;git 提交信息也用-F从文件读取。 - 无法完整备份就刷机,要事先接受出厂固件无法恢复。
待确认和后续
- 下次重启后确认:计划任务是否在登录时运行;LibreHardwareMonitor 是否开机自启(否则 CPU 和主板温度会显示 --);Tailscale 托盘程序是否自启(可以在它的设置里打开 Run at startup)。
- 如果主板温度和 BIOS 显示的对不上,换一个传感器。
- 第二阶段:xiaozhi 固件加 MCP,用上麦克风和喇叭,复用现在的采集代码。