状态:固件 v0.1.0 已写好并烧录(2026-08-26),待现场配网与联调;服务端接口已加到共用后端,待部署
目标硬件:本仓库 PCB V2.3x/V2.4x + 4.2 寸 HINK-E04A13-A0(Z96)
服务端:server/(submodule → dashboard-backend,与 Kindle 看板共用采集器,新增 JSON 接口)
| 需求 | 方案要点 |
|---|---|
| 保留当前屏幕驱动 | 原样使用 GxEPD2_420_Z96.{h,cpp}(已从 flash 比对确认机器上是 Z96) |
| 时间、温湿度、电量逻辑与现有固件一致 | 直接移植 Clock_8025T.ino / Get_bat_vcc.ino 的逻辑,见 §5–§7 |
| 时间、温湿度每分钟更新 | 每分钟整分唤醒一次,只局刷顶栏(同现有时钟模式的整分对齐睡眠) |
| 看板数据由服务端 JSON 提供,本地绘制 | 每 N 分钟(默认 5)连 WiFi 拉 /api/eink.json,rev 变化才重画内容区 |
| 有变化才局刷 | 顶栏、内容区分别做区域局刷;文本没变的区域不刷;每小时整点一次全刷清残影 |
| 首次配网与现有一致 | 按住按键 3 复位进入配网;AP ESP8266 E-Paper;网页填 SSID/密码 |
| 上游 WiFi 需要 Captive Portal 登录 | 配置页可填门户账号/密码;设备检测到被门户拦截时自动提交登录后再拉数据,见 §8.3 |
| 按键功能 | 深睡期间只有 RST 能唤醒,按键功能设计为「组合键」和「唤醒后 10 s 交互窗口」两种形态,见 §8.5 |
| 手机连热点自动弹配置页(Captive Portal 形态一) | AP 模式下起 DNS 劫持 + 各系统探测 URL 302 跳转 |
| 支持 HTTPS | 服务端 URL 可为 https://;BearSSL + 会话复用 + 小缓冲,握手只在会话过期时发生,见 §8.6 |
| 省电 | 分钟级唤醒不开 WiFi;WiFi 快连;无变化不刷屏;细节与估算见 §10 |
不做:触摸、SD 卡、阅读器、OTA(首版先不做,后续可加)。
2026-08-29 追加:万年历页(设备端查表算农历/节气/节日)和天气页(服务端 weather.py 下发,默认 Open-Meteo + IP 定位),见 README.zh-CN.md 的「日历与天气页」。
支持 HTTPS:服务端接口和门户登录都可走 TLS(BearSSL),见 §8.6。
| 项 | 内容 |
|---|---|
| MCU | ESP8266(4 MB flash),深睡靠 GPIO16→RST,已接好 |
| 屏幕 SPI | CS=15, DC=0, RST=2, BUSY=4(同现有固件 GxEPD2_BW<...>(15,0,2,4)) |
| I²C | SDA=GPIO13, SCL=GPIO14(与 SPI MOSI/CLK 复用,现有固件每次用前 Wire.begin(13,14)) |
| 温湿度 | SHT30 @0x44,由 GPIO12 通过 Q7 供电,读前拉高、读后拉低并设 INPUT |
| 时钟芯片 | BL8025T(兼容拆机 RX8025T),库 BL8025_RTC_V2.0 |
| 电池电压 | A0,分压后经 Q4/Q5 开关,开关也是 GPIO12;系数 adc/1024 × 5.607 |
| 充电 | TC4056,CHRG 只接 LED,STDBY 悬空,无 GPIO 可读充电状态 |
| 按键(4.2 寸板 revB-230621,由 Gerber 铜层连通性反推) | SW1 = EN/CH_PD → GND(复位,唯一能从深睡唤醒的键);SW2 = GPIO0 经串阻 → GND;SW3 = GPIO3/RX 经串阻 → GND;USB-C 旁的小侧键 = 与 SW2 并联在 GPIO0 线上(第 4 个键,电气上等同 SW2);左侧边为电源拨动开关(BAT → LDO)。GPIO16 已与 RST 短接(深睡唤醒) |
| 电池 | 660 mAh 锂电池,TC4056 充电 |
| 功耗 | 深睡 0.03–0.04 mA;WiFi 工作 70–120 mA;WiFi 关闭运行 17–25 mA |
| 屏幕 | 400×300,局刷 ~0.2 s,全刷 ~1.7 s,支持 setPartialWindow 区域局刷 |
┌────────── dashboard-backend (NAS Docker) ────────┐
│ collectors.py ──► 状态快照 ──► /dashboard.png (Kindle, 不变) │
│ └─► /api/eink.json (新增) │
└──────────────────────────────┬────────────────────┘
│ HTTP, 每 N 分钟一次, ~1 KB
┌──────────────────────────────▼────────────────────┐
│ ESP8266 eink-dashboard.ino │
│ 每分钟醒: RTC芯片 → 时间 | SHT30 → 温湿度 | A0 → 电压 │
│ 顶栏区域局刷(内容有变才刷) │
│ 每N分钟: +WiFi → JSON → rev变化 → 内容区局刷 │
│ 每小时: 全刷一次 │
│ 深睡到下一个整分 │
└────────────────────────────────────────────────────┘
设计原则:设备端自己能拿到的信息(时间/温湿度/电量)不依赖网络;网络只负责 homelab 状态;服务端挂了看板仍显示时间温湿度,内容区标"离线 + 最后更新时间"。
y=0 ┌──────────────────────────────────────────────────────┐
│ 14:32 周三 08-26 🌡 23.5°C 💧 58% ⚡▮▮▮ 3.9V │ 顶栏 h=40
y=40 ├──────────────────────────────────────────────────────┤
│ 绿联 NAS ●在线 CPU 12% MEM 45% 41°C up 12d │
│ 飞牛 NAS ●在线 CPU 8% MEM 61% 38°C up 3d │ 服务器卡片
│ 树莓派 ○离线 ── 整行反白 ── │ 每行 h=34
│ 路由器 ●在线 CPU 5% MEM 30% 52°C up 30d │
y=180 ├──────────────────────────────────────────────────────┤
│ Claude 5h ▮▮▮▮▮▯▯▯▯▯ 48% 重置 16:00 │ Claude 用量
│ 周 ▮▮▯▯▯▯▯▯▯▯ 21% 重置 周一 │ h=60
y=240 ├──────────────────────────────────────────────────────┤
│ 机柜 27.1°C 风扇 1200rpm · 更新 14:30 · ⚠ 1 离线 │ 底栏 h=60
y=300 └──────────────────────────────────────────────────────┘
- 顶栏(本地数据)与其余区域(远端数据)物理上分开,便于分别局刷。
- 内容区的行数/卡片由 JSON 决定,设备端按模板渲染;服务器最多 4 行,多余截断并在底栏提示。
- 字体:U8g2
u8g2_font_wqy14_t_gb2312(中文,~170 KB PROGMEM)+logisoso数字字体做时间;不再从 LittleFS 读大字库(那是现有固件慢的根源)。 - 显示的百分比条、图标用 GFX 画线/矩形,不放位图资源。
- 中英双语:固件自己的文案两套都在
i18n.h的I18N_STRINGS表里,按cfg.lang取(配网页切换)。字模的 32–126 段本来就在,英文不额外占字库。 服务端下发的字符串(服务器名、Claude 窗口名、天气提示语、城市名)不翻译;天气现象例外——按心知天气码在本地取英文名,预报列另有一套短名。
移植自 Clock_8025T.ino / Other.ino::read_RTC_time(),保留以下行为:
- 数据存放:时分秒放
ESP.rtcUserMemory(深睡不丢);年月日星期放 EEPROM。 - 时钟芯片自动识别(
ClockChipCheck):写一组测试时间,按 BL 顺序(type 0)和 RX 偏移(type 1)各读一次,匹配者存 EEPROMtype_8025T。 - 合法性校验(
ClockReadCheck):年 2020–2099、月日时分秒范围合法才认为芯片可用;否则标记rtc_error,退化为软件累加。 - 首次开机 / RTC memory 无效:连 WiFi → NTP(6 次重试、轮换 5 个国内 NTP)→ 写 RTC memory →
rtc.write()写芯片 → 回读校验。- 改进:用 NTP 的 epoch 直接算年月日星期(
localtime),不再请求api.xygeng.cn/day。
- 改进:用 NTP 的 epoch 直接算年月日星期(
- 每次唤醒:芯片可用则
rtc.read()覆盖 RTC memory;不可用则minute += 1软件累加并处理进位。 - 定期校准:每天 03:00 的那次 JSON 拉取顺带 NTP 一次并回写芯片(合并进同一次 WiFi 连接,不额外耗电);芯片不可用时改为每
clockJZJG(默认 60)次唤醒校准一次,与现有一致。 - 校准失败补偿(
ManualCompensation8025T):保留,累计误差 ≥1 s 时修正芯片秒寄存器。 - 整分对齐睡眠(
DisplaySetup.ino:567-577):sleep = 60000 − 秒×1000 − 本次运行耗时 − 上次运行耗时,使下次醒来落在整分;深睡前display.powerOff()(不 hibernate,保住局刷基准)。
移植 Get_bat_vcc.ino::get_dht30_data():
- 每次唤醒读一次:GPIO12 拉高供电 →
Wire.begin(13,14)→sht3xd.begin(0x44)→readTempAndHumidity(REPEATABILITY_LOW, CLOCK_STRETCH, 50)→ GPIO12 拉低设 INPUT。 - 读失败保留上次值并在图标旁画"?";连续 3 次失败视为无传感器,顶栏隐藏温湿度。
- 显示一位小数;只有当四舍五入到 0.1 后的值与上次不同才触发顶栏局刷(上次值存 RTC memory)。
- 随 JSON 请求上报
?t=23.5&h=58,服务端可转发到 HA(可选)。
电压:同现有 getBatVolNew()(GPIO12 开测量开关,A0 采样 30 次取平均,系数 5.607);百分比用现有的四次多项式 getBatVolBfb();三格电量图标阈值 3.7/3.5 V 与现有一致;显示电压或百分比可配置(batDisplayType)。
低压保护:同现有——≤3.1 V 立即永久深睡;≤3.3 V(BatLow)显示"电量过低"并永久深睡,插电后按 RST 恢复。
充电状态(新增,硬件无信号,只能推断):
- 每分钟采样一次电压,最近 5 个样本存 RTC memory。
- 判定为"充电中":
V ≥ 4.15(恒压段)或 5 分钟内电压上升 ≥ 0.04 V; 判定为"放电":5 分钟内电压下降或V < 4.10且不再上升。带迟滞,避免图标闪烁。 - 充电中在电量图标旁画闪电(复用现有
Bitmap_dlsd图标)。 - 局限:刚插电的第 1–2 分钟不会立刻显示;充满后 TC4056 停充,电压稳定在 ~4.2 V 时仍显示闪电(可接受,也可改为"已充满")。
- 如果以后想要精确检测:把 TC4056
STDBY/CHRG飞线到 GPIO3(RX)(V2.4x 上 RX 空闲),代码留编译开关CHARGE_DETECT_PIN。
进入方式不变:按住按键 3 再按 RST;或 EEPROM 里没有服务端地址 / WiFi 从未连上过时自动进入。
AP: SSID "ESP8266 E-Paper" 密码 333333333 IP 192.168.3.3 (与现有相同)
屏幕显示: 热点名 / 密码 / "连接后自动弹出配置页, 或浏览器打开 192.168.3.3"
Captive Portal 实现(新增):
DNSServer监听 53 端口,*全部解析到 192.168.3.3。- WebServer 对各系统的联网探测路径返回 302 →
http://192.168.3.3/:- Android:
/generate_204、/gen_204 - iOS/macOS:
/hotspot-detect.html、/library/test/success.html - Windows:
/ncsi.txt、/connecttest.txt、/redirect、/fwlink - 其他未知路径:
onNotFound也 302 到首页
- Android:
- 首页
Host不是 192.168.3.3 时同样 302,保证所有系统的弹窗都落在配置页。 - 配置页(单文件 HTML 内嵌 PROGMEM,不依赖 bootstrap/jquery,保证弱网也能秒开):
- WiFi:扫描列表下拉 + 手动输入 SSID + 密码
- Dashboard 服务端 URL(必填,默认预填
http://192.168.1.20:8090)- 接受
http://host[:port][/path]和https://host[:port][/path],host 可为 IP 或域名;设备只在其后拼/api/eink.json - 前端校验:scheme 必须是 http/https、host 非空、端口 1–65535;不合法不允许提交
- 选
https://时展开"证书验证"单选:不验证 / 证书指纹(填 SHA-1,AA:BB:…格式,自动去冒号和空格)/ 内置根 CA(Let's Encrypt);默认"不验证",页面给一句提示说明各自适用场景 - "测试连接"按钮:设备用当前填写的 URL 和验证方式实际请求一次
/api/eink.json,页面显示结果:HTTP 状态码、耗时、rev、是否为门户拦截、TLS 错误原因(证书不符 / 握手内存不足 / 超时);测试通过后再点保存。测试期间保持 AP+STA 共存,不断热点 - 保存时 URL、验证方式、指纹一起写 EEPROM;正常模式下每次轮询直接使用,不再重新解析
- 接受
- 轮询间隔(分钟,默认 5)、夜间模式时段(默认 00:00–06:00 → 30 min)
- 电量显示方式(电压/百分比)
- 提交 → 屏幕显示"正在连接 xxx",成功后显示 IP 并 5 s 后进入正常模式;失败保留热点让用户重填(同现有
ap_state状态机)
- 配网模式超时 10 分钟无操作深睡(同现有
overtime)。 - 仍保留
/status简单 JSON 供调试(电压、温湿度、上次 rev、错误计数)。
WiFi.persistent(false),凭据在 EEPROM 自己管理,避免每次写 flash。- RTC memory 缓存 BSSID + 信道 + 上次成功的 IP/网关/掩码;唤醒后
WiFi.config(ip,gw,mask)+WiFi.begin(ssid,pwd,channel,bssid),跳过扫描和 DHCP,通常 <1 s 连上。 - 快连失败 → 回退为普通
WiFi.begin(ssid,pwd)并刷新缓存;普通连接也失败 → 记错误计数,本轮跳过网络,屏幕底栏标"WiFi ✕"。 - 连续 3 轮失败后把轮询间隔拉长到 15 分钟,成功后恢复。
- 请求完成立即
WiFi.mode(WIFI_OFF); WiFi.forceSleepBegin(),画图和刷屏阶段在低电流下进行。
场景:设备要连的 WiFi 本身有门户认证(公司/宿舍/酒店网络),DHCP 能拿到 IP,但 HTTP 会被 302 到登录页,需要提交用户名/密码后 MAC 才放行。
配置(配网页新增"门户登录"折叠区,默认关闭):
| 字段 | 说明 | 默认 |
|---|---|---|
| 启用 | 开关 | 关 |
| 登录 URL | 门户的表单提交地址:完整 URL,或以 / 开头的路径(拼到跳转地址的主机上);留空则按跳转地址识别门户类型(Aruba 预设),识别不出就直接 POST 跳转地址 |
空 |
| 用户名 / 密码 | 门户账号 | |
| 字段名 | 表单里用户名/密码的字段名,逗号分隔(Aruba 预设自动改为 user,password) |
username,password |
| 附加参数 | 固定要带的 k=v&k2=v2(有些门户要 action=login) |
空 |
| 成功判定 | 响应里包含的字符串;留空则以探测通过为准 | 空 |
运行逻辑(正常情况下不多发任何请求,只有主请求失败才探测):
GET /api/eink.json
├─ 200 且 Content-Type 是 application/json → 正常
├─ 302/303 或 200 但正文不是 JSON(HTML)→ 被门户拦截,Location 即门户地址
├─ 其它失败(超时 / 证书错 / 拒绝)且启用了门户登录 → 门户通常只劫持 80/443,非标端口和 HTTPS 看不到 302:
│ 纯 HTTP GET connect.rom.miui.com/generate_204(备用 captive.apple.com/hotspot-detect.html)
│ 204 / Success → 不是门户问题,按原错误上报
│ 3xx → 门户,取 Location
│ 200 别的页面 → 门户,从正文找 <meta refresh url=…> / location.href / <form action>(Aruba 就是 200 + meta refresh)
└─ 被拦截
├─ 门户登录未启用 → 底栏标"门户认证" 并跳过本轮
└─ 已启用 → 沿跳转链走到真正的门户页(只 GET、不自动跟随、最多 4 跳,到 https 或命中门户路径为止)
Aruba 实测链路:探测点 200 meta refresh → 同主机 ?cmd=redirect&arubalp=… → 302
https://<门户>/upload/custom/<profile>/login.html?cmd=login&mac=…&ip=…&essid=…&url=…
登录地址:配置的完整 URL > 配置的路径(拼到门户主机)> 门户类型预设 > 跳转地址本身
Aruba 预设(路径 /cgi-bin/login、/auth/index.html、/upload/…):
POST <门户主机>/auth/index.html/u,user / password / cmd=authenticate&Login=Log+In
成功回 200 "External Welcome Page",失败 302 回登录页且 Location 带 errmsg=Authentication%20failed
→ POST application/x-www-form-urlencoded,不跟 302
→ 再探测一次确认已放行(204)→ 再 GET /api/eink.json;仍被拦截则计失败
连续 3 次登录失败 → 底栏标"门户登录失败",间隔拉长到 15 min,避免账号被锁
- 门户页动辄 20 KB,探测/登录响应只读前 4 KB 到 JSON 缓冲里做字符串判定,不用
getString()占堆。 - 门户 TLS 用
setInsecure(),接收缓冲 6 KB(企业门户不支持 MFLN 小分片,证书链一整条记录进来);登录时实测剩余堆 ~19 KB。 - 必须
ssl=all编译:ssl=basic没有 ECDHE,门户直接 handshake_failure。 - cont 栈只有 4 KB:
HTTPClient(~300 B)放堆上、Dash临时对象(~1.7 KB)放静态区,否则同步 DNS + lwIP 发包路径把栈踩爆(表现为 Soft WDT / 随机异常,end:标记被改写)。 - 门户走 HTTPS 时按 §8.6 的 TLS 方案处理(门户证书千奇百怪,默认
setInsecure(),可选指纹)。 - 门户会话通常按 MAC 放行数小时到数天,实际登录频率很低;
WiFi.persistent(false)不影响 MAC。 - 不支持:需要 JavaScript 执行、验证码、企业 802.1X(后者是另一套
WPA2-Enterprise,如需要另议)。 - 安全:门户账号密码存 EEPROM 明文(与 WiFi 密码同等级),配网页只在 AP 模式可访问。
WiFi ✕ → 门户认证 → 服务端离线 → 数据过期 HH:MM,四种状态互斥,从左到右优先级递减。
硬件事实:4.2 寸板 4 个按键中只有 3 个独立信号——SW1(EN 复位)、SW2(GPIO0)、SW3(GPIO3/RX);USB-C 旁的侧键与 SW2 并联在 GPIO0 上,固件无法区分,只是换个位置按「按键 2」(外壳装好后顶边按键可能不好按,侧键就是给这个用的)。深睡期间只有 SW1 能唤醒;SW2/SW3 在深睡时按下无效。因此按键功能分两种形态。
形态 A:组合键(按住 + SW1 复位) —— 开机瞬间在 setup() 读取 GPIO3 电平。
注意 GPIO0(SW2)按住时复位会让 ROM 进入串口下载模式,固件根本不会启动,所以不存在"SW2 + 复位"组合;恢复出厂改为配网页里的按钮。
| 组合 | 功能 |
|---|---|
| 单独 SW1 | 立即刷新:强制拉一次 JSON、NTP 校时、全刷整屏;随后 3 秒内按 SW2/SW3 进入交互窗口 |
| 按住 SW3 + SW1 | 进入配网模式(与现有一致) |
| 配网页"恢复出厂" | 清 EEPROM/RTC memory,重新配网 |
形态 B:交互窗口 —— 由"SW1 复位后 3 s 内按 SW2/SW3"进入(唤醒后先停留 3 s 检测按键,这 3 s 用 20 mA 的 WiFi 关闭状态等待,约 0.017 mAh,仅在手动按 RST 时发生,不影响定时唤醒)。窗口期设备不睡,每次按键把窗口延长到 10 s,无操作后深睡并恢复定时节奏。
| 按键 | 短按 | 长按(≥1 s) |
|---|---|---|
| 按键 2 | 翻页:看板 → 服务器详情页(每台一整块:磁盘、负载、网络流量等 JSON 扩展字段)→ Claude 详情页(按模型周限额)→ 设备信息页(电压曲线、WiFi RSSI/IP、上次同步、错误计数、固件版本)→ 回看板 | 切换电量显示方式(电压/百分比),存 EEPROM |
| 按键 3 | 立即拉一次 JSON 并重画当前页 | 切换夜间模式开/关(顶栏画月亮图标),存 EEPROM |
| 按键 1 | 退出窗口,回看板并深睡 | (硬件复位,等同"立即刷新") |
- 翻到非看板页后,下一次定时唤醒自动回到看板页(详情页不常驻,避免用户忘了切回去)。
- 服务器详情页需要 JSON 额外字段(
disk、load、net),服务端一并下发,设备只在详情页渲染,看板页忽略;整包仍控制在 2 KB 内。 - 按键去抖沿用现有
key_lb_max=3次滤波。 - 如果后续想要"任意键唤醒",硬件改法:按键 2/3 各经一只二极管并到 RST,代码无需改动(开机时读电平即可区分是哪个键)。
ESP8266 的 TLS 用 core 自带的 BearSSL,两个硬约束:堆内存(TLS 连接峰值 ~20 KB)和握手时间(RSA-2048 完整握手 1.5–3 s @80 MHz,直接影响每次轮询的耗电)。设计如下:
内存预算(ESP8266 可用堆约 45–50 KB)
| 项 | 占用 | 说明 |
|---|---|---|
| GxEPD2 页缓冲 | 7.5 KB | 用 HEIGHT/2 分页(两页画完一屏),而不是 15 KB 整屏缓冲 |
| WiFi 协议栈 | ~10 KB | 连接期间 |
| BearSSL 收/发缓冲 | 1 KB + 0.5 KB | setBufferSizes(1024, 512);服务端启用 MFLN(TLS 最大分片协商)时可谈到 512,否则收缓冲需满足服务端记录大小,1 KB 对我们 <2 KB 的 JSON 足够(分多记录接收) |
| BearSSL 内部状态 | ~6–7 KB | 握手期间峰值 |
| ArduinoJson | 3 KB | TLS 断开后再解析(文档缓冲复用 HTTP 正文缓冲) |
| 合计峰值 | ~28 KB | 留 15 KB 以上余量;ESP.getFreeHeap() 会记到设备信息页 |
关键顺序:读传感器 → 连 WiFi → TLS 取 JSON 到 2 KB 静态缓冲 → 断开并销毁 client → 关 WiFi → 解析 → 画图。TLS 和绘图不同时存在,避免碎片化导致握手失败。
握手时间与省电
- 会话复用:用
BearSSL::Session保存握手结果,序列化后存 RTC memory(br_ssl_session_parameters约 100 B);深睡后下一次连接setSession()恢复,握手从 ~2 s 缩到 ~0.3 s,且不做 RSA 验签。服务端需允许 session ID 复用(Caddy/nginx 默认开;Python uvicorn 用ssl.SSLContext默认也开),过期(服务端通常 5–24 h)后自动完整握手一次并刷新缓存。 - 握手期间
system_update_cpu_freq(160),握手完切回 80 MHz——功耗几乎不变但时间减半。 - 服务端优先用 ECDSA (P-256) 证书:ESP8266 验 ECDSA 比 RSA-2048 快 3–4 倍。Let's Encrypt 可直接申请 EC 证书;自签也可以。
- 功耗影响:完整握手一次约 +0.05 mAh,会话复用后每次 +0.01 mAh;按 5 min 轮询、会话 24 h 有效算,每天约 +3 mAh(§11 的 17 mAh/天 → 20 mAh/天,660 mAh 仍约 4 周)。
证书验证方式(配网页可选,默认第 2 种)
| 方式 | 安全性 | 维护 | 适用 |
|---|---|---|---|
setInsecure() 不验证 |
仅加密不防中间人 | 零 | 局域网自签、门户登录 |
| 证书指纹(SHA-1 fingerprint,配网页填) | 防中间人 | 证书续期后要改指纹(LE 每 90 天) | 自签长期证书(推荐给 NAS 自建) |
根 CA(内置 ISRG Root X1,BearSSL::X509List PROGMEM ~1.5 KB) |
完整验证 | 零;但需要正确的系统时间 | 公网域名 + Let's Encrypt |
根 CA 方式需要时间——正好有 RTC 芯片,每次唤醒 settimeofday() 把芯片时间灌给系统再握手;首次开机 RTC 无效时先用 setInsecure() 拉一次 NTP,校准后再切回验证模式。验证失败时底栏标"证书错误",不降级为不验证(防止静默被劫持),用户可在配网页改成指纹或不验证。
服务端侧配合(后端只提供 HTTP,要 HTTPS 自己在前面加一层反代)
- 反代到
dashboard-backend:8080,公网域名用自动签发的 EC 证书,内网用自签 + 指纹;开session tickets off, session cache on(ESP8266 BearSSL 只支持 session ID 复用,不支持 ticket);如可能开 MFLN。 - Kindle 那条链路不受影响,继续走 8090 HTTP。
- 2026-09-08:实际部署里这层反代(原来的 Caddy)已撤掉,设备走 HTTP 8090。
门户登录的 HTTPS:门户证书来源不可控(常是自签或企业 CA),默认 setInsecure();登录只在被拦截时发生一次,不做会话缓存。
HTTP 回退:URL 写 http:// 就不加载 BearSSL,行为和 §8.2 一致;两条路径共用同一套请求/解析代码,只是 WiFiClient 换 BearSSL::WiFiClientSecure。
GET /api/eink.json?vbat=3.87&t=23.5&h=58&rev=<上次rev>
{
"rev": "a1b2c3d4", // 内容哈希;与请求 rev 相同 → 服务端直接返回 304
"next_poll": 300, // 秒;服务端可依据告警/夜间下发 120 / 1800
"ts": "2026-08-26T14:30:12+08:00",
"alert": 1, // 离线/告警数量, 0 表示全部正常
"servers": [
{"n": "绿联 NAS", "ok": 1, "cpu": 12, "mem": 45, "temp": 41, "up": "12d",
"d": {"disk": "3.2T/8T", "load": "0.4 0.5 0.6", "net": "12M↓ 3M↑"}}, // d: 详情页用, 看板页忽略
{"n": "树莓派", "ok": 0}
],
"claude": {
"h5": {"pct": 48, "reset": "16:00"},
"week": {"pct": 21, "reset": "周一"}
},
"env": [{"i": "temp", "v": "27.1°C"}, {"i": "fan", "v": "1200rpm"}]
}- 字段名刻意短,看板字段 <1 KB,含详情
d字段 <2 KB;ArduinoJson 静态缓冲 3 KB。 rev只覆盖显示相关字段(不含ts),否则每次都"有变化"。- 服务端逻辑:复用现有
_refresh_loop的快照,render.py不动;新增eink.py负责裁剪字段、算 rev、决定next_poll(夜间时段、alert>0时缩短)。 - 设备上报的
vbat/t/h写入快照,可在 Kindle 看板角落显示或推到 HA(可选,默认只记日志)。
RST/定时唤醒
├─ 读 RTC memory(时间、上次显示值、计数器、WiFi缓存)
├─ 读时钟芯片 → 时间 (~5 ms)
├─ 读 SHT30 → 温湿度 (~20 ms)
├─ 读 A0 → 电压 → 充电推断 (~5 ms)
├─ 判断本轮是否要联网: minute % poll_min == 0 或 强制(首次/重试/校准)
│ 是 → WiFi快连 → GET json → 关WiFi (~1–1.5 s)
│ 200 且 rev 变化 → 标记"内容区需重画", 保存 JSON 摘要到 RTC memory
│ 304 / 失败 → 内容区不动 (失败时底栏标"离线")
├─ 决定刷新:
│ minute == 0 (整点) → 全刷整屏 (清残影, 顺便重画一切)
│ 顶栏任一显示值变化 → setPartialWindow(0,0,400,40) 局刷
│ 内容区需重画 → setPartialWindow(0,40,400,260) 局刷
│ 都没变 → 不碰屏幕
├─ display.powerOff()
└─ 计算到下一整分的睡眠时间 → deepSleep
"顶栏任一显示值变化"指:分钟数(必然变)、温度/湿度(0.1 精度)、电量格数或百分比、充电图标。因为分钟必变,顶栏每分钟都会局刷一次(0.2 s),这是需求要求的;温湿度不变时只是少画几个字,不影响刷屏次数。
- Z96 局刷残影随次数累积,现有固件用
clockQSJG计数控制全刷;本方案固定为每小时整点全刷一次(60 次局刷一次全刷),并在 JSONalert从 0 变为非 0 时立刻全刷一次以突出告警。 - 局刷用
display.init(0, 0, 10, 0)(不做初始全刷),全刷用display.init(0, 1, 10, 0),与现有display_clock()相同。 - 局刷用 GxEPD2 分页模式
firstPage/nextPage,页缓冲 15 KB,堆余量足够(无 SD、无 HTTPS)。
服务端 next_poll 在夜间时段下发 1800 s;设备端分钟级唤醒照常(时间要走),但 minute % 30 才联网。也可在配置页关闭"夜间刷新顶栏",关闭后夜间每分钟只更新 RTC memory 不刷屏,进一步省电。
| 事件 | 频率 | 单次电量 | 每日电量 |
|---|---|---|---|
| 分钟唤醒(不联网,读传感器 + 顶栏局刷 0.2 s) | 1440/天 | ~0.5 s × 20 mA ≈ 0.003 mAh | ~4 mAh |
| 联网轮询(快连 + JSON + 关闭,HTTP) | 288/天(5 min) | ~1.5 s × 90 mA ≈ 0.04 mAh | ~11 mAh |
| HTTPS 额外开销(会话复用,每天 1 次完整握手) | 288/天 | +0.01 mAh(复用)/ +0.05 mAh(完整) | ~+3 mAh |
| 内容区局刷(rev 变化时才有) | ≤288/天 | 0.2 s × 30 mA ≈ 0.002 mAh | ≤0.5 mAh |
| 整点全刷 | 24/天 | 1.7 s × 30 mA ≈ 0.014 mAh | ~0.3 mAh |
| 深睡 | 全天 | 0.035 mA | ~0.8 mAh |
| 合计 | HTTP ≈ 17 mAh/天、HTTPS ≈ 20 mAh/天 → 660 mAh 约 4–5 周 |
夜间 30 min 轮询可再省 ~2 mAh/天。联网是大头,轮询间隔是最有效的旋钮。
| 偏移 | 内容 |
|---|---|
| 0 | magic + 版本(无效则视为首次开机) |
| 4 | 时、分、秒(软件时钟备份) |
| 8 | 上次运行耗时、上次睡眠误差(整分对齐用) |
| 16 | 唤醒计数、局刷计数、WiFi 失败计数、校准状态 |
| 32 | WiFi 缓存:BSSID[6]、channel、IP/GW/Mask |
| 48 | 上次显示的顶栏值:温度×10、湿度×10、电量格数、充电标志 |
| 56 | 电压历史 5 个样本(充电推断) |
| 72 | 上次 rev(8 字节) |
| 88 | TLS 会话参数(br_ssl_session_parameters,~100 B)+ 有效期戳 |
| 200 | 上次 JSON 摘要(已渲染内容的紧凑表示,用于离线时重画及全刷时重画内容区,≤ 300 B) |
EEPROM(掉电保持):WiFi SSID/密码、服务端 URL(http/https)、TLS 验证方式(不验证/指纹/根 CA)与指纹、轮询间隔、夜间时段、夜间开关、电量显示方式、时钟芯片类型、时钟补偿累计值、门户登录配置(启用/URL/账号/密码/字段名/附加参数/成功判定)。
eink-dashboard/
├── DESIGN.md 本文档
├── eink-dashboard.ino setup(): 一次唤醒的全部流程; 无 loop 常驻
├── config.h 引脚、编译开关(屏型号/按键3引脚/充电检测脚)、默认值
├── GxEPD2_420_Z96.h / .cpp 原样来自 源码&SD例程/4.2寸z96和z98驱动.zip
├── rtc_mem.h/.cpp RTC memory 结构体读写 + CRC
├── settings.h/.cpp EEPROM 配置结构体(ESP_EEPROM)
├── clock.cpp 移植 Clock_8025T.ino: 芯片识别/读写/NTP 校准/补偿/整分睡眠
├── sensors.cpp 移植 Get_bat_vcc.ino: SHT30、电压、百分比、充电推断
├── net.cpp 快连、JSON 拉取、304 处理、门户拦截检测与登录
├── tls.cpp / ca_certs.h BearSSL 客户端封装:会话缓存到 RTC memory、指纹/根 CA 验证、缓冲大小
├── i18n.h/.cpp 中英文案表(PROGMEM + 轮转缓冲)、星期/月份/天气码/风向英文名
├── keys.cpp 开机组合键检测、交互窗口、去抖
├── pages.cpp 详情页/设备信息页绘制
├── portal.cpp / portal_html.h 配网 AP + DNSServer + Captive Portal 页面
├── ui.cpp 布局绘制:顶栏、服务器行、Claude 条、底栏;区域局刷调度
└── README.md / README.zh-CN.md 烧录、配网、服务端配置说明(英文 / 中文)
依赖库:GxEPD2、Adafruit_GFX、U8g2_for_Adafruit_GFX(原版即可,字库用 PROGMEM 的 wqy)、ArduinoJson 6、ESP_EEPROM、ClosedCube_SHT31D、BL8025_RTC(仓库 zip)、DNSServer/ESP8266WebServer/ESP8266WiFi(core 自带)、NTPClient 或 core 的 configTime。
服务端:server/app/eink.py + main.py 加一个路由;config.yaml 加 eink: 段(轮询间隔、夜间时段、告警时间隔)。
构建/烧录:本机装 arduino-cli + esp8266 core 3.x;esptool 已可用(uvx)。烧录前先把当前 4 MB flash 完整读出保存,随时可刷回现有天气固件(文件系统区 0x200000 不动)。
| 情况 | 行为 |
|---|---|
| 时钟芯片读数非法 | 标记 rtc_error,改软件累加,下次联网 NTP 后重写芯片并重新校验 |
| SHT30 连续 3 次失败 | 顶栏隐藏温湿度,每小时重试 |
| WiFi 快连失败 | 回退普通连接;再失败则本轮跳过,底栏标"WiFi ✕",3 轮失败后间隔拉长到 15 min |
| HTTP 超时/非 200/JSON 解析失败 | 内容区保持上次,底栏标"离线 最后更新 HH:MM";alert 视为 1 |
| TLS 握手失败(内存不足/证书不符) | 证书不符 → 底栏"证书错误",不降级;内存不足 → 清会话缓存重试一次,仍失败按离线处理并记录 getFreeHeap 到设备信息页 |
| 电压 ≤ 3.3 V | 显示"电量过低"后永久深睡(同现有) |
| RTC memory CRC 错 | 视为首次开机:全刷、NTP、重建缓存 |
| 配网 10 分钟无操作 | 深睡;下次 RST 若仍无配置则再次进入配网 |
供电方式已确认:660 mAh 电池,按深睡方案。- 布局取舍:§4 的行高/区域划分按"4 台服务器 + Claude 两条 + 底栏"设计;如果服务器数量或 Claude 卡片优先级不同请指出。
- 充电显示的推断规则(§7)是否可接受;若要精确检测需飞一根线。
- 夜间是否刷新顶栏:默认刷(时间要走),可在配置页关。
按键 3 引脚按 V2.43 原理图为 GPIO3/RX,留KEY3_PIN编译开关。按键数量已从 4.2 寸板 Gerber 反推:SW1=EN、SW2=GPIO0、SW3=GPIO3、侧键=GPIO0(与 SW2 并联),电源拨动开关切 BAT。没有第 4 个独立信号,交互窗口按 3 键设计;侧键当「按键 2」用。- 门户登录:请提供一次实际门户登录页的表单(浏览器 F12 看 POST 的 URL 和字段名),以便默认值贴合实际;也确认是否为 HTTPS。