feat: E1002 顶部时钟改 F84+用量行 12px 三档配色、标签右对齐

- 顶部时钟 F48→F84 占满 A 段,星期/日期移到时钟右侧并与基线对齐
- 用量行:标签常规字重右对齐贴进度条,百分比与 pace 数字 12px 粗体
- 配色:进度条/边框/百分比同色,用量与 pace 各自三档绿/黑/红、独立取色
  (曾按要求加过黄档,同日被否并移除)
- 用量块标题右侧「快照」改为「上次更新」,与今日实时统一
- 今日实时标题与指标名间距加大 4px
- 基类抽出 PCT_TRACK / LABEL_BOLD / LABEL_ALIGN / FPACE 供宽版覆写,
  400×300 小屏输出逐像素不变

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:39:55 +08:00
parent b58e1decf0
commit 2015947812
7 changed files with 79 additions and 51 deletions

View File

@@ -35,7 +35,7 @@ PUSH_STATE_PATH=
PUSH_URL= PUSH_URL=
# 时区(服务端可能在境外 VPS所有时间按此时区 # 时区(服务端可能在境外 VPS所有时间按此时区
TZ_NAME=Asia/Shanghai TZ_NAME=Asia/Shanghai
# 用量三档阈值(宽版)与 BTC K 线 # 用量/pace 三档阈值(宽版,颜色顺序 绿→黑→红,两者独立取色)与 BTC K 线
USAGE_WARN=80 USAGE_WARN=80
USAGE_OK=30 USAGE_OK=30
PACE_TOL=10 PACE_TOL=10

View File

@@ -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/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**`E1002Renderer.__init__` 把基类的 F13/F16 换成 12/24px),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC 走**服务端内存缓存 + 有效期**`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`,默认 900/60/300 秒0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**Claude/ChatGPT 用量**由本机 `main.py --target push` 推送原始快照**`X-Push-Token` 鉴权),服务端内存暂存、渲染时现算 pace。静态形态 `main.py --target e1002``output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**四段版式A 时钟/天气B 左 **Claude Usage 三行all 5h / all 7d / fable 7d标签沿用 statusline 写法、小写开头)**、右 **ChatGPT Usagemock标「示例数据」**紧凑单行样式曾试过撑满高度的大卡片被否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 线替换)。六色语义:用量进度条(含边框)与 pace 各自**三档**——绿=健康(用量 ≤ `USAGE_OK` 30 / pace ≤ `PACE_TOL` 10、黑=正常、红=危险(用量 ≥ `USAGE_WARN` 80 / pace ≥ +10百分比数字只在红档转红绿小字在白底发虚。红另用于涨/定位/太阳闪电,蓝=雨雪;**黄不用**——用户明确不要黄色进度条(曾有 60%80% 黄「注意」档,已移除) > **分支 `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`,默认 900/60/300 秒0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**Claude/ChatGPT 用量**由本机 `main.py --target push` 推送原始快照**`X-Push-Token` 鉴权),服务端内存暂存、渲染时现算 pace。静态形态 `main.py --target e1002``output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**四段版式A 时钟/天气B 左 **Claude Usage 三行all 5h / all 7d / fable 7d标签沿用 statusline 写法、小写开头)**、右 **ChatGPT Usagemock标「示例数据」**紧凑单行样式曾试过撑满高度的大卡片被否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 绿「健康」3080 黑「正常」|≥ `USAGE_WARN` 80 红「危险」pace ≤ `PACE_TOL` 10 绿「节余」±10 内黑「持平」|≥ +10 红「偏快」。**黄不用**2026-09-14 曾按要求加过 60%80% 黄档(含 `USAGE_NOTICE`/`PACE_WARN` 两个阈值),同日被否并整体移除,别再加回。红另用于涨/定位/太阳闪电,蓝=雨雪
## 命令 ## 命令
@@ -40,7 +40,7 @@ EPD_HOST=epd400a44.local python3 main.py
`main.py` 串联三层,各层职责单一、低耦合: `main.py` 串联三层,各层职责单一、低耦合:
- **`data.py`** — 两种形态:本机 `get_dashboard_data()`(带文件缓存,用量读本地快照);服务端用纯拉取函数 `weather_fetch()`/`mao_fetch()`/`btc_fetch()` + `usage_from_raw(now, raw, windows, title)``_usage(now, raw=)`/`_usage_gpt(now, raw=)` 都基于它raw=None 时前者读本地快照、后者用 `gpt_mock_raw()`)。所有「现在」用 `now_tz()`、时间戳格式化用 `_local()``TZ=ZoneInfo(config.TZ_NAME)`),本机与境外服务端结果一致。返回固定结构的 dict`date`/`weather`/`mao`/`usage`/`usage_gpt`/`btc`。日期时间用真实系统时间;橘喵今日经营由 `_mao()``jm_api`、天气由 `_weather()``weather_api`和风定位南京·江宁、Claude 用量由 `_usage()``usage_local`**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可renderer 依赖其 key`weather.icon` 为和风图标代码;`mao.cols[]``{k, v, hb, tb, wide}`,五列由 `REALTIME_COLS`(标签、接口字段前缀、格式化、是否仅宽版)驱动,`_col_from()``.get()``today{Base}`/`{base}WowRate`/`{base}YoyRate`,字段缺失(旧后端未上团购)该列占位 `--``wide=True`(团购两列)小屏 `render()` 跳过、宽版全显;`hb/tb` 为环比/同比 `(direction, text)`direction 为 `None` 时不画三角;接口文档见 `../docs/首页看板接口文档-20260911.md`(毛利口径已含团购、金额与增长率为字符串、增长率可 `null``usage.bars[]``{k(短标签,小屏用), label(长标签 all 5h/fable 7d宽版用), pct(或 None), time_pct?, reset?, scoped}``usage.warn`/`usage.ok`/`usage.pace_tol`红/绿/pace 阈值(小屏只用 warn`usage.updated` 为快照落盘时刻;`usage_gpt``usage` 同构,目前是 **mock**`mock: True`;三行故意取 88/71/22 以常驻红/黑/绿三档),接真实数据源时替换 `_usage_gpt()` 即可)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不用它。宽版另有 `btc``_btc()``btc_api.get_candles(BTC_BAR, BTC_LIMIT)` 取 K 线(默认 1H×48结构 `{symbol, bar, src, candles:[{t, o, h, l, c}], last, chg, chg_label}`chg 时 K 取相对 24 根前收盘24h、日 K 取相对上一根;缓存 `.btc_cache.json`TTL=`BTC_TTL` 默认 5 分钟,周期变更即失效,失败沿用旧缓存,仍无则 candles 为空 → 面板 `--`)。 - **`data.py`** — 两种形态:本机 `get_dashboard_data()`(带文件缓存,用量读本地快照);服务端用纯拉取函数 `weather_fetch()`/`mao_fetch()`/`btc_fetch()` + `usage_from_raw(now, raw, windows, title)``_usage(now, raw=)`/`_usage_gpt(now, raw=)` 都基于它raw=None 时前者读本地快照、后者用 `gpt_mock_raw()`)。所有「现在」用 `now_tz()`、时间戳格式化用 `_local()``TZ=ZoneInfo(config.TZ_NAME)`),本机与境外服务端结果一致。返回固定结构的 dict`date`/`weather`/`mao`/`usage`/`usage_gpt`/`btc`。日期时间用真实系统时间;橘喵今日经营由 `_mao()``jm_api`、天气由 `_weather()``weather_api`和风定位南京·江宁、Claude 用量由 `_usage()``usage_local`**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可renderer 依赖其 key`weather.icon` 为和风图标代码;`mao.cols[]``{k, v, hb, tb, wide}`,五列由 `REALTIME_COLS`(标签、接口字段前缀、格式化、是否仅宽版)驱动,`_col_from()``.get()``today{Base}`/`{base}WowRate`/`{base}YoyRate`,字段缺失(旧后端未上团购)该列占位 `--``wide=True`(团购两列)小屏 `render()` 跳过、宽版全显;`hb/tb` 为环比/同比 `(direction, text)`direction 为 `None` 时不画三角;接口文档见 `../docs/首页看板接口文档-20260911.md`(毛利口径已含团购、金额与增长率为字符串、增长率可 `null``usage.bars[]``{k(短标签,小屏用), label(长标签 all 5h/fable 7d宽版用), pct(或 None), time_pct?, reset?, scoped}``usage.warn`/`usage.ok`/`usage.pace_tol`用量与 pace 的三档阈值(小屏只用 warn`usage.updated` 为快照落盘时刻;`usage_gpt``usage` 同构,目前是 **mock**`mock: True`;三行故意取 88/71/22 以常驻红/黑/绿三档),接真实数据源时替换 `_usage_gpt()` 即可)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不用它。宽版另有 `btc``_btc()``btc_api.get_candles(BTC_BAR, BTC_LIMIT)` 取 K 线(默认 1H×48结构 `{symbol, bar, src, candles:[{t, o, h, l, c}], last, chg, chg_label}`chg 时 K 取相对 24 根前收盘24h、日 K 取相对上一根;缓存 `.btc_cache.json`TTL=`BTC_TTL` 默认 5 分钟,周期变更即失效,失败沿用旧缓存,仍无则 candles 为空 → 面板 `--`)。
- **缓存**(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 `.weather_cache.json`TTL=`WEATHER_TTL`,默认 15min、趋势 `.trend_cache.json`(按日期 key每天只拉一次、BTC K 线 `.btc_cache.json`TTL=`BTC_TTL`,默认 5min三者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore` - **缓存**(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 `.weather_cache.json`TTL=`WEATHER_TTL`,默认 15min、趋势 `.trend_cache.json`(按日期 key每天只拉一次、BTC K 线 `.btc_cache.json`TTL=`BTC_TTL`,默认 5min三者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore`
- **用量的 pace时间维度对比**`_usage()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)``pace=pcttime_pct`>0 超前↑ / <0 节余↓);故即使 `utilization` 来自可能滞后的本地缓存pace 仍随时钟准确推进5h 窗口 18000s7d 窗口 604800s - **用量的 pace时间维度对比**`_usage()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)``pace=pcttime_pct`>0 超前↑ / <0 节余↓);故即使 `utilization` 来自可能滞后的本地缓存pace 仍随时钟准确推进5h 窗口 18000s7d 窗口 604800s
- **`jm_api.py`** jm-devops 后端统计接口客户端**设备密钥授权**`grant_type=client_secret`免验证码)。先用 `CLIENT_ID + CLIENT_SECRET` `POST /auth/login` token进程内缓存 `expire_in` 提前刷新再带 `Authorization: Bearer <token>` + `clientid` 头调 `/system/statistics/{realtime,recent-days}` 401 token 重登一次重试token 字段为蛇形 `access_token`/`expire_in`历史上曾免鉴权直调更早曾用账号密码+验证码 OCR 登录ddddocr 不稳定现统一为 client_secret 授权——密钥即长期凭证务必走 HTTPS优先用 `JM_CLIENT_SECRET` 环境变量可在后端 `sys_client` 轮换 - **`jm_api.py`** jm-devops 后端统计接口客户端**设备密钥授权**`grant_type=client_secret`免验证码)。先用 `CLIENT_ID + CLIENT_SECRET` `POST /auth/login` token进程内缓存 `expire_in` 提前刷新再带 `Authorization: Bearer <token>` + `clientid` 头调 `/system/statistics/{realtime,recent-days}` 401 token 重登一次重试token 字段为蛇形 `access_token`/`expire_in`历史上曾免鉴权直调更早曾用账号密码+验证码 OCR 登录ddddocr 不稳定现统一为 client_secret 授权——密钥即长期凭证务必走 HTTPS优先用 `JM_CLIENT_SECRET` 环境变量可在后端 `sys_client` 轮换
@@ -55,7 +55,7 @@ EPD_HOST=epd400a44.local python3 main.py
- **`_trend_chart`当前隐藏**近30天三指标折线 `_styled_polyline()` solid/dashed/dotted 区分主指标(流水)红线各指标按自身极值独立归一化已保留但 `render()` 未调用想恢复即在 C 前挂回 - **`_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 描边画出改天气展示从这里动 - **天气图标全矢量手绘**无图片资源`_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://<EPD_HOST>/` 调试同一套接口)。`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`现统一为局域网直连 - **`pusher.py`** 直连局域网墨水屏设备喵喵固件开源https://gitee.com/gxp666111/miaomiao`web/` 即设备自带管理页可浏览器打开 `http://<EPD_HOST>/` 调试同一套接口)。`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 与三色版一致只重写版式与配色钩子四段三道横规各段 y 由上一段实际底部推算A 顶部 090 时钟/日期左天气右 x=790B 用量 `_usage_block`×2 `usage` 10386竖规 x=400 `usage_gpt` 414790`_label` 标题 + 右侧快照 HH:MM mock 示例数据」,行用基类紧凑 `_usage_row`行距 31宽版把 `bar["label"]` 长标签代入 `k``label_w` 按本块最长标签自适应C `_realtime` 今日实时五列全宽`mao.cols` 全部 `wide` 列内四行 `ctext`/`_cmp_line` 逐行水平居中—— F12 y0+17 / F24 y0+34 / 环比 y0+68 / 同比 y0+84按墨迹框 ~9px环比 ~6px竖规 y0+2298区块底 y0+100D `_btc_chart` BTC K 线`data.btc`标题按 `bar` 显示时K/日K」:右侧最新价 F12 + 口径 `chg_label`24h/1d+ 涨跌三角 + 涨跌幅涨绿跌红每根蜡烛 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`宽版置 0——百分比已是整数倍字号不需手动加字距基类另抽 `FPACE` pace 字体与标签 F13 解耦)。四段三道横规各段 y 由上一段实际底部推算A 顶部 090 **大时钟 F84 粗 3** `getbbox` 把墨迹顶沿放到 16底沿 79占满整段且保住原上//下边距星期/日期 F12 紧随其右 14px按日期数字底沿与时钟**基线对齐**2026-09-14 此前 F48 + 日期另起一行天气右 x=790B 用量 `_usage_block`×2 `usage` 10386竖规 x=400 `usage_gpt` 414790`_label` 标题 + 右侧上次更新 HH:MM与今日实时同款文案 mock 示例数据」,行用基类紧凑 `_usage_row`行距 31宽版把 `bar["label"]` 长标签代入 `k``label_w` 按本块最长标签自适应C `_realtime` 今日实时五列全宽`mao.cols` 全部 `wide` 列内四行 `ctext`/`_cmp_line` 逐行水平居中—— F12 y0+21 / F24 y0+38 / 环比 y0+72 / 同比 y0+88按墨迹框标题 ~9px——2026-09-14 整列下移 4px ~5px 用户觉得挤 ~9px环比 ~6px竖规 y0+26102区块底 y0+104D `_btc_chart` BTC K 线`data.btc`标题按 `bar` 显示时K/日K」:右侧最新价 F12 + 口径 `chg_label`24h/1d+ 涨跌三角 + 涨跌幅涨绿跌红每根蜡烛 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` 时区现算三路上游经 `_cached(key, ttl, fetch, fallback)`——`STATE.last_good[key]` 未过期`time - STATE.fetched_at[key] < ttl`直接用过期才调 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`成功更新数据与时间戳失败沿用旧值状态 `stale`)、从未成功用占位`none`日志每行末尾 `[weather:cache(12s) mao:fetched btc:cache(3s)]` 可直接看命中情况`/health` `cache_age_s`/`ttl_s`用量取 `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.11.3s天气 0.28s橘喵 0.06sOKX 0.7s 串行全部命中缓存约 0.02s渲染 20ms + 编码 14ms)。 - **`server.py`** E1002 动态服务stdlib `ThreadingHTTPServer`无第三方依赖)。`build_data()` `(data, 各路状态)`日期按 `config.TZ_NAME` 时区现算三路上游经 `_cached(key, ttl, fetch, fallback)`——`STATE.last_good[key]` 未过期`time - STATE.fetched_at[key] < ttl`直接用过期才调 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`成功更新数据与时间戳失败沿用旧值状态 `stale`)、从未成功用占位`none`日志每行末尾 `[weather:cache(12s) mao:fetched btc:cache(3s)]` 可直接看命中情况`/health` `cache_age_s`/`ttl_s`用量取 `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.11.3s天气 0.28s橘喵 0.06sOKX 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.3sChrome 启动为主)。 - **`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.3sChrome 启动为主)。
- **`html_renderer.py`** 方案二网页版 HTML+CSS+内联 SVG JS数据服务端写死字体栈 PingFang/微软雅黑/Noto Sans SC + Google Fonts 兜底平台截图机若无中文字体会掉字能否加载取决于其出网能力颜色只用六色纯值配色规则与 PNG 版一致用量/pace 三档涨红跌黑K 线涨绿跌红版式同构K 线标题右侧价格用 `<strong>``h3 b::before` 的方块装饰只给标题)。 - **`html_renderer.py`** 方案二网页版 HTML+CSS+内联 SVG JS数据服务端写死字体栈 PingFang/微软雅黑/Noto Sans SC + Google Fonts 兜底平台截图机若无中文字体会掉字能否加载取决于其出网能力颜色只用六色纯值配色规则与 PNG 版一致用量/pace 三档涨红跌黑K 线涨绿跌红版式同构K 线标题右侧价格用 `<strong>``h3 b::before` 的方块装饰只给标题)。

View File

@@ -111,6 +111,7 @@ BTC_CACHE_PATH = os.path.join(BASE_DIR, ".btc_cache.json")
# 活跃使用时该快照几乎持续刷新、准确;空闲时停在最后一次。文件缺失时用量区显示 "--"。 # 活跃使用时该快照几乎持续刷新、准确;空闲时停在最后一次。文件缺失时用量区显示 "--"。
USAGE_LOCAL_PATH = os.environ.get("USAGE_LOCAL_PATH", os.path.expanduser("~/.claude/usage-snapshot.json")) USAGE_LOCAL_PATH = os.environ.get("USAGE_LOCAL_PATH", os.path.expanduser("~/.claude/usage-snapshot.json"))
USAGE_WARN = int(os.environ.get("USAGE_WARN", "80")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警(两个版面统一) USAGE_WARN = int(os.environ.get("USAGE_WARN", "80")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警(两个版面统一)
# 以下两项仅六色宽版E1002使用三色小屏只有 黑/红 两档 # 以下两项仅六色宽版E1002使用三色小屏只有 黑/红 两档。用量与 pace 各自三档、独立取色,颜色顺序固定为 绿 → 黑 → 红
USAGE_OK = int(os.environ.get("USAGE_OK", "30")) # 用量 ≤ 此百分比 → 进度条(含边框)绿色「健康」;介于 OK~WARN 之间为黑 # 黄不用2026-09-14 曾按要求加过 60%80% 黄档,随即被否——黄小字压白底发虚、且四档在三行里过于花):
PACE_TOL = int(os.environ.get("PACE_TOL", "10")) # pace=用量%−时间%:≥ +TOL 红(超前过多)/ ≤ TOL 绿(节余充足)/ 其间黑 USAGE_OK = int(os.environ.get("USAGE_OK", "30")) # 用量 ≤ OK 绿「健康」OK~WARN 黑「正常」|≥ WARN 红「危险」;进度条、边框与百分比数字同色
PACE_TOL = int(os.environ.get("PACE_TOL", "10")) # pace=用量%−时间%:≤ TOL 绿「节余」±TOL 内黑「持平」|≥ +TOL 红「偏快」

View File

@@ -333,7 +333,8 @@ def usage_from_raw(now, raw, windows, title):
utilization 为已用百分比resets_at 为 epoch 秒或 ISO缺哪个窗口哪个显示 "--" utilization 为已用百分比resets_at 为 epoch 秒或 ISO缺哪个窗口哪个显示 "--"
time_pct时间已流逝%)与 pace 按传入的 now **现算**——所以推送端只需推原始快照,服务端渲染时 pace 仍随时钟准确推进。 time_pct时间已流逝%)与 pace 按传入的 now **现算**——所以推送端只需推原始快照,服务端渲染时 pace 仍随时钟准确推进。
结构契约renderer 依赖):{title, warn, ok, pace_tol, updated?, bars:[{k, label, pct(0~100 或 None), time_pct?, reset?, scoped}]} 结构契约renderer 依赖):{title, warn, ok, pace_tol, updated?, bars:[{k, label, pct(0~100 或 None), time_pct?, reset?, scoped}]}
pct ≥ warn 时该条进度条与百分比转红(宽版另有 pct ≤ ok 绿档pace = pct time_pct>0 超前↑ / <0 节余↓,宽版按 ±pace_tol 分三色); pct ≥ warn 时该条进度条与百分比转红(宽版另按 ok/warn 分 绿/黑/红 三档,百分比随条色);
pace = pct time_pct>0 超前↑ / <0 节余↓,宽版按 ±pace_tol 分 绿/黑/红 三档);
reset 为重置时刻文本updated 为快照落盘时刻 HH:MM活跃使用时持续刷新空闲时停在最后一次""" reset 为重置时刻文本updated 为快照落盘时刻 HH:MM活跃使用时持续刷新空闲时停在最后一次"""
raw = raw or {} raw = raw or {}
bars = [] bars = []

View File

@@ -56,15 +56,12 @@ html,body{width:800px;height:480px;overflow:hidden;background:#fff;color:#000;
def _tier_bar(bar, usage): def _tier_bar(bar, usage):
"""用量条颜色三档(与 renderer_e1002 一致)。返回 (填充/边框色, 百分比文字色)。""" """用量条颜色三档 绿/黑/红(与 renderer_e1002 一致)。返回 (填充/边框色, 百分比文字色),两者同色"""
pct = bar.get("pct") pct = bar.get("pct")
if not isinstance(pct, (int, float)): if not isinstance(pct, (int, float)):
return BLACK, BLACK return BLACK, BLACK
if pct >= usage.get("warn", 80): c = RED if pct >= usage.get("warn", 80) else GREEN if pct <= usage.get("ok", 30) else BLACK
return RED, RED return c, c
if pct <= usage.get("ok", 30):
return GREEN, BLACK
return BLACK, BLACK
def _tier_pace(diff, usage): def _tier_pace(diff, usage):
@@ -73,7 +70,7 @@ def _tier_pace(diff, usage):
def _usage_card(usage): def _usage_card(usage):
note = "示例数据" if usage.get("mock") else (f'快照 {usage["updated"]}' if usage.get("updated") else "") note = "示例数据" if usage.get("mock") else (f'上次更新 {usage["updated"]}' if usage.get("updated") else "")
rows = [] rows = []
for b in usage.get("bars", []): for b in usage.get("bars", []):
pct = b.get("pct") pct = b.get("pct")

View File

@@ -31,6 +31,14 @@ BLACK, WHITE, RED = (0, 0, 0), (255, 255, 255), (255, 0, 0)
class DashboardRenderer: class DashboardRenderer:
# 百分比数字的字符间距(px乘 k)F16 为 16/12x 非整数倍缩放,数字墨迹无侧边距、相邻紧贴,需手动加距;
# 子类若把百分比换成原生 12px 字号(宽版),字形自带侧边距,应覆写为 0。
PCT_TRACK = 2
# 用量行左标签的字重与对齐:三色版 粗体、左对齐(贴 x0宽版覆写为 常规字重、右对齐(贴进度条左侧留 LABEL_GAP
LABEL_BOLD = True
LABEL_ALIGN = "l"
LABEL_GAP = 8
def __init__(self, font_path=config.FONT_PATH, size=(config.WIDTH, config.HEIGHT), k=1): def __init__(self, font_path=config.FONT_PATH, size=(config.WIDTH, config.HEIGHT), k=1):
"""k整体缩放系数整数。k=1 为 400x300 原版k=2 供 800x480 设备reTerminal E1002复用组件 """k整体缩放系数整数。k=1 为 400x300 原版k=2 供 800x480 设备reTerminal E1002复用组件
字体按 k 倍创建(点阵字体整数倍最锐利),组件内的像素常量一律乘 k故各组件在两种尺寸下形态一致。 字体按 k 倍创建(点阵字体整数倍最锐利),组件内的像素常量一律乘 k故各组件在两种尺寸下形态一致。
@@ -40,6 +48,7 @@ class DashboardRenderer:
self.F12 = ImageFont.truetype(font_path, 12 * k) self.F12 = ImageFont.truetype(font_path, 12 * k)
self.F13 = ImageFont.truetype(font_path, 13 * k) # 5h/7d 与 pace 用;接近原生 12px畸变很小 self.F13 = ImageFont.truetype(font_path, 13 * k) # 5h/7d 与 pace 用;接近原生 12px畸变很小
self.F16 = ImageFont.truetype(font_path, 16 * k) # 非整数倍(1.33x):靠 fontmode='1' 保持纯色,笔画略不均可接受 self.F16 = ImageFont.truetype(font_path, 16 * k) # 非整数倍(1.33x):靠 fontmode='1' 保持纯色,笔画略不均可接受
self.FPACE = self.F13 # pace 数值字体;与标签解耦,宽版把它换成 24px 而标签仍 12px
self.F24 = ImageFont.truetype(font_path, 24 * k) self.F24 = ImageFont.truetype(font_path, 24 * k)
self.F36 = ImageFont.truetype(font_path, 36 * k) # 3 倍整数缩放,仍锐利;宽版今日实时数值用 self.F36 = ImageFont.truetype(font_path, 36 * k) # 3 倍整数缩放,仍锐利;宽版今日实时数值用
self.F48 = ImageFont.truetype(font_path, 48 * k) self.F48 = ImageFont.truetype(font_path, 48 * k)
@@ -149,12 +158,12 @@ class DashboardRenderer:
def _pace(self, xr, cy, diff): def _pace(self, xr, cy, diff):
"""时间维度对比pace = 用量% 时间%>0 超前↑红;<0 节余↓黑;=0 持平。 """时间维度对比pace = 用量% 时间%>0 超前↑红;<0 节余↓黑;=0 持平。
数值(F13)两位补零、右端对齐 xr小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。""" 数值(FPACE默认 F13)两位补零、右端对齐 xr小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。"""
up = diff > 0 up = diff > 0
color = self._pace_color(diff) color = self._pace_color(diff)
txt = f"{abs(int(diff)):02d}" txt = f"{abs(int(diff)):02d}"
tw = self._tw(txt, self.F13) tw = self._tw(txt, self.FPACE)
self._mid(xr, cy, txt, self.F13, fill=color, bold=self.k, align="r") self._mid(xr, cy, txt, self.FPACE, fill=color, bold=self.k, align="r")
if diff == 0: if diff == 0:
return tw return tw
TRI_GAP, TRI = 4 * self.k, 6 * self.k TRI_GAP, TRI = 4 * self.k, 6 * self.k
@@ -173,9 +182,13 @@ class DashboardRenderer:
k = self.k k = self.k
LABEL_W = 26 * k if label_w is None else label_w LABEL_W = 26 * k if label_w is None else label_w
G1, G2, H = 13 * k, 8 * k, 15 * k # G1 进度条↔百分比、G2 百分比↔pace视觉约 12/11 G1, G2, H = 13 * k, 8 * k, 15 * k # G1 进度条↔百分比、G2 百分比↔pace视觉约 12/11
PCT_TRACK = 2 * k # F16(16/12x 非整数倍缩放)数字墨迹无侧边距、紧贴,手动加字符间距 PCT_TRACK = self.PCT_TRACK * k # 见类属性 PCT_TRACK
cy = y + 10 * k # 行竖直中心 cy = y + 10 * k # 行竖直中心
self._mid(x0, cy, bar["k"], self.F13, bold=k) # 左标签 F13 lb = k if self.LABEL_BOLD else 0 # 左标签 F13
if self.LABEL_ALIGN == "r":
self._mid(x0 + LABEL_W - self.LABEL_GAP * k, cy, bar["k"], self.F13, bold=lb, align="r")
else:
self._mid(x0, cy, bar["k"], self.F13, bold=lb)
# 右侧从右往左pace(最右) → 百分比 → 进度条 # 右侧从右往左pace(最右) → 百分比 → 进度条
pct_r = x1 pct_r = x1
if ok and "time_pct" in bar: if ok and "time_pct" in bar:

View File

@@ -5,11 +5,14 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(
利用 800x480 的四倍面积重新排版为「AI 用量看板」,输出 **E Ink Spectra 6 原生六色**(黑/白/黄/红/绿/蓝)纯色图, 利用 800x480 的四倍面积重新排版为「AI 用量看板」,输出 **E Ink Spectra 6 原生六色**(黑/白/黄/红/绿/蓝)纯色图,
并生成一张固定 800x480 的 HTML 页面供 SenseCraft HMI 的 HTML 控件云端截图拉取。 并生成一张固定 800x480 的 HTML 页面供 SenseCraft HMI 的 HTML 控件云端截图拉取。
版式k=1点阵字体且**只用整数倍字号 12/24/36/48**,不放大;四段,三道横规): 版式k=1点阵字体且**只用整数倍字号 12/24/36/48/84**,不放大;四段,三道横规):
A 顶部090 :大时钟+日期(左)|天气(右,贴 790——与三色版同一套组件与尺寸。 A 顶部090 **大时钟 F84**(墨迹 1679占满上下边距内的整段高度 星期/日期 F12 紧随其右、与时钟数字**基线对齐**(左)
|天气(右,贴 790——天气组件与三色版同尺寸。
B 用量96206 :左半 **Claude Usage**all 5h / all 7d / fable 7d竖规 x=400右半 **ChatGPT Usage**mockall 5h / all 7d / codex 7d B 用量96206 :左半 **Claude Usage**all 5h / all 7d / fable 7d竖规 x=400右半 **ChatGPT Usage**mockall 5h / all 7d / codex 7d
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31,标签用 bar["label"]statusline 同款写法),列宽按最长标签自适应。 行组件沿用三色版的紧凑单行 `_usage_row`(行距 31**三处文字统一 12px**(基类 F13/F16/FPACE 都映射到 12px
C 今日218318**橘喵·今日实时** 五列全宽(探店单量/探店流水/团购单量/团购流水/毛利):列内四行逐行居中—— 标签**常规字重、在标签列内右对齐**(贴进度条),百分比与 pace 数字**粗体**(曾试过 24px用户嫌大
标签用 bar["label"]statusline 同款写法),列宽按最长标签自适应;标题右侧「上次更新 HH:MM」与今日实时同款。
C 今日218322**橘喵·今日实时** 五列全宽(探店单量/探店流水/团购单量/团购流水/毛利):列内四行逐行居中——
名 F12 / 值 F24 粗 / 环比 / 同比(五列每列 ~157px原「值 F36 右侧叠放环比/同比」并排组放不下)。 名 F12 / 值 F24 粗 / 环比 / 同比(五列每列 ~157px原「值 F36 右侧叠放环比/同比」并排组放不下)。
D 行情(~336470**BTC/USDT 时K近 48 小时)** 蜡烛图(`_btc_chart`,数据来自 btc_apiOKX 主源、Coinbase/Huobi 备源5 分钟缓存; D 行情(~336470**BTC/USDT 时K近 48 小时)** 蜡烛图(`_btc_chart`,数据来自 btc_apiOKX 主源、Coinbase/Huobi 备源5 分钟缓存;
周期/根数由 BTC_BAR/BTC_LIMIT 配置)——标题右侧最新价 + 24h 涨跌幅;**涨(收≥开)绿、跌红**,实心蜡烛 + 1px 影线; 周期/根数由 BTC_BAR/BTC_LIMIT 配置)——标题右侧最新价 + 24h 涨跌幅;**涨(收≥开)绿、跌红**,实心蜡烛 + 1px 影线;
@@ -17,9 +20,12 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(
可选470479六色测试色条config.E1002_COLOR_STRIP默认关真机校准色彩量化时打开 可选470479六色测试色条config.E1002_COLOR_STRIP默认关真机校准色彩量化时打开
六色的语义分工(每种颜色只有一个含义,不为「有」而用): 六色的语义分工(每种颜色只有一个含义,不为「有」而用):
- 黑/白:结构与正文。红:危险(用量 ≥ warn 的进度条+边框+百分比、pace ≥ +tol、涨、定位、太阳/闪电。 - 黑/白:结构与正文。红:危险、涨、定位、太阳/闪电绿健康、K 线上涨(红另用于 K 线下跌);蓝:天气雨滴/雪花
- 绿:健康(用量 ≤ ok 的进度条+边框、pace ≤ tol、K 线上涨;红另用于 K 线下跌。用量与 pace 各自三档:绿 / 黑 / 红(阈值 USAGE_OK / USAGE_WARN / PACE_TOL)。 黄:**不用**2026-09-14 曾按要求加过四档含黄,随即被否)。
- 蓝:天气雨滴/雪花。黄:不用(用户明确不要黄色进度条;曾试过 60%80% 黄「注意」档,已移除)。 - 用量与 pace **各自三档、颜色顺序固定 绿 → 黑 → 红**,两者独立计算(`_usage_colors` / `_pace_color`
用量 pct≤ USAGE_OK(30) 绿「健康」|(30, 80) 黑「正常」|≥ USAGE_WARN(80) 红「危险」——进度条填充、边框与**百分比数字三者同色**
pace = 用量% 时间%:≤ PACE_TOL(10) 绿「节余」|(10, +10) 黑「持平」|≥ +10 红「偏快」。
绿色数字在白底上比黑字淡一些24px 字号下可读;这是「百分比随条色」的已知取舍。
- 整图只允许六种纯色;渲染后可用 `img.getcolors()` 自查。 - 整图只允许六种纯色;渲染后可用 `img.getcolors()` 自查。
为什么是「固定 800x480 + 一张 PNG」而不是自适应 HTML 为什么是「固定 800x480 + 一张 PNG」而不是自适应 HTML
@@ -33,6 +39,7 @@ import time
import config import config
from renderer import DashboardRenderer, BLACK, WHITE, RED from renderer import DashboardRenderer, BLACK, WHITE, RED
import config as _cfg import config as _cfg
from PIL import ImageFont
# E Ink Spectra 6 原生六色。取纯饱和值作为给平台量化器的明确信号(与 ESPHome/GxEPD2 驱动的 BWYRGB 映射一致)。 # E Ink Spectra 6 原生六色。取纯饱和值作为给平台量化器的明确信号(与 ESPHome/GxEPD2 驱动的 BWYRGB 映射一致)。
YELLOW, GREEN, BLUE = (255, 255, 0), (0, 255, 0), (0, 0, 255) YELLOW, GREEN, BLUE = (255, 255, 0), (0, 255, 0), (0, 0, 255)
@@ -42,30 +49,32 @@ SPECTRA6 = [BLACK, WHITE, YELLOW, RED, GREEN, BLUE]
class E1002Renderer(DashboardRenderer): class E1002Renderer(DashboardRenderer):
"""800x480 六色版。组件继承k=1字号不变只重写版式与配色钩子。""" """800x480 六色版。组件继承k=1字号不变只重写版式与配色钩子。"""
PCT_TRACK = 0 # 百分比是整数倍字号(自带侧边距),不再手动加字符间距
LABEL_BOLD = False # 标签常规字重(数值才加粗,层级:数值 > 标签)
LABEL_ALIGN = "r" # 标签右对齐贴进度条长短标签all 5h / fable 7d右缘齐平
def __init__(self, font_path=config.FONT_PATH): def __init__(self, font_path=config.FONT_PATH):
super().__init__(font_path, size=(config.E1002_WIDTH, config.E1002_HEIGHT), k=1) super().__init__(font_path, size=(config.E1002_WIDTH, config.E1002_HEIGHT), k=1)
# 宽版只用点阵字体的**整数倍字号**12 / 24 / 36 / 48基类给用量行用的 F13标签pace F16百分比 # 宽版只用点阵字体的**整数倍字号**12 / 24 / 36 / 48 / 84):基类给用量行用的 F13标签/ FPACEpace/ F16百分比
# 是非整数倍缩放(笔画不均),这里分别替换成 12px24px;行几何靠 _mid 按墨迹中心对齐,不受字号变化影响。 # 是非整数倍缩放(笔画不均),这里统一映射到 12px24px 试过、用户嫌大);标签常规字重、百分比与 pace 数字粗体;
from PIL import ImageFont # 行几何靠 _mid 按墨迹中心对齐不受字号变化影响。F84 给顶部大时钟7 倍整数缩放,墨迹 63px 恰好占满 A 段)。
self.F13 = ImageFont.truetype(font_path, 12) self.F13 = ImageFont.truetype(font_path, 12)
self.F16 = ImageFont.truetype(font_path, 24) self.F16 = self.F13
self.FPACE = self.F13
self.F84 = ImageFont.truetype(font_path, 84)
# ---------- 配色钩子(三档:绿 健康 / 黑 正常 / 红 危险---------- # ---------- 配色钩子(三档,顺序固定 绿 → 黑 → 红;用量与 pace 各自独立计算----------
_ok, _pace_tol = _cfg.USAGE_OK, _cfg.PACE_TOL # 由 _usage_block 按数据覆盖 _ok, _pace_tol = _cfg.USAGE_OK, _cfg.PACE_TOL # 由 _usage_block 按数据覆盖
def _usage_colors(self, ok, pct, warn, bar): def _usage_colors(self, ok, pct, warn, bar):
"""进度条填充边框同色pct ≥ warn 红pct ≤ ok 绿|其间黑。 """进度条填充边框、百分比数字三者同色pct ≤ ok 绿「健康」其间黑「正常」pct ≥ warn 红「危险」。"""
百分比文字只在红档转红,绿档仍黑——绿在白底上偏淡,做大块填充清楚、做小字发虚。"""
if not ok: if not ok:
return BLACK, BLACK, BLACK return BLACK, BLACK, BLACK
if pct >= warn: c = RED if pct >= warn else GREEN if pct <= self._ok else BLACK
return RED, RED, RED return c, c, c
if pct <= self._ok:
return GREEN, GREEN, BLACK
return BLACK, BLACK, BLACK
def _pace_color(self, diff): def _pace_color(self, diff):
"""pace = 用量% 时间%:≥ +tol 红(烧得比时间快,超前过多)|tol 绿节余充足)|其间黑""" """pace = 用量% 时间%(与用量档位独立):tol 绿节余」|(tol, +tol) 黑「持平」|≥ +tol 红「偏快」"""
if diff >= self._pace_tol: if diff >= self._pace_tol:
return RED return RED
if diff <= -self._pace_tol: if diff <= -self._pace_tol:
@@ -86,14 +95,14 @@ class E1002Renderer(DashboardRenderer):
self.d.rectangle([bx0, y0, bx1, y1], outline=BLACK, width=1) self.d.rectangle([bx0, y0, bx1, y1], outline=BLACK, width=1)
def _usage_block(self, x0, x1, y0, usage): def _usage_block(self, x0, x1, y0, usage):
"""一个用量区块标题_label右侧「快照 HH:MM」mock 「示例数据」)+ 逐行紧凑 `_usage_row`(行距 31 """一个用量区块标题_label右侧「上次更新 HH:MM」——与今日实时同款文案;mock 数据则标「示例数据」)
标签列宽按本块最长标签墨迹 + 8px 算,保证 "Fable 7d" 这类长标签不压进度条。返回末行底部 y。""" + 逐行紧凑 `_usage_row`(行距 31。标签列宽按本块最长标签墨迹 + LABEL_GAP 算(标签右对齐贴进度条,短标签左侧留白)。返回末行底部 y。"""
right = "示例数据" if usage.get("mock") else (f'快照 {usage["updated"]}' if usage.get("updated") else None) right = "示例数据" if usage.get("mock") else (f'上次更新 {usage["updated"]}' if usage.get("updated") else None)
self._label(y0, usage.get("title", "Usage"), right=right, x0=x0, x1=x1) self._label(y0, usage.get("title", "Usage"), right=right, x0=x0, x1=x1)
warn = usage.get("warn", 80) warn = usage.get("warn", 80)
self._ok, self._pace_tol = usage.get("ok", _cfg.USAGE_OK), usage.get("pace_tol", _cfg.PACE_TOL) self._ok, self._pace_tol = usage.get("ok", _cfg.USAGE_OK), usage.get("pace_tol", _cfg.PACE_TOL)
bars = [dict(b, k=b.get("label") or b["k"]) for b in usage.get("bars", [])] # 宽版用长标签all 5h / fable 7d bars = [dict(b, k=b.get("label") or b["k"]) for b in usage.get("bars", [])] # 宽版用长标签all 5h / fable 7d
label_w = max([self._tw(b["k"], self.F13) + 1 for b in bars] + [26]) + 8 label_w = max([self._tw(b["k"], self.F13) for b in bars] + [26]) + self.LABEL_GAP
y = y0 + 27 y = y0 + 27
for b in bars: for b in bars:
self._usage_row(x0, x1, y, b, warn, label_w=label_w) self._usage_row(x0, x1, y, b, warn, label_w=label_w)
@@ -102,7 +111,8 @@ class E1002Renderer(DashboardRenderer):
def _realtime(self, y0, mao, x0=8, x1=None): def _realtime(self, y0, mao, x0=8, x1=None):
"""橘喵·今日实时五列(宽版):探店单量 / 探店流水 / 团购单量 / 团购流水 / 毛利,列内四行逐行水平居中: """橘喵·今日实时五列(宽版):探店单量 / 探店流水 / 团购单量 / 团购流水 / 毛利,列内四行逐行水平居中:
指标名 F12y0+17)→ 值 F24 粗y0+34)→ 环比y0+68)→ 同比y0+84,行距 16按墨迹框算↔值 ~9px、值↔环比 ~6px、环比↔同比 5px。 指标名 F12y0+21→ 值 F24 粗y0+38)→ 环比y0+72)→ 同比y0+88,行距 16按墨迹框算标题↔名 ~9px(原 ~5px 太挤,整列下移 4px
名↔值 ~9px、值↔环比 ~6px、环比↔同比 5px。
为什么不再是「值 F36 右侧叠放环比/同比」的并排组:五列每列仅 ~157pxF36 并排组如「2.12万」102间隙环比 77 为什么不再是「值 F36 右侧叠放环比/同比」的并排组:五列每列仅 ~157pxF36 并排组如「2.12万」102间隙环比 77
近 194px 放不下;降到 F24 后并排组仍有 156px贴着两侧分隔线太挤。竖排最宽元素「1234.56%」一行 89px余量充足 近 194px 放不下;降到 F24 后并排组仍有 156px贴着两侧分隔线太挤。竖排最宽元素「1234.56%」一行 89px余量充足
与 400×300 小屏同构。(更早曾试过名与值同行、名贴左上角角标,均已否。)返回区块底部 y。""" 与 400×300 小屏同构。(更早曾试过名与值同行、名贴左上角角标,均已否。)返回区块底部 y。"""
@@ -110,16 +120,16 @@ class E1002Renderer(DashboardRenderer):
self._label(y0, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}', x0=x0 + 2, x1=x1 - 2) self._label(y0, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}', x0=x0 + 2, x1=x1 - 2)
cols = mao["cols"]; n = len(cols) cols = mao["cols"]; n = len(cols)
cw = (x1 - x0) / n cw = (x1 - x0) / n
TAG_Y, VAL_Y, HB_Y, TB_Y = y0 + 17, y0 + 34, y0 + 68, y0 + 84 TAG_Y, VAL_Y, HB_Y, TB_Y = y0 + 21, y0 + 38, y0 + 72, y0 + 88
for i in range(1, n): for i in range(1, n):
self.vdash(x0 + cw * i, y0 + 22, y0 + 98) self.vdash(x0 + cw * i, y0 + 26, y0 + 102)
for i, c in enumerate(cols): for i, c in enumerate(cols):
cx = x0 + cw * (i + 0.5) cx = x0 + cw * (i + 0.5)
self.ctext(cx, TAG_Y, c["k"], self.F12) self.ctext(cx, TAG_Y, c["k"], self.F12)
self.ctext(cx, VAL_Y, c["v"], self.F24, bold=1) self.ctext(cx, VAL_Y, c["v"], self.F24, bold=1)
self._cmp_line(cx, HB_Y, "环比", c["hb"]) self._cmp_line(cx, HB_Y, "环比", c["hb"])
self._cmp_line(cx, TB_Y, "同比", c["tb"]) self._cmp_line(cx, TB_Y, "同比", c["tb"])
return y0 + 100 return y0 + 104
@staticmethod @staticmethod
def _fmt_price(v): def _fmt_price(v):
@@ -195,10 +205,16 @@ class E1002Renderer(DashboardRenderer):
dt, wt, mao = data["date"], data["weather"], data["mao"] dt, wt, mao = data["date"], data["weather"], data["mao"]
usage, gpt = data["usage"], data.get("usage_gpt") or {} usage, gpt = data["usage"], data.get("usage_gpt") or {}
# ---- A. 顶部:大时钟+日期(左)/ 天气(右)——与三色版同尺寸 ---- # ---- A. 顶部:大时钟 F84 占满整段(左)+ 星期/日期紧随其右、与时钟数字基线对齐 / 天气(右)----
DIV_Y = 90 DIV_Y = 90
self.text(10, 0, dt["time"], self.F48, bold=2) TIME_BOLD, INK_TOP = 3, 16 # 伪粗体 3px墨迹顶沿 16 与旧版 F48 相同,保住上边距
self.text(12, 64, f'{dt["week"]} {dt["greg"]}', self.F12) _, tt, _, tb = self.F84.getbbox(dt["time"]) # F84 数字墨迹 2891 → 高 63摆到 1679距横规 11px
time_y = INK_TOP - tt
base_y = time_y + tb # 时钟数字基线(墨迹底沿)
self.text(10, time_y, dt["time"], self.F84, bold=TIME_BOLD)
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)
loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}' loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}'
self.rtext(R, 8, loc, self.F12) self.rtext(R, 8, loc, self.F12)
self.rtext(R, 30, temp, self.F24, bold=1) self.rtext(R, 30, temp, self.F24, bold=1)