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>
This commit is contained in:
2026-09-10 15:42:17 +08:00
parent 110f710ff0
commit 6dc89b504e
5 changed files with 90 additions and 50 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 字号与三色版完全相同(不放大,放大后字太大很傻)**。版面定位为 **AI 用量看板**:顶部时钟/天气不变;主体左 **Claude Usage 三行5h / 7d / Fable 7d**、右 **ChatGPT Usage目前 mock标「示例数据」**每行是一张「用量卡」(标签 + F24 百分比 + pace / 全宽进度条 / 重置时刻 + 已过时间)。橘喵今日实时与 30 天趋势图**不在**本版面(业务数据留给 400×300 小屏)。六色语义:红=预警/pace 超前/定位/太阳闪电,黄=用量「注意」档填充,蓝=雨雪,绿暂未用
> **分支 `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% 黄「注意」档,已移除)
## 命令
@@ -29,7 +29,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_CAUTION`(仅 E1002 黄档)`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``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,13 +37,13 @@ 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, pct(或 None), time_pct?, reset?, scoped}``usage.warn`/`usage.caution`红/转黄阈值`usage.updated` 为快照落盘时刻;`usage_gpt``usage` 同构,目前是 **mock**`mock: True`),接真实数据源时替换 `_usage_gpt()` 即可)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不画趋势图renderer 不使用它)
- **`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`
- **用量的 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` 未配置时直接抛错天气区显示 `"--"`
- **`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()`用量行 填充/边框/文字 三色元组)、`_weather_icon(wet=)`降水元素颜色`_label(x0,x1)` 可指定左右边界`render_to_file()` 落盘布局硬编码为**上中下三段**两道 `hdash` 横规分隔y=90 / 200
- **`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)+日期/ 天气红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。
- **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 下边距
@@ -51,7 +51,7 @@ 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 与三色版一致只重写版式与配色钩子版式A 顶部 090 时钟/日期左天气右 x=790B 主体 y=100 `usage`10386竖规 x=400 `usage_gpt`414790各自一个 `_usage_block``_label` 标题 + 右侧快照 HH:MM mock 示例数据」,然后逐 `_usage_card`行距 116)。`_usage_card` 三层:① 标签 F13 + 百分比 F24 + `_pace` 三角 全宽进度条 H=14 F12 说明行重置 …」/「已过 NN%」;pct None 显示 `--`配色钩子 `_usage_colors` 三档`USAGE_WARN` `USAGE_CAUTION`默认 60黄填充+黑框黑字——黄字压白底不可读黄只作填充天气图标 `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 `_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 折线;「用量大卡片撑满
- **`config.py`** 顶部 `_load_dotenv()` 先加载同目录 `.env`再定义 API设备画布尺寸字体路径推送参数凭证 fallback 为空串真实值来自 `.env`/环境变量
## 墨水屏渲染约束(改 renderer.py 必读)