feat: 渲染升级为黑/白/红三色,红作稀缺强调点缀

新设备在黑白外支持红色。将 renderer 画布从 'L' 灰度改为 'RGB' 三色,
RED=(255,0,0) 作为设备红通道的明确信号;fontmode='1' 保持点阵纯色不发糊。

红只承载语义、占墨极小(实测占墨迹 <10%),点缀四处:
- 定位图标(红水滴)
- 天气图标的太阳与闪电(云/雨/雪/雾/月仍黑)
- 环比/同比「涨」的三角与百分比数值(红涨;跌/标签仍黑)
- 趋势图主指标(流水)折线 + 末点红点 + 图例同步红

整图仅三种纯色,无灰阶、不抖动;dither 保持 false。
同步更新 CLAUDE.md / README.md 的三色约束与红点缀清单。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 18:19:38 +08:00
parent 3e0e6c73a1
commit 9b39eacecf
3 changed files with 52 additions and 28 deletions

View File

@@ -6,7 +6,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
`eink-push` 是 JM monorepo 中一个**独立的 Python 工具**,与其余 Java/Vue 子项目技术栈无关,不受父级 CLAUDE.md 的 jm-cloud/uniapp/admin 开发约束。
作用把橘喵今日经营、近30天趋势、日期与天气合成为一张 **400×300 纯黑白1bit** PNG推送到 Zectrix 极趣云墨水屏设备`zectrix-s3-epaper-4.2`。纯 Python + Pillow**无浏览器依赖**,适合 cron 定时运行。
作用把橘喵今日经营、近30天趋势、日期与天气合成为一张 **400×300 三色(黑/白/红BWR** PNG推送到 Zectrix 极趣云墨水屏设备。纯 Python + Pillow**无浏览器依赖**,适合 cron 定时运行。
> **分支 `feature/colorful`**:新设备在黑白之外支持红色。本分支把渲染从纯黑白升级为黑/白/红三色,红作为**稀缺强调色**只承载语义(方向·警示·强调),占墨极小(实测红≈全图 0.6%、占墨迹 <10%)。仍**无真灰阶、不抖动**。
## 命令
@@ -36,14 +38,17 @@ ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py
- **两套文件缓存**(为 cron 每分钟独立进程而设,必须落盘):天气缓存 `.weather_cache.json`TTL=`WEATHER_TTL`,默认 15min未过期不发请求趋势缓存 `.trend_cache.json`(按日期 key当天命中即不再调 `recent-days`,每天只拉一次)。两者**拉取失败均沿用旧缓存**避免整片 `"--"`,仍无缓存才回退占位。两个缓存文件已在 `.gitignore`
- **`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` 未配置时直接抛错→天气区显示 `"--"`
- **`renderer.py`** — `DashboardRenderer.render(data)` 返回 PIL `'L'` 模式纯 0/255 图;`render_to_file()` 落盘。布局硬编码为四区render 内 A/B/C/DA 大时钟+日期 / B 天气 / C 橘喵今日三列 / D 近30天趋势折线图。趋势线用 `_styled_polyline()` 按 solid/dashed/dotted 区分(设备无颜色),各指标按自身极值独立归一化(故只看趋势形状、不可横向比绝对值)。
- **`renderer.py`** — `DashboardRenderer.render(data)` 返回 PIL `'RGB'` 模式、**仅黑/白/红三色**图;`render_to_file()` 落盘。布局硬编码为四区render 内 A/B/C/DA 大时钟+日期 / B 天气 / C 橘喵今日三列 / D 近30天趋势折线图。趋势线用 `_styled_polyline()` 按 solid/dashed/dotted 区分,各指标按自身极值独立归一化(故只看趋势形状、不可横向比绝对值)。
- **红色点缀系统**(改配色从这里动,务必保持克制):模块顶部定义 `BLACK/WHITE/RED` 三元组,`RED=(255,0,0)` 是给设备红通道的明确信号。红只用在四处:① 定位水滴(`_loc_pin(fg=RED)`)② 天气图标的太阳与闪电(`_weather_icon(accent=RED)`,云/雨/雪/雾/月仍黑)③ 环比/同比**涨=红·跌=黑**——箭头与百分比数值一起着色(`_cmp_line`,「环比/同比」标签仍黑,红涨绿跌之「红涨」)④ 趋势图**主指标(流水)**折线+末点红点(按 `k=="流水"` 命中,图例同步红)。加红先问「是否承载语义」,避免红蔓延稀释注意力。
- **天气图标全矢量手绘**(无图片资源):`_weather_category()` 把和风 icon 代码归并为 8 类sunny/clear_night/cloudy/overcast/thunder/rain/snow/fog识别不了回退 cloudy再由 `_weather_icon()``_sun`/`_cloud` 等基元按 1bit 描边画出;改天气展示从这里动。
- **`pusher.py`** — `push_image()` 走 Zectrix API`POST /devices/{id}/display/image`,手写 multipart。设备 ID 直接取 `config.DEVICE_ID`(即 `.env``ZECTRIX_DEVICE_ID` 硬件 MAC`resolve_device_id()` 仅校验非空、未配置即报错——**已废弃获取设备列表再取第一个的逻辑**(原 `get_devices()` 已删除,不再调 `GET /devices`)。
- **`config.py`** — 顶部 `_load_dotenv()` 先加载同目录 `.env`,再定义 API、设备、画布尺寸、字体路径、推送参数凭证 fallback 为空串,真实值来自 `.env`/环境变量。
## 墨水屏渲染约束(改 renderer.py 必读)
设备是 **1bit、无真灰阶**——灰阶只能抖动成网点(难看),所以全程纯黑白,靠构图/字号/留白建立层级,**不要引入灰色填充或抖动**
设备是 **三色(黑/白/红)、无真灰阶**——灰阶只能抖动成网点(难看),所以底色只用黑白 + 红作稀缺强调,靠构图/字号/留白建立层级,**不要引入灰色填充或抖动**
- **红只作语义强调**:方向(涨)、警示(闪电)、强调(定位/主折线);红占墨保持很小(<10% 墨迹滥用会稀释注意力破坏层级整图**只允许三种纯色** `(0,0,0)/(255,255,255)/(255,0,0)`——渲染后可用 `img.getcolors()` 自查出现第四种色多为抗锯齿灰边即为 bug矢量图元(line/ellipse/polygon/arc)默认不抗锯齿故为纯色文字靠 `fontmode="1"` 保持纯色
- 字体固定用 **Fusion Pixel 12px 点阵字体**`fonts/`OFL 协议只按整数倍尺寸 **12/24/48** 渲染`F12/F24/F48`并设 `d.fontmode = "1"` 关闭抗锯齿否则像素会糊
- 加粗用伪粗体」:`_draw()` 1~bold px 水平偏移叠绘笔画点阵字体只有单一字重)。