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
@@ -6,6 +6,7 @@
|
||||
|
||||
- **定时控制**:根据配置的时间段自动亮屏/息屏
|
||||
- **超时保护**:屏幕亮起超过设定时长且不在亮屏时间段内 → 自动息屏
|
||||
- **GPIO 触发**:(新增)检测 GPIO 电平变化 → 亮屏 + 60 秒倒计时 → 倒计时结束息屏
|
||||
- **无线 ADB**:支持通过 TCP/IP 连接安卓设备(无线调试)
|
||||
- **TUI 管理**:提供终端界面工具,方便查看状态和修改配置
|
||||
- **IPC 通信**:通过 Unix Socket 与 TUI 工具通信
|
||||
@@ -40,26 +41,43 @@ pip install -r requirements.txt
|
||||
|
||||
依赖列表:
|
||||
- `adbutils>=2.12.0` — Python ADB 库
|
||||
- `rpi-lgpio>=0.6` — 树莓派 GPIO 控制(仅在树莓派上需要)
|
||||
|
||||
### 3. 快速安装
|
||||
|
||||
```bash
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
安装脚本会将程序部署到 `/opt/apps/padsleep`,创建命令行入口和 systemd 服务。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 启动守护程序
|
||||
|
||||
```bash
|
||||
# 直接启动
|
||||
python padsleep.py
|
||||
|
||||
# 或使用安装后的命令行入口
|
||||
padsleep
|
||||
```
|
||||
|
||||
守护程序会自动:
|
||||
1. 加载配置文件 `config.json`
|
||||
2. 检测 ADB 设备
|
||||
3. 启动 IPC 服务(`/tmp/padsleep.sock`)
|
||||
4. 进入主循环,定期检查屏幕状态
|
||||
3. 启动 GPIO 电平检测(默认 BCM 17)
|
||||
4. 启动 IPC 服务(`/tmp/padsleep.sock`)
|
||||
5. 进入主循环,定期检查屏幕状态
|
||||
|
||||
### 启动 TUI 管理工具
|
||||
|
||||
```bash
|
||||
# 新终端窗口
|
||||
python padsleep_tui.py
|
||||
|
||||
# 或使用安装后的命令行入口
|
||||
padsleep-config
|
||||
```
|
||||
|
||||
TUI 快捷键:
|
||||
@@ -94,7 +112,10 @@ TUI 快捷键:
|
||||
"adb_device_serial": "",
|
||||
"adb_wireless_host": "",
|
||||
"adb_wireless_port": 5555,
|
||||
"adb_wireless_auto_connect": false
|
||||
"adb_wireless_auto_connect": false,
|
||||
"gpio_enabled": true,
|
||||
"gpio_pin": 17,
|
||||
"gpio_countdown_seconds": 60
|
||||
}
|
||||
```
|
||||
|
||||
@@ -109,6 +130,48 @@ TUI 快捷键:
|
||||
| `adb_wireless_host` | string | `""` | 无线设备 IP 地址,空表示不使用无线连接 |
|
||||
| `adb_wireless_port` | int | `5555` | 无线设备端口 |
|
||||
| `adb_wireless_auto_connect` | bool | `false` | 启动时是否自动连接无线设备 |
|
||||
| `gpio_enabled` | bool | `true` | 是否启用 GPIO 电平检测 |
|
||||
| `gpio_pin` | int | `17` | GPIO 引脚号(BCM 编号),如 GPIO17 = 物理 pin 11 |
|
||||
| `gpio_countdown_seconds` | int | `60` | GPIO 触发后倒计时秒数,到期无新变化则息屏 |
|
||||
|
||||
## GPIO 电平检测
|
||||
|
||||
> 由于购买的模块有问题,无法稳定输出电平,但是人来人走时会发生短暂电平切换,
|
||||
> 因此采用这个折中的法子:检测到 GPIO 电平变化就亮屏并重置倒计时,
|
||||
> 倒计时结束且无新变化则息屏。
|
||||
|
||||
### 工作原理
|
||||
|
||||
1. **后台线程** 以 50ms 间隔高频轮询 GPIO 引脚
|
||||
2. **检测到任意电平变化**(上升或下降沿)→ 立即亮屏,重置倒计时
|
||||
3. **倒计时期间再次触发** → 倒计时重置,保持亮屏
|
||||
4. **倒计时到期无新变化** → 息屏(除非当前在定时亮屏时段内)
|
||||
|
||||
### 决策优先级
|
||||
|
||||
```
|
||||
GPIO 触发(60s 倒计时) > 定时亮屏时段 > 超时息屏
|
||||
```
|
||||
|
||||
### 接线参考
|
||||
|
||||
GPIO 引脚号(BCM 编号)对照:
|
||||
|
||||
| BCM 编号 | 物理引脚 | 功能 |
|
||||
|----------|----------|------|
|
||||
| GPIO17 | Pin 11 | 默认 GPIO 检测引脚 |
|
||||
| GND | Pin 6 / 9 / 14 / ... | 接地 |
|
||||
|
||||
将传感器信号线接 GPIO17,GND 接树莓派 GND 即可。
|
||||
|
||||
### 日志记录
|
||||
|
||||
所有 GPIO 事件记录在 `padsleep.log` 中:
|
||||
|
||||
```
|
||||
[2026-07-19 15:59:25] GPIO触屏 — BCM 17 电平变化,60s 倒计时
|
||||
[2026-07-19 15:59:28] GPIO息屏 — BCM 17 在 60s 内无电平变化
|
||||
```
|
||||
|
||||
## 无线 ADB 连接
|
||||
|
||||
@@ -147,19 +210,23 @@ TUI 快捷键:
|
||||
|
||||
```
|
||||
padsleep_adb/
|
||||
├── padsleep.py # 守护程序主程序
|
||||
├── padsleep.py # 守护程序主程序(含 GPIO 检测)
|
||||
├── padsleep_tui.py # TUI 管理工具
|
||||
├── config.json # 配置文件(首次运行自动生成)
|
||||
├── config.json # 配置文件
|
||||
├── requirements.txt # Python 依赖
|
||||
└── README.md # 本文档
|
||||
├── install.sh # 安装脚本
|
||||
├── padsleep.service # systemd 服务文件
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 开发说明
|
||||
|
||||
- 在 Windows 上开发,在树莓派上运行
|
||||
- 使用 `adbutils` 库与 ADB Server 通信
|
||||
- GPIO 使用 `RPi.GPIO`(通过 `rpi-lgpio` 驱动 `lgpio` 内核模块)
|
||||
- IPC 使用 Unix Socket + JSON 行协议
|
||||
- TUI 使用 Python `curses` 库
|
||||
- 非树莓派环境 GPIO 自动降级为模拟模式(不影响主功能)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+4
-1
@@ -7,5 +7,8 @@
|
||||
"check_interval_seconds": 30,
|
||||
"auto_sleep_delay_minutes": 1,
|
||||
"adb_device_serial": "",
|
||||
"adb_path": "adb"
|
||||
"adb_path": "adb",
|
||||
"gpio_enabled": true,
|
||||
"gpio_pin": 17,
|
||||
"gpio_countdown_seconds": 60
|
||||
}
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ fi
|
||||
|
||||
# ── 安装 Python 依赖 ──
|
||||
info "正在安装 Python 依赖..."
|
||||
"$VENV_DIR/bin/pip" install --quiet adbutils 2>&1 || {
|
||||
"$VENV_DIR/bin/pip" install --quiet adbutils rpi-lgpio 2>&1 || {
|
||||
warn "pip 安装失败,尝试离线安装..."
|
||||
if [ -f "$SRC_DIR/.venv" ]; then
|
||||
cp -r "$SRC_DIR/.venv" "$VENV_DIR"
|
||||
|
||||
+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
|
||||
|
||||
@@ -434,6 +434,24 @@ class TuiApp:
|
||||
self.stdscr.addstr(f"{s.get('config_path', '?')}", curses.A_DIM)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
# ── GPIO 状态 ──
|
||||
gpio_avail = s.get("gpio_available", False)
|
||||
if gpio_avail:
|
||||
y += 1
|
||||
gpio_trig = s.get("gpio_triggered", False)
|
||||
gpio_cnt = s.get("gpio_countdown", 0)
|
||||
trig_text = f"触发中 ⏱ {gpio_cnt}s" if gpio_trig else "等待中"
|
||||
trig_color = 3 if gpio_trig else 2
|
||||
gpio_pin = s.get("gpio_pin", "?")
|
||||
if y < h:
|
||||
self._clear_line(y)
|
||||
try:
|
||||
self.stdscr.addstr(y, 4, "GPIO: ")
|
||||
self.stdscr.addstr(f"BCM {gpio_pin} ", curses.A_DIM)
|
||||
self.stdscr.addstr(f"{trig_text}", curses.color_pair(trig_color) | curses.A_BOLD)
|
||||
except curses.error:
|
||||
pass
|
||||
else:
|
||||
if y < h:
|
||||
self._clear_line(y)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# .venv/bin/pip install -r requirements.txt
|
||||
|
||||
adbutils>=2.12.0
|
||||
rpi-lgpio>=0.6 # GPIO 电平检测(树莓派硬件传感器触发息屏/亮屏)
|
||||
|
||||
# 系统依赖:adb (Android Debug Bridge)
|
||||
# 安装: sudo apt-get install adb
|
||||
|
||||
Reference in New Issue
Block a user