- ScreenManager: 基于优先级(9/5/0)的统一屏幕控制引擎,替代 _tick 中的 散乱 if-else 逻辑 - 四个亮屏事件: scheduled(p9), sensor(p9), manual(p5), external(p0) - GPIO 回调: 电平变化立即触发亮屏(异步线程,不阻塞轮询) - _fast_check: 每秒轻量评估,提高息屏超时精度(±1s 替代 ±30s) - padsleep_web: 通过 IPC 读取守护进程状态的实时 Web 面板 (GPIO 电平/屏幕状态/ADB 连接,端口 31400) - padsleep-web.service: 关联 padsleep.service 的自启动服务 - install.sh: 修复 venv 初始化目录上下文,使用 requirements.txt - IpcClient: 修复 socket 复用导致交替失败的问题 - padsleep_tui: 适配 ScreenManager 状态显示 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
7.4 KiB
Python
202 lines
7.4 KiB
Python
#!/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()
|