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())
|