- BTC_LIMIT 默认 72→48(.env 未覆盖时即生效),文档同步 - BTC 文件缓存判定加入根数校验:改周期或根数后 TTL 内不再沿用旧范围 - CLAUDE.md 补充 cloudflared 快速隧道运维经验:隧道会被静默回收, 重启用 --protocol http2 并剥掉代理环境变量 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
32 KiB
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 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 Usage(mock,标「示例数据」),紧凑单行样式(曾试过撑满高度的大卡片,被否);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_OK30 / pace ≤ −PACE_TOL10)、黑=正常、红=危险(用量 ≥USAGE_WARN80 / pace ≥ +10);百分比数字只在红档转红(绿小字在白底发虚)。红另用于涨/定位/太阳闪电,蓝=雨雪;黄不用——用户明确不要黄色进度条(曾有 60%~80% 黄「注意」档,已移除)。
命令
pip install -r requirements.txt # 仅 Pillow>=10.0;urllib 用标准库
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/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
# 临时覆盖:真实环境变量优先级高于 .env
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/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)。
架构(数据 → 渲染 → 推送 三层)
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 为空 → 面板--)。- 缓存(为 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=pct−time_pct(>0 超前↑ / <0 节余↓);故即使utilization来自可能滞后的本地缓存,pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800s。
- 缓存(为 launchd 每 5 分钟独立进程而设,必须落盘):天气
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 顶部(0–90):大时钟(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/RED,RED=(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/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 顶部 0–90 时钟/日期左、天气右(贴 x=790);B 用量_usage_block×2(左usage10–386|竖规 x=400|右usage_gpt414–790):_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+22~98,区块底 y0+100;D_btc_chartBTC 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 动态服务(stdlibThreadingHTTPServer,无第三方依赖)。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 /healthJSON;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 线标题右侧价格用<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 必读)
设备是 三色(黑/白/红)、无真灰阶——灰阶只能抖动成网点(难看),所以底色只用黑白 + 红作稀缺强调,靠构图/字号/留白建立层级,不要引入灰色填充或抖动:
-
红只作语义强调:方向(涨)、警示(闪电)、强调(定位/主折线);红占墨保持很小(<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=1(k=2 整体放大在 800×480 上字过大,已否决),k 参数保留供更高分辩率设备使用。字体属性名(F12/F24/F48)表达层级而非绝对像素。 -
字体固定用 Fusion Pixel 12px 点阵字体(
fonts/,OFL 协议),首选整数倍尺寸 12/24/48(F12/F24/F48)最锐利;用量区为拿到合适的字号层级另用了 F13/F16(1.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、本机专用的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 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。 - 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 在 localStorageuser-storage):POST /api/v1/oss/file/upload(multipartfile+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})。未采用(未公开、可随时变更),仅作备选记录。