#!/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())