Files
eink-push/CLAUDE.md
YANG JIANKUAN f96ae29590 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>
2026-09-14 15:02:34 +08:00

43 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

项目定位

eink-push 是 JM monorepo 中一个独立的 Python 工具,与其余 Java/Vue 子项目技术栈无关,不受父级 CLAUDE.md 的 jm-cloud/uniapp/admin 开发约束。

作用把日期天气、Claude Code 用量、橘喵今日经营合成为一张 400×300 三色(黑/白/红BWR PNG推送到局域网墨水屏设备喵喵固件开源https://gitee.com/gxp666111/miaomiao。纯 Python + Pillow无浏览器依赖,适合 cron 定时运行。

分支 feature/colorful:新设备在黑白外支持红色,本分支把渲染升级为黑/白/红三色。当前版式为上中下三段A 大时钟+日期 / 天气定位南京·江宁B Claude Usage 主角5h/7d 用量进度条 + 时间维度对比 pace用量吃紧转红预警C 橘喵今日实时三列。红作语义强调(用量预警、涨、定位、天气太阳/闪电),仍无真灰阶、不抖动全图只三种纯色。近30天趋势图当前隐藏renderer._trend_chart 及依赖保留、未调用,随时可挂回)。

分支 feature/reterminal-e1002(基于 feature/colorful):新增第二个目标设备 Seeed reTerminal E10027.3 寸 800×480E Ink Spectra 6 原生六色:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 SenseCraft HMI 云端的 HTML 控件按设备刷新间隔(最小 5 分钟)每周期重新抓取一个公网 HTTPS URL、无头浏览器截图后量化下发不支持 ETag/304内容不变也会整屏重绘全刷约 17 秒)。真机验证2026-09Image 控件只在填写 URL 时抓一次快照、此后不再更新HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 server.py 动态服务(部署公网 HTTPS局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 IMAGE_PATH(默认 /e1002.png)每次请求内存中现渲染 PNGPillow 点阵版);方案二 HTML_PATH(默认 /e1002.html)每次请求生成 800×480 现代网页html_renderer.py,系统/网络字体 + 内联 SVG K 线,无 JS由平台截图——真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊;方案三 HTML_PNG_PATH(默认 /e1002-web.png)同一网页由服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNGhtml_shot.py),平台拿到纯色图无从抖动,需服务器装 Chrome。结论2026-09-10方案一为主力——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 output/e1002/font_compare.png:矢量字体单色渲染 ≈ 抗锯齿后二值化都不如点阵Canvas 逐像素画也改变不了这点Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版只用点阵字体的整数倍字号 12/24/36/48/84E1002Renderer.__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/usageCodex 经 codex app-server RPC后推送原始快照X-Push-Token 鉴权),服务端内存暂存、不持任何凭证、渲染时现算 pace。静态形态 main.py --target e1002output/e1002/index.html + dashboard.png)仍保留。渲染由 renderer_e1002.py 完成:继承 DashboardRenderer 组件、k=1 字号与三色版完全相同(不放大,放大后字太大很傻)四段版式A 大时钟 F84(墨迹 1679 其右一列三行、左缘对齐F12行距 22① 电池图标电量充电中绿、≤20% 红)② 温度计+温度 蓝色水滴+湿度 ③ 星期/日期(与时钟基线对齐)——设备数据来自 SenseCraft 设备遥测 sensecraft_api.py2026-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-14Codex Plus 接口 secondary_window 就是 null原始 wham/usage 已核对5h 行为空是数据如此紧凑单行样式曾试过撑满高度的大卡片被否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.py5 分钟缓存;周期/根数 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 两个阈值),同日被否并整体移除,别再加回。今日实时的环比/同比「三角 + 数值」涨绿跌红2026-09-14 改,与 K 线一致;基类钩子 _cmp_color,三色小屏仍涨红);红另用于定位/太阳闪电,蓝=雨雪。

命令

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 server.py                      # reTerminal E1002 动态服务部署公网GET /e1002.png内存现渲染 PNG/ GET /e1002.html现代网页/ GET /e1002-web.png网页截图固化六色/ /health
python3 main.py --target push          # 本机:采集 Claude钥匙串登录态→/api/oauth/usage与 Codexcodex app-server RPC账户级用量并推送到 server.py需 .env 的 PUSH_URL + PUSH_TOKEN
python3 claude_usage.py [--force]      # 单独调试 Claude 采集(--force 跳过 TTL 缓存)
python3 codex_usage.py [--force]       # 单独调试 Codex 采集;需 Codex 已用 ChatGPT 账号 `codex login`(默认 ~/.codex
python3 probe_server.py                # 验证工具:同一 URL 是否被平台周期重抓(带访问日志)

sh run.sh                              # cron 包装脚本cd 项目目录 + vfox python 绝对路径 + 追加日志到 output/cron.log

# 临时覆盖:真实环境变量优先级高于 .env
EPD_HOST=epd400a44.local python3 main.py

无测试、无 lint。调试渲染效果就跑 --render-onlyoutput/dashboard.png

配置统一走 .env:所有凭证/参数放同目录 .env(已在 .gitignore,本机专用;模板见已提交的 .env.example)。config.py 顶部用零依赖加载器 _load_dotenv().env 写入 os.environ,再由各 os.environ.get(...) 读取;采用 setdefault,故真实环境变量cron/命令行注入)优先级高于 .envconfig.py 内不再保留任何明文凭证fallback 为空串)。可配置项:EPD_HOST(局域网设备 mDNS host默认 epd400a44.local)、JM_BASE_API/JM_CLIENT_ID/JM_CLIENT_SECRET/JM_GRANT_TYPE/JM_TENANT_IDQWEATHER_HOST/QWEATHER_KEY/WEATHER_LOCATION(默认南京·江宁)/WEATHER_TTL、设备遥测 SENSECRAFT_API_KEY/SENSECRAFT_DEVICE_EUI/DEVICE_TTL、服务端 FETCH_BUDGETUSAGE_WARN、Claude 采集 CLAUDE_USAGE_TTL(快照有效期,语义同 WEATHER_TTL0=每次实拉)/CLAUDE_USAGE_BACKOFF/CLAUDE_USAGE_SCOPE_MODEL、Codex 采集 CODEX_BIN/CODEX_USAGE_HOME/CODEX_USAGE_TTL/CODEX_USAGE_BACKOFF、仅宽版的 USAGE_OK/PACE_TOL/BTC_BAR/BTC_LIMIT/BTC_TTL/MAO_TTLE1002_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 Code 凭证)、~/.codexCodex 登录态)、.env,必须在用户会话下运行——机器需已登录(或开了自动登录)才能保证开机即触发。run.sh 内部逻辑不变cron 不继承登录环境,故脚本内 python 写死 vfox 绝对路径、并 cd 到项目目录后追加日志到 output/cron.logcdconfig.py 才能就近找到 .envlaunchd 自身的 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 分钟触发:业务数据每次都刷新,天气靠缓存、两路用量各有 CLAUDE_USAGE_TTL/CODEX_USAGE_TTL 有效期缓存(默认 300s同一分钟内小屏渲染与上报两个进程只打一次接口避免高频打接口data.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=, allow_local=) 都基于它raw=None 时前者经 claude_usage 本机采集、后者经 codex_usage 本机采集Codex 采集失败或服务端 allow_local=False 时用 gpt_mock_raw() 并标 mock。所有「现在」用 now_tz()、时间戳格式化用 _local()TZ=ZoneInfo(config.TZ_NAME)),本机与境外服务端结果一致。返回固定结构的 dictdate/weather/mao/usage/usage_gpt/btc/devicedevice = {battery, charging, temp, humidity},由 _device(now)sensecraft_api 拉取、文件缓存 .device_cache.json TTL=DEVICE_TTL,服务端用纯拉取 device_fetch()/占位 device_empty())。日期时间用真实系统时间;橘喵今日经营由 _mao()jm_api、天气由 _weather()weather_api和风定位南京·江宁、Claude 用量由 _usage()claude_usage、Codex 用量由 _usage_gpt()codex_usageClaude 任何异常都回退占位 "--",不回退 mockCodex 无数据时回退 mock 并显式标「示例数据」。改数据保持返回结构不变即可renderer 依赖其 keyweather.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(毛利口径已含团购、金额与增长率为字符串、增长率可 nullusage.bars[]{k(短标签,小屏用), label(长标签 5h/7d/fable 7d宽版用), pct(或 None), time_pct?, reset?, scoped}(宽版把 scoped 行并入紧邻其前的非 scoped 行作同一边框内的下半实心)、usage.warn/usage.ok/usage.pace_tol 为用量与 pace 的三档阈值(小屏只用 warnusage.updated 为快照采集时刻;usage_gptusage 同构、标题「Codex Usage」、只有 5h/7d 两行(快照各窗口可带 window 真实秒数pace 优先按它算),服务端未收到推送时为 mockmock: True88/22 覆盖红/绿两端)。注:_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.jsonTTL=BTC_TTL 默认 5 分钟,周期变更即失效,失败沿用旧缓存,仍无则 candles 为空 → 面板 --)。
    • 缓存(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 .weather_cache.jsonTTL=WEATHER_TTL,默认 15min、趋势 .trend_cache.json(按日期 key每天只拉一次、BTC K 线 .btc_cache.jsonTTL=BTC_TTL,默认 5min三者拉取失败沿用旧缓存。两路用量各有采集缓存 .claude_usage_cache.json/.codex_usage_cache.jsonusage_common.cached_snapshot:距上次成功 <TTL.envCLAUDE_USAGE_TTL/CODEX_USAGE_TTL)直接用缓存;失败沿用旧快照并退避 BACKOFF 秒;只存快照与时间戳、不存凭证)。以上缓存文件均已在 .gitignore
    • 用量的 pace时间维度对比usage_from_raw() 每次按当前时间现算 time_pct=(窗口已流逝/窗口长)pace=pcttime_pct>0 超前↑ / <0 节余↓);故即使 utilization 来自缓存/推送暂存pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800sCodex 快照带 window 时用真实值)。
  • jm_api.py — jm-devops 后端统计接口客户端,设备密钥授权grant_type=client_secret,免验证码)。先用 CLIENT_ID + CLIENT_SECRETPOST /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 HostQWEATHER_HOST+ X-QW-Api-Key 头鉴权;响应 gzip 压缩(按 magic number 手动解压)、返回 code字符串get_now() 取实时(now.text 中文天气 / now.temp / now.icon 图标代码),get_today()/3ddaily[0] 今日温区。QWEATHER_HOST/QWEATHER_KEY 未配置时直接抛错→天气区显示 "--"
  • sensecraft_api.py — SenseCraft HMI 设备遥测客户端:GET {SENSECRAFT_API_BASE}/api/v1/user/device/iot_data/{SENSECRAFT_DEVICE_EUI},头 api-key(用户在 SenseCraft 后台生成,放 .env2026-09-14 该 key 曾出现在对话里,建议轮换),取 result.battery.level/chargingresult.sensor.temp/humidity;设备每 dataaccess.interval=300s 上报一次,故 DEVICE_TTL 默认 300。直连不走代理、超时 10s、code!=200 抛错 → 状态栏 --
  • 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不列入。
  • claude_usage.py — Claude 账户级用量采集(形态 A跑在本机复用 Claude Code 自身的 OAuth 登录态,直接 GET https://api.anthropic.com/api/oauth/usage(头 Authorization: Bearer <accessToken> + anthropic-beta: oauth-2025-04-20)——与 Claude Code /usage、claude.ai 用量页同源CLI/桌面 App/网页消耗都算,不依赖 Claude Code 进程在跑、不依赖 statusline 或任何插件2026-09-14 起替代旧的「statusline 补丁落盘 ~/.claude/usage-snapshot.json、本项目读文件」方案,usage_local.py 已删。token 来源按序:环境变量 CLAUDE_CODE_OAUTH_TOKEN → macOS 钥匙串 generic passwordservice Claude Code-credentialsJSON claudeAiOauth.accessToken/expiresAt(ms))→ ~/.claude/.credentials.json(尊重 CLAUDE_CONFIG_DIR)。铁律:① 只读、绝不刷新 tokenrefresh token 每次使用即轮换,第三方刷新不写回会让 Claude Code 强制重登,多个官方 issue 证实access token 约 8 小时有效,过期抛 TokenUnavailable → 沿用旧快照,等 Claude Code 下次启动自动刷新;② 有效期 CLAUDE_USAGE_TTL 内不再调、建议 ≥300 秒(突发调用 429 会封约 24 小时并连带 Claude Code /usage、claude.ai 一起看不到);③ 直连不走代理。normalize() 优先解析 limits[]kind=session/weekly_all/weekly_scoped模型限定周额度按 scope.model.display_name == CLAUDE_USAGE_SCOPE_MODEL「Fable」识别——2026 年中起顶层 seven_day_opus/sonnet 已置 null只能这样取顶层 five_hour/seven_day 兜底;输出与旧快照同构 {five_hour, seven_day, seven_day_fable|None, updated_at, source}get_usage(force=)usage_common.cached_snapshot 带缓存/最小间隔/退避,status ∈ fetched/cache/stale/backoff。官方合规页禁止第三方「收集、存储、中介」claude.ai 凭证,未专门点名此端点,个人自用只读监控属灰色地带、无封号报告——所以凭证只留本机、服务端不碰。
  • codex_usage.py — Codex 账户级用量采集(形态 A跑在本机经官方 codex app-server stdio JSON-RPC 读 ChatGPT 订阅的 Codex 额度池CLI/IDE 扩展/Codex Web/ChatGPT 桌面 App 内 Codex 共用)。调研结论2026-09-14ChatGPT 订阅的普通聊天额度没有任何可编程的账户级读取途径(私有端点在 Sentinel/Cloudflare 门后、且只在被限流时才有值;官方明确聊天与 Codex 分开计量故右侧块如实命名「Codex Usage」。协议本机 codex-cli 0.154 验证):initialize → 通知 initializedaccount/rateLimits/readparams {excludeResetCreditDetails:true}),响应 rateLimits.primary/secondary{usedPercent, windowDurationMins, resetsAt(epoch 秒)} + planType + 多桶 rateLimitsByLimitId;未登录 ChatGPT 账号时 error「codex account authentication required to read rate limits」。窗口归类按 windowDurationMins≤12h → five_hour≥5 天 → seven_dayPro 计划常只回一个窗口、另一个 null不能按 primary/secondary 位置判断;快照各窗口带 window 真实秒数供 pace 用。为什么不直调 chatgpt.com/backend-api/wham/usagetoken 刷新由 Codex CLI 代管refresh token 轮换,外部自刷会踢掉 CLI 登录,官方 CI 文档也明示「不要自己调刷新接口」)。登录2026-09-14 用户直接在默认 ~/.codex 用 ChatGPT 账号 codex loginauth_mode=chatgpt,原 API Key 仍保留在 auth.jsonconfig.toml 的第三方中转 base_url 只影响推理、不影响额度读取),故 CODEX_USAGE_HOME 默认留空跟随默认目录;若要隔离可设独立 CODEX_HOME(目录不存在会自动建)并在其下 codex loginCODEX_BINconfig._which_codex() 解析为绝对路径launchd 的 PATH 不含 /opt/homebrew/bin,曾报「找不到 codex 可执行文件」进入退避;.env 已写死 /opt/homebrew/bin/codex。子进程剥掉代理环境变量直连chatgpt.com 本地直连可达)。真实数据Plus 计划)rateLimits.primary 只有周窗口(windowDurationMins=10080)、secondary=nullrateLimitsByLimitId 仅一个桶 codex——故 5h 行为 --、7d 行有值;buckets 原样附带,若将来出现第二个桶再决定是否加第三行。
  • usage_common.py — 两个采集器共用的骨架 cached_snapshot(path, ttl, backoff, fetch, force):距上次成功 <ttl 直接返回缓存ttl 来自 .env,与 WEATHER_TTL/MAO_TTL/BTC_TTL 同语义0=每次实拉launchd 每 5 分钟先后起「小屏渲染」「上报」两个进程,只打一次接口);失败沿用旧快照并退避 backoff 秒(期内不再尝试);从未成功且失败则抛出。缓存文件 .claude_usage_cache.json/.codex_usage_cache.json 只存快照与时间戳,不存凭证
  • renderer.pyDashboardRenderer.render(data) 返回 PIL 'RGB' 模式、仅黑/白/红三色图;组件层提供可覆写钩子/参数供宽版复用:_usage_colors()(用量行 填充/边框/文字 三色元组)、_pace_color()pace 数值与三角颜色)、_usage_row(label_w=)_weather_icon(wet=)(降水元素颜色)、_label(x0,x1)_cmp_at()(环比/同比左对齐版,_cmp_line 居中版基于它)、_cmp_color(direction, fg)(三角与数值颜色:三色版涨红,宽版覆写涨绿跌红);字体另有 F363 倍整数缩放)。render_to_file() 落盘。布局硬编码为上中下三段,两道 hdash 横规分隔y=90 / 200
    • A 顶部090大时钟(F48)+日期(左)/ 天气(右:红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。
    • B 中部主角 Claude Usage_label 标题 + 两条 _usage_row):每条单行 = 左标签 5h/7d(F13) + 粗进度条 + 百分比(F16) + 时间维度对比 pace(F13_pace)pct≥warn→该条进度条(含边框)与百分比转红预警pace 用上/下三角替代 +/−(超前↑ / 节余↓)。整行「标签·进度条·百分比·三角」用 _mid() 按墨迹竖直中心对齐到同一中线。
    • C 底部 橘喵今日实时_label + 三列——过滤掉 wide 列,只显示探店单量/探店流水/毛利,版式对齐主分支):■ 橘喵·今日实时 + 上次更新,列内 名(F12)/值(F24 粗)/环比·同比(_cmp_line),相对标题偏移 +18/+34/+64/+82底部留 ~11px 下边距。
    • 红色点缀系统(改配色务必克制):模块顶部 BLACK/WHITE/REDRED=(255,0,0) 是给设备红通道的明确信号。红只用在:① 定位水滴(_loc_pin(fg=RED))② 天气太阳/闪电(_weather_icon(accent=RED),云/雨/雪/雾/月仍黑)③ 用量进度条与百分比(pct≥warn) + pace 超前(↑) ④ 环比/同比「涨」的三角与数值(_cmp_line,跌/标签仍黑)。配色铁律:红与黑同属深色、红叠黑对比极低,故红只压白底或作大块实心(白字压红),绝不红字压黑底。加红先问「是否承载语义」。
    • _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/miaomiaoweb/ 即设备自带管理页,可浏览器打开 http://<EPD_HOST>/ 调试同一套接口)。push_image() 先调 GET /images 拿画廊已存图片名单,逐个 POST /delete_image?name=清空(单张删除失败不阻塞后续),再 POST /uploadContent-Type: image/pngbody 为原始 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 公网 APIPOST /devices/{id}/display/image + 设备 MAC + ZECTRIX_API_KEY),现统一为局域网直连。
  • 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占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(_status_battery/_status_env)、星期/日期 F12按日期数字底沿与时钟基线对齐;三行中心约 30.5/52.5/74.5,行距 22天气右贴 x=790B 用量 _usage_block×2usage 10386竖规 x=400usage_gpt 414790_label 标题 + 右侧「上次更新 HH:MM」与今日实时同款文案或 mock 的「示例数据」,行用基类紧凑 _usage_row(行距 31宽版把 bar["label"] 长标签代入 klabel_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_label24h/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_stripE1002_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_refreshdata.weather_fetch()/data.mao_fetch(now, with_trend=False)/data.btc_fetch(),成功更新数据与时间戳),build_dataconcurrent.futures.wait 整体最多等 FETCH_BUDGET 秒(默认 5.env 可调),再由 _settle 结算:完成→fetched(0.27s)(带耗时)/失败沿用旧值 stale/从未成功占位 none未完成→ slow→stale(用上一次成功值)或 slow→none(占位)拉取线程继续跑完写入缓存供下次请求stale-while-revalidateSTATE.inflight[key] 保证同一路进行中不重复实拉。响应上限 ≈ 预算 + 渲染任一路上游卡死socket 超时和风/橘喵 30s、BTC 8s不再拖垮整图。2026-09-14 前为串行实拉(三路全拉约 1.12.6s,单路卡住即整体超时);改并行后三路全拉约 0.75s(取最慢一路)。日志每行末尾 [weather:cache(12s) mao:fetched(0.06s) btc:slow→stale] 可直接看命中/耗时/超预算情况,/healthcache_age_s/ttl_s/fetch_budget_s/inflight;用量取 STATE.raw(推送暂存)→ data._usage(now, raw=…)/data._usage_gpt(now, raw=…),未推送时 Claude 全 --、ChatGPT 用 mock。端点GET IMAGE_PATHrender_png_bytes()_RENDER_LOCK 串行化Pillow 渲染器非线程安全);GET HTML_PATHhtml_renderer.render_html()GET HTML_PNG_PATHhtml_shot.screenshot_html()+quantize6()?raw=1 跳过量化);GET /health JSONPOST /push/usage|/push/usage_gptbody=原始快照 JSON≤64KBX-Push-Token==PUSH_TOKEN,为空拒绝);GET /push/* 查看暂存(同令牌)。所有响应 Cache-Control: no-store;每请求打一行日志(含 CF-Connecting-IP)。PUSH_STATE_PATH 非空时推送数据落盘(原子替换),重启恢复;图片永不落盘。实测:三路全实拉约 1.11.3s(天气 0.28s、橘喵 0.06s、OKX 0.7s 串行),全部命中缓存约 0.02s(渲染 20ms + 编码 14ms
  • html_shot.py — 方案三:screenshot_html(html) 用无头 Chromefind_chrome() 自动探测或 CHROME_BINLinux 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 — 本机推送端:SOURCES 两路——claude_usage.get_usage()POST {PUSH_URL}/push/usagecodex_usage.get_usage()POST {PUSH_URL}/push/usage_gpt(推前去掉 status 键);两路彼此独立,一路失败只打日志、两路都失败才非零退出;直连不走代理、连接类异常重试 2 次、4xx/5xx 直接报错。
  • probe_server.py — 验证工具(不参与正常流程):极简 HTTP 服务,/probe.png--every 秒重绘一张带大号时刻+序号的 800×480 探针图,响应 Cache-Control: no-store,每次请求打印时间/UA/来源 IPCF-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 必读)

设备是 三色(黑/白/红)、无真灰阶——灰阶只能抖动成网点(难看),所以底色只用黑白 + 红作稀缺强调,靠构图/字号/留白建立层级,不要引入灰色填充或抖动

  • 红只作语义强调:方向(涨)、警示(闪电)、强调(定位/主折线);红占墨保持很小(<10% 墨迹),滥用会稀释注意力、破坏层级。整图只允许三种纯色 (0,0,0)/(255,255,255)/(255,0,0)——渲染后可用 img.getcolors() 自查,出现第四种色(多为抗锯齿灰边)即为 bug。矢量图元(line/ellipse/polygon/arc)默认不抗锯齿故为纯色;文字靠 fontmode="1" 保持纯色。

  • DashboardRenderer(k=…) 缩放系数:所有组件(_label/_cmp_line/_usage_row/_pace/tri 等)的像素常量与伪粗体偏移都乘 k,字体按 12*k/24*k/48*k 创建;k=1 为 400×300 原版(改组件后务必用固定夹具比对 k=1 输出逐像素不变E1002 版当前用 k=1k=2 整体放大在 800×480 上字过大已否决k 参数保留供更高分辩率设备使用。字体属性名(F12/F24/F48)表达层级而非绝对像素。

  • 字体固定用 Fusion Pixel 12px 点阵字体fonts/OFL 协议),首选整数倍尺寸 12/24/48F12/F24/F48)最锐利;用量区为拿到合适的字号层级另用了 F13/F161.08x/1.33x 非整数倍,笔画略不均,属有意取舍),靠 d.fontmode = "1" 关抗锯齿仍保持纯色不糊。跨字号竖直居中统一用 _mid()(按 getbbox 墨迹中心对齐)。

  • 加粗用「伪粗体」:_draw() 按 1~bold px 水平偏移叠绘笔画(点阵字体只有单一字重)。

  • 推送时 DITHER = False(硬阈值),纯黑白图最锐利,勿改成 true

  • 坐标、字号均为像素网格上的硬编码常量;调布局时注意各区分隔线 hdash 的 y 值与下方组件位置联动。

注意

  • 所有凭证均在 .env(不提交);config.py 不再内置明文 fallback。分享代码时给出 .env.example 即可,真实 .env 切勿入库。
  • .gitignore 已忽略:output/__pycache__/*.pyc、缓存文件 .weather_cache.json/.trend_cache.json/.btc_cache.json/.device_cache.json/.claude_usage_cache.json/.codex_usage_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 曾漏杀,且 *:8790127.0.0.1:8790 两个监听可并存、旧代码继续应答(表现为新端点 404
  • cloudflared 快速隧道(免账号,地址每次重启都变,需到 SenseCraft 更新 URL nohup env -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u ALL_PROXY cloudflared tunnel --url http://127.0.0.1:8790 --no-autoupdate --protocol http2 > output/cloudflared.log 2>&1 & 地址 grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' output/cloudflared.log(已写入 output/tunnel_url.txt)。 快速隧道会被 Cloudflare 侧静默回收2026-09-13 一条跑了约 2 天后失效进程仍在、server 仍正常,但日志循环报 Unauthorized: Tunnel not found,公网地址 530。 「设备收不到数据」先看 output/cloudflared.log 而不是 server修复只能重启 cloudflared 拿新地址并更新 SenseCraft。 重启要点:旧进程对 SIGTERM 退出很慢(数秒),确认 ps aux | grep "cloudflared tunnel" 已清空再起新的;默认 QUIC 注册曾 context deadline exceeded 后自行退出,加 --protocol http2 一次成功。
  • shell 坑:本机是 fish——env -u … 不能存进变量再当命令执行;含中文的 Python heredoc 用 python3 -X utf8 - <<'EOF',否则 stdin 解码报错;gh 未登录且 GitHub API 匿名限流,取源码走 curl raw.githubusercontent.com
  • 用量采集验证python3 claude_usage.py --force 应打出 status: fetchedseven_day_fable 非空;紧接着不带 --force 再跑应为 cache;缓存文件里不得出现 token。python3 codex_usage.py --force 未登录时报「authentication required…请先执行 CODEX_HOME=… codex login」登录后应有 five_hour/seven_day。历史statusline 补丁(~/.claude/statusline.sh)仍在本机产出 ~/.claude/usage-snapshot.json,但本项目已不读它。
  • 定时任务run.sh 末尾已追加 main.py --target pushvfox python 绝对路径,|| trueClaude/Codex 两路账户级用量随喵喵推送每 5 分钟采集并同步到 server.py推送暂存落盘 output/push_state.json
  • 上游配额server 三路上游已有 TTL 内存缓存(天气默认 15 分钟 → 和风约 192 次/天);改有效期只动 .env,不动代码。本机 .env 当前 MAO_TTL=0/BTC_TTL=0(每次请求实拉)。
  • 「平台抓取失败」排查顺序:① output/cloudflared.log 隧道是否被回收;② output/server.log 该时刻有无该请求(平台 IP 13.91.x.x每 ~5m45s 一次)——没有记录说明请求根本没到我们这里,问题在平台/隧道;③ 有记录看末尾状态串与耗时:slow→ 表示某路超 FETCH_BUDGET、本次用了旧值。2026-09-14 一次怀疑「多路 TTL 同时到期导致超时」,核查日志:所有平台请求均 200、最慢 4.23s 且该次天气还是缓存,三路同时实拉的两次仅 1.21.5s——猜想不成立MAO/BTC TTL=0 本就每次同时实拉);真正的隐患是当时串行拉取 + 30s socket 超时,已改并行 + 预算。
  • SenseCraft 未公开后端(从网页前端 bundle 提取,https://sensecraft-hmi-api.seeed.cc,头 Authorization: <token>token 在 localStorage user-storage POST /api/v1/oss/file/uploadmultipart file+type=imagefile_url)→ POST /api/v2/user/page{pages:[{name,type:img|url|layout,data,dither,thumbnail,resolution:"800x480"}]}POST /api/v2/user/playlist/upsert_pagesPOST /api/v2/user/device/down_link{mac_address,playlist_id,type,refresh_interval?,deep_sleep_enabled?} 另有 POST /api/v2/user/device/configPOST /render/preview{url,img_format,resolution,dither})。未采用(未公开、可随时变更),仅作备选记录。