Files
padsleep_adb/padsleep_web.py
T
kushidouandClaude Fable 5 e6fbe550bb 重构屏幕控制引擎 + 新增 Web 监控面板
- 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>
2026-07-20 00:15:07 +08:00

202 lines
7.4 KiB
Python
Raw 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
"""
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()