基本功能实现和验证
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"screen_on_periods": [
|
||||||
|
{ "start": "06:40", "end": "7:30" },
|
||||||
|
{ "start": "19:00", "end": "19:05" },
|
||||||
|
{ "start": "23:00", "end": "23:05" }
|
||||||
|
],
|
||||||
|
"check_interval_seconds": 30,
|
||||||
|
"auto_sleep_delay_minutes": 1,
|
||||||
|
"adb_device_serial": "",
|
||||||
|
"adb_path": "adb"
|
||||||
|
}
|
||||||
+589
@@ -0,0 +1,589 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""padsleep_adb 守护程序
|
||||||
|
|
||||||
|
基于树莓派,通过 ADB 控制安卓平板自动亮屏和息屏。
|
||||||
|
|
||||||
|
阶段一功能:
|
||||||
|
- 根据配置的亮屏时间段自动亮屏/息屏
|
||||||
|
- 屏幕亮起超过设定时长且不在亮屏时间段内 → 自动息屏
|
||||||
|
- 通过 Unix Socket 提供 IPC 接口,供 TUI 工具管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from socketserver import ThreadingUnixStreamServer, StreamRequestHandler
|
||||||
|
|
||||||
|
# ── 常量 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
|
CONFIG_PATH = SCRIPT_DIR / "config.json"
|
||||||
|
SOCKET_PATH = "/tmp/padsleep.sock"
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"screen_on_periods": [
|
||||||
|
{"start": "08:00", "end": "22:00"}
|
||||||
|
],
|
||||||
|
"check_interval_seconds": 30,
|
||||||
|
"auto_sleep_delay_minutes": 5,
|
||||||
|
"adb_device_serial": "",
|
||||||
|
"adb_path": "adb",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 日志 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
log = logging.getLogger("padsleep")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# ConfigManager — 配置加载/保存/监控
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
"""管理 JSON 配置文件的加载、保存和变更检测。"""
|
||||||
|
|
||||||
|
def __init__(self, path: Path = CONFIG_PATH):
|
||||||
|
self.path = path
|
||||||
|
self.config = dict(DEFAULT_CONFIG)
|
||||||
|
self._mtime: float = 0
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def load(self) -> bool:
|
||||||
|
"""从 JSON 文件加载配置,失败时使用默认值并不中断运行。"""
|
||||||
|
try:
|
||||||
|
with open(self.path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if "screen_on_periods" not in data:
|
||||||
|
raise ValueError("缺少 screen_on_periods")
|
||||||
|
for p in data["screen_on_periods"]:
|
||||||
|
if "start" not in p or "end" not in p:
|
||||||
|
raise ValueError("时间段缺少 start 或 end")
|
||||||
|
with self._lock:
|
||||||
|
self.config.update(data)
|
||||||
|
self._mtime = os.path.getmtime(self.path)
|
||||||
|
log.info("配置已加载: %s", self.path)
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
log.warning("配置文件不存在,写入默认配置: %s", self.path)
|
||||||
|
self.save()
|
||||||
|
return True
|
||||||
|
except (json.JSONDecodeError, ValueError, OSError) as e:
|
||||||
|
log.error("配置加载失败: %s,使用默认配置", e)
|
||||||
|
return True # 不崩溃,用默认配置继续运行
|
||||||
|
|
||||||
|
def save(self) -> bool:
|
||||||
|
"""将当前配置写入 JSON 文件。"""
|
||||||
|
with self._lock:
|
||||||
|
data = dict(self.config)
|
||||||
|
try:
|
||||||
|
with open(self.path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
self._mtime = os.path.getmtime(self.path)
|
||||||
|
log.info("配置已保存")
|
||||||
|
return True
|
||||||
|
except OSError as e:
|
||||||
|
log.error("配置保存失败: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self) -> dict:
|
||||||
|
"""线程安全地获取当前配置副本。"""
|
||||||
|
with self._lock:
|
||||||
|
return dict(self.config)
|
||||||
|
|
||||||
|
def update(self, new_config: dict) -> bool:
|
||||||
|
"""合并新配置并写入文件。"""
|
||||||
|
with self._lock:
|
||||||
|
self.config.update(new_config)
|
||||||
|
self.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_changed(self) -> bool:
|
||||||
|
"""检查配置文件是否被外部修改。"""
|
||||||
|
try:
|
||||||
|
return os.path.getmtime(self.path) != self._mtime
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reload_if_changed(self):
|
||||||
|
"""检测到外部修改则自动重载。"""
|
||||||
|
if self.has_changed():
|
||||||
|
log.info("配置文件已变更,重新加载")
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# TimeChecker — 时间判断工具
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class TimeChecker:
|
||||||
|
"""亮屏时间段判断,支持跨天(如 22:00 ~ 08:00)。"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_minutes(tstr: str) -> int:
|
||||||
|
h, m = tstr.strip().split(":")
|
||||||
|
return int(h) * 60 + int(m)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def now_minutes() -> int:
|
||||||
|
now = datetime.now()
|
||||||
|
return now.hour * 60 + now.minute
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_within_period(period: dict, now_m: int | None = None) -> bool:
|
||||||
|
"""判断 now_m 是否在单个时间段内(支持跨天)。"""
|
||||||
|
if now_m is None:
|
||||||
|
now_m = TimeChecker.now_minutes()
|
||||||
|
start = TimeChecker._to_minutes(period["start"])
|
||||||
|
end = TimeChecker._to_minutes(period["end"])
|
||||||
|
|
||||||
|
if end > start:
|
||||||
|
return start <= now_m < end
|
||||||
|
elif end == start:
|
||||||
|
return True # 全天
|
||||||
|
else:
|
||||||
|
return now_m >= start or now_m < end # 跨天
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_within_any_period(periods: list, now_m: int | None = None) -> bool:
|
||||||
|
"""判断当前时间是否在任意一个亮屏时间段内。"""
|
||||||
|
if not periods:
|
||||||
|
return False
|
||||||
|
if now_m is None:
|
||||||
|
now_m = TimeChecker.now_minutes()
|
||||||
|
return any(TimeChecker.is_within_period(p, now_m) for p in periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# AdbManager — ADB 操作封装
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class AdbManager:
|
||||||
|
"""封装所有 ADB CLI 操作,所有异常都被捕获,不会导致进程退出。"""
|
||||||
|
|
||||||
|
def __init__(self, config_getter):
|
||||||
|
self._config_getter = config_getter
|
||||||
|
self._ready = False
|
||||||
|
self._last_error = ""
|
||||||
|
self._screen_on_since: float | None = None
|
||||||
|
self._last_known_screen_on = False
|
||||||
|
self._last_adb_check = 0 # 限频用
|
||||||
|
|
||||||
|
# ── 底层命令执行 ──
|
||||||
|
|
||||||
|
def _exec(self, args: list, timeout: int = 10) -> tuple:
|
||||||
|
"""执行 ADB 命令,返回 (returncode, stdout, stderr)。永不抛出。"""
|
||||||
|
config = self._config_getter()
|
||||||
|
adb_path = config.get("adb_path", "adb")
|
||||||
|
serial = config.get("adb_device_serial", "")
|
||||||
|
|
||||||
|
cmd = [adb_path]
|
||||||
|
if serial:
|
||||||
|
cmd.extend(["-s", serial])
|
||||||
|
cmd.extend(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return r.returncode, r.stdout.strip(), r.stderr.strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
self._ready = False
|
||||||
|
self._last_error = f"ADB 未找到({adb_path}),请安装: sudo apt install adb"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self._ready = False
|
||||||
|
self._last_error = "ADB 命令超时"
|
||||||
|
except Exception as e:
|
||||||
|
self._ready = False
|
||||||
|
self._last_error = str(e)
|
||||||
|
return -1, "", self._last_error
|
||||||
|
|
||||||
|
# ── 设备管理 ──
|
||||||
|
|
||||||
|
def check_available(self) -> bool:
|
||||||
|
"""检查 ADB 是否可执行。"""
|
||||||
|
rc, _, _ = self._exec(["--version"], timeout=5)
|
||||||
|
return rc == 0
|
||||||
|
|
||||||
|
def list_devices(self) -> list[dict]:
|
||||||
|
"""返回 [{"serial": "...", "status": "..."}]。"""
|
||||||
|
rc, out, _ = self._exec(["devices"])
|
||||||
|
if rc != 0:
|
||||||
|
return []
|
||||||
|
devices = []
|
||||||
|
for line in out.split("\n"):
|
||||||
|
parts = line.strip().split("\t")
|
||||||
|
if len(parts) >= 2 and not line.startswith("List"):
|
||||||
|
devices.append({"serial": parts[0], "status": parts[1]})
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def get_ready_device(self) -> str | None:
|
||||||
|
"""返回第一个状态为 device 的串号,无则返回 None。"""
|
||||||
|
for d in self.list_devices():
|
||||||
|
if d["status"] == "device":
|
||||||
|
return d["serial"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_ready(self) -> bool:
|
||||||
|
"""检查是否有已授权的 ADB 设备可用。"""
|
||||||
|
now = time.time()
|
||||||
|
# 限频:每秒最多检测一次
|
||||||
|
if now - self._last_adb_check < 1 and self._ready:
|
||||||
|
return True
|
||||||
|
self._last_adb_check = now
|
||||||
|
|
||||||
|
config = self._config_getter()
|
||||||
|
target = config.get("adb_device_serial", "")
|
||||||
|
|
||||||
|
devices = self.list_devices()
|
||||||
|
for d in devices:
|
||||||
|
if d["status"] == "device" and (not target or d["serial"] == target):
|
||||||
|
self._ready = True
|
||||||
|
self._last_error = ""
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 诊断
|
||||||
|
unauthorized = [d for d in devices if d["status"] == "unauthorized"]
|
||||||
|
if unauthorized:
|
||||||
|
self._last_error = f"设备未授权: {[d['serial'] for d in unauthorized]},请在平板上确认授权"
|
||||||
|
elif not devices:
|
||||||
|
self._last_error = "未检测到 ADB 设备,请连接平板"
|
||||||
|
else:
|
||||||
|
self._last_error = "没有已就绪的 ADB 设备"
|
||||||
|
self._ready = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── 屏幕控制 ──
|
||||||
|
|
||||||
|
def get_screen_state(self) -> tuple[bool, str]:
|
||||||
|
"""返回 (is_on, wakefulness),失败时返回上次已知状态。"""
|
||||||
|
rc, out, _ = self._exec(["shell", "dumpsys", "power"])
|
||||||
|
if rc != 0:
|
||||||
|
return self._last_known_screen_on, "unknown"
|
||||||
|
|
||||||
|
for line in out.split("\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if "mWakefulness" in line:
|
||||||
|
state = line.split("=")[-1].strip()
|
||||||
|
is_on = state == "Awake"
|
||||||
|
self._last_known_screen_on = is_on
|
||||||
|
if is_on and self._screen_on_since is None:
|
||||||
|
self._screen_on_since = time.time()
|
||||||
|
elif not is_on:
|
||||||
|
self._screen_on_since = None
|
||||||
|
return is_on, state
|
||||||
|
|
||||||
|
# fallback
|
||||||
|
for line in out.split("\n"):
|
||||||
|
if "Display Power" in line or "mScreenOn" in line:
|
||||||
|
is_on = "ON" in line.upper() or "=true" in line.lower()
|
||||||
|
return is_on, "unknown"
|
||||||
|
return self._last_known_screen_on, "unknown"
|
||||||
|
|
||||||
|
def get_screen_on_duration(self) -> float:
|
||||||
|
"""返回屏幕已持续亮起的秒数,灭屏时返回 0。"""
|
||||||
|
if self._screen_on_since is not None:
|
||||||
|
return time.time() - self._screen_on_since
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def screen_off(self) -> bool:
|
||||||
|
"""发送息屏命令。"""
|
||||||
|
log.info("执行息屏操作")
|
||||||
|
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
|
||||||
|
if rc == 0:
|
||||||
|
self._screen_on_since = None
|
||||||
|
return True
|
||||||
|
log.error("息屏失败: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def screen_on(self) -> bool:
|
||||||
|
"""发送亮屏并解锁命令。"""
|
||||||
|
log.info("执行亮屏操作")
|
||||||
|
rc, _, err = self._exec(["shell", "input", "keyevent", "26"])
|
||||||
|
if rc == 0:
|
||||||
|
self._screen_on_since = time.time()
|
||||||
|
self._exec(["shell", "input", "keyevent", "82"]) # 解锁(MENU)
|
||||||
|
return True
|
||||||
|
log.error("亮屏失败: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# IPC 服务端 — Unix Socket JSON 行协议
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class IpcHandler(StreamRequestHandler):
|
||||||
|
"""每个客户端连接由独立线程处理。"""
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
try:
|
||||||
|
raw = self.rfile.readline()
|
||||||
|
if not raw:
|
||||||
|
return
|
||||||
|
req = json.loads(raw.decode("utf-8").strip())
|
||||||
|
resp = self.server.app.handle_ipc(req)
|
||||||
|
self.wfile.write((json.dumps(resp) + "\n").encode("utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.wfile.write('{"ok":false,"error":"JSON 解析失败"}\n'.encode("utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
self.wfile.write(json.dumps({"ok": False, "error": str(e)}).encode() + b"\n")
|
||||||
|
|
||||||
|
|
||||||
|
class IpcServer:
|
||||||
|
"""管理 Unix Socket 生命周期。"""
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.server: ThreadingUnixStreamServer | None = None
|
||||||
|
self._thread: threading.Thread | None = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
try:
|
||||||
|
os.unlink(SOCKET_PATH)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.server = ThreadingUnixStreamServer(SOCKET_PATH, IpcHandler)
|
||||||
|
self.server.app = self.app
|
||||||
|
self.server.socket.settimeout(1.0)
|
||||||
|
self._thread = threading.Thread(target=self._serve, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
log.info("IPC 服务已启动: %s", SOCKET_PATH)
|
||||||
|
|
||||||
|
def _serve(self):
|
||||||
|
while self.app.running:
|
||||||
|
try:
|
||||||
|
self.server.handle_request()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
try:
|
||||||
|
os.unlink(SOCKET_PATH)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.server:
|
||||||
|
self.server.shutdown()
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# PadSleepApp — 主应用
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class PadSleepApp:
|
||||||
|
"""守护程序核心:加载配置 → 主循环检查屏幕状态 → IPC 通信。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.running = True
|
||||||
|
self.config_mgr = ConfigManager()
|
||||||
|
self.adb = AdbManager(self._get_config)
|
||||||
|
self.ipc = IpcServer(self)
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._start_time = time.time()
|
||||||
|
self._status = {
|
||||||
|
"screen_on": False,
|
||||||
|
"wakefulness": "unknown",
|
||||||
|
"within_period": False,
|
||||||
|
"adb_ready": False,
|
||||||
|
"device": None,
|
||||||
|
"uptime": 0,
|
||||||
|
"start_time": self._start_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 辅助 ──
|
||||||
|
|
||||||
|
def _get_config(self) -> dict:
|
||||||
|
return self.config_mgr.get()
|
||||||
|
|
||||||
|
def _update_status(self, **kwargs):
|
||||||
|
with self._lock:
|
||||||
|
self._status.update(kwargs)
|
||||||
|
|
||||||
|
def get_status(self) -> dict:
|
||||||
|
with self._lock:
|
||||||
|
s = dict(self._status)
|
||||||
|
s["uptime"] = int(time.time() - self._start_time)
|
||||||
|
return s
|
||||||
|
|
||||||
|
# ── IPC 命令处理 ──
|
||||||
|
|
||||||
|
def handle_ipc(self, request: dict) -> dict:
|
||||||
|
cmd = request.get("cmd", "")
|
||||||
|
log.debug("IPC 请求: %s", cmd)
|
||||||
|
|
||||||
|
if cmd == "status":
|
||||||
|
status = self.get_status()
|
||||||
|
cfg = self.config_mgr.get()
|
||||||
|
status["periods_count"] = len(cfg.get("screen_on_periods", []))
|
||||||
|
status["check_interval"] = cfg.get("check_interval_seconds", 30)
|
||||||
|
return {"ok": True, "data": status}
|
||||||
|
|
||||||
|
if cmd == "get_config":
|
||||||
|
return {"ok": True, "data": self.config_mgr.get()}
|
||||||
|
|
||||||
|
if cmd == "update_config":
|
||||||
|
new = request.get("config", {})
|
||||||
|
if "screen_on_periods" in new:
|
||||||
|
for p in new["screen_on_periods"]:
|
||||||
|
if "start" not in p or "end" not in p:
|
||||||
|
return {"ok": False, "error": "时间段格式错误"}
|
||||||
|
self.config_mgr.update(new)
|
||||||
|
cfg = self.config_mgr.get()
|
||||||
|
# 重设串号以使 is_ready 立即用新串号检查
|
||||||
|
self.config_mgr.config["adb_device_serial"] = cfg.get("adb_device_serial", "")
|
||||||
|
return {"ok": True, "data": {"reloaded": True}}
|
||||||
|
|
||||||
|
if cmd == "reload":
|
||||||
|
self.config_mgr.load()
|
||||||
|
return {"ok": True, "data": {"reloaded": True}}
|
||||||
|
|
||||||
|
if cmd == "list_devices":
|
||||||
|
return {"ok": True, "data": {"devices": self.adb.list_devices()}}
|
||||||
|
|
||||||
|
if cmd == "select_device":
|
||||||
|
serial = request.get("serial", "")
|
||||||
|
self.config_mgr.update({"adb_device_serial": serial})
|
||||||
|
return {"ok": True, "data": {"selected": serial}}
|
||||||
|
|
||||||
|
if cmd == "shutdown":
|
||||||
|
log.info("收到 IPC 关闭命令")
|
||||||
|
self.running = False
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
if cmd == "screen_off":
|
||||||
|
ok = self.adb.screen_off()
|
||||||
|
return {"ok": ok}
|
||||||
|
|
||||||
|
if cmd == "screen_on":
|
||||||
|
ok = self.adb.screen_on()
|
||||||
|
return {"ok": ok}
|
||||||
|
|
||||||
|
return {"ok": False, "error": f"未知命令: {cmd}"}
|
||||||
|
|
||||||
|
# ── 主循环 ──
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
log.info("=" * 50)
|
||||||
|
log.info("padsleep_adb 守护程序启动")
|
||||||
|
log.info("=" * 50)
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
self.config_mgr.load()
|
||||||
|
|
||||||
|
# 检测 ADB
|
||||||
|
if not self.adb.check_available():
|
||||||
|
log.warning("ADB 未就绪,请安装: sudo apt install adb")
|
||||||
|
log.warning("安装后将自动检测到 ADB,无需重启本程序")
|
||||||
|
else:
|
||||||
|
log.info("ADB 可执行文件已就绪")
|
||||||
|
|
||||||
|
# 启动 IPC
|
||||||
|
self.ipc.start()
|
||||||
|
|
||||||
|
# 注册信号
|
||||||
|
def sighandler(sig, _frame):
|
||||||
|
log.info("收到信号 %s,正在退出...", sig)
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, sighandler)
|
||||||
|
signal.signal(signal.SIGINT, sighandler)
|
||||||
|
|
||||||
|
# 主循环
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
self._tick()
|
||||||
|
except Exception as e:
|
||||||
|
log.error("主循环异常: %s", e, exc_info=True)
|
||||||
|
|
||||||
|
# 检测配置文件外部变更
|
||||||
|
self.config_mgr.reload_if_changed()
|
||||||
|
|
||||||
|
# 分段睡眠以快速响应退出信号
|
||||||
|
interval = self.config_mgr.get().get("check_interval_seconds", 30)
|
||||||
|
for _ in range(max(1, interval)):
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
self.ipc.cleanup()
|
||||||
|
log.info("padsleep_adb 守护程序已退出")
|
||||||
|
|
||||||
|
def _tick(self):
|
||||||
|
"""单次检查逻辑。"""
|
||||||
|
config = self.config_mgr.get()
|
||||||
|
|
||||||
|
# ── 检查 ADB 就绪 ──
|
||||||
|
if not self.adb.is_ready():
|
||||||
|
self._update_status(adb_ready=False, device=None)
|
||||||
|
now = time.time()
|
||||||
|
if not hasattr(self, "_last_adb_warn") or now - self._last_adb_warn > 60:
|
||||||
|
log.warning("ADB 未就绪: %s", self.adb._last_error)
|
||||||
|
self._last_adb_warn = now
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── 获取屏幕状态 ──
|
||||||
|
screen_on, wakefulness = self.adb.get_screen_state()
|
||||||
|
periods = config.get("screen_on_periods", [])
|
||||||
|
within_period = TimeChecker.is_within_any_period(periods)
|
||||||
|
|
||||||
|
device = self.adb.get_ready_device()
|
||||||
|
self._update_status(
|
||||||
|
adb_ready=True,
|
||||||
|
device=device,
|
||||||
|
screen_on=screen_on,
|
||||||
|
wakefulness=wakefulness,
|
||||||
|
within_period=within_period,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 决策逻辑 ──
|
||||||
|
delay_sec = config.get("auto_sleep_delay_minutes", 5) * 60
|
||||||
|
|
||||||
|
if not within_period and screen_on:
|
||||||
|
# 非亮屏时段但屏幕亮着 → 超时后息屏
|
||||||
|
on_duration = self.adb.get_screen_on_duration()
|
||||||
|
if on_duration >= delay_sec:
|
||||||
|
log.info("不在亮屏时段(已亮 %.0fs)→ 自动息屏", on_duration)
|
||||||
|
self.adb.screen_off()
|
||||||
|
else:
|
||||||
|
remaining = delay_sec - on_duration
|
||||||
|
if remaining <= 10:
|
||||||
|
log.info("不在亮屏时段,%.0f 秒后将息屏", remaining)
|
||||||
|
|
||||||
|
elif within_period and not screen_on:
|
||||||
|
# 在亮屏时段但屏幕灭着 → 亮屏
|
||||||
|
log.info("在亮屏时段内 → 自动亮屏")
|
||||||
|
self.adb.screen_on()
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# 入口
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = PadSleepApp()
|
||||||
|
try:
|
||||||
|
app.run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("用户中断")
|
||||||
|
finally:
|
||||||
|
app.ipc.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=padsleep_adb — 基于 ADB 的安卓平板亮屏/息屏控制
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=kushidou
|
||||||
|
WorkingDirectory=/home/kushidou/apps/padsleep_adb
|
||||||
|
ExecStart=/usr/bin/python3 /home/kushidou/apps/padsleep_adb/padsleep.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+659
@@ -0,0 +1,659 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""padsleep_tui — padsleep_adb 的 TUI 配置工具
|
||||||
|
|
||||||
|
通过 Unix Socket 与 padsleep.py 守护进程通信,提供:
|
||||||
|
- 查看/新增/编辑/删除亮屏时间段
|
||||||
|
- 刷新 ADB 设备列表和选择设备
|
||||||
|
- 查看守护进程运行状态
|
||||||
|
- 重载配置、手动息屏/亮屏
|
||||||
|
|
||||||
|
依赖:Python 标准库(curses,需系统安装 python3-curses 或无额外操作)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import curses
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ── 常量 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SOCKET_PATH = "/tmp/padsleep.sock"
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
|
CONFIG_PATH = SCRIPT_DIR / "config.json"
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# IpcClient — 与守护进程通信
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class IpcClient:
|
||||||
|
"""Unix Socket JSON 行协议客户端。"""
|
||||||
|
|
||||||
|
def __init__(self, socket_path=SOCKET_PATH):
|
||||||
|
self.socket_path = socket_path
|
||||||
|
self.sock: socket.socket | None = None
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
try:
|
||||||
|
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.sock.settimeout(3)
|
||||||
|
self.sock.connect(self.socket_path)
|
||||||
|
self.connected = True
|
||||||
|
return True
|
||||||
|
except (socket.error, OSError):
|
||||||
|
self.connected = False
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def send(self, cmd: str, **kwargs) -> dict:
|
||||||
|
if not self.sock:
|
||||||
|
if not self.connect():
|
||||||
|
return {"ok": False, "error": "未连接守护进程"}
|
||||||
|
req = {"cmd": cmd, **kwargs}
|
||||||
|
try:
|
||||||
|
self.sock.sendall((json.dumps(req) + "\n").encode("utf-8"))
|
||||||
|
resp = self.sock.recv(65536)
|
||||||
|
return json.loads(resp.decode("utf-8").strip())
|
||||||
|
except (socket.error, json.JSONDecodeError) as e:
|
||||||
|
self.connected = False
|
||||||
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# TuiApp — curses 界面
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class TuiApp:
|
||||||
|
"""TUI 主应用,使用 curses 实现。"""
|
||||||
|
|
||||||
|
def __init__(self, stdscr):
|
||||||
|
self.stdscr = stdscr
|
||||||
|
self.client = IpcClient()
|
||||||
|
# 数据缓存
|
||||||
|
self.config: dict | None = None
|
||||||
|
self.status: dict | None = None
|
||||||
|
self.devices: list[dict] = []
|
||||||
|
self.connected = False
|
||||||
|
self.selected = 0 # 列表选中索引
|
||||||
|
self.message = ""
|
||||||
|
self.msg_time = 0.0
|
||||||
|
self._last_refresh = 0.0
|
||||||
|
|
||||||
|
# ── 通信 ──
|
||||||
|
|
||||||
|
def try_connect(self) -> bool:
|
||||||
|
self.connected = self.client.connect()
|
||||||
|
if self.connected:
|
||||||
|
self.refresh()
|
||||||
|
return self.connected
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""从守护进程拉取全量数据。"""
|
||||||
|
if not self.connected:
|
||||||
|
return
|
||||||
|
now = time.time()
|
||||||
|
if now - self._last_refresh < 0.5:
|
||||||
|
return
|
||||||
|
self._last_refresh = now
|
||||||
|
|
||||||
|
r = self.client.send("get_config")
|
||||||
|
if r.get("ok"):
|
||||||
|
self.config = r["data"]
|
||||||
|
|
||||||
|
r = self.client.send("status")
|
||||||
|
if r.get("ok"):
|
||||||
|
self.status = r["data"]
|
||||||
|
|
||||||
|
r = self.client.send("list_devices")
|
||||||
|
if r.get("ok"):
|
||||||
|
self.devices = r["data"].get("devices", [])
|
||||||
|
|
||||||
|
# ── 界面提示 ──
|
||||||
|
|
||||||
|
def msg(self, text: str):
|
||||||
|
self.message = text
|
||||||
|
self.msg_time = time.time()
|
||||||
|
|
||||||
|
# ── 主循环 ──
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.try_connect()
|
||||||
|
if not self.connected:
|
||||||
|
self.msg("未连接守护进程 — 请先运行 padsleep.py")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.height, self.width = self.stdscr.getmaxyx()
|
||||||
|
self.stdscr.clear()
|
||||||
|
self._draw()
|
||||||
|
self.stdscr.refresh()
|
||||||
|
|
||||||
|
curses.halfdelay(10) # 1 秒超时,用于自动刷新
|
||||||
|
key = self.stdscr.getch()
|
||||||
|
|
||||||
|
# ── 按键处理 ──
|
||||||
|
if key in (ord("q"), ord("Q")):
|
||||||
|
break
|
||||||
|
|
||||||
|
elif key == ord("t") or key == ord("T"):
|
||||||
|
if self.try_connect():
|
||||||
|
self.msg("已连接守护进程")
|
||||||
|
else:
|
||||||
|
self.msg("连接失败")
|
||||||
|
|
||||||
|
elif key == ord("r") or key == ord("R"):
|
||||||
|
self.refresh()
|
||||||
|
self.msg("已刷新")
|
||||||
|
|
||||||
|
elif key == ord("l") or key == ord("L"):
|
||||||
|
r = self.client.send("reload")
|
||||||
|
if r.get("ok"):
|
||||||
|
self.msg("配置已重载")
|
||||||
|
else:
|
||||||
|
self.msg(f"重载失败: {r.get('error', '')}")
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
elif key == ord("f") or key == ord("F"):
|
||||||
|
r = self.client.send("screen_off")
|
||||||
|
self.msg("已发送息屏指令" if r.get("ok") else f"息屏失败: {r.get('error','')}")
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
elif key == ord("n") or key == ord("N"):
|
||||||
|
r = self.client.send("screen_on")
|
||||||
|
self.msg("已发送亮屏指令" if r.get("ok") else f"亮屏失败: {r.get('error','')}")
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
elif key in (curses.KEY_UP, ord("k"), ord("K")):
|
||||||
|
if self.selected > 0:
|
||||||
|
self.selected -= 1
|
||||||
|
|
||||||
|
elif key in (curses.KEY_DOWN, ord("j"), ord("J")):
|
||||||
|
max_idx = len((self.config or {}).get("screen_on_periods", [])) - 1
|
||||||
|
if self.selected < max_idx:
|
||||||
|
self.selected += 1
|
||||||
|
|
||||||
|
elif key == ord("a") or key == ord("A"):
|
||||||
|
if self.config is not None:
|
||||||
|
self._dlg_add_period()
|
||||||
|
|
||||||
|
elif key == ord("e") or key == ord("E"):
|
||||||
|
if self.config is not None:
|
||||||
|
self._dlg_edit_period(self.selected)
|
||||||
|
|
||||||
|
elif key == ord("d") or key == ord("D"):
|
||||||
|
if self.config is not None:
|
||||||
|
self._dlg_delete_period(self.selected)
|
||||||
|
|
||||||
|
elif key == ord("s") or key == ord("S"):
|
||||||
|
self._dlg_select_device()
|
||||||
|
|
||||||
|
elif key == ord("h") or key == ord("H"):
|
||||||
|
self._show_help()
|
||||||
|
|
||||||
|
# 定时自动刷新
|
||||||
|
if key == -1 and self.connected:
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
# 绘制
|
||||||
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _draw(self):
|
||||||
|
h, w = self.height, self.width
|
||||||
|
if h < 12 or w < 50:
|
||||||
|
self._center_text(0, "窗口太小,请放大终端 (至少 50×12)")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._draw_titlebar()
|
||||||
|
y = 2
|
||||||
|
y = self._draw_periods_panel(y)
|
||||||
|
y += 1
|
||||||
|
y = self._draw_adb_panel(y)
|
||||||
|
y += 1
|
||||||
|
y = self._draw_status_panel(y)
|
||||||
|
self._draw_bottombar()
|
||||||
|
|
||||||
|
def _draw_titlebar(self):
|
||||||
|
h, w = self.height, self.width
|
||||||
|
title = " padsleep 配置工具 "
|
||||||
|
st = "已连接" if self.connected else "未连接"
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(0, 0, "─" * w)
|
||||||
|
self.stdscr.addstr(0, 2, title, curses.A_BOLD)
|
||||||
|
sx = w - len(st) - 4
|
||||||
|
if sx > len(title) + 4:
|
||||||
|
pair = 3 if self.connected else 2
|
||||||
|
self.stdscr.addstr(0, sx, f"[{st}]", curses.color_pair(pair) | curses.A_BOLD)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _draw_separator(self, y: int, label: str = ""):
|
||||||
|
if y >= self.height:
|
||||||
|
return y + 1
|
||||||
|
try:
|
||||||
|
if label:
|
||||||
|
self.stdscr.addstr(y, 0, "─" * self.width, curses.A_DIM)
|
||||||
|
self.stdscr.addstr(y, 2, label, curses.A_BOLD)
|
||||||
|
else:
|
||||||
|
self.stdscr.addstr(y, 0, "─" * self.width, curses.A_DIM)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
return y + 1
|
||||||
|
|
||||||
|
def _draw_periods_panel(self, y: int) -> int:
|
||||||
|
h = self.height
|
||||||
|
y = self._draw_separator(y, "[1] 亮屏时间段")
|
||||||
|
|
||||||
|
periods = (self.config or {}).get("screen_on_periods", [])
|
||||||
|
if not periods:
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "(暂无时间段 — 按 A 新增)")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
else:
|
||||||
|
# 表头
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, " # │ 开始 │ 结束 │", curses.A_UNDERLINE)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
for i, p in enumerate(periods):
|
||||||
|
if y >= h:
|
||||||
|
break
|
||||||
|
sel = "▸" if i == self.selected else " "
|
||||||
|
line = f"{sel}{i+1:>2} │ {p['start']} │ {p['end']} "
|
||||||
|
try:
|
||||||
|
if i == self.selected:
|
||||||
|
self.stdscr.addstr(y, 4, line, curses.A_REVERSE)
|
||||||
|
else:
|
||||||
|
self.stdscr.addstr(y, 4, line)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "[A]新增 [E]编辑 [D]删除 ↑↓选择", curses.A_DIM)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
return y + 1
|
||||||
|
|
||||||
|
def _draw_adb_panel(self, y: int) -> int:
|
||||||
|
h = self.height
|
||||||
|
y = self._draw_separator(y, "[2] ADB 配置")
|
||||||
|
|
||||||
|
if self.status:
|
||||||
|
dev = self.status.get("device") or "未选择"
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, f"当前设备: {dev}")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
|
||||||
|
if self.devices:
|
||||||
|
for d in self.devices:
|
||||||
|
if y >= h:
|
||||||
|
break
|
||||||
|
icon = "✓" if d["status"] == "device" else "✗"
|
||||||
|
pair = 3 if d["status"] == "device" else 2
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, f"{icon} {d['serial']}", curses.color_pair(pair))
|
||||||
|
self.stdscr.addstr(f" ({d['status']})")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
else:
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "(无设备 — 请连接平板)")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "[S]选择设备 [R]刷新", curses.A_DIM)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
return y + 1
|
||||||
|
|
||||||
|
def _draw_status_panel(self, y: int) -> int:
|
||||||
|
h = self.height
|
||||||
|
y = self._draw_separator(y, "[3] 守护状态")
|
||||||
|
|
||||||
|
if self.status:
|
||||||
|
s = self.status
|
||||||
|
sc = "亮" if s.get("screen_on") else "灭"
|
||||||
|
sc_color = 3 if s.get("screen_on") else 2
|
||||||
|
wp = "是" if s.get("within_period") else "否"
|
||||||
|
wp_color = 3 if s.get("within_period") else 2
|
||||||
|
upt = s.get("uptime", 0)
|
||||||
|
hh, mm = upt // 3600, (upt % 3600) // 60
|
||||||
|
ar = "就绪" if s.get("adb_ready") else "未就绪"
|
||||||
|
ar_color = 3 if s.get("adb_ready") else 2
|
||||||
|
wf = s.get("wakefulness", "?")
|
||||||
|
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "屏幕: ")
|
||||||
|
self.stdscr.addstr(f"{sc}", curses.color_pair(sc_color) | curses.A_BOLD)
|
||||||
|
self.stdscr.addstr(f" ({wf})")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "时段内: ")
|
||||||
|
self.stdscr.addstr(f"{wp}", curses.color_pair(wp_color) | curses.A_BOLD)
|
||||||
|
self.stdscr.addstr(f" │ 运行: {hh}h{mm:02d}m")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "ADB: ")
|
||||||
|
self.stdscr.addstr(f"{ar}", curses.color_pair(ar_color) | curses.A_BOLD)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
else:
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "(未连接守护进程 — 按 T 重连)")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
y += 1
|
||||||
|
|
||||||
|
if y < h:
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, 4, "[L]重载 [F]息屏 [N]亮屏 [T]重连 [H]帮助 [Q]退出",
|
||||||
|
curses.A_DIM)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
return y + 1
|
||||||
|
|
||||||
|
def _draw_bottombar(self):
|
||||||
|
h, w = self.height, self.width
|
||||||
|
bar_y = h - 2
|
||||||
|
if bar_y <= 0:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(bar_y, 0, "─" * w, curses.A_DIM)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
bar_y += 1
|
||||||
|
if bar_y >= h:
|
||||||
|
return
|
||||||
|
msg = self.message
|
||||||
|
if msg and time.time() - self.msg_time < 5:
|
||||||
|
pass # show message below separator
|
||||||
|
else:
|
||||||
|
if self.connected:
|
||||||
|
msg = "已连接 | ↑↓/jk 选择 A新增 E编辑 D删除 S选设备 H帮助"
|
||||||
|
else:
|
||||||
|
msg = "未检测到守护进程 | T 尝试连接"
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(bar_y, 2, msg[: w - 4])
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── 辅助 ──
|
||||||
|
|
||||||
|
def _center_text(self, y: int, text: str):
|
||||||
|
try:
|
||||||
|
self.stdscr.addstr(y, max(0, (self.width - len(text)) // 2), text)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
# 弹窗
|
||||||
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _dlg_input(self, prompt: str, default: str = "") -> str | None:
|
||||||
|
"""输入弹窗,返回字符串或 None(取消)。"""
|
||||||
|
h, w = self.height, self.width
|
||||||
|
bh, bw = 5, 52
|
||||||
|
by = max(0, (h - bh) // 2)
|
||||||
|
bx = max(0, (w - bw) // 2)
|
||||||
|
|
||||||
|
win = curses.newwin(bh, bw, by, bx)
|
||||||
|
win.box()
|
||||||
|
disp = prompt[: bw - 4]
|
||||||
|
win.addstr(1, 2, disp)
|
||||||
|
win.addstr(3, 2, "─" * (bw - 4), curses.A_DIM)
|
||||||
|
|
||||||
|
# 在框内显示默认值
|
||||||
|
if default:
|
||||||
|
win.addstr(3, 2, default, curses.A_DIM)
|
||||||
|
|
||||||
|
win.refresh()
|
||||||
|
curses.echo()
|
||||||
|
curses.curs_set(1)
|
||||||
|
|
||||||
|
raw = b""
|
||||||
|
try:
|
||||||
|
raw = win.getstr(3, 2, 10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
curses.noecho()
|
||||||
|
curses.curs_set(0)
|
||||||
|
|
||||||
|
result = raw.decode("utf-8") if isinstance(raw, bytes) else str(raw) if raw else ""
|
||||||
|
return result.strip() or None
|
||||||
|
|
||||||
|
def _dlg_confirm(self, message: str) -> bool:
|
||||||
|
h, w = self.height, self.width
|
||||||
|
bh, bw = 5, 60
|
||||||
|
by = max(0, (h - bh) // 2)
|
||||||
|
bx = max(0, (w - bw) // 2)
|
||||||
|
|
||||||
|
win = curses.newwin(bh, bw, by, bx)
|
||||||
|
win.box()
|
||||||
|
win.addstr(1, 2, message[: bw - 4])
|
||||||
|
win.addstr(3, 2, "[Y]确认 [N]取消")
|
||||||
|
win.refresh()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
k = win.getch()
|
||||||
|
if k in (ord("y"), ord("Y")):
|
||||||
|
return True
|
||||||
|
if k in (ord("n"), ord("N"), 27):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _dlg_menu(self, title: str, options: list[str]) -> int | None:
|
||||||
|
"""菜单选择弹窗,返回索引或 None。"""
|
||||||
|
h, w = self.height, self.width
|
||||||
|
bh = min(len(options) + 4, h - 4)
|
||||||
|
bw = 54
|
||||||
|
by = max(0, (h - bh) // 2)
|
||||||
|
bx = max(0, (w - bw) // 2)
|
||||||
|
|
||||||
|
win = curses.newwin(bh, bw, by, bx)
|
||||||
|
cur = 0
|
||||||
|
while True:
|
||||||
|
win.box()
|
||||||
|
win.addstr(0, 2, title[: bw - 4])
|
||||||
|
for i, opt in enumerate(options):
|
||||||
|
if i >= bh - 3:
|
||||||
|
break
|
||||||
|
d = opt[: bw - 6]
|
||||||
|
if i == cur:
|
||||||
|
win.addstr(i + 2, 2, f"▸ {d}", curses.A_REVERSE)
|
||||||
|
else:
|
||||||
|
win.addstr(i + 2, 2, f" {d}")
|
||||||
|
win.refresh()
|
||||||
|
|
||||||
|
k = win.getch()
|
||||||
|
if k in (curses.KEY_UP, ord("k")):
|
||||||
|
cur = max(0, cur - 1)
|
||||||
|
elif k in (curses.KEY_DOWN, ord("j")):
|
||||||
|
cur = min(len(options) - 1, cur + 1)
|
||||||
|
elif k in (ord("\n"), ord(" ")):
|
||||||
|
return cur
|
||||||
|
elif k == 27:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
# 对话框业务逻辑
|
||||||
|
# ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _get_periods(self) -> list:
|
||||||
|
return (self.config or {}).get("screen_on_periods", [])
|
||||||
|
|
||||||
|
def _update_periods(self, periods: list):
|
||||||
|
r = self.client.send("update_config", config={"screen_on_periods": periods})
|
||||||
|
if r.get("ok"):
|
||||||
|
self.msg(f"已更新亮屏时间段")
|
||||||
|
self.refresh()
|
||||||
|
else:
|
||||||
|
self.msg(f"更新失败: {r.get('error', '')}")
|
||||||
|
|
||||||
|
def _dlg_add_period(self):
|
||||||
|
start = self._dlg_input("开始时间 (HH:MM):", "08:00")
|
||||||
|
if not start:
|
||||||
|
return
|
||||||
|
end = self._dlg_input("结束时间 (HH:MM):", "22:00")
|
||||||
|
if not end:
|
||||||
|
return
|
||||||
|
if not self._validate_time(start) or not self._validate_time(end):
|
||||||
|
self.msg("时间格式错误,请使用 HH:MM (00-23:00-59)")
|
||||||
|
return
|
||||||
|
periods = self._get_periods()
|
||||||
|
periods.append({"start": start, "end": end})
|
||||||
|
self._update_periods(periods)
|
||||||
|
|
||||||
|
def _dlg_edit_period(self, idx: int):
|
||||||
|
periods = self._get_periods()
|
||||||
|
if idx < 0 or idx >= len(periods):
|
||||||
|
return
|
||||||
|
p = periods[idx]
|
||||||
|
new_start = self._dlg_input("开始时间 (HH:MM):", p["start"])
|
||||||
|
if not new_start:
|
||||||
|
return
|
||||||
|
new_end = self._dlg_input("结束时间 (HH:MM):", p["end"])
|
||||||
|
if not new_end:
|
||||||
|
return
|
||||||
|
if not self._validate_time(new_start) or not self._validate_time(new_end):
|
||||||
|
self.msg("时间格式错误")
|
||||||
|
return
|
||||||
|
periods[idx] = {"start": new_start, "end": new_end}
|
||||||
|
self._update_periods(periods)
|
||||||
|
|
||||||
|
def _dlg_delete_period(self, idx: int):
|
||||||
|
periods = self._get_periods()
|
||||||
|
if idx < 0 or idx >= len(periods):
|
||||||
|
return
|
||||||
|
p = periods[idx]
|
||||||
|
if not self._dlg_confirm(f"删除时间段 {p['start']}—{p['end']}?"):
|
||||||
|
return
|
||||||
|
periods.pop(idx)
|
||||||
|
if self.selected >= len(periods) and self.selected > 0:
|
||||||
|
self.selected -= 1
|
||||||
|
self._update_periods(periods)
|
||||||
|
|
||||||
|
def _dlg_select_device(self):
|
||||||
|
if not self.devices:
|
||||||
|
self.msg("未检测到设备,请先连接平板")
|
||||||
|
return
|
||||||
|
opts = [f"{d['serial']} ({d['status']})" for d in self.devices]
|
||||||
|
opts.append("(自动选择首个设备)")
|
||||||
|
idx = self._dlg_menu("选择 ADB 设备", opts)
|
||||||
|
if idx is None:
|
||||||
|
return
|
||||||
|
serial = self.devices[idx]["serial"] if idx < len(self.devices) else ""
|
||||||
|
r = self.client.send("select_device", serial=serial)
|
||||||
|
if r.get("ok"):
|
||||||
|
self.msg(f"设备已选择: {serial or '自动'}")
|
||||||
|
self.refresh()
|
||||||
|
else:
|
||||||
|
self.msg(f"选择失败: {r.get('error', '')}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_time(tstr: str) -> bool:
|
||||||
|
try:
|
||||||
|
h, m = tstr.strip().split(":")
|
||||||
|
h, m = int(h), int(m)
|
||||||
|
return 0 <= h <= 23 and 0 <= m <= 59
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── 帮助 ──
|
||||||
|
|
||||||
|
def _show_help(self):
|
||||||
|
h, w = self.height, self.width
|
||||||
|
bh, bw = 20, 60
|
||||||
|
by = max(1, (h - bh) // 2)
|
||||||
|
bx = max(1, (w - bw) // 2)
|
||||||
|
|
||||||
|
win = curses.newwin(bh, bw, by, bx)
|
||||||
|
win.box()
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
" padsleep 配置工具 — 快捷键",
|
||||||
|
"",
|
||||||
|
" ↑/k / ↓/j 选择亮屏时间段",
|
||||||
|
" A 新增亮屏时间段",
|
||||||
|
" E 编辑选中时间段",
|
||||||
|
" D 删除选中时间段",
|
||||||
|
" S 选择 ADB 设备",
|
||||||
|
" R 刷新状态",
|
||||||
|
" L 重载守护进程配置",
|
||||||
|
" F 强制息屏",
|
||||||
|
" N 强制亮屏",
|
||||||
|
" T 重新连接守护进程",
|
||||||
|
" H 显示此帮助",
|
||||||
|
" Q 退出",
|
||||||
|
"",
|
||||||
|
" 提示: 先运行 padsleep.py 启动守护进程",
|
||||||
|
]
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if i + 2 < bh - 2:
|
||||||
|
try:
|
||||||
|
win.addstr(i + 1, 2, line[: bw - 4])
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
win.addstr(bh - 2, 2, "按任意键返回")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
win.refresh()
|
||||||
|
win.getch()
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# 入口
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def main(stdscr):
|
||||||
|
curses.start_color()
|
||||||
|
curses.use_default_colors()
|
||||||
|
curses.init_pair(1, curses.COLOR_WHITE, -1) # default
|
||||||
|
curses.init_pair(2, curses.COLOR_RED, -1) # 未就绪
|
||||||
|
curses.init_pair(3, curses.COLOR_GREEN, -1) # 已就绪
|
||||||
|
curses.init_pair(4, curses.COLOR_YELLOW, -1) # 警告
|
||||||
|
curses.curs_set(0)
|
||||||
|
|
||||||
|
app = TuiApp(stdscr)
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
curses.wrapper(main)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n已退出")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# padsleep_adb 依赖
|
||||||
|
# 本程序仅使用 Python 标准库,无需额外安装第三方包。
|
||||||
|
# 系统依赖:adb (Android Debug Bridge)
|
||||||
|
# 安装: sudo apt-get install adb
|
||||||
|
#
|
||||||
|
# TUI 使用 curses(Python 标准库),
|
||||||
|
# 某些系统需要: sudo apt-get install python3-curses
|
||||||
Reference in New Issue
Block a user