213 lines
6.9 KiB
Python
213 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""IN1 (12V 输入) 事件监听。
|
||
|
||
背景:
|
||
IN1 在硬件上接到某条 GPIO,但那条 GPIO 已被内核 gpio-keys 驱动占用,
|
||
/sys/class/gpio 导不出来。驱动把它映射到 /dev/input/event1,
|
||
上报键码 KEY_UP (103)。
|
||
|
||
本机光耦输入是反相的:施加 12V 时 GPIO 被拉低,驱动上报“释放”(0);
|
||
撤除 12V 时 GPIO 被上拉,驱动上报“按下”(1)。
|
||
因此默认 active_low=True,把语义翻回来,让“加 12V = 有电”。
|
||
|
||
本模块只做一件事:盯着 IN1,有电/断电时回调。
|
||
依赖:仅标准库。
|
||
"""
|
||
|
||
import argparse
|
||
import datetime
|
||
import glob
|
||
import os
|
||
import select
|
||
import struct
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
|
||
# ---- input 事件协议 ----
|
||
EV_KEY = 0x01
|
||
EVENT_STRUCT = struct.Struct("llHHi") # aarch64 / x86_64 都是 24 字节
|
||
|
||
KEY_UP = 103
|
||
VALUE_RELEASE = 0
|
||
VALUE_PRESS = 1
|
||
VALUE_REPEAT = 2
|
||
|
||
DEFAULT_DEVICE = "/dev/input/event1"
|
||
|
||
|
||
def now():
|
||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||
|
||
|
||
def find_event_device(name="gpio-keys"):
|
||
"""按设备名找 eventN,找不到返回 None。设备号变化时很有用。"""
|
||
for path in sorted(glob.glob("/dev/input/event*")):
|
||
ev = path.rsplit("/", 1)[-1]
|
||
namefile = "/sys/class/input/%s/device/name" % ev
|
||
try:
|
||
with open(namefile) as f:
|
||
if f.read().strip() == name:
|
||
return path
|
||
except OSError:
|
||
continue
|
||
return None
|
||
|
||
|
||
class IN1Listener:
|
||
"""监听 IN1,有电/断电触发回调。
|
||
|
||
active_low=True(默认):驱动上报“释放”(0) 视为有电,“按下”(1) 视为断电。
|
||
适配本机光耦反相电路。
|
||
active_low=False: 按驱动原始语义,value=1 视为有电。
|
||
|
||
用法:
|
||
def on_press(ts): print("IN1 有电", ts)
|
||
def on_release(ts): print("IN1 断电", ts)
|
||
|
||
with IN1Listener(on_press=on_press, on_release=on_release) as lis:
|
||
lis.run() # 阻塞,Ctrl-C 退出
|
||
# lis.run(duration=10) # 跑 10 秒后返回
|
||
"""
|
||
|
||
def __init__(self, device=DEFAULT_DEVICE, key=KEY_UP,
|
||
on_press=None, on_release=None,
|
||
on_event=None, log=None,
|
||
active_low=True):
|
||
self.device = device
|
||
self.key = key
|
||
self.on_press = on_press # 12V 接通
|
||
self.on_release = on_release # 12V 撤除
|
||
self.on_event = on_event # 原始事件 (ts, code, value),可选
|
||
self.log = log or (lambda msg: print(msg, flush=True))
|
||
self.active_low = active_low
|
||
|
||
self.fd = None
|
||
self.buf = b""
|
||
|
||
# ---- 生命周期 ----
|
||
def open(self):
|
||
if self.fd is None:
|
||
self.fd = os.open(self.device, os.O_RDONLY | os.O_NONBLOCK)
|
||
|
||
def close(self):
|
||
if self.fd is not None:
|
||
try:
|
||
os.close(self.fd)
|
||
finally:
|
||
self.fd = None
|
||
|
||
def __enter__(self):
|
||
self.open()
|
||
return self
|
||
|
||
def __exit__(self, *exc):
|
||
self.close()
|
||
|
||
# ---- 事件处理 ----
|
||
def _drain(self):
|
||
if self.fd is None:
|
||
return
|
||
try:
|
||
data = os.read(self.fd, EVENT_STRUCT.size * 64)
|
||
except OSError:
|
||
return
|
||
self.buf += data
|
||
while len(self.buf) >= EVENT_STRUCT.size:
|
||
chunk, self.buf = self.buf[:EVENT_STRUCT.size], self.buf[EVENT_STRUCT.size:]
|
||
sec, usec, etype, code, value = EVENT_STRUCT.unpack(chunk)
|
||
if etype != EV_KEY:
|
||
continue
|
||
ts = sec + usec / 1e6
|
||
if self.on_event:
|
||
self.on_event(ts, code, value)
|
||
if code != self.key:
|
||
continue
|
||
|
||
if value == VALUE_REPEAT:
|
||
self.log("%s IN1 重复" % now())
|
||
continue
|
||
|
||
# 硬件极性:active_low 时,value=0 表示 12V 接通
|
||
if self.active_low:
|
||
powered = (value == VALUE_RELEASE)
|
||
else:
|
||
powered = (value == VALUE_PRESS)
|
||
|
||
if powered:
|
||
self.log("%s IN1 有电" % now())
|
||
if self.on_press:
|
||
self.on_press(ts)
|
||
else:
|
||
self.log("%s IN1 断电" % now())
|
||
if self.on_release:
|
||
self.on_release(ts)
|
||
|
||
# ---- 主循环 ----
|
||
def run(self, duration=None):
|
||
"""阻塞监听。duration 秒后返回;None = 一直跑到 Ctrl-C。"""
|
||
if self.fd is None:
|
||
self.open()
|
||
if self.fd is None:
|
||
return
|
||
poller = select.poll()
|
||
poller.register(self.fd, select.POLLIN)
|
||
|
||
deadline = time.time() + duration if duration else None
|
||
while True:
|
||
timeout = 500 # ms
|
||
if deadline:
|
||
remain = deadline - time.time()
|
||
if remain <= 0:
|
||
return
|
||
timeout = int(min(remain, 0.5) * 1000)
|
||
poller.poll(timeout)
|
||
self._drain()
|
||
|
||
|
||
# ---- CLI 演示 ----
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="监听 IN1 (12V 输入)")
|
||
ap.add_argument("-d", "--device", default=DEFAULT_DEVICE,
|
||
help="事件设备,默认 %(default)s;传 auto 自动按名字查找")
|
||
ap.add_argument("-k", "--key", type=lambda s: int(s, 0), default=KEY_UP,
|
||
help="键码,默认 103 (KEY_UP)")
|
||
ap.add_argument("-t", "--duration", type=float, default=0,
|
||
help="监听秒数,0 = 一直跑")
|
||
ap.add_argument("--press-command", help="IN1 有电时执行的 shell 命令")
|
||
ap.add_argument("--release-command", help="IN1 断电时执行的 shell 命令")
|
||
ap.add_argument("--active-low", dest="active_low", action="store_true",
|
||
default=True,
|
||
help="12V 接通上报为“有电”(默认,适配本机光耦反相)")
|
||
ap.add_argument("--no-active-low", dest="active_low", action="store_false",
|
||
help="不翻转,按驱动原始语义")
|
||
args = ap.parse_args()
|
||
|
||
device = args.device
|
||
if device == "auto":
|
||
device = find_event_device("gpio-keys") or DEFAULT_DEVICE
|
||
|
||
def run_cmd(cmd):
|
||
if cmd:
|
||
subprocess.Popen(cmd, shell=True)
|
||
|
||
with IN1Listener(
|
||
device=device,
|
||
key=args.key,
|
||
active_low=args.active_low,
|
||
on_press=lambda ts: run_cmd(args.press_command),
|
||
on_release=lambda ts: run_cmd(args.release_command),
|
||
) as lis:
|
||
print("监听 %s key=%d active_low=%s,Ctrl-C 退出"
|
||
% (device, args.key, args.active_low), flush=True)
|
||
try:
|
||
lis.run(duration=args.duration or None)
|
||
except KeyboardInterrupt:
|
||
print("\n退出。")
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main()) |