Files
eink-push/probe_server.py
YANG JIANKUAN 85b5113ba4 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>
2026-09-10 18:55:47 +08:00

143 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
验证工具 —— SenseCraft HMI 的 Image/HTML 控件对**同一 URL** 是否每个刷新周期都重新抓取。
原理:本地起一个极简 HTTP 服务,固定路径 /probe.png 每 --every 秒重绘一张 800x480 探针图
(大号生成时刻 HH:MM:SS + 递增序号 + 六色校准块),响应头 Cache-Control: no-store
每次有人请求就在终端打一行日志时间、路径、User-Agent、来源 IP含经隧道透传的 CF-Connecting-IP
判定:
- 终端日志每个设备刷新周期都出现一次 /probe.png 请求,且设备画面上的时刻/序号随之推进 → **每周期重抓,方案二成立**
此后只要不断覆盖同一 URL 指向的文件即可,无需再经 HTML 页。
- 日志只在「部署」那一刻出现一次、之后再无请求 → 平台只在部署时抓一次并缓存,方案二不成立。
- 也可同时把 /index.htmlHTML 控件方案)放到另一台画布对照。
用法:
python3 probe_server.py [--port 8787] [--every 60]
# 另开终端暴露成公网 HTTPS云端抓不到局域网/明文 HTTP。cloudflared 快速隧道无需账号:
brew install cloudflared
cloudflared tunnel --url http://localhost:8787
# 把打印出的 https://xxxx.trycloudflare.com/probe.png 填入 SenseCraft 画布的 Image 控件(铺满 800x480→ 部署。
"""
import argparse
import io
import threading
import time
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from PIL import Image, ImageDraw, ImageFont
import config
BLACK, WHITE, RED = (0, 0, 0), (255, 255, 255), (255, 0, 0)
SIX = [BLACK, WHITE, (255, 255, 0), RED, (0, 255, 0), (0, 0, 255)]
_state = {"png": b"", "seq": 0, "stamp": ""}
_lock = threading.Lock()
def render_probe(seq, now):
"""800x480 探针图:序号(左上)/ 大号时刻居中F96/ 日期(下)/ 底部六色块。全部纯色。"""
img = Image.new("RGB", (800, 480), WHITE)
d = ImageDraw.Draw(img)
d.fontmode = "1"
f24 = ImageFont.truetype(config.FONT_PATH, 24)
f96 = ImageFont.truetype(config.FONT_PATH, 96)
d.text((20, 16), f"probe #{seq:04d}", font=f24, fill=BLACK)
d.text((20, 48), "SenseCraft re-fetch test / 同一 URL 每周期是否重抓", font=f24, fill=BLACK)
t = now.strftime("%H:%M:%S")
w = d.textlength(t, font=f96)
for dx in range(3): # 伪粗体
d.text(((800 - w) / 2 + dx, 150), t, font=f96, fill=RED)
ds = now.strftime("%Y/%m/%d %A")
d.text(((800 - d.textlength(ds, font=f24)) / 2, 290), ds, font=f24, fill=BLACK)
d.text((20, 400), "画面上的时刻若随设备刷新周期推进 → 平台每周期重新抓取了同一 URL", font=f24, fill=BLACK)
n = len(SIX)
for i, c in enumerate(SIX):
d.rectangle([round(800 / n * i), 456, round(800 / n * (i + 1)) - 1, 479], fill=c)
buf = io.BytesIO()
img.save(buf, "PNG")
return buf.getvalue()
def regenerate(every):
while True:
now = datetime.now()
with _lock:
_state["seq"] += 1
_state["png"] = render_probe(_state["seq"], now)
_state["stamp"] = now.strftime("%H:%M:%S")
print(f"[{now:%H:%M:%S}] 重绘 probe.png seq={_state['seq']}", flush=True)
time.sleep(every)
_INDEX = """<!doctype html><html><head><meta charset="utf-8">
<meta http-equiv="Cache-Control" content="no-store"><title>probe</title>
<style>html,body{margin:0;width:800px;height:480px;overflow:hidden;background:#fff}
img{display:block;width:800px;height:480px;image-rendering:pixelated}</style></head>
<body><img src="/probe.png" width="800" height="480"></body></html>"""
class Handler(BaseHTTPRequestHandler):
server_version = "probe/1.0"
def log_message(self, fmt, *args): # 关闭默认日志,用自定义格式
pass
def _log(self, code):
ua = self.headers.get("User-Agent", "-")
ip = self.headers.get("CF-Connecting-IP") or self.headers.get("X-Forwarded-For") or self.client_address[0]
print(f"[{datetime.now():%H:%M:%S}] {code} GET {self.path} ip={ip} ua={ua}", flush=True)
def _send(self, code, ctype, body):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = self.path.split("?", 1)[0]
if path == "/probe.png":
with _lock:
body = _state["png"]
self._log(200)
return self._send(200, "image/png", body)
if path in ("/", "/index.html"):
self._log(200)
return self._send(200, "text/html; charset=utf-8", _INDEX.encode("utf-8"))
if path == "/favicon.ico":
return self._send(204, "image/x-icon", b"")
self._log(404)
self._send(404, "text/plain; charset=utf-8", b"not found")
do_HEAD = do_GET
def main():
ap = argparse.ArgumentParser(description="SenseCraft 同 URL 重抓验证服务")
ap.add_argument("--port", type=int, default=8787)
ap.add_argument("--every", type=int, default=60, help="探针图重绘间隔(秒)")
args = ap.parse_args()
threading.Thread(target=regenerate, args=(args.every,), daemon=True).start()
while not _state["png"]:
time.sleep(0.05)
srv = ThreadingHTTPServer(("0.0.0.0", args.port), Handler)
print(f"探针服务已启动http://localhost:{args.port}/probe.png HTML 对照页:/index.html")
print("下一步:另开终端执行 cloudflared tunnel --url http://localhost:%d ,把得到的 https 地址 + /probe.png 填入 Image 控件。" % args.port)
print("观察本终端:每个设备刷新周期是否出现一条 GET /probe.png。Ctrl+C 退出。", flush=True)
try:
srv.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()