feat: 添加 GPIO 电平检测触发亮屏/息屏
新增 GPIOEventDetector 后台线程,以 50ms 间隔轮询 GPIO 引脚。 检测到电平变化时唤醒屏幕并启动 60 秒倒计时,到期无新变化则息屏。 - 添加 GPIOEventDetector 类,支持 RPi.GPIO 硬读取和 Mock 回退 - 集成到 PadSleepApp 主循环,GPIO 触发优先级高于定时亮屏时段 - TUI 状态面板新增 GPIO 行,显示引脚号和倒计时 - 支持通过配置文件调整引脚号(gpio_pin)和倒计时长度 - install.sh 修复:补装 rpi-lgpio 依赖 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
da8ad4d17d
commit
9940db451f
+157
-5
@@ -42,6 +42,10 @@ DEFAULT_CONFIG = {
|
||||
"adb_wireless_host": "",
|
||||
"adb_wireless_port": 5555,
|
||||
"adb_wireless_auto_connect": False,
|
||||
# ── GPIO 电平触发(人来亮屏 / 人走息屏) ──
|
||||
"gpio_enabled": True,
|
||||
"gpio_pin": 17,
|
||||
"gpio_countdown_seconds": 60,
|
||||
}
|
||||
|
||||
# ── 日志 ────────────────────────────────────────────────────────
|
||||
@@ -257,6 +261,94 @@ class TimeChecker:
|
||||
return "; ".join(descs)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# GPIOEventDetector — GPIO 电平变化检测(人来亮屏 / 人走息屏)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# 由于购买的模块有问题,无法稳定输出电平,但是人来人走时会发生短暂电平切换,
|
||||
# 因此采用这个折中的法子:检测到 GPIO 电平变化就亮屏并重置 60 秒倒计时,
|
||||
# 倒计时结束且无新变化则息屏。
|
||||
|
||||
class GPIOEventDetector:
|
||||
"""在后台线程高频轮询 GPIO 引脚,检测电平变化并记录时间戳。
|
||||
|
||||
用法:
|
||||
detector = GPIOEventDetector(pin=17)
|
||||
detector.start()
|
||||
...
|
||||
elapsed = detector.seconds_since_last_event # 距上次变化秒数
|
||||
if elapsed < 60:
|
||||
... # 有人/有活动
|
||||
detector.stop()
|
||||
"""
|
||||
|
||||
POLL_INTERVAL = 0.05 # 轮询间隔 50ms
|
||||
|
||||
def __init__(self, pin: int, callback=None):
|
||||
self.pin = pin
|
||||
self.callback = callback # 可选:每条变化时触发
|
||||
self.last_event_time: float = 0.0 # 0 表示从未触发
|
||||
self.last_value: int | None = None
|
||||
self.available = False # 硬件是否可用
|
||||
self._running = False
|
||||
self._thread: threading.Thread | None = None
|
||||
self._reader = None
|
||||
|
||||
@property
|
||||
def seconds_since_last_event(self) -> float:
|
||||
"""距上次 GPIO 电平变化的秒数。从未触发时返回 inf。"""
|
||||
if self.last_event_time == 0:
|
||||
return float("inf")
|
||||
return time.time() - self.last_event_time
|
||||
|
||||
def start(self):
|
||||
"""启动后台轮询线程。"""
|
||||
# ── 初始化 GPIO ──
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(self.pin, GPIO.IN)
|
||||
self._gpio = GPIO
|
||||
self.available = True
|
||||
log.info("[GPIO] BCM %d — 电平检测已启动", self.pin)
|
||||
except (ImportError, RuntimeError, OSError) as e:
|
||||
log.warning("[GPIO] 无法初始化 GPIO%d: %s — 电平检测已禁用", self.pin, e)
|
||||
self.available = False
|
||||
return
|
||||
|
||||
self.last_value = self._gpio.input(self.pin)
|
||||
self.last_event_time = time.time() # 启动瞬间作为一次"事件",防止开机立刻息屏
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._poll, daemon=True, name="gpio-poll")
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止轮询并清理。"""
|
||||
self._running = False
|
||||
if self._thread:
|
||||
self._thread.join(timeout=1)
|
||||
if hasattr(self, "_gpio") and self._gpio:
|
||||
self._gpio.cleanup(self.pin)
|
||||
self.available = False
|
||||
log.info("[GPIO] BCM %d — 电平检测已停止", self.pin)
|
||||
|
||||
def _poll(self):
|
||||
"""后台轮询:50ms 间隔检测电平变化。"""
|
||||
while self._running:
|
||||
try:
|
||||
val = self._gpio.input(self.pin)
|
||||
if val != self.last_value:
|
||||
self.last_value = val
|
||||
self.last_event_time = time.time()
|
||||
edge = "上升" if val else "下降"
|
||||
log.debug("[GPIO] 电平变化: %s → %d", edge, val)
|
||||
if self.callback:
|
||||
self.callback(val)
|
||||
except Exception as e:
|
||||
log.error("[GPIO] 读取异常: %s", e)
|
||||
time.sleep(self.POLL_INTERVAL)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# AdbManager — ADB 操作封装(使用 adbutils 库)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
@@ -716,6 +808,7 @@ class PadSleepApp:
|
||||
self.audit = AuditLogger()
|
||||
self.config_mgr = ConfigManager()
|
||||
self.adb = AdbManager(self._get_config, self.audit)
|
||||
self.gpio: GPIOEventDetector | None = None
|
||||
self.ipc = IpcServer(self)
|
||||
self._lock = threading.Lock()
|
||||
self._start_time = time.time()
|
||||
@@ -728,6 +821,11 @@ class PadSleepApp:
|
||||
"device_serial": "",
|
||||
"uptime": 0,
|
||||
"start_time": self._start_time,
|
||||
# ── GPIO 状态 ──
|
||||
"gpio_available": False,
|
||||
"gpio_triggered": False,
|
||||
"gpio_countdown": 0,
|
||||
"gpio_pin": 17,
|
||||
}
|
||||
|
||||
# ── 辅助 ──
|
||||
@@ -859,6 +957,20 @@ class PadSleepApp:
|
||||
else:
|
||||
log.warning("无线设备连接失败: %s", msg)
|
||||
|
||||
# ── 启动 GPIO 电平检测 ──
|
||||
if config.get("gpio_enabled", True):
|
||||
gpio_pin = config.get("gpio_pin", 17)
|
||||
self.gpio = GPIOEventDetector(pin=gpio_pin)
|
||||
self.gpio.start()
|
||||
if self.gpio.available:
|
||||
log.info("GPIO 电平检测已启用 (BCM %d, 倒计时 %ds)",
|
||||
gpio_pin, config.get("gpio_countdown_seconds", 60))
|
||||
self.audit.log("GPIO检测", f"已启用 (BCM {gpio_pin})")
|
||||
self._update_status(gpio_available=self.gpio.available, gpio_pin=gpio_pin)
|
||||
else:
|
||||
log.info("GPIO 电平检测已禁用 (配置 gpio_enabled=false)")
|
||||
self._update_status(gpio_available=False)
|
||||
|
||||
# 启动 IPC
|
||||
self.ipc.start()
|
||||
|
||||
@@ -892,6 +1004,8 @@ class PadSleepApp:
|
||||
time.sleep(1)
|
||||
|
||||
# 清理
|
||||
if self.gpio:
|
||||
self.gpio.stop()
|
||||
self.ipc.cleanup()
|
||||
self.audit.log("守护进程", "已退出")
|
||||
log.info("padsleep_adb 守护程序已退出")
|
||||
@@ -914,6 +1028,43 @@ class PadSleepApp:
|
||||
periods = config.get("screen_on_periods", [])
|
||||
within_period = TimeChecker.is_within_any_period(periods)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# GPIO 触发检测(人来亮屏 / 人走息屏)
|
||||
#
|
||||
# 由于购买的模块有问题,无法稳定输出电平,但是人来人走时会发生
|
||||
# 短暂电平切换,因此采用这个折中的法子:检测到 GPIO 电平变化
|
||||
# 就亮屏并重置 gpio_countdown_seconds 秒倒计时,倒计时结束
|
||||
# 且无新变化则息屏(除非当前在亮屏时段)。
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
gpio_triggered = False
|
||||
gpio_countdown_remain = 0
|
||||
if self.gpio and self.gpio.available:
|
||||
gpio_pin = config.get("gpio_pin", 17)
|
||||
countdown = config.get("gpio_countdown_seconds", 60)
|
||||
elapsed = self.gpio.seconds_since_last_event
|
||||
gpio_triggered = elapsed < countdown
|
||||
gpio_countdown_remain = max(0, countdown - int(elapsed))
|
||||
|
||||
if gpio_triggered:
|
||||
# GPIO 刚刚触发过 → 确保屏幕亮起
|
||||
if not screen_on:
|
||||
log.info("GPIO 检测到电平变化 → 亮屏 (%ds 倒计时)", gpio_countdown_remain)
|
||||
self.audit.log("GPIO触屏", f"BCM {gpio_pin} 电平变化,{countdown}s 倒计时")
|
||||
self.adb.screen_on()
|
||||
screen_on = True
|
||||
else:
|
||||
# 屏幕已亮,仅做调试日志
|
||||
if gpio_countdown_remain >= countdown - 1:
|
||||
log.info("GPIO 电平变化 → 重置倒计时 %ds", countdown)
|
||||
else:
|
||||
# GPIO 倒计时已结束且屏幕不在亮屏时段 → 息屏
|
||||
if screen_on and not within_period and elapsed > countdown:
|
||||
log.info("GPIO 倒计时结束(%.0fs 无变化)→ 息屏", elapsed)
|
||||
self.audit.log("GPIO息屏", f"BCM {gpio_pin} 在 {countdown}s 内无电平变化")
|
||||
self.adb.screen_off()
|
||||
screen_on = False
|
||||
|
||||
# ── 更新状态(含 GPIO 信息)──
|
||||
device = self.adb.get_ready_device()
|
||||
self._update_status(
|
||||
adb_ready=True,
|
||||
@@ -922,6 +1073,8 @@ class PadSleepApp:
|
||||
screen_on=screen_on,
|
||||
wakefulness=wakefulness,
|
||||
within_period=within_period,
|
||||
gpio_triggered=gpio_triggered,
|
||||
gpio_countdown=gpio_countdown_remain,
|
||||
)
|
||||
|
||||
# ── 检测外部屏幕状态变化 ──
|
||||
@@ -933,7 +1086,7 @@ class PadSleepApp:
|
||||
log.info("检测到屏幕被熄灭 (外部操作)")
|
||||
self.audit.log("检测到屏幕被熄灭", "外部操作或用户手动")
|
||||
|
||||
# ── 决策逻辑 ──
|
||||
# ── 原有决策逻辑(仅在非 GPIO 触发时有效) ──
|
||||
|
||||
delay_sec = config.get("auto_sleep_delay_minutes", 5) * 60
|
||||
period_desc = TimeChecker.nearest_period_description(periods)
|
||||
@@ -945,8 +1098,8 @@ class PadSleepApp:
|
||||
self.adb.screen_on()
|
||||
return True
|
||||
|
||||
if not within_period and screen_on:
|
||||
# 非亮屏时段但屏幕亮着 → 超时后息屏
|
||||
if not within_period and screen_on and not gpio_triggered:
|
||||
# 非亮屏时段、非 GPIO 触发、屏幕亮着 → 超时后息屏
|
||||
on_duration = self.adb.get_screen_on_duration()
|
||||
if on_duration >= delay_sec:
|
||||
log.info("不在亮屏时段(已亮 %.0fs)→ 自动息屏", on_duration)
|
||||
@@ -959,9 +1112,8 @@ class PadSleepApp:
|
||||
log.info("不在亮屏时段,%.0f 秒后将息屏", remaining)
|
||||
|
||||
elif within_period and screen_on:
|
||||
# 在亮屏时段且屏幕已亮 — 一切正常,但记录一个调试信息
|
||||
# 在亮屏时段且屏幕已亮 — 一切正常
|
||||
if prev_screen_on is False:
|
||||
# 这是上一步我们刚刚点亮了屏幕
|
||||
pass
|
||||
|
||||
return screen_on
|
||||
|
||||
Reference in New Issue
Block a user