Files
eink-push/CLAUDE.md
YANG JIANKUAN 6dc89b504e feat: E1002 宽版用量配色改三档绿/黑/红+今日实时与近7天版式重做
- 用量与 pace 配色由「红/黑」二档、黄「注意」档,改为绿(健康)/黑(正常)/红(危险)三档,新增 USAGE_OK/PACE_TOL 阈值,去掉黄色进度条
- Claude/ChatGPT 用量行标签改用 statusline 同款长标签(all 5h/fable 7d 等)
- 今日实时区块改为指标名角标+数值与环比同比整组居中;近7天区块改为真实数据表(取30天趋势缓存最后7天),不画折线
- renderer.py 抽出 _cmp_at/_pace_color 钩子+新增 F36 字体供宽版复用;CLAUDE.md 同步版式与配置项说明

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 15:42:17 +08:00

20 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 秒)。因此本分支产出的是一张固定 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% 黄「注意」档,已移除)。

命令

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/,不推送

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_TTLUSAGE_LOCAL_PATH/USAGE_WARN、仅宽版的 USAGE_OK/PACE_TOLE1002_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.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 分钟触发:业务数据每次都刷新,天气靠缓存、用量读本地文件,避免高频打接口(见 data.py)。

架构(数据 → 渲染 → 推送 三层)

main.py 串联三层,各层职责单一、低耦合:

  • data.py — 唯一数据入口 get_dashboard_data(),返回固定结构的 dictdate/weather/mao/usage。日期时间用真实系统时间;橘喵今日经营由 _mao()jm_api、天气由 _weather()weather_api和风定位南京·江宁、Claude 用量由 _usage()usage_local任何异常都回退占位 "--",不回退 mock。改数据保持返回结构不变即可renderer 依赖其 keyweather.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 阈值(小屏只用 warnusage.updated 为快照落盘时刻;usage_gptusage 同构,目前是 mockmock: True;三行故意取 88/71/22 以常驻红/黑/绿三档),接真实数据源时替换 _usage_gpt() 即可)。注:mao.trend 两个版面都不画折线;宽版用 _recent_days(trend, 7) 得到 mao.week(结构 {dates, rows:[(name, [text...], total)]}做「近7天」数据表。
    • 缓存(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 .weather_cache.jsonTTL=WEATHER_TTL,默认 15min、趋势 .trend_cache.json(按日期 key每天只拉一次两者拉取失败沿用旧缓存。用量不自建缓存——直接读 statusline 生产的本地快照 ~/.claude/usage-snapshot.json(见 usage_local),失败即 "--"。两个缓存文件已在 .gitignore
    • 用量的 pace时间维度对比_usage() 每次按当前时间现算 time_pct=(窗口已流逝/窗口长)pace=pcttime_pct>0 超前↑ / <0 节余↓);故即使 utilization 来自可能滞后的本地缓存pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800s。
  • 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 未配置时直接抛错→天气区显示 "--"
  • usage_local.py — Claude Code 用量本地读取,不发任何网络请求生产者/消费者模式):只读 config.USAGE_LOCAL_PATH(默认 ~/.claude/usage-snapshot.json)。生产者 = 打过补丁的 claude-statusline(本机 ~/.claude/statusline.sh):把每次 Claude Code 经 stdin 喂来的权威实时额度落盘为该快照schema 同 /api/oauth/usagefive_hour/seven_dayutilization + resets_at),并额外写入 seven_day_fableFable 模型限定的 7 天额度——stdin 里没有,只在 /api/oauth/usage 返回的 limits[]scope.model.display_name == "Fable" 那项statusline 在 stdin 分支也会每 5 分钟刷新一次 API 缓存 /tmp/claude/statusline-usage-cache.json 以拿到它,无 token/断网时该键为 nulldata.USAGE_WINDOWSscoped=True 标记这一行400×300 小屏 render() 跳过 scoped 行、800×480 宽版显示。本模块作消费者读取。活跃使用时快照几乎持续刷新、准确;空闲时停在最后一次。文件缺失/损坏→用量区 "--"。注:resets_at 可能是 epoch 数字Claude Code stdin 给的就是 epoch或 ISO 串,data._parse_reset_ts 两者兼容。(曾评估直调 /api/oauth/usage,改为读本地快照以免网络请求。)
  • 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 居中版基于它);字体另有 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 + 三列,版式对齐主分支):■ 橘喵·今日实时 + 上次更新,列内 名(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 与三色版一致),只重写版式与配色钩子。四段、三道横规,各段 y 由上一段实际底部推算A 顶部 090 时钟/日期左、天气右(贴 x=790B 用量 _usage_block×2usage 10386竖规 x=400usage_gpt 414790_label 标题 + 右侧「快照 HH:MM」或 mock 的「示例数据」,行用基类紧凑 _usage_row(行距 31宽版把 bar["label"] 长标签代入 klabel_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_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 天折线」版;「用量大卡片撑满」版。
  • 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、本机专用的 run.sh、含凭证的 .env(模板 .env.example 入库)。改缓存逻辑后想强制重拉,删掉对应缓存文件即可。