feat: E1002 顶部加设备电量/温湿度列+server 四路上游并行拉取与响应预算

- 新增 SenseCraft 设备遥测客户端(api-key 走 .env),读电量/充电/温度/湿度,DEVICE_TTL 与设备 300s 上报周期同步
- 大时钟 F84 右侧一列三行左缘对齐:电池(充电绿/≤20% 红)、温度计+蓝色水滴、星期日期(与时钟基线对齐)
- server 过期上游由串行改并行,整体最多等 FETCH_BUDGET 秒(默认 5);超预算的一路本次用旧值、后台拉完回填缓存,同一路不重复实拉
- 此前串行且上游 socket 超时 30s,任一路卡住即整图拖到平台抓取超时;「多路 TTL 同时到期」猜想经日志核查不成立,排查顺序写入 CLAUDE.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 15:02:34 +08:00
parent 7378d6bb88
commit f96ae29590
8 changed files with 255 additions and 41 deletions

View File

@@ -6,7 +6,9 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(
并生成一张固定 800x480 的 HTML 页面供 SenseCraft HMI 的 HTML 控件云端截图拉取。
版式k=1点阵字体且**只用整数倍字号 12/24/36/48/84**,不放大;四段,三道横规):
A 顶部090 **大时钟 F84**(墨迹 1679,占满上下边距内的整段高度)+ 星期/日期 F12 紧随其右、与时钟数字**基线对齐**(左)
A 顶部090 **大时钟 F84**(墨迹 1679 其右一列三行、左缘对齐:① 电池图标+电量 ② 温度计+温度 水滴+湿度 ③ 星期/日期
F12行距 22日期与时钟数字**基线对齐**;设备数据来自 SenseCraft 遥测 `data.device`2026-09-14 定稿,
此前「状态栏横排在时钟上方+时钟缩 F60」版被否
|天气(右,贴 790——天气组件与三色版同尺寸。
B 用量96206 :左半 **Claude Usage**两行5h 合并行 7d——一个大边框内上下两条实心 = 总 7d / fable 7d右侧数字取较大者`_usage_row_split`)|竖规 x=400右半 **Codex Usage**5h / 7dChatGPT 订阅的 Codex 额度池;未推送时 mock 标「示例数据」)。
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31**三处文字统一 12px**(基类 F13/F16/FPACE 都映射到 12px
@@ -98,6 +100,43 @@ class E1002Renderer(DashboardRenderer):
return BLACK
# ---------- 组件 ----------
def _status_battery(self, x, cy, dev):
"""状态项·电池F12墨迹中心 cy外框 16×9 + 正极 2×4内部按电量实心填充——充电中 GREEN、≤20% RED、其余 BLACK
右侧电量数字,缺失 "--"。返回末端 x。"""
F, GAP_I = self.F12, 5
lvl, chg = (dev or {}).get("battery"), (dev or {}).get("charging")
self.d.rectangle([x, cy - 4, x + 15, cy + 4], outline=BLACK, width=1)
self.d.rectangle([x + 16, cy - 2, x + 17, cy + 2], fill=BLACK)
if lvl is not None:
fill = GREEN if chg else RED if lvl <= 20 else BLACK
fw = round(12 * max(0, min(100, lvl)) / 100)
if fw > 0:
self.d.rectangle([x + 2, cy - 2, x + 2 + fw, cy + 2], fill=fill)
x += 18 + GAP_I
txt = f"{lvl}%" if lvl is not None else "--"
self._mid(x, cy, txt, F)
return x + self._tw(txt, F)
def _status_env(self, x, cy, dev):
"""状态项·环境F12墨迹中心 cy温度计细管 3×8 + 底部球 7×7管内水银线温度 蓝色水滴(上尖下圆)+湿度,
项间距 16、图标↔数字 5缺失 "--"。返回末端 x。"""
F, GAP_I, GAP = self.F12, 5, 16
dev = dev or {}
self.d.rectangle([x + 2, cy - 6, x + 4, cy + 1], outline=BLACK, width=1)
self.d.ellipse([x, cy + 1, x + 6, cy + 7], fill=BLACK)
self.d.line([x + 3, cy - 2, x + 3, cy + 1], fill=BLACK)
x += 7 + GAP_I
t = dev.get("temp")
txt = f"{t:.1f}" if t is not None else "--"
self._mid(x, cy, txt, F); x += self._tw(txt, F) + GAP
self.d.polygon([(x + 4, cy - 6), (x, cy + 2), (x + 8, cy + 2)], fill=BLUE)
self.d.ellipse([x, cy - 1, x + 8, cy + 6], fill=BLUE)
x += 9 + GAP_I
h = dev.get("humidity")
txt = f"{h:.0f}%" if h is not None else "--"
self._mid(x, cy, txt, F)
return x + self._tw(txt, F)
def _color_strip(self, y0, y1, x0=None, x1=None):
"""六色测试色条:六块等宽实心色块,白块加 1px 黑框以便可见。真机校准用。"""
x0 = 10 if x0 is None else x0
@@ -255,16 +294,24 @@ class E1002Renderer(DashboardRenderer):
dt, wt, mao = data["date"], data["weather"], data["mao"]
usage, gpt = data["usage"], data.get("usage_gpt") or {}
# ---- A. 顶部:大时钟 F84 占满整段(左)+ 星期/日期紧随其右、与时钟数字基线对齐 / 天气(右)----
# ---- A. 顶部:大时钟 F84 占满整段(左);其右一列三行、左缘与日期对齐:① 电池 ② 温度+湿度 ③ 星期/日期与时钟基线对齐/ 天气(右)----
DIV_Y = 90
TIME_BOLD, INK_TOP = 3, 16 # 伪粗体 3px墨迹顶沿 16 与旧版 F48 相同,保住上边距
_, tt, _, tb = self.F84.getbbox(dt["time"]) # F84 数字墨迹 2891 → 高 63摆到 1679距横规 11px
TIME_BOLD, INK_TOP = 3, 16 # 伪粗体 3px墨迹顶沿 16,底沿 79F84 数字墨迹高 63
_, tt, _, tb = self.F84.getbbox(dt["time"])
time_y = INK_TOP - tt
base_y = time_y + tb # 时钟数字基线(墨迹底沿)
self.text(10, time_y, dt["time"], self.F84, bold=TIME_BOLD)
col_x = 10 + self._tw(dt["time"], self.F84) + TIME_BOLD + 14 # 右侧信息列左缘
date_txt = f'{dt["week"]} {dt["greg"]}'
_, _, _, db = self.F12.getbbox(dt["greg"]) # 用日期数字的底沿对齐基线(汉字底沿比数字低 1px忽略
self.text(10 + self._tw(dt["time"], self.F84) + TIME_BOLD + 14, base_y - db, date_txt, self.F12)
date_y = base_y - db
self.text(col_x, date_y, date_txt, self.F12)
# 状态两行叠在日期上方:行距 22F12 墨迹中心 ≈ 文字 y+8.5),三行中心约 30.5 / 52.5 / 74.5,落在时钟墨迹 1679 之内
PITCH = 22
cy_date = date_y + 8.5
dev = data.get("device")
self._status_battery(col_x, cy_date - 2 * PITCH, dev)
self._status_env(col_x, cy_date - PITCH, dev)
loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}'
self.rtext(R, 8, loc, self.F12)
self.rtext(R, 30, temp, self.F24, bold=1)