feat: E1002 新增公网图片服务(恒定 URL 现渲染)+底部改为 BTC 时K
- server.py:三条恒定 URL 并行——/e1002.png 内存现渲染 PNG(主力)、 /e1002.html 现代网页、/e1002-web.png 网页经无头 Chrome 截图后固化六色; 天气/橘喵/BTC 按 WEATHER_TTL/MAO_TTL/BTC_TTL 走内存缓存,过期才实拉、 失败沿用旧值;用量不读本机文件,改由 POST /push/usage(_gpt) 推送暂存 - push_client.py:本机把 Claude 用量快照推送到服务(X-Push-Token 鉴权) - btc_api.py:OKX/Coinbase/Huobi 免费接口逐源兜底,时K 近 72 小时,涨绿跌红 - data.py 拆出 weather_fetch/mao_fetch/btc_fetch 与 usage_from_raw, 统一按 TZ_NAME 时区;宽版只用点阵字体整数倍字号 12/24/36/48 - probe_server.py:验证平台对同一 URL 是否周期重抓(结论:Image 控件 不重抓,HTML 控件重抓) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,18 +5,19 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(
|
||||
利用 800x480 的四倍面积重新排版为「AI 用量看板」,输出 **E Ink Spectra 6 原生六色**(黑/白/黄/红/绿/蓝)纯色图,
|
||||
并生成一张固定 800x480 的 HTML 页面供 SenseCraft HMI 的 HTML 控件云端截图拉取。
|
||||
|
||||
版式(k=1,12/24/48px 点阵字体,不放大;四段,三道横规):
|
||||
版式(k=1,点阵字体且**只用整数倍字号 12/24/36/48**,不放大;四段,三道横规):
|
||||
A 顶部(0~90) :大时钟+日期(左)|天气(右,贴 790)——与三色版同一套组件与尺寸。
|
||||
B 用量(96~206) :左半 **Claude Usage**(all 5h / all 7d / fable 7d)|竖规 x=400|右半 **ChatGPT Usage**(mock:all 5h / all 7d / codex 7d)。
|
||||
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31),标签用 bar["label"](statusline 同款写法),列宽按最长标签自适应。
|
||||
C 今日(218~304):**橘喵·今日实时** 三列全宽:指标名 F12 作左上角角标;「值 F36 粗 + 右侧叠放的环比/同比」整组居中。
|
||||
D 近7天(328~438):**橘喵·近7天** 数据表(真实数据:30 天趋势缓存的最后 7 天;不画折线)——
|
||||
列 = 指标名 + 7 个日期 + 合计,行 = 单量 / 流水 / 毛利;合计列伪粗体。
|
||||
C 今日(218~304):**橘喵·今日实时** 三列全宽:指标名 F12 列顶居中;「值 F36 粗 + 右侧叠放的环比/同比」整组居中。
|
||||
D 行情(~322~470):**BTC/USDT 时K(近 72 小时)** 蜡烛图(`_btc_chart`,数据来自 btc_api:OKX 主源、Coinbase/Huobi 备源,5 分钟缓存;
|
||||
周期/根数由 BTC_BAR/BTC_LIMIT 配置)——标题右侧最新价 + 24h 涨跌幅;**涨(收≥开)绿、跌红**,实心蜡烛 + 1px 影线;
|
||||
右侧最高/最低/最新价标签,最新价一道点规;底部首/中/末时间。
|
||||
可选(470~479):六色测试色条(config.E1002_COLOR_STRIP,默认关;真机校准色彩量化时打开)。
|
||||
|
||||
六色的语义分工(每种颜色只有一个含义,不为「有」而用):
|
||||
- 黑/白:结构与正文。红:危险(用量 ≥ warn 的进度条+边框+百分比、pace ≥ +tol)、涨、定位、太阳/闪电。
|
||||
- 绿:健康(用量 ≤ ok 的进度条+边框、pace ≤ −tol)。用量与 pace 各自三档:绿 / 黑 / 红(阈值 USAGE_OK / USAGE_WARN / PACE_TOL)。
|
||||
- 绿:健康(用量 ≤ ok 的进度条+边框、pace ≤ −tol)、K 线上涨;红另用于 K 线下跌。用量与 pace 各自三档:绿 / 黑 / 红(阈值 USAGE_OK / USAGE_WARN / PACE_TOL)。
|
||||
- 蓝:天气雨滴/雪花。黄:不用(用户明确不要黄色进度条;曾试过 60%~80% 黄「注意」档,已移除)。
|
||||
- 整图只允许六种纯色;渲染后可用 `img.getcolors()` 自查。
|
||||
|
||||
@@ -42,6 +43,11 @@ class E1002Renderer(DashboardRenderer):
|
||||
|
||||
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):基类给用量行用的 F13(标签、pace)与 F16(百分比)
|
||||
# 是非整数倍缩放(笔画不均),这里分别替换成 12px 与 24px;行几何靠 _mid 按墨迹中心对齐,不受字号变化影响。
|
||||
from PIL import ImageFont
|
||||
self.F13 = ImageFont.truetype(font_path, 12)
|
||||
self.F16 = ImageFont.truetype(font_path, 24)
|
||||
|
||||
# ---------- 配色钩子(三档:绿 健康 / 黑 正常 / 红 危险)----------
|
||||
_ok, _pace_tol = _cfg.USAGE_OK, _cfg.PACE_TOL # 由 _usage_block 按数据覆盖
|
||||
@@ -91,12 +97,12 @@ class E1002Renderer(DashboardRenderer):
|
||||
for b in bars:
|
||||
self._usage_row(x0, x1, y, b, warn, label_w=label_w)
|
||||
y += 31
|
||||
return y - 31 + 20
|
||||
return y - 31 + 18 # 末行进度条底部(cy=y+10,条高 15 → 底 y+17)
|
||||
|
||||
def _realtime(self, y0, mao, x0=8, x1=None):
|
||||
"""橘喵·今日实时三列(宽版):指标名 F12 贴在每列左上角作角标;
|
||||
「值 F36 粗 + 右侧上下叠放的 环比/同比(F12)」整组在列内水平居中、竖直略偏下(给角标让位)。
|
||||
(曾试过名在值上方居中——小字压大数字、上半显空;也试过名与值同行——名参与居中会把整组推偏。)返回区块底部 y。"""
|
||||
"""橘喵·今日实时三列(宽版):指标名 F12 在每列顶部、列内水平居中作小标题;
|
||||
「值 F36 粗 + 右侧上下叠放的 环比/同比(F12)」整组在列内水平居中、竖直略偏下(给小标题让位)。
|
||||
(曾试过名与值同行——名参与居中会把整组推偏;也试过名贴左上角——用户要求改回居中。)返回区块底部 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)
|
||||
@@ -107,7 +113,7 @@ class E1002Renderer(DashboardRenderer):
|
||||
for i, c in enumerate(cols):
|
||||
cx0 = x0 + cw * i
|
||||
cx = cx0 + cw / 2
|
||||
self.text(cx0 + 12, TAG_Y, c["k"], self.F12) # 左上角角标
|
||||
self.ctext(cx, TAG_Y, c["k"], self.F12) # 列顶小标题,水平居中
|
||||
vw = self._tw(c["v"], self.F36) + 1
|
||||
cmpw = max(self._cmp_w("环比", c["hb"]), self._cmp_w("同比", c["tb"]))
|
||||
gx = cx - (vw + GAP + cmpw) / 2 # 整组左端(不含角标)
|
||||
@@ -117,34 +123,68 @@ class E1002Renderer(DashboardRenderer):
|
||||
self._cmp_at(gx + vw + GAP, CY + 9 - (t + b) / 2, "同比", c["tb"])
|
||||
return y0 + 86
|
||||
|
||||
def _week_table(self, y0, week, x0=10, x1=None):
|
||||
"""橘喵·近7天数据表:表头 = 指标名列 + 7 个日期 + 合计;行 = 单量/流水/毛利。
|
||||
全部 F12、右对齐数字;合计列伪粗体;表头下一道点规。无数据居中 "--"。返回底部 y。"""
|
||||
@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
|
||||
self._label(y0, "橘喵·近7天", right="不含今日", x0=x0, x1=x1)
|
||||
dates, rows = (week or {}).get("dates") or [], (week or {}).get("rows") or []
|
||||
if not dates or not rows:
|
||||
self.ctext((x0 + x1) / 2, y0 + 40, "--", self.F24)
|
||||
return y0 + 80
|
||||
NAME_W, ROW = 44, 24
|
||||
ncol = len(dates) + 1 # 日期列 + 合计列
|
||||
cw = (x1 - x0 - NAME_W) / ncol
|
||||
col_r = lambda i: x0 + NAME_W + cw * (i + 1) - 4 # 第 i 列右端(含右内边距)
|
||||
hy = y0 + 24
|
||||
for i, d in enumerate(dates):
|
||||
self.rtext(col_r(i), hy, d, self.F12)
|
||||
self.rtext(col_r(len(dates)), hy, "合计", self.F12)
|
||||
self.hdash(x0, x1, hy + 17, dash=2, gap=3)
|
||||
y = hy + ROW
|
||||
# 合计列前一道竖点规,把「逐日」与「合计」分开
|
||||
self.vdash(x0 + NAME_W + cw * len(dates), hy + 2, hy + ROW * (len(rows) + 1) - 6, dash=2, gap=3)
|
||||
for name, vals, total in rows:
|
||||
self.text(x0, y, name, self.F12)
|
||||
for i, v in enumerate(vals):
|
||||
self.rtext(col_r(i), y, v, self.F12)
|
||||
self.rtext(col_r(len(dates)), y, total, self.F12, bold=1)
|
||||
y += ROW
|
||||
return y - ROW + 14
|
||||
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):
|
||||
@@ -179,8 +219,8 @@ class E1002Renderer(DashboardRenderer):
|
||||
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))
|
||||
DIV2_Y = max(y_l, y_r) + 6
|
||||
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. 橘喵·今日实时(全宽三列)----
|
||||
@@ -188,8 +228,9 @@ class E1002Renderer(DashboardRenderer):
|
||||
DIV3_Y = end_c + 8
|
||||
self.hdash(8, self.W - 8, DIV3_Y)
|
||||
|
||||
# ---- D. 橘喵·近7天 数据表 ----
|
||||
self._week_table(DIV3_Y + 6, mao.get("week"))
|
||||
# ---- 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:
|
||||
|
||||
Reference in New Issue
Block a user