#!/usr/bin/env python3 """ padsleep Web Monitor ==================== 通过 Unix Socket 从 padsleep 守护进程读取全量状态(含 GPIO 实时电平), 通过 WebSocket 推送到浏览器实时展示。不与守护进程竞争硬件资源。 用法: # 先启动 padsleep 守护进程 python padsleep.py # 再启动 Web 监控(新终端) python padsleep_web.py # 浏览器打开 http://树莓派IP:31400 依赖: pip install flask flask-socketio 配置项(环境变量): PORT=31400 Web 端口 """ import json import os import socket import threading import time from flask import Flask, render_template from flask_socketio import SocketIO, emit # ── 配置 ──────────────────────────────────────────── PORT = int(os.environ.get("PORT", "31400")) SOCKET_PATH = "/tmp/padsleep.sock" POLL_FAST_S = 0.2 # GPIO 电平轮询间隔(200ms,跟随守护进程 50ms 更新) POLL_SLOW_S = 1 # 其他状态轮询间隔 # ═══════════════════════════════════════════════════════════════════ # IpcClient — 与 padsleep 守护进程通信(Unix Socket) # ═══════════════════════════════════════════════════════════════════ class IpcClient: """Unix Socket JSON 行协议客户端(每次 send 新建连接)。""" def __init__(self, socket_path=SOCKET_PATH): self.socket_path = socket_path self.last_error = "" def _connect(self) -> socket.socket | None: try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(2) sock.connect(self.socket_path) return sock except (socket.error, OSError) as e: self.last_error = str(e) return None def send(self, cmd: str, **kwargs) -> dict: """发送命令,每次新建连接(守护进程每请求关闭连接)。""" sock = self._connect() if sock is None: return {"ok": False, "error": f"未连接: {self.last_error}"} req = {"cmd": cmd, **kwargs} try: sock.sendall((json.dumps(req) + "\n").encode("utf-8")) resp = sock.recv(65536) return json.loads(resp.decode("utf-8").strip()) except (socket.error, json.JSONDecodeError) as e: return {"ok": False, "error": str(e)} finally: try: sock.close() except Exception: pass # ═══════════════════════════════════════════════════════════════════ # Flask 应用 # ═══════════════════════════════════════════════════════════════════ app = Flask(__name__) app.config["SECRET_KEY"] = os.urandom(16).hex() socketio = SocketIO(app, cors_allowed_origins="*") ipc = IpcClient() # ── 全局缓存(供不同频率的轮询线程共享)── _data_lock = threading.Lock() _cached = { "gpio_level": -1, "gpio_time": "--", "status": None, } # ── 路由 ───────────────────────────────────────────── @app.route("/") def index(): return render_template("index.html") # ── 后台线程:高频轮询 GPIO 电平 ──────────────────── def poll_gpio(): """通过 IPC 高频读取 GPIO 电平,变化时 WebSocket 广播。""" last_val = -1 while True: r = ipc.send("status") if r.get("ok"): level = r["data"].get("gpio_level", -1) now = time.strftime("%H:%M:%S") + f".{int(time.time() * 1000) % 1000:03d}" if level != -1 and level != last_val: socketio.emit("gpio_update", { "value": level, "time": now, }) last_val = level # 更新缓存(补上 connected 字段,前端据此判断连接状态) r["data"]["connected"] = True with _data_lock: _cached["gpio_level"] = level _cached["gpio_time"] = now _cached["status"] = r["data"] socketio.sleep(POLL_FAST_S) # ── 后台线程:慢速轮询全量状态 ────────────────────── def poll_status(): """定期查询守护进程全量状态,通过 WebSocket 广播。""" while True: with _data_lock: status = _cached.get("status") if status: socketio.emit("status_update", status) socketio.sleep(POLL_SLOW_S) # ── WebSocket 事件 ────────────────────────────────── @socketio.on("connect") def on_connect(auth=None): print("[WS] 客户端已连接") # 推送初始状态 r = ipc.send("status") if r.get("ok"): data = r["data"] data["connected"] = True level = data.get("gpio_level", -1) now = time.strftime("%H:%M:%S") + f".{int(time.time() * 1000) % 1000:03d}" with _data_lock: _cached["gpio_level"] = level _cached["gpio_time"] = now _cached["status"] = data socketio.emit("gpio_update", {"value": level, "time": now}) socketio.emit("status_update", data) else: socketio.emit("status_update", { "connected": False, "error": r.get("error", "无法连接守护进程"), }) @socketio.on("disconnect") def on_disconnect(): print("[WS] 客户端已断开") # ── 入口 ───────────────────────────────────────────── def main(): print("╔══════════════════════════════════════════╗") print("║ padsleep Web Monitor ║") print("║ (通过 IPC 读取守护进程状态) ║") print("╠══════════════════════════════════════════╣") print(f"║ 监听地址: http://0.0.0.0:{PORT}") print(f"║ 数据源: {SOCKET_PATH}") print(f"║ GPIO 轮询: {POLL_FAST_S*1000:.0f}ms — 跟随守护进程 50ms 线程") print(f"║ 状态轮询: {POLL_SLOW_S}s") print(f"║ 请先启动 padsleep.py 守护进程") print(f"║ 按 Ctrl+C 停止 ║") print("╚══════════════════════════════════════════╝") # 启动后台线程 socketio.start_background_task(poll_gpio) socketio.start_background_task(poll_status) try: socketio.run(app, host="0.0.0.0", port=PORT, allow_unsafe_werkzeug=True) except KeyboardInterrupt: pass finally: print("👋 已退出") if __name__ == "__main__": main()