- Claude:复用 Claude Code 钥匙串登录态直调 /api/oauth/usage,零依赖 statusline 与 Claude Code 进程;只读不刷新 token - Codex:经 codex app-server JSON-RPC 读 ChatGPT 订阅的 Codex 额度池(普通聊天额度无账户级途径,右块改名 Codex Usage) - 两路采集共用 TTL 缓存+失败退避(CLAUDE_USAGE_TTL/CODEX_USAGE_TTL,语义同 WEATHER_TTL),推送端两路独立上报 - E1002:Claude 7d 与 fable 7d 合并为一行(单边框内上下对半实心,数字取较大者)、标签去 all 前缀、空值按 00% 00 占位等宽 - E1002/网页版:今日实时环比同比改涨绿跌红,与 K 线一致 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
335 lines
21 KiB
Python
335 lines
21 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(**k=1,字号与 400x300 版完全相同**),
|
||
利用 800x480 的四倍面积重新排版为「AI 用量看板」,输出 **E Ink Spectra 6 原生六色**(黑/白/黄/红/绿/蓝)纯色图,
|
||
并生成一张固定 800x480 的 HTML 页面供 SenseCraft HMI 的 HTML 控件云端截图拉取。
|
||
|
||
版式(k=1,点阵字体且**只用整数倍字号 12/24/36/48/84**,不放大;四段,三道横规):
|
||
A 顶部(0~90) :**大时钟 F84**(墨迹 16~79,占满上下边距内的整段高度)+ 星期/日期 F12 紧随其右、与时钟数字**基线对齐**(左)
|
||
|天气(右,贴 790)——天气组件与三色版同尺寸。
|
||
B 用量(96~206) :左半 **Claude Usage**(两行:5h + 合并行 7d——一个大边框内上下两条实心 = 总 7d / fable 7d,右侧数字取较大者,`_usage_row_split`)|竖规 x=400|右半 **Codex Usage**(5h / 7d,ChatGPT 订阅的 Codex 额度池;未推送时 mock 标「示例数据」)。
|
||
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31);**三处文字统一 12px**(基类 F13/F16/FPACE 都映射到 12px):
|
||
标签**常规字重、在标签列内右对齐**(贴进度条),百分比与 pace 数字**粗体**(曾试过 24px,用户嫌大),
|
||
标签用 bar["label"](statusline 同款写法),列宽按最长标签自适应;标题右侧「上次更新 HH:MM」与今日实时同款。
|
||
C 今日(218~322):**橘喵·今日实时** 五列全宽(探店单量/探店流水/团购单量/团购流水/毛利):列内四行逐行居中——
|
||
名 F12 / 值 F24 粗 / 环比 / 同比(五列每列 ~157px,原「值 F36 + 右侧叠放环比/同比」并排组放不下)。
|
||
D 行情(~336~470):**BTC/USDT 时K(近 48 小时)** 蜡烛图(`_btc_chart`,数据来自 btc_api:OKX 主源、Coinbase/Huobi 备源,5 分钟缓存;
|
||
周期/根数由 BTC_BAR/BTC_LIMIT 配置)——标题右侧最新价 + 24h 涨跌幅;**涨(收≥开)绿、跌红**,实心蜡烛 + 1px 影线;
|
||
右侧最高/最低/最新价标签,最新价一道点规;底部首/中/末时间。
|
||
可选(470~479):六色测试色条(config.E1002_COLOR_STRIP,默认关;真机校准色彩量化时打开)。
|
||
|
||
六色的语义分工(每种颜色只有一个含义,不为「有」而用):
|
||
- 黑/白:结构与正文。红:危险、涨、定位、太阳/闪电;绿:健康、K 线上涨(红另用于 K 线下跌);蓝:天气雨滴/雪花。
|
||
黄:**不用**(2026-09-14 曾按要求加过四档含黄,随即被否)。
|
||
- 用量与 pace **各自三档、颜色顺序固定 绿 → 黑 → 红**,两者独立计算(`_usage_colors` / `_pace_color`):
|
||
用量 pct:≤ USAGE_OK(30) 绿「健康」|(30, 80) 黑「正常」|≥ USAGE_WARN(80) 红「危险」——进度条填充、边框与**百分比数字三者同色**;
|
||
pace = 用量% − 时间%:≤ −PACE_TOL(10) 绿「节余」|(−10, +10) 黑「持平」|≥ +10 红「偏快」。
|
||
注:绿色数字在白底上比黑字淡一些,24px 字号下可读;这是「百分比随条色」的已知取舍。
|
||
- 整图只允许六种纯色;渲染后可用 `img.getcolors()` 自查。
|
||
|
||
为什么是「固定 800x480 + 一张 PNG」而不是自适应 HTML:
|
||
- SenseCraft 的 HTML 控件是云端无头浏览器截图后再量化到六色下发。自适应布局在未知视口/DPR 下会被重排、
|
||
缩放、抗锯齿,产生大量中间色,最终被平台抖动成网点。页面锁死 800x480、内容就是 1:1 的 PNG
|
||
(`image-rendering: pixelated`),控件铺满画布即逐像素一致,每个像素本就是六色之一,量化零损失。
|
||
"""
|
||
import os
|
||
import time
|
||
|
||
import config
|
||
from renderer import DashboardRenderer, BLACK, WHITE, RED
|
||
import config as _cfg
|
||
from PIL import ImageFont
|
||
|
||
# E Ink Spectra 6 原生六色。取纯饱和值作为给平台量化器的明确信号(与 ESPHome/GxEPD2 驱动的 BWYRGB 映射一致)。
|
||
YELLOW, GREEN, BLUE = (255, 255, 0), (0, 255, 0), (0, 0, 255)
|
||
SPECTRA6 = [BLACK, WHITE, YELLOW, RED, GREEN, BLUE]
|
||
|
||
|
||
class E1002Renderer(DashboardRenderer):
|
||
"""800x480 六色版。组件继承(k=1,字号不变),只重写版式与配色钩子。"""
|
||
|
||
PCT_TRACK = 0 # 百分比是整数倍字号(自带侧边距),不再手动加字符间距
|
||
LABEL_BOLD = False # 标签常规字重(数值才加粗,层级:数值 > 标签)
|
||
LABEL_ALIGN = "r" # 标签右对齐贴进度条,长短标签(5h / 7d)右缘齐平
|
||
|
||
def __init__(self, font_path=config.FONT_PATH):
|
||
super().__init__(font_path, size=(config.E1002_WIDTH, config.E1002_HEIGHT), k=1)
|
||
# 宽版只用点阵字体的**整数倍字号**(12 / 24 / 36 / 48 / 84):基类给用量行用的 F13(标签)/ FPACE(pace)/ F16(百分比)
|
||
# 是非整数倍缩放(笔画不均),这里统一映射到 12px(24px 试过、用户嫌大);标签常规字重、百分比与 pace 数字粗体;
|
||
# 行几何靠 _mid 按墨迹中心对齐,不受字号变化影响。F84 给顶部大时钟(7 倍整数缩放,墨迹 63px 恰好占满 A 段)。
|
||
self.F13 = ImageFont.truetype(font_path, 12)
|
||
self.F16 = self.F13
|
||
self.FPACE = self.F13
|
||
self.F84 = ImageFont.truetype(font_path, 84)
|
||
|
||
# ---------- 配色钩子(三档,顺序固定 绿 → 黑 → 红;用量与 pace 各自独立计算)----------
|
||
_ok, _pace_tol = _cfg.USAGE_OK, _cfg.PACE_TOL # 由 _usage_block 按数据覆盖
|
||
|
||
def _usage_row(self, x0, x1, y, bar, warn, label_w=None):
|
||
"""宽版:pct 为 None(接口未返回该窗口,如 Codex Plus 的 5h)时不画 "--",而是按 0 画——百分比 "00%"、pace "00"、
|
||
空条、全黑(无数据的中性色,不落绿档)——使各行进度条右端对齐、宽度一致(2026-09-14 用户要求;"--" 会让该行条变短)。"""
|
||
if not isinstance(bar.get("pct"), (int, float)):
|
||
bar = dict(bar, pct=0, time_pct=0, _null=True)
|
||
return super()._usage_row(x0, x1, y, bar, warn, label_w=label_w)
|
||
|
||
def _pace(self, xr, cy, diff):
|
||
"""宽版:diff=0(持平/无数据)不画三角,但**仍预留三角位**(6 + 间隙 4),使每行 pace 列等宽、进度条右端齐平。"""
|
||
w = super()._pace(xr, cy, diff)
|
||
return w if diff != 0 else w + 10 * self.k
|
||
|
||
def _usage_colors(self, ok, pct, warn, bar):
|
||
"""进度条填充、边框、百分比数字三者同色:pct ≤ ok 绿「健康」|其间黑「正常」|pct ≥ warn 红「危险」;无数据(_null)黑。"""
|
||
if not ok or bar.get("_null"):
|
||
return BLACK, BLACK, BLACK
|
||
c = RED if pct >= warn else GREEN if pct <= self._ok else BLACK
|
||
return c, c, c
|
||
|
||
def _cmp_color(self, direction, fg=BLACK):
|
||
"""环比/同比 涨绿 / 跌红(与底部 K 线「涨绿跌红」一致,2026-09-14 用户要求);无方向沿用 fg。"""
|
||
return GREEN if direction == "up" else RED if direction == "down" else fg
|
||
|
||
def _pace_color(self, diff):
|
||
"""pace = 用量% − 时间%(与用量档位独立):≤ −tol 绿「节余」|(−tol, +tol) 黑「持平」|≥ +tol 红「偏快」。"""
|
||
if diff >= self._pace_tol:
|
||
return RED
|
||
if diff <= -self._pace_tol:
|
||
return GREEN
|
||
return BLACK
|
||
|
||
# ---------- 组件 ----------
|
||
def _color_strip(self, y0, y1, x0=None, x1=None):
|
||
"""六色测试色条:六块等宽实心色块,白块加 1px 黑框以便可见。真机校准用。"""
|
||
x0 = 10 if x0 is None else x0
|
||
x1 = self.W - 10 if x1 is None else x1
|
||
n = len(SPECTRA6)
|
||
w = (x1 - x0) / n
|
||
for i, c in enumerate(SPECTRA6):
|
||
bx0, bx1 = round(x0 + w * i), round(x0 + w * (i + 1)) - 1
|
||
self.d.rectangle([bx0, y0, bx1, y1], fill=c)
|
||
if c == WHITE:
|
||
self.d.rectangle([bx0, y0, bx1, y1], outline=BLACK, width=1)
|
||
|
||
def _usage_block(self, x0, x1, y0, usage):
|
||
"""一个用量区块:标题(_label,右侧「上次更新 HH:MM」——与今日实时同款文案;mock 数据则标「示例数据」)
|
||
+ 逐行紧凑 `_usage_row`(行距 31)。标签列宽按本块最长标签墨迹 + LABEL_GAP 算(标签右对齐贴进度条,短标签左侧留白)。返回末行底部 y。"""
|
||
right = "示例数据" if usage.get("mock") else (f'上次更新 {usage["updated"]}' if usage.get("updated") else None)
|
||
self._label(y0, usage.get("title", "Usage"), right=right, x0=x0, x1=x1)
|
||
warn = usage.get("warn", 80)
|
||
self._ok, self._pace_tol = usage.get("ok", _cfg.USAGE_OK), usage.get("pace_tol", _cfg.PACE_TOL)
|
||
bars = [dict(b, k=b.get("label") or b["k"]) for b in usage.get("bars", [])] # 宽版用长标签(5h / 7d)
|
||
# scoped 行(fable 7d)并入紧邻其前的非 scoped 行,成为该行的下半细条 → 左右两块都是两行,对称
|
||
rows = []
|
||
for b in bars:
|
||
if b.get("scoped") and rows and rows[-1][1] is None:
|
||
rows[-1] = (rows[-1][0], b)
|
||
else:
|
||
rows.append((b, None))
|
||
label_w = max([self._tw(m["k"], self.F13) for m, _ in rows] + [26]) + self.LABEL_GAP
|
||
y = y0 + 27
|
||
for m, sub in rows:
|
||
if sub is None:
|
||
self._usage_row(x0, x1, y, m, warn, label_w=label_w)
|
||
else:
|
||
self._usage_row_split(x0, x1, y, m, sub, warn, label_w)
|
||
y += 31
|
||
return y - 31 + 18 # 末行进度条底部(cy=y+10,条高 15 → 底 y+17)
|
||
|
||
def _usage_row_split(self, x0, x1, y, main, sub, warn, label_w):
|
||
"""合并行「7d」(2026-09-14 定稿):**一个大边框**、整行高度与单行相同(H=15);边框内部上下**精确对半**两条**实心**填充
|
||
(各 7px、紧挨无空隙)——上 = main(总 7d),下 = sub(模型限定 fable 7d)。右侧百分比与 pace **只显示两者中较大者**
|
||
(同一窗口 time_pct 相同,pct 大者 pace 也大);边框与文字颜色随该较大者的档位,两条填充各按自身档位取色。
|
||
实现:以「较大者」合成一条 bar 交给基类 _usage_row 画边框/标签/文字,再把边框内部刷白、重绘两条半高填充。"""
|
||
k = self.k
|
||
def _p(b):
|
||
v = (b or {}).get("pct")
|
||
return int(v) if isinstance(v, (int, float)) else None
|
||
pm, ps = _p(main), _p(sub)
|
||
dom = sub if (ps is not None and (pm is None or ps > pm)) else main
|
||
bx0, bx1, by, H = self._usage_row(x0, x1, y, dict(dom, k=main["k"]), warn, label_w=label_w)
|
||
# 边框内部刷白(保留 1px 边框),再画上下两条实心
|
||
self.d.rectangle([bx0 + k, by + k, bx1 - k, by + H - k], fill=WHITE)
|
||
halves = ((pm, main, by + k, by + 7 * k), (ps, sub, by + 8 * k, by + 14 * k)) # 边框内 14px 精确对半:上 by+1..by+7,下 by+8..by+14,无空隙
|
||
for pct, b, top, bot in halves:
|
||
if pct is None:
|
||
continue
|
||
pct = max(0, min(100, pct))
|
||
fill = self._usage_colors(True, pct, warn, b or {})[0]
|
||
fw = (bx1 - bx0) * pct / 100.0
|
||
if fw >= 2:
|
||
self.d.rectangle([bx0 + k, top, min(bx0 + fw, bx1 - k), bot], fill=fill)
|
||
|
||
def _realtime(self, y0, mao, x0=8, x1=None):
|
||
"""橘喵·今日实时五列(宽版):探店单量 / 探店流水 / 团购单量 / 团购流水 / 毛利,列内四行逐行水平居中:
|
||
指标名 F12(y0+21)→ 值 F24 粗(y0+38)→ 环比(y0+72)→ 同比(y0+88,行距 16);按墨迹框算,标题↔名 ~9px(原 ~5px 太挤,整列下移 4px)、
|
||
名↔值 ~9px、值↔环比 ~6px、环比↔同比 5px。
|
||
为什么不再是「值 F36 + 右侧叠放环比/同比」的并排组:五列每列仅 ~157px,F36 并排组(如「2.12万」102+间隙+环比 77)
|
||
近 194px 放不下;降到 F24 后并排组仍有 156px,贴着两侧分隔线太挤。竖排最宽元素(「1234.56%」一行 89px)余量充足,
|
||
与 400×300 小屏同构。(更早曾试过名与值同行、名贴左上角角标,均已否。)返回区块底部 y。"""
|
||
x1 = self.W - 8 if x1 is None else x1
|
||
self._label(y0, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}', x0=x0 + 2, x1=x1 - 2)
|
||
cols = mao["cols"]; n = len(cols)
|
||
cw = (x1 - x0) / n
|
||
TAG_Y, VAL_Y, HB_Y, TB_Y = y0 + 21, y0 + 38, y0 + 72, y0 + 88
|
||
for i in range(1, n):
|
||
self.vdash(x0 + cw * i, y0 + 26, y0 + 102)
|
||
for i, c in enumerate(cols):
|
||
cx = x0 + cw * (i + 0.5)
|
||
self.ctext(cx, TAG_Y, c["k"], self.F12)
|
||
self.ctext(cx, VAL_Y, c["v"], self.F24, bold=1)
|
||
self._cmp_line(cx, HB_Y, "环比", c["hb"])
|
||
self._cmp_line(cx, TB_Y, "同比", c["tb"])
|
||
return y0 + 104
|
||
|
||
@staticmethod
|
||
def _fmt_price(v):
|
||
"""价格文本:≥1000 千分位取整,否则两位小数。"""
|
||
return f"{v:,.0f}" if abs(v) >= 1000 else f"{v:.2f}"
|
||
|
||
def _btc_chart(self, y0, y1, btc, x0=10, x1=None):
|
||
"""BTC K 线蜡烛图(y0 标题行顶 ~ y1 底;周期由 btc["bar"] 决定标题「时K/日K」):
|
||
标题右侧 最新价(F12 粗)+ 涨跌口径(24h/1d)+ 涨跌三角 + 涨跌幅(F12),涨绿 / 跌红;
|
||
绘图区:每根蜡烛 = 1px 影线(最高↔最低)+ 实心实体(开↔收,宽≈槽位 60%、奇数像素居中),收≥开 绿、否则 红;
|
||
右侧价格标签列:最高(顶)、最低(底)、最新(粗,随最新价位置浮动、避让顶底标签),最新价横贯一道点规;
|
||
底部基线点规 + 首/中/末三个日期。无数据时居中 "--"。"""
|
||
x1 = self.W - 10 if x1 is None else x1
|
||
btc = btc or {}
|
||
candles = btc.get("candles") or []
|
||
kind = "时K" if btc.get("bar", "1H") == "1H" else "日K"
|
||
self._label(y0, f'{btc.get("symbol") or "BTC/USDT"}·{kind}', x0=x0, x1=x1)
|
||
if candles and btc.get("last") is not None:
|
||
chg = btc.get("chg") or 0.0
|
||
col = GREEN if chg >= 0 else RED
|
||
xr = x1
|
||
pct_txt = f"{abs(chg):.2f}%"
|
||
self.rtext(xr, y0, pct_txt, self.F12, fill=col); xr -= self._tw(pct_txt, self.F12) + 4
|
||
self.tri(xr - 4, y0 + 8.5, 7, up=chg >= 0, fill=col); xr -= 8 + 6
|
||
if btc.get("chg_label"): # 涨跌幅口径(24h / 1d)
|
||
self.rtext(xr, y0, btc["chg_label"], self.F12); xr -= self._tw(btc["chg_label"], self.F12) + 10
|
||
self.rtext(xr, y0, self._fmt_price(btc["last"]), self.F12, bold=1)
|
||
if not candles:
|
||
self.ctext((x0 + x1) / 2, (y0 + y1) / 2 - 12, "--", self.F24)
|
||
return
|
||
LBL_W = 66 # 右侧价格标签列
|
||
px0, px1 = x0, x1 - LBL_W
|
||
py0, py1 = y0 + 26, y1 - 16 # 底部留 16px 给日期
|
||
hi_raw, lo_raw = max(c["h"] for c in candles), min(c["l"] for c in candles)
|
||
pad = (hi_raw - lo_raw or 1) * 0.04
|
||
lo, hi = lo_raw - pad, hi_raw + pad
|
||
span = hi - lo
|
||
Y = lambda v: py1 - (v - lo) / span * (py1 - py0)
|
||
n = len(candles)
|
||
slot = (px1 - px0) / n
|
||
bw = max(3, int(slot * 0.6)) | 1 # 实体宽度取奇数,影线居中
|
||
self.hdash(px0, px1, py1, dash=2, gap=3) # 基线
|
||
for i, c in enumerate(candles):
|
||
cx = round(px0 + slot * (i + 0.5))
|
||
col = GREEN if c["c"] >= c["o"] else RED
|
||
self.d.line([cx, round(Y(c["h"])), cx, round(Y(c["l"]))], fill=col, width=1)
|
||
top, bot = sorted((round(Y(c["o"])), round(Y(c["c"]))))
|
||
self.d.rectangle([cx - bw // 2, top, cx + bw // 2, bot], fill=col) # 十字星时 top==bot 退化为 1px 横线
|
||
# ---- 右侧价格标签:最高 / 最低 / 最新 ----
|
||
hi_y, lo_y = py0 - 6, py1 - 12
|
||
self.rtext(x1, hi_y, self._fmt_price(hi_raw), self.F12)
|
||
self.rtext(x1, lo_y, self._fmt_price(lo_raw), self.F12)
|
||
last = candles[-1]["c"]
|
||
ly = round(Y(last))
|
||
self.hdash(px0, px1, ly, dash=1, gap=3) # 最新价水平点规
|
||
ty = min(max(ly - 8, hi_y + 15), lo_y - 15) # 避让顶/底标签
|
||
self.rtext(x1, ty, self._fmt_price(last), self.F12, bold=1)
|
||
# ---- X 轴日期:首 / 中 / 末 ----
|
||
xl_y = py1 + 3
|
||
lab = lambda c: c.get("t") or c.get("d") or ""
|
||
self.text(px0, xl_y, lab(candles[0]), self.F12)
|
||
self.ctext((px0 + px1) / 2, xl_y, lab(candles[n // 2]), self.F12)
|
||
self.rtext(px1, xl_y, lab(candles[-1]), self.F12)
|
||
|
||
# ---------- 入口 ----------
|
||
def render(self, data):
|
||
from PIL import Image, ImageDraw
|
||
self.img = Image.new("RGB", (self.W, self.H), WHITE)
|
||
self.d = ImageDraw.Draw(self.img)
|
||
self.d.fontmode = "1"
|
||
R = self.W - 10 # 右页边 790
|
||
|
||
dt, wt, mao = data["date"], data["weather"], data["mao"]
|
||
usage, gpt = data["usage"], data.get("usage_gpt") or {}
|
||
|
||
# ---- A. 顶部:大时钟 F84 占满整段(左)+ 星期/日期紧随其右、与时钟数字基线对齐 / 天气(右)----
|
||
DIV_Y = 90
|
||
TIME_BOLD, INK_TOP = 3, 16 # 伪粗体 3px;墨迹顶沿 16 与旧版 F48 相同,保住上边距
|
||
_, tt, _, tb = self.F84.getbbox(dt["time"]) # F84 数字墨迹 28~91 → 高 63,摆到 16~79,距横规 11px
|
||
time_y = INK_TOP - tt
|
||
base_y = time_y + tb # 时钟数字基线(墨迹底沿)
|
||
self.text(10, time_y, dt["time"], self.F84, bold=TIME_BOLD)
|
||
date_txt = f'{dt["week"]} {dt["greg"]}'
|
||
_, _, _, db = self.F12.getbbox(dt["greg"]) # 用日期数字的底沿对齐基线(汉字底沿比数字低 1px,忽略)
|
||
self.text(10 + self._tw(dt["time"], self.F84) + TIME_BOLD + 14, base_y - db, date_txt, self.F12)
|
||
loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}'
|
||
self.rtext(R, 8, loc, self.F12)
|
||
self.rtext(R, 30, temp, self.F24, bold=1)
|
||
self.rtext(R, 64, cr, self.F12)
|
||
PIN_W, PIN_GAP = 8, 6
|
||
loc_pin_x = R - self._tw(loc, self.F12) - PIN_GAP - PIN_W
|
||
self._loc_pin(loc_pin_x, 10, PIN_W, fg=RED)
|
||
s = 1.5
|
||
iw, ih = 36 * s, 38 * s
|
||
text_left = min(loc_pin_x, R - self._tw(temp, self.F24) - 1, R - self._tw(cr, self.F12))
|
||
self._weather_icon(text_left - 12 - iw, (DIV_Y - ih) / 2,
|
||
self._weather_category(wt.get("icon")), s=s, accent=RED, wet=BLUE)
|
||
self.hdash(8, self.W - 8, DIV_Y)
|
||
|
||
# ---- B. 用量:左 Claude | 右 Codex(紧凑单行,与三色版同款)----
|
||
MID = self.W // 2 # 400
|
||
y_l = self._usage_block(10, MID - 14, DIV_Y + 6, usage)
|
||
y_r = self._usage_block(MID + 14, R, DIV_Y + 6, gpt) if gpt else y_l
|
||
self.vdash(MID, DIV_Y + 6, max(y_l, y_r) + 6)
|
||
DIV2_Y = max(y_l, y_r) + 16 # 分割线离末条进度条 16px,避免拥挤
|
||
self.hdash(8, self.W - 8, DIV2_Y)
|
||
|
||
# ---- C. 橘喵·今日实时(全宽五列)----
|
||
end_c = self._realtime(DIV2_Y + 6, mao)
|
||
DIV3_Y = end_c + 8
|
||
self.hdash(8, self.W - 8, DIV3_Y)
|
||
|
||
# ---- D. BTC/USDT 日 K ----
|
||
strip = config.E1002_COLOR_STRIP
|
||
self._btc_chart(DIV3_Y + 6, self.H - (16 if strip else 10), data.get("btc"))
|
||
|
||
# ---- C. 可选:六色测试色条 ----
|
||
if config.E1002_COLOR_STRIP:
|
||
self._color_strip(self.H - 10, self.H - 1)
|
||
return self.img
|
||
|
||
|
||
# ---------- HTML 页面(给 SenseCraft HMI HTML 控件抓取)----------
|
||
_HTML = """<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=800, initial-scale=1">
|
||
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate">
|
||
<meta http-equiv="Pragma" content="no-cache">
|
||
<meta http-equiv="Expires" content="0">
|
||
<title>eink dashboard 800x480</title>
|
||
<style>
|
||
/* 固定 800x480、零边距:让云端截图与 PNG 逐像素对应;即便被缩放也用最近邻,避免产生中间色 */
|
||
html, body {{ margin: 0; padding: 0; background: #fff; width: 800px; height: 480px; overflow: hidden; }}
|
||
img {{ display: block; width: 800px; height: 480px;
|
||
image-rendering: pixelated; image-rendering: crisp-edges; -ms-interpolation-mode: nearest-neighbor; }}
|
||
</style>
|
||
</head>
|
||
<body><img src="{png}?v={stamp}" width="800" height="480" alt=""></body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
def write_page(html_path, png_path, stamp=None):
|
||
"""写出固定 800x480 的静态页。PNG 以相对路径引用并带时间戳参数,防止云端抓取器命中旧图缓存。"""
|
||
stamp = stamp or str(int(time.time()))
|
||
rel = os.path.relpath(png_path, os.path.dirname(html_path))
|
||
os.makedirs(os.path.dirname(html_path), exist_ok=True)
|
||
with open(html_path, "w", encoding="utf-8") as f:
|
||
f.write(_HTML.format(png=rel.replace(os.sep, "/"), stamp=stamp))
|
||
return html_path
|