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:
142
probe_server.py
Normal file
142
probe_server.py
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/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.html(HTML 控件方案)放到另一台画布对照。
|
||||
|
||||
用法:
|
||||
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()
|
||||
Reference in New Issue
Block a user