diff --git a/CLAUDE.md b/CLAUDE.md index afb9d74..481392b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co > **分支 `feature/colorful`**:新设备在黑白外支持红色,本分支把渲染升级为黑/白/红三色。当前版式为**上中下三段**:A 大时钟+日期 / 天气(定位南京·江宁);B **Claude Usage** 主角(5h/7d 用量进度条 + 时间维度对比 pace,用量吃紧转红预警);C 橘喵今日实时三列。红作**语义强调**(用量预警、涨、定位、天气太阳/闪电),仍**无真灰阶、不抖动**,全图只三种纯色。近30天趋势图当前**隐藏**(`renderer._trend_chart` 及依赖保留、未调用,随时可挂回)。 -> **分支 `feature/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**(7.3 寸 **800×480**,E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发;不支持 ETag/304,内容不变也会整屏重绘(全刷约 17 秒)。**真机验证(2026-09)**:Image 控件只在填写 URL 时抓一次快照、此后不再更新;HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 **`server.py` 动态服务**(部署公网 HTTPS;局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 `IMAGE_PATH`(默认 `/e1002.png`)每次请求**内存中现渲染 PNG**(Pillow 点阵版);方案二 `HTML_PATH`(默认 `/e1002.html`)每次请求生成 **800×480 现代网页**(`html_renderer.py`,系统/网络字体 + 内联 SVG K 线,无 JS)由平台截图——**真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊**;方案三 `HTML_PNG_PATH`(默认 `/e1002-web.png`)同一网页由**服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNG**(`html_shot.py`),平台拿到纯色图无从抖动,需服务器装 Chrome。**结论(2026-09-10):方案一为主力**——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 `output/e1002/font_compare.png`:矢量字体单色渲染 ≈ 抗锯齿后二值化,都不如点阵),Canvas 逐像素画也改变不了这点(Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版**只用点阵字体的整数倍字号 12/24/36/48/72/84**(`E1002Renderer.__init__` 把基类的 F13/F16/FPACE 都映射到 12px——用量行 标签常规字重、右对齐贴进度条(基类类属性 `LABEL_BOLD`/`LABEL_ALIGN`/`LABEL_GAP`),百分比与 pace 数字粗体;24px 试过被嫌大;另加 F84 给顶部大时钟),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC/设备遥测 走**服务端内存缓存 + 有效期**(`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`/`DEVICE_TTL`,默认 900/60/300/300 秒,0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**;Claude/Codex 用量为**账户级、形态 A**:本机 `main.py --target push` 采集(Claude 复用 Claude Code 钥匙串登录态调 `/api/oauth/usage`;Codex 经 `codex app-server` RPC)后推送原始快照(`X-Push-Token` 鉴权),服务端内存暂存、**不持任何凭证**、渲染时现算 pace。静态形态 `main.py --target e1002`(`output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**,四段版式:A 大时钟 **F84**(墨迹 16~79)+ 其右一列三行、左缘对齐(F12,行距 22):① 电池图标+电量(充电中绿、≤20% 红)② 温度计+温度 | 蓝色水滴+湿度(①② 数字 F12 伪粗体、字号不变,占位 -- 不加粗,2026-09-14)③ 星期/日期(与时钟基线对齐);**右侧天气(2026-09-14)从左到右**:两行左缘对齐(定位水滴+城市 / 天气·温区,底行与时钟同基线、行距 22)| 天气图标(竖直居中)| **实时温度 F72 粗 3**(比时钟小一号——先做过 F84 与时钟同高被嫌大;与时钟同基线 79、数字墨迹 25~79、右贴页边 790;「°」不用大字号——F84 下 35px 宽且比数字高 7px——改 F36 粗 2 作上标、墨迹顶沿对齐数字顶沿、补回字形右侧 3px 空白使墨迹右缘贴页边;此前为「城市 / F24 温度 / 天气·温区」三行右对齐+图标在左,再之前短暂为「图标|两行|F84 温度」)——设备数据来自 **SenseCraft 设备遥测** `sensecraft_api.py`(2026-09-14 新增;先做过「状态栏横排在时钟上方+时钟缩 F60」被否)/天气;B 左 **Claude Usage 两行(5h / 合并行 7d——整行高度不变、**一个大边框**,边框内上下精确对半两条 7px **实心**填充(紧挨、无空隙,2026-09-14 用户要求去掉白隔):上=总 7d、下=fable 7d,各按自身档位取色;右侧百分比与 pace **只显示两者中较大者**,边框与文字随其档位取色;`E1002Renderer._usage_row_split` 以较大者合成一条交给基类 `_usage_row`(已返回条几何)画边框/文字后在内部重绘两条半高实心。2026-09-14 为与右块对称而合并,先做过「两条各带边框的细条+两行数字」版被否;标签去掉 all 前缀)**、右 **Codex Usage 两行(5h / 7d——ChatGPT 订阅的 Codex 额度池;服务端未收到推送时 mock 标「示例数据」)**。宽版用量行的**空值不显示 "--"**:pct 为 None 按 0 画("00%"、pace "00"、空条、全黑中性色),且 pace 列在 diff=0 时三角位画一个等宽等号「=」(2026-09-14 用户要求,原为留空、后短暂用过「-」;已下沉到基类 `_pace`,小屏同样)——各行进度条右端齐平、等宽(2026-09-14;Codex Plus 接口 `secondary_window` 就是 null,原始 `wham/usage` 已核对,5h 行为空是数据如此)。**真 0 的窗口也必须有 pace 列**:Claude 5h 空闲时接口回 `utilization: 0.0, resets_at: null`,算不出 time_pct,早先渲染器遇「有 pct 无 time_pct」整列跳过,该行条就比别行长——现 `data.usage_from_raw` 对 pct=0 且无重置时刻的窗口记 time_pct=0(窗口未开始),基类 `_usage_row` 只要有 pct 就画 pace(time_pct 缺失按 0 差值画 "00"),紧凑单行样式(曾试过撑满高度的大卡片,被否);C **橘喵·今日实时**五列全宽(探店单量/探店流水/团购单量/团购流水/毛利,2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px,此前「值 F36 粗 + 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线,故改竖排;更早试过名与值同行、左上角角标,均已否);D **BTC/USDT 时 K 蜡烛图(近 48 小时 = 2 天,2026-09-14 由 72 根改 48 根)**(免费公开行情:OKX 主源、Coinbase/Huobi 备源,`btc_api.py`,5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换)。六色语义:用量进度条(含边框、百分比数字三者同色——2026-09-14 用户要求百分比随条色)与 pace 各自**三档、颜色顺序固定 绿→黑→红、两者独立取色**:用量 ≤ `USAGE_OK` 30 绿「健康」|30~80 黑「正常」|≥ `USAGE_WARN` 80 红「危险」;pace ≤ −`PACE_TOL` 10 绿「节余」|±10 内黑「持平」|≥ +10 红「偏快」。**黄不用**:2026-09-14 曾按要求加过 60%~80% 黄档(含 `USAGE_NOTICE`/`PACE_WARN` 两个阈值),同日被否并整体移除,别再加回。**今日实时的环比/同比「三角 + 数值」涨绿跌红**(2026-09-14 改,与 K 线一致;基类钩子 `_cmp_color`,三色小屏仍涨红);红另用于定位/太阳闪电,蓝=雨雪。 +> **分支 `feature/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**(7.3 寸 **800×480**,E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发;不支持 ETag/304,内容不变也会整屏重绘(全刷约 17 秒)。**真机验证(2026-09)**:Image 控件只在填写 URL 时抓一次快照、此后不再更新;HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 **`server.py` 动态服务**(部署公网 HTTPS;局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 `IMAGE_PATH`(默认 `/e1002.png`)每次请求**内存中现渲染 PNG**(Pillow 点阵版);方案二 `HTML_PATH`(默认 `/e1002.html`)每次请求生成 **800×480 现代网页**(`html_renderer.py`,系统/网络字体 + 内联 SVG K 线,无 JS)由平台截图——**真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊**;方案三 `HTML_PNG_PATH`(默认 `/e1002-web.png`)同一网页由**服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNG**(`html_shot.py`),平台拿到纯色图无从抖动,需服务器装 Chrome。**结论(2026-09-10):方案一为主力**——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 `output/e1002/font_compare.png`:矢量字体单色渲染 ≈ 抗锯齿后二值化,都不如点阵),Canvas 逐像素画也改变不了这点(Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版**只用点阵字体的整数倍字号 12/24/36/48/84**(`E1002Renderer.__init__` 把基类的 F13/F16/FPACE 都映射到 12px——用量行 标签常规字重、右对齐贴进度条(基类类属性 `LABEL_BOLD`/`LABEL_ALIGN`/`LABEL_GAP`),百分比与 pace 数字粗体;24px 试过被嫌大;另加 F84 给顶部大时钟),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC/设备遥测 走**服务端内存缓存 + 有效期**(`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`/`DEVICE_TTL`,默认 900/60/300/300 秒,0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**;Claude/Codex 用量为**账户级、形态 A**:本机 `main.py --target push` 采集(Claude 复用 Claude Code 钥匙串登录态调 `/api/oauth/usage`;Codex 经 `codex app-server` RPC)后推送原始快照(`X-Push-Token` 鉴权),服务端内存暂存、**不持任何凭证**、渲染时现算 pace。静态形态 `main.py --target e1002`(`output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**,四段版式:A 大时钟 **F84**(墨迹 16~79)+ 其右一列三行、左缘对齐(F12,行距 22):① 电池图标+电量(充电中绿、≤20% 红)② 温度计+温度 | 蓝色水滴+湿度(①② 数字 F12 伪粗体、字号不变,占位 -- 不加粗,2026-09-14)③ 星期/日期(与时钟基线对齐);**右侧天气(2026-09-15 定稿)上下两层**:上层 天气图标 **×1.25** | **实时温度 F48 粗 3**(墨迹顶沿 16 与时钟齐平、底沿 52,右贴页边 790;「°」用 F24 粗 1 作上标、墨迹顶沿对齐数字顶沿、补回字形右侧空白使墨迹右缘贴页边;图标按类别**真实墨迹范围**(`E1002Renderer._ICON_EXTENT`,多云太阳光芒探出名义框顶部 4 格、雷阵雨闪电垂到云下至 46)竖直居中于温度墨迹中心);下层 **一行「红定位水滴+城市 天气 温区」F12 右对齐贴 790**,与日期行同基线(底沿 79,与时钟基线对齐)。历史(均已否):同日上午「三行信息(天气/温区/城市)左缘对齐|图标 ×1.75|温度 F84 与时钟同高」;2026-09-14「两行(城市/天气·温区)|图标 ×1.5|温度 F72 小一号」;更早「城市 / F24 温度 / 天气·温区」三行右对齐+图标在左)——设备数据来自 **SenseCraft 设备遥测** `sensecraft_api.py`(2026-09-14 新增;先做过「状态栏横排在时钟上方+时钟缩 F60」被否)/天气;B 左 **Claude Usage 两行(5h / 合并行 7d——整行高度不变、**一个大边框**,边框内上下精确对半两条 7px **实心**填充(紧挨、无空隙,2026-09-14 用户要求去掉白隔):上=总 7d、下=fable 7d,各按自身档位取色;右侧百分比与 pace **只显示两者中较大者**,边框与文字随其档位取色;`E1002Renderer._usage_row_split` 以较大者合成一条交给基类 `_usage_row`(已返回条几何)画边框/文字后在内部重绘两条半高实心。2026-09-14 为与右块对称而合并,先做过「两条各带边框的细条+两行数字」版被否;标签去掉 all 前缀)**、右 **Codex Usage 两行(5h / 7d——ChatGPT 订阅的 Codex 额度池;服务端未收到推送时 mock 标「示例数据」)**。宽版用量行的**空值不显示 "--"**:pct 为 None 按 0 画("00%"、pace "00"、空条、全黑中性色),且 pace 列在 diff=0 时三角位画一个等宽等号「=」(2026-09-14 用户要求,原为留空、后短暂用过「-」;已下沉到基类 `_pace`,小屏同样)——各行进度条右端齐平、等宽(2026-09-14;Codex Plus 接口 `secondary_window` 就是 null,原始 `wham/usage` 已核对,5h 行为空是数据如此)。**真 0 的窗口也必须有 pace 列**:Claude 5h 空闲时接口回 `utilization: 0.0, resets_at: null`,算不出 time_pct,早先渲染器遇「有 pct 无 time_pct」整列跳过,该行条就比别行长——现 `data.usage_from_raw` 对 pct=0 且无重置时刻的窗口记 time_pct=0(窗口未开始),基类 `_usage_row` 只要有 pct 就画 pace(time_pct 缺失按 0 差值画 "00"),紧凑单行样式(曾试过撑满高度的大卡片,被否);C **橘喵·今日实时**五列全宽(探店单量/探店流水/团购单量/团购流水/毛利,2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px,此前「值 F36 粗 + 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线,故改竖排;更早试过名与值同行、左上角角标,均已否);D **BTC/USDT 时 K 蜡烛图(近 48 小时 = 2 天,2026-09-14 由 72 根改 48 根)**(免费公开行情:OKX 主源、Coinbase/Huobi 备源,`btc_api.py`,5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换)。六色语义:用量进度条(含边框、百分比数字三者同色——2026-09-14 用户要求百分比随条色)与 pace 各自**三档、颜色顺序固定 绿→黑→红、两者独立取色**:用量 ≤ `USAGE_OK` 30 绿「健康」|30~80 黑「正常」|≥ `USAGE_WARN` 80 红「危险」;pace ≤ −`PACE_TOL` 10 绿「节余」|±10 内黑「持平」|≥ +10 红「偏快」。**黄不用**:2026-09-14 曾按要求加过 60%~80% 黄档(含 `USAGE_NOTICE`/`PACE_WARN` 两个阈值),同日被否并整体移除,别再加回。**今日实时的环比/同比「三角 + 数值」涨绿跌红**(2026-09-14 改,与 K 线一致;基类钩子 `_cmp_color`,三色小屏仍涨红);红另用于定位/太阳闪电,蓝=雨雪。 ## 命令 @@ -60,7 +60,7 @@ EPD_HOST=epd400a44.local python3 main.py - **`_trend_chart`(当前隐藏)**:近30天三指标折线,用 `_styled_polyline()` 按 solid/dashed/dotted 区分、主指标(流水)红线,各指标按自身极值独立归一化。已保留但 `render()` 未调用,想恢复即在 C 前挂回。 - **天气图标全矢量手绘**(无图片资源):`_weather_category()` 把和风 icon 代码归并为 8 类(sunny/clear_night/cloudy/overcast/thunder/rain/snow/fog,识别不了回退 cloudy),再由 `_weather_icon()` 用 `_sun`/`_cloud` 等基元按 1bit 描边画出;改天气展示从这里动。 - **`pusher.py`** — 直连局域网墨水屏设备(喵喵固件,开源:https://gitee.com/gxp666111/miaomiao,`web/` 即设备自带管理页,可浏览器打开 `http:///` 调试同一套接口)。`push_image()` 先调 `GET /images` 拿画廊已存图片名单,逐个 `POST /delete_image?name=`清空(单张删除失败不阻塞后续),再 `POST /upload`(`Content-Type: image/png`,body 为原始 PNG 字节,非 multipart)触发设备转换+刷屏。**清空画廊是刻意设计**:设备把每次 `/upload` 都追加存一份到画廊(`GET /images` 可见),不清理会越推越多、且设备当前 `current_mode` 为 slideshow 会轮播到旧图;先清后传使画廊恒为 1 张,等价于始终静态展示最新一张。**清理非核心**:拉取画廊列表本身失败也不阻塞后续 `/upload`(跳过清理,直接推)。`_request()` 对连接类异常(`urllib.error.URLError`,含 mDNS `.local` 域名解析瞬时失败)内置重试(`RETRY_TIMES=2`,间隔 `RETRY_DELAY=2s`)——mDNS 基于组播 UDP,偶发丢包很正常,单次失败不该让整轮推送直接跳过。注:历史上曾走 Zectrix 公网 API(`POST /devices/{id}/display/image` + 设备 MAC + `ZECTRIX_API_KEY`),现统一为局域网直连。 -- **`renderer_e1002.py`** — reTerminal E1002 目标:`E1002Renderer(DashboardRenderer)` 以 **k=1** 复用组件(字号 12/24/48 与三色版一致),只重写版式与配色钩子(基类抽出类属性 `PCT_TRACK`(小屏 F16 百分比的手动字距),宽版置 0——其 F16 映射到 12px、加粗后由 `_track()` 自动加 1px 字距;基类另抽 `FPACE` 让 pace 字体与标签 F13 解耦)。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(`_status_battery`/`_status_env`)、星期/日期 F12(按日期数字底沿与时钟**基线对齐**;三行中心约 30.5/52.5/74.5,行距 22);天气右(定位+城市 / 天气·温区两行 | 图标 | 温度 F72+「°」F36 上标,右贴 x=790,见分支说明);B 用量 `_usage_block`×2(左 `usage` 10–386|竖规 x=400|右 `usage_gpt` 414–790):`_label` 标题 + 右侧「上次更新 HH:MM」(与今日实时同款文案)或 mock 的「示例数据」,行用基类紧凑 `_usage_row`(行距 31,宽版把 `bar["label"]` 长标签代入 `k`,`label_w` 按本块最长标签自适应);C `_realtime` 今日实时五列全宽(`mao.cols` 全部,含 `wide` 列):列内四行 `ctext`/`_cmp_line` 逐行水平居中——名 F12 y0+21 / 值 `_value` F24 粗+小号 ¥/万 y0+38 / 环比 y0+72 / 同比 y0+88(按墨迹框:标题↔名 ~9px——2026-09-14 整列下移 4px,原 ~5px 用户觉得挤;名↔值 ~9px、值↔环比 ~6px),竖规 y0+26~102,区块底 y0+104;D `_btc_chart` BTC K 线(`data.btc`,标题按 `bar` 显示「时K/日K」:右侧最新价 F12 粗 + 口径 `chg_label`(24h/1d)+ 涨跌三角 + 涨跌幅(F12 粗,2026-09-14 加粗),涨绿跌红;每根蜡烛 1px 影线 + 实心实体(宽≈槽位 60% 取奇数)、收≥开 绿 否则 红;右侧 66px 标签列放最高/最低/最新价,最新价横贯一道点规、标签避让顶底;底部基线 + 首/中/末时间标签 `t`;无数据居中 `--`)。用量块 `_usage_block` 返回末条进度条底部,B/C 分割线取其 +16px(用户反馈太挤)。配色钩子覆写:`_usage_colors` 三档(≥`USAGE_WARN` 红|≤`USAGE_OK` 绿|其间黑;填充、边框、百分比文字三者同色)、`_pace_color` 三档(≥+`PACE_TOL` 红|≤−`PACE_TOL` 绿|其间黑),阈值由 `usage.ok`/`usage.pace_tol`(来自 config)在 `_usage_block` 内注入。天气图标 `wet=BLUE` 让雨滴/雪花着蓝。可选六色测试色条 `_color_strip`(`E1002_COLOR_STRIP=1` 打开,默认关)。`write_page()` 写出固定 800×480 的 `index.html`(零边距、`image-rendering: pixelated`、PNG 带时间戳参数防缓存)。**为何固定尺寸而非自适应**:云端截图视口/DPR 不可控,自适应会重排+抗锯齿产生中间色再被平台抖动;固定页里的 1:1 PNG 每个像素本就是六色之一,截图零损失——已用无头 Chrome `--window-size=800,480` 截图验证与 PNG 逐像素一致。设备端 HTML 控件须铺满整个画布。历史(均已弃用):k=2 整体放大版;「左用量/右今日实时/下 30 天折线」版;「用量大卡片撑满」版。 +- **`renderer_e1002.py`** — reTerminal E1002 目标:`E1002Renderer(DashboardRenderer)` 以 **k=1** 复用组件(字号 12/24/48 与三色版一致),只重写版式与配色钩子(基类抽出类属性 `PCT_TRACK`(小屏 F16 百分比的手动字距),宽版置 0——其 F16 映射到 12px、加粗后由 `_track()` 自动加 1px 字距;基类另抽 `FPACE` 让 pace 字体与标签 F13 解耦)。四段、三道横规,各段 y 由上一段实际底部推算:A 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(`_status_battery`/`_status_env`)、星期/日期 F12(按日期数字底沿与时钟**基线对齐**;三行中心约 30.5/52.5/74.5,行距 22);天气右(上层 图标 ×1.25 | 温度 F48 顶沿齐时钟+「°」F24 上标;下层 一行 定位+城市·天气·温区 右对齐,均贴 x=790,见分支说明);B 用量 `_usage_block`×2(左 `usage` 10–386|竖规 x=400|右 `usage_gpt` 414–790):`_label` 标题 + 右侧「上次更新 HH:MM」(与今日实时同款文案)或 mock 的「示例数据」,行用基类紧凑 `_usage_row`(行距 31,宽版把 `bar["label"]` 长标签代入 `k`,`label_w` 按本块最长标签自适应);C `_realtime` 今日实时五列全宽(`mao.cols` 全部,含 `wide` 列):列内四行 `ctext`/`_cmp_line` 逐行水平居中——名 F12 y0+21 / 值 `_value` F24 粗+小号 ¥/万 y0+38 / 环比 y0+72 / 同比 y0+88(按墨迹框:标题↔名 ~9px——2026-09-14 整列下移 4px,原 ~5px 用户觉得挤;名↔值 ~9px、值↔环比 ~6px),竖规 y0+26~102,区块底 y0+104;D `_btc_chart` BTC K 线(`data.btc`,标题按 `bar` 显示「时K/日K」:右侧最新价 F12 粗 + 口径 `chg_label`(24h/1d)+ 涨跌三角 + 涨跌幅(F12 粗,2026-09-14 加粗),涨绿跌红;每根蜡烛 1px 影线 + 实心实体(宽≈槽位 60% 取奇数)、收≥开 绿 否则 红;右侧 66px 标签列放最高/最低/最新价,最新价横贯一道点规、标签避让顶底;底部基线 + 首/中/末时间标签 `t`;无数据居中 `--`)。用量块 `_usage_block` 返回末条进度条底部,B/C 分割线取其 +16px(用户反馈太挤)。配色钩子覆写:`_usage_colors` 三档(≥`USAGE_WARN` 红|≤`USAGE_OK` 绿|其间黑;填充、边框、百分比文字三者同色)、`_pace_color` 三档(≥+`PACE_TOL` 红|≤−`PACE_TOL` 绿|其间黑),阈值由 `usage.ok`/`usage.pace_tol`(来自 config)在 `_usage_block` 内注入。天气图标 `wet=BLUE` 让雨滴/雪花着蓝。可选六色测试色条 `_color_strip`(`E1002_COLOR_STRIP=1` 打开,默认关)。`write_page()` 写出固定 800×480 的 `index.html`(零边距、`image-rendering: pixelated`、PNG 带时间戳参数防缓存)。**为何固定尺寸而非自适应**:云端截图视口/DPR 不可控,自适应会重排+抗锯齿产生中间色再被平台抖动;固定页里的 1:1 PNG 每个像素本就是六色之一,截图零损失——已用无头 Chrome `--window-size=800,480` 截图验证与 PNG 逐像素一致。设备端 HTML 控件须铺满整个画布。历史(均已弃用):k=2 整体放大版;「左用量/右今日实时/下 30 天折线」版;「用量大卡片撑满」版。 - **`server.py`** — E1002 动态服务(stdlib `ThreadingHTTPServer`,无第三方依赖)。`build_data()` → `(data, 各路状态)`:日期按 `config.TZ_NAME` 时区现算;四路上游(天气/橘喵/BTC/设备遥测)经 `_cached(key, ttl, fetch)`——`STATE.last_good[key]` 未过期(`time - STATE.fetched_at[key] < ttl`)直接用;过期的**并行**提交到线程池 `_POOL`(`_refresh` 跑 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`,成功更新数据与时间戳),`build_data` 用 `concurrent.futures.wait` 整体最多等 **`FETCH_BUDGET`** 秒(默认 5,`.env` 可调),再由 `_settle` 结算:完成→`fetched(0.27s)`(带耗时)/失败沿用旧值 `stale`/从未成功占位 `none`;**未完成→ `slow→stale`(用上一次成功值)或 `slow→none`(占位)**,拉取线程继续跑完写入缓存供下次请求(stale-while-revalidate),`STATE.inflight[key]` 保证同一路进行中不重复实拉。响应上限 ≈ 预算 + 渲染,任一路上游卡死(socket 超时和风/橘喵 30s、BTC 8s)不再拖垮整图。**2026-09-14 前为串行实拉**(三路全拉约 1.1~2.6s,单路卡住即整体超时);改并行后三路全拉约 0.75s(取最慢一路)。日志每行末尾 `[weather:cache(12s) mao:fetched(0.06s) btc:slow→stale]` 可直接看命中/耗时/超预算情况,`/health` 有 `cache_age_s`/`ttl_s`/`fetch_budget_s`/`inflight`;用量取 `STATE.raw`(推送暂存)→ `data._usage(now, raw=…)`/`data._usage_gpt(now, raw=…)`,未推送时 Claude 全 `--`、ChatGPT 用 mock。端点:`GET IMAGE_PATH` → `render_png_bytes()`(`_RENDER_LOCK` 串行化,Pillow 渲染器非线程安全);`GET HTML_PATH` → `html_renderer.render_html()`;`GET HTML_PNG_PATH` → `html_shot.screenshot_html()`+`quantize6()`(`?raw=1` 跳过量化);`GET /health` JSON;`POST /push/usage|/push/usage_gpt`(body=原始快照 JSON,≤64KB,头 `X-Push-Token`==`PUSH_TOKEN`,为空拒绝);`GET /push/*` 查看暂存(同令牌)。所有响应 `Cache-Control: no-store`;每请求打一行日志(含 `CF-Connecting-IP`)。`PUSH_STATE_PATH` 非空时推送数据落盘(原子替换),重启恢复;图片永不落盘。实测:三路全实拉约 1.1~1.3s(天气 0.28s、橘喵 0.06s、OKX 0.7s 串行),全部命中缓存约 0.02s(渲染 20ms + 编码 14ms)。 - **`html_shot.py`** — 方案三:`screenshot_html(html)` 用无头 Chrome(`find_chrome()` 自动探测或 `CHROME_BIN`;Linux root 需 `CHROME_EXTRA_ARGS=--no-sandbox`)把 HTML 截成 800×480,临时目录用后即删,`_SHOT_LOCK` 串行化;**切勿传 `--user-data-dir` 指向全新目录**——新版 `--headless=new` 对空 profile 首启会卡死到超时(实测 A/B/D/E 全超时、只有不带它的 C 2 秒完成)。`quantize6(img)`:每通道二值化(阈值 `HTML_PNG_THRESHOLD` 默认 160,偏向保墨迹,细笔画浅灰边归黑而非丢白)→ Pillow 调色板 `quantize(dither=NONE)` 映射到 `SPECTRA6`,输出保证只含六色。服务端 `?raw=1` 返回未量化截图供对照。实测单次约 3.3s(Chrome 启动为主)。 - **`html_renderer.py`** — 方案二网页版:纯 HTML+CSS+内联 SVG、无 JS,数据服务端写死;字体栈 PingFang/微软雅黑/Noto Sans SC + Google Fonts 兜底(平台截图机若无中文字体会掉字,能否加载取决于其出网能力);颜色只用六色纯值;配色规则与 PNG 版一致(用量/pace 三档、涨红跌黑、K 线涨绿跌红),版式同构。K 线标题右侧价格用 ``(`h3 b::before` 的方块装饰只给标题)。 diff --git a/renderer_e1002.py b/renderer_e1002.py index 72f5803..87a2521 100644 --- a/renderer_e1002.py +++ b/renderer_e1002.py @@ -7,7 +7,7 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件( 版式(k=1,点阵字体且**只用整数倍字号 12/24/36/48/84**,不放大;四段,三道横规): A 顶部(0~90) :**大时钟 F84**(墨迹 16~79)+ 其右一列三行、左缘对齐:① 电池图标+电量 ② 温度计+温度 | 水滴+湿度 ③ 星期/日期 - / 右侧:两行(定位+城市 / 天气·温区,底行与时钟同基线)| 天气图标 | **实时温度 F72**(比时钟小一号、同基线、「°」F36 上标、右贴页边) + / 右侧上下两层:上 天气图标 ×1.25 | **实时温度 F48 粗 3**(墨迹顶沿 16 与时钟齐平、「°」F24 上标、右贴页边 790);下 一行「定位水滴+城市 天气 温区」F12 右对齐贴 790、与日期同基线 (F12,行距 22,日期与时钟数字**基线对齐**;设备数据来自 SenseCraft 遥测 `data.device`;2026-09-14 定稿, 此前「状态栏横排在时钟上方+时钟缩 F60」版被否) |天气(右,贴 790)——天气组件与三色版同尺寸。 @@ -64,8 +64,7 @@ class E1002Renderer(DashboardRenderer): self.F13 = ImageFont.truetype(font_path, 12) self.F16 = self.F13 self.FPACE = self.F13 - self.F84 = ImageFont.truetype(font_path, 84) - self.F72 = ImageFont.truetype(font_path, 72) # 6 倍整数缩放;右上角实时温度(比时钟小一号,2026-09-14 用户要求) + self.F84 = ImageFont.truetype(font_path, 84) # 顶部大时钟 # ---------- 配色钩子(三档,顺序固定 绿 → 黑 → 红;用量与 pace 各自独立计算)---------- _ok, _pace_tol = _cfg.USAGE_OK, _cfg.PACE_TOL # 由 _usage_block 按数据覆盖 @@ -96,6 +95,10 @@ class E1002Renderer(DashboardRenderer): return GREEN return BLACK + # 天气图标各类别的真实墨迹上下沿(基类 _weather_icon 名义框 36×38 的坐标系、未乘 s):用于按墨迹而非名义框竖直居中。 + _ICON_EXTENT = {"sunny": (1, 37), "clear_night": (6, 32), "overcast": (10, 34), "fog": (9, 34), + "cloudy": (-4, 38), "rain": (5, 39), "snow": (5, 40), "thunder": (5, 46)} + # ---------- 组件 ---------- def _status_battery(self, x, cy, dev): """状态项·电池(F12,数字伪粗体,墨迹中心 cy):外框 16×9 + 正极 2×4,内部按电量实心填充——充电中 GREEN、≤20% RED、其余 BLACK; @@ -295,7 +298,7 @@ class E1002Renderer(DashboardRenderer): usage, gpt = data["usage"], data.get("usage_gpt") or {} # ---- A. 顶部:大时钟 F84 占满整段(左);其右一列三行、左缘与日期对齐:① 电池 ② 温度+湿度 ③ 星期/日期(与时钟基线对齐) - # / 天气(右):两行(定位+城市 / 天气·温区,底行同基线)| 图标 | 温度 F72 右贴页边 ---- + # / 天气(右):上层 图标 ×1.25 | 温度 F48(顶沿与时钟齐平、右贴页边);下层 一行「定位+城市 天气 温区」右对齐、与日期同基线 ---- DIV_Y = 90 TIME_BOLD, INK_TOP = 3, 16 # 伪粗体 3px;墨迹顶沿 16,底沿 79(F84 数字墨迹高 63) _, tt, _, tb = self.F84.getbbox(dt["time"]) @@ -313,34 +316,33 @@ class E1002Renderer(DashboardRenderer): dev = data.get("device") self._status_battery(col_x, cy_date - 2 * PITCH, dev) self._status_env(col_x, cy_date - PITCH, dev) - # 天气(右,2026-09-14 用户要求)从左到右:两行信息(左缘对齐:① 定位水滴+城市 ② 天气·温区,底行与时钟同基线)| 天气图标(竖直居中) - # | 实时温度 F72(比时钟小一号;与时钟同粗、同基线 79,数字墨迹 25~79)右贴页边 R。曾试过 F84 与时钟同高,被嫌大。 - loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}' - # 度数符号不用大字号(F84 下 35px 宽且比数字高 7px):「°」F36 粗 2 作上标,墨迹顶沿对齐数字顶沿 - FT = self.F72 + # 天气(右,2026-09-15 用户要求)上下两层:上层 天气图标 | 实时温度 F48(右贴页边 R,墨迹顶沿 16 与时钟齐平); + # 下层 一行「定位水滴+城市 天气 温区」F12 右对齐贴 R,与日期行同基线(底沿 79,与时钟基线对齐)。 + # 此前(同日上午)为「三行信息|图标 ×1.75|温度 F84 与时钟同高」,用户随即改为本版;更早为 F72「小一号」版(2026-09-14)。 + loc, temp, cond, rng = wt["loc"], wt["temp"], wt["cond"], wt["range"] + FT, DEG_F = self.F48, self.F24 # 「°」小一号作上标(大字号度号又宽又高),墨迹顶沿对齐数字顶沿 has_deg = temp.endswith("°") digits = temp[:-1] if has_deg else temp - DEG_BOLD, DEG_GAP = 2, 2 - deg_w = self._tw_b("°", self.F36, DEG_BOLD) + DEG_GAP if has_deg else 0 + DEG_BOLD, DEG_GAP = 1, 2 + deg_w = self._tw_b("°", DEG_F, DEG_BOLD) + DEG_GAP if has_deg else 0 _, tdt, _, tdb = FT.getbbox(digits) temp_x = R - deg_w - self._tw_b(digits, FT, TIME_BOLD) - temp_y = base_y - tdb # 数字底沿与时钟基线对齐 + temp_y = INK_TOP - tdt # 数字墨迹顶沿与时钟顶沿齐平(16),底沿 52 self.text(temp_x, temp_y, digits, FT, bold=TIME_BOLD) if has_deg: - rb = self.F36.getlength("°") - self.F36.getmask("°").getbbox()[2] # 字形右侧空白 3px,补回使墨迹右缘贴 R - self.rtext(R + rb, temp_y + tdt - self.F36.getbbox("°")[1], "°", self.F36, bold=DEG_BOLD) - s = 1.5 - iw, ih = 36 * s, 38 * s - icon_x = temp_x - 14 - iw - self._weather_icon(icon_x, (DIV_Y - ih) / 2, - self._weather_category(wt.get("icon")), s=s, accent=RED, wet=BLUE) - PIN_W, PIN_GAP = 8, 6 - col_w = max(PIN_W + PIN_GAP + self._tw(loc, self.F12), self._tw(cr, self.F12)) - col_x = icon_x - 12 - col_w - self.text(col_x, date_y, cr, self.F12) # 底行:天气·温区,与日期同基线 - loc_y = date_y - PITCH - self._loc_pin(col_x, loc_y + 2, PIN_W, fg=RED) - self.text(col_x + PIN_W + PIN_GAP, loc_y, loc, self.F12) + rb = DEG_F.getlength("°") - DEG_F.getmask("°").getbbox()[2] # 字形右侧空白,补回使墨迹右缘贴 R + self.rtext(R + rb, temp_y + tdt - DEG_F.getbbox("°")[1], "°", DEG_F, bold=DEG_BOLD) + temp_cy = INK_TOP + (tdb - tdt) / 2 # 温度墨迹竖直中心,图标以此居中 + s = 1.25 + cat = self._weather_category(wt.get("icon")) + et, eb = self._ICON_EXTENT.get(cat, (0, 38)) # 该类图标真实墨迹的上下沿(名义框 36×38 之外:多云太阳光芒探出顶部、雷阵雨闪电垂到云下) + icon_x = temp_x - 14 - 36 * s + self._weather_icon(icon_x, temp_cy - (et + eb) * s / 2, cat, s=s, accent=RED, wet=BLUE) + PIN_W, PIN_GAP, SEP = 8, 6, " " + info = f"{loc}{SEP}{cond}{SEP}{rng}" + info_x = R - self._tw(info, self.F12) + self.text(info_x, date_y, info, self.F12) # 底行与日期同基线 + self._loc_pin(info_x - PIN_GAP - PIN_W, date_y + 2, PIN_W, fg=RED) self.hdash(8, self.W - 8, DIV_Y) # ---- B. 用量:左 Claude | 右 Codex(紧凑单行,与三色版同款)----