feat: E1002 新增公网图片服务(恒定 URL 现渲染)+底部改为 BTC 时K

- server.py:三条恒定 URL 并行——/e1002.png 内存现渲染 PNG(主力)、
  /e1002.html 现代网页、/e1002-web.png 网页经无头 Chrome 截图后固化六色;
  天气/橘喵/BTC 按 WEATHER_TTL/MAO_TTL/BTC_TTL 走内存缓存,过期才实拉、
  失败沿用旧值;用量不读本机文件,改由 POST /push/usage(_gpt) 推送暂存
- push_client.py:本机把 Claude 用量快照推送到服务(X-Push-Token 鉴权)
- btc_api.py:OKX/Coinbase/Huobi 免费接口逐源兜底,时K 近 72 小时,涨绿跌红
- data.py 拆出 weather_fetch/mao_fetch/btc_fetch 与 usage_from_raw,
  统一按 TZ_NAME 时区;宽版只用点阵字体整数倍字号 12/24/36/48
- probe_server.py:验证平台对同一 URL 是否周期重抓(结论:Image 控件
  不重抓,HTML 控件重抓)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 18:55:47 +08:00
parent 6dc89b504e
commit 85b5113ba4
14 changed files with 1192 additions and 114 deletions

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/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**7.3 寸 **800×480**E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发不支持 ETag/304内容不变也会整屏重绘全刷约 17 秒)。因此本分支产出的是**一张固定 800×480 的静态页 `output/e1002/index.html` + 同目录 `dashboard.png`**,需部署到公网 HTTPS局域网地址/明文 HTTP 云端抓不到)。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**四段版式A 时钟/天气B 左 **Claude Usage 三行all 5h / all 7d / fable 7d标签沿用 statusline 写法、小写开头)**、右 **ChatGPT Usagemock标「示例数据」**紧凑单行样式曾试过撑满高度的大卡片被否C **橘喵·今日实时**三列全宽(指标名 F12 作每列左上角角标;「值 F36 粗 右侧叠放的环比/同比」整组在列内居中——名不放在值上方否则小字压大数字、区块上半显空也不与值同行否则名参与居中把整组推偏D **橘喵·近7天**数据表(真实数据,取 30 天趋势缓存最后 7 天;**不画折线图**,用户明确不要)。六色语义:用量进度条(含边框)与 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**`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 **橘喵·今日实时**三列全宽(指标名 F12 在列顶、列内水平居中;「值 F36 粗 右侧叠放的环比/同比」整组在列内居中——名不与值同行否则名参与居中把整组推偏曾按用户要求试过左上角角标又改回居中D **BTC/USDT 时 K 蜡烛图(近 72 小时 = 3 天)**免费公开行情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% 黄「注意」档,已移除)。
## 命令
@@ -19,7 +19,10 @@ pip install -r requirements.txt # 仅 Pillow>=10.0urllib 用标准库
cp .env.example .env # 首次:复制模板并填入真实凭证 + 设备 host
python3 main.py # 取数据 → 渲染 → 推送(设备 host 取自 .env 的 EPD_HOST
python3 main.py --render-only # 只渲染到 output/dashboard.png不推送本地调试渲染时用这个
python3 main.py --target e1002 # reTerminal E1002渲染 800x480 六色图 + 固定尺寸 index.html 到 output/e1002/,不推送
python3 main.py --target e1002 # reTerminal E1002 静态形态:渲染 800x480 六色图 + 固定尺寸 index.html 到 output/e1002/,不推送
python3 server.py # reTerminal E1002 动态服务部署公网GET /e1002.png内存现渲染 PNG/ GET /e1002.html现代网页/ GET /e1002-web.png网页截图固化六色/ /health
python3 main.py --target push # 本机:把 ~/.claude/usage-snapshot.json 推送到 server.py需 .env 的 PUSH_URL + PUSH_TOKEN
python3 probe_server.py # 验证工具:同一 URL 是否被平台周期重抓(带访问日志)
sh run.sh # cron 包装脚本cd 项目目录 + vfox python 绝对路径 + 追加日志到 output/cron.log
@@ -29,7 +32,7 @@ EPD_HOST=epd400a44.local python3 main.py
无测试、无 lint。调试渲染效果就跑 `--render-only``output/dashboard.png`
**配置统一走 `.env`**:所有凭证/参数放同目录 `.env`(已在 `.gitignore`,本机专用;模板见已提交的 `.env.example`)。`config.py` 顶部用零依赖加载器 `_load_dotenv()``.env` 写入 `os.environ`,再由各 `os.environ.get(...)` 读取;采用 `setdefault`,故**真实环境变量cron/命令行注入)优先级高于 `.env`**。`config.py` 内不再保留任何明文凭证fallback 为空串)。可配置项:`EPD_HOST`(局域网设备 mDNS host默认 `epd400a44.local`)、`JM_BASE_API`/`JM_CLIENT_ID`/`JM_CLIENT_SECRET`/`JM_GRANT_TYPE`/`JM_TENANT_ID``QWEATHER_HOST`/`QWEATHER_KEY`/`WEATHER_LOCATION`(默认南京·江宁)/`WEATHER_TTL``USAGE_LOCAL_PATH`/`USAGE_WARN`、仅宽版的 `USAGE_OK`/`PACE_TOL``E1002_COLOR_STRIP`
**配置统一走 `.env`**:所有凭证/参数放同目录 `.env`(已在 `.gitignore`,本机专用;模板见已提交的 `.env.example`)。`config.py` 顶部用零依赖加载器 `_load_dotenv()``.env` 写入 `os.environ`,再由各 `os.environ.get(...)` 读取;采用 `setdefault`,故**真实环境变量cron/命令行注入)优先级高于 `.env`**。`config.py` 内不再保留任何明文凭证fallback 为空串)。可配置项:`EPD_HOST`(局域网设备 mDNS host默认 `epd400a44.local`)、`JM_BASE_API`/`JM_CLIENT_ID`/`JM_CLIENT_SECRET`/`JM_GRANT_TYPE`/`JM_TENANT_ID``QWEATHER_HOST`/`QWEATHER_KEY`/`WEATHER_LOCATION`(默认南京·江宁)/`WEATHER_TTL``USAGE_LOCAL_PATH`/`USAGE_WARN`、仅宽版的 `USAGE_OK`/`PACE_TOL`/`BTC_BAR`/`BTC_LIMIT`/`BTC_TTL`/`MAO_TTL``E1002_COLOR_STRIP`
**定时部署**:由 macOS **launchd LaunchAgent** 触发 `run.sh`(不再用 cron——cron 在现代 macOS 上易被 TCC/权限静默拦截launchd 是系统原生调度器。LaunchAgent plist 装在 `~/Library/LaunchAgents/com.jm.eink-push.plist`(机器专用,未入库,等价于 `run.sh`/`.env` 的本机产物),`StartInterval=300` + `RunAtLoad=true`:登录/重启后立即跑一次,此后每 5 分钟一次。用 LaunchAgent非 LaunchDaemon是因为脚本依赖当前用户目录下的文件`~/.claude/usage-snapshot.json``.env`),必须在用户会话下运行——机器需已登录(或开了自动登录)才能保证开机即触发。`run.sh` 内部逻辑不变cron 不继承登录环境,故脚本内 python 写死 vfox 绝对路径、并 `cd` 到项目目录后追加日志到 `output/cron.log``cd``config.py` 才能就近找到 `.env`launchd 自身的 stdout/stderr 另落 `output/launchd.log`(正常应为空,非空说明进程本身起不来,而非业务报错)。常用排查:`launchctl list | grep com.jm.eink-push` 看是否常驻及最近退出码,`launchctl kickstart -k gui/$(id -u)/com.jm.eink-push` 手动立即触发一次;改 plist 后需 `launchctl bootout` + `bootstrap` 重新加载才生效(热改文件不会自动生效)。每 5 分钟触发:业务数据每次都刷新,天气靠缓存、用量读本地文件,避免高频打接口(见 `data.py`)。
@@ -37,11 +40,12 @@ EPD_HOST=epd400a44.local python3 main.py
`main.py` 串联三层,各层职责单一、低耦合:
- **`data.py`** — 唯一数据入口 `get_dashboard_data()`返回固定结构的 dict`date`/`weather`/`mao`/`usage`。日期时间用真实系统时间;橘喵今日经营由 `_mao()``jm_api`、天气由 `_weather()``weather_api`和风定位南京·江宁、Claude 用量由 `_usage()``usage_local`**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可renderer 依赖其 key`weather.icon` 为和风图标代码;`mao.cols[].hb/tb` 为环比/同比 `(direction, text)`direction 为 `None` 时不画三角;`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.trend` 两个版面都不画折线;宽版用 `_recent_days(trend, 7)` 得到 `mao.week`(结构 `{dates, rows:[(name, [text...], total)]}`做「近7天」数据表
- **缓存**(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 `.weather_cache.json`TTL=`WEATHER_TTL`,默认 15min、趋势 `.trend_cache.json`(按日期 key每天只拉一次;两者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore`
- **`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[].hb/tb` 为环比/同比 `(direction, text)`direction 为 `None` 时不画三角;`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×72结构 `{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`
- **用量的 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` 轮换
- **`weather_api.py`** 和风天气QWeather客户端**用户专属 API Host**`QWEATHER_HOST`+ `X-QW-Api-Key` 头鉴权响应 **gzip 压缩** magic number 手动解压)、返回 `code` **字符串**。`get_now()` 取实时`now.text` 中文天气 / `now.temp` / `now.icon` 图标代码`get_today()` `/3d` `daily[0]` 今日温区`QWEATHER_HOST`/`QWEATHER_KEY` 未配置时直接抛错天气区显示 `"--"`
- **`btc_api.py`** 比特币 K 线客户端免费公开接口无需密钥`get_candles(bar, limit)`周期代号 `BARS = {"1H", "1D"}` 映射到各源写法逐源兜底 `SOURCES = (okx, coinbase, huobi)`OKX `/api/v5/market/candles?instId=BTC-USDT&bar=`主源本地直连可达且最新)、Coinbase `/products/BTC-USD/candles?granularity=`Huobi `/market/history/kline?period=`各源字段顺序不同OKX/Huobi 最新在前Coinbase [time, low, high, open, close]统一归一为升序 `{ts, o, h, l, c}`Binance 在本地区返回 restricted location不列入
- **`usage_local.py`** Claude Code 用量本地读取**不发任何网络请求****生产者/消费者**模式只读 `config.USAGE_LOCAL_PATH`默认 `~/.claude/usage-snapshot.json`)。**生产者 = 打过补丁的 claude-statusline**本机 `~/.claude/statusline.sh`把每次 Claude Code stdin 喂来的**权威实时**额度落盘为该快照schema `/api/oauth/usage``five_hour`/`seven_day` `utilization` + `resets_at`并额外写入 **`seven_day_fable`**Fable 模型限定的 7 天额度——stdin 里没有只在 `/api/oauth/usage` 返回的 `limits[]` `scope.model.display_name == "Fable"` 那项statusline stdin 分支也会每 5 分钟刷新一次 API 缓存 `/tmp/claude/statusline-usage-cache.json` 以拿到它 token/断网时该键为 `null``data.USAGE_WINDOWS` `scoped=True` 标记这一行400×300 小屏 `render()` 跳过 scoped 800×480 宽版显示本模块作消费者读取活跃使用时快照几乎持续刷新准确空闲时停在最后一次文件缺失/损坏用量区 `"--"``resets_at` 可能是 **epoch 数字**Claude Code stdin 给的就是 epoch ISO `data._parse_reset_ts` 两者兼容。(曾评估直调 `/api/oauth/usage`改为读本地快照以免网络请求。)
- **`renderer.py`** `DashboardRenderer.render(data)` 返回 PIL `'RGB'` 模式、**仅黑//红三色**组件层提供可覆写钩子/参数供宽版复用`_usage_colors()`用量行 填充/边框/文字 三色元组)、`_pace_color()`pace 数值与三角颜色)、`_usage_row(label_w=)``_weather_icon(wet=)`降水元素颜色)、`_label(x0,x1)``_cmp_at()`环比/同比左对齐版`_cmp_line` 居中版基于它字体另有 `F36`3 倍整数缩放)。`render_to_file()` 落盘布局硬编码为**上中下三段**两道 `hdash` 横规分隔y=90 / 200
- **A 顶部**090大时钟(F48)+日期/ 天气红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。
@@ -51,7 +55,12 @@ 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://<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` 今日实时三列全宽指标名 F12 贴列左上角x=列左+12y=y0+22作角标「值 F36 右侧叠放的环比/同比`_cmp_at` 左对齐版行距 18)」按墨迹中心对齐到组中线 y0+56整组在列内水平居中不含角标D `_week_table` 7 天表`mao.week`指标名列 + 7 个日期 + 合计列 单量/流水/毛利 F12 右对齐合计伪粗体合计列前一道点规表头下一道点规无数据居中 `--`)。配色钩子覆写`_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 与三色版一致只重写版式与配色钩子四段三道横规各段 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` 今日实时三列全宽指标名 F12 贴列左上角x=列左+12y=y0+22作角标「值 F36 右侧叠放的环比/同比`_cmp_at` 左对齐版行距 18)」按墨迹中心对齐到组中线 y0+56整组在列内水平居中不含角标D `_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)。
- **`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` 的方块装饰只给标题)。
- **`push_client.py`** 本机推送端 `~/.claude/usage-snapshot.json` 原文 `POST {PUSH_URL}/push/usage`直连连接类异常重试 2 4xx/5xx 直接报错ChatGPT 暂无本机源不推将来按 `data.GPT_WINDOWS` 键组 dict `/push/usage_gpt`)。
- **`probe_server.py`** 验证工具不参与正常流程极简 HTTP 服务`/probe.png` `--every` 秒重绘一张带大号时刻+序号的 800×480 探针图响应 `Cache-Control: no-store`每次请求打印时间/UA/来源 IP `CF-Connecting-IP`)。用途配合 `cloudflared tunnel --url http://localhost:8787` 暴露成公网 HTTPS填入 SenseCraft Image 控件**访问日志**判断平台对同一 URL 是否每个刷新周期重抓成立则可直接覆盖同一 URL 的图片省掉 HTML 页那一层)。**结论已验证**Image 控件不重抓HTML 控件重抓见分支说明
- **`config.py`** 顶部 `_load_dotenv()` 先加载同目录 `.env`再定义 API设备画布尺寸字体路径推送参数凭证 fallback 为空串真实值来自 `.env`/环境变量
## 墨水屏渲染约束(改 renderer.py 必读)
@@ -69,4 +78,25 @@ EPD_HOST=epd400a44.local python3 main.py
## 注意
- 所有凭证均在 `.env`不提交`config.py` 不再内置明文 fallback分享代码时给出 `.env.example` 即可真实 `.env` 切勿入库
- `.gitignore` 已忽略`output/``__pycache__/``*.pyc`两个缓存文件 `.weather_cache.json`/`.trend_cache.json`本机专用的 `run.sh`含凭证的 `.env`模板 `.env.example` 入库)。改缓存逻辑后想强制重拉删掉对应缓存文件即可
- `.gitignore` 已忽略`output/``__pycache__/``*.pyc`两个缓存文件 `.weather_cache.json`/`.trend_cache.json`/`.btc_cache.json`本机专用的 `run.sh`含凭证的 `.env`模板 `.env.example` 入库)。改缓存逻辑后想强制重拉删掉对应缓存文件即可
## 验证与运维手法(会话沉淀)
- **像素回归** `renderer.py` 组件后用固定夹具比对 HEAD 版与工作区版 `DashboardRenderer` k=1 输出逐像素相同
`git show HEAD:renderer.py > /tmp/orig/renderer.py`切换 `sys.path` 分别 import 渲染`ImageChops.difference(a,b).getbbox() is None`
六色版渲染后 `set(img.getcolors())` 必须 `SPECTRA6`夹具需覆盖pct=None、≥warn 红档、≤ok 绿档direction=None、空 candles
- **HTML截图逐像素核验**`"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 --window-size=800,480 --screenshot=/tmp/s.png file://…`**勿加指向空目录的 `--user-data-dir`**卡死到超时)。
- **server.py 本机运维**启动 `nohup env -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u ALL_PROXY python3 server.py >> output/server.log 2>&1 &`
重启前按 PID 杀干净`for p in $(lsof -nP -iTCP:8790 -sTCP:LISTEN -t); do kill $p; done`——`pkill -f` 曾漏杀 `*:8790` `127.0.0.1:8790` 两个监听可并存旧代码继续应答表现为新端点 404)。
- **cloudflared 快速隧道**免账号地址每次重启都变需到 SenseCraft 更新 URL
`nohup cloudflared tunnel --url http://127.0.0.1:8790 --no-autoupdate > output/cloudflared.log 2>&1 &`
地址 `grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' output/cloudflared.log`已写入 `output/tunnel_url.txt`)。
- **shell **本机是 fish——`env -u …` 不能存进变量再当命令执行含中文的 Python heredoc `python3 -X utf8 - <<'EOF'`否则 stdin 解码报错`gh` 未登录且 GitHub API 匿名限流取源码走 `curl raw.githubusercontent.com`
- **statusline 补丁验证**`~/.claude/statusline.sh`备份 `statusline.sh.bak-*`用假 HOME stdin 不污染真实快照——
`echo '<stdin json>' | HOME=/tmp/fakehome bash ~/.claude/statusline.sh` `/tmp/fakehome/.claude/usage-snapshot.json` 是否含 `seven_day_fable`
- **定时任务**`run.sh` 末尾已追加 `main.py --target push`vfox python 绝对路径`|| true`用量随喵喵推送每 5 分钟同步到 server.py推送暂存落盘 `output/push_state.json`
- **上游配额**server 三路上游已有 TTL 内存缓存天气默认 15 分钟 和风约 192 /改有效期只动 `.env`不动代码
- **SenseCraft 未公开后端**从网页前端 bundle 提取`https://sensecraft-hmi-api.seeed.cc` `Authorization: <token>`token localStorage `user-storage`
`POST /api/v1/oss/file/upload`multipart `file`+`type=image``file_url`)→ `POST /api/v2/user/page``{pages:[{name,type:img|url|layout,data,dither,thumbnail,resolution:"800x480"}]}`
`POST /api/v2/user/playlist/upsert_pages` `POST /api/v2/user/device/down_link``{mac_address,playlist_id,type,refresh_interval?,deep_sleep_enabled?}`
另有 `POST /api/v2/user/device/config``POST /render/preview``{url,img_format,resolution,dither}`)。**未采用**未公开可随时变更仅作备选记录