#!/usr/bin/env python3 # -*- coding: utf-8 -*- """DI 通道的底层读取后端。 每个后端只负责:open / close / read。 电平语义(0=低,1=高)由上层 DIChannel 统一处理。 后端类型: - EventBackend:/dev/input/eventN,事件驱动,可 poll。SE5 的 IN1。 - SysfsBackend:/sys/.../gpio_status 或 /dev/tv_gpioin_N,轮询 + 抗抖。 SE9 的 IN1、所有设备的 IN2。 """ import errno import glob import os import select # noqa: F401 (留给上层使用) import struct import time from .config import ( EV_KEY, VALUE_RELEASE, VALUE_PRESS, VALUE_REPEAT, DEFAULT_EVENT_DEVICE, GPIO_KEYS_NAME, DEBOUNCE, DEFAULT_INTERVAL, find_status_file, ) EVENT_STRUCT = struct.Struct("llHHi") # aarch64 / x86_64 都是 24 字节 # --------------------------------------------------------------------------- # 基类 # --------------------------------------------------------------------------- class Backend: """后端接口。""" can_poll = False def open(self): raise NotImplementedError def close(self): raise NotImplementedError def read(self): """非阻塞读一次,返回 (level, ts) 或 None。 level 是原始电平 0/1,尚未做 active_low 翻转。 """ raise NotImplementedError @property def name(self): raise NotImplementedError # --------------------------------------------------------------------------- # /dev/input/eventN 后端(SE5 的 IN1) # --------------------------------------------------------------------------- class EventBackend(Backend): """基于 input 事件设备,事件驱动。 原始 value 语义: VALUE_PRESS(1) -> 硬件“按下” VALUE_RELEASE(0) -> 硬件“释放” 本后端返回原始 value,极性翻转留给 DIChannel。 """ can_poll = True def __init__(self, device=DEFAULT_EVENT_DEVICE, key=None, find_by_name=GPIO_KEYS_NAME): self.device = device self.key = key self.find_by_name = find_by_name self.fd = None self.buf = b"" # ---- 自动查找 ---- @staticmethod def find_event_device(name=GPIO_KEYS_NAME): """按设备名找 eventN,找不到返回 None。""" for path in sorted(glob.glob("/dev/input/event*")): ev = path.rsplit("/", 1)[-1] namefile = f"/sys/class/input/{ev}/device/name" try: with open(namefile) as f: if f.read().strip() == name: return path except OSError: continue return None def resolve_device(self): if not self.device or self.device == "auto": return self.find_event_device(self.find_by_name) or DEFAULT_EVENT_DEVICE return self.device @property def name(self): dev = self.device or "(auto)" return f"event:{dev}" # ---- 生命周期 ---- def open(self): if self.fd is None: dev = self.resolve_device() if not dev or not os.path.exists(dev): raise FileNotFoundError( errno.ENOENT, f"event 设备不存在:{dev}", dev, ) self.device = dev self.fd = os.open(dev, 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 fileno(self): return self.fd # ---- 读取 ---- def read(self): """返回 (level, ts) 或 None;只汇报关心的 key。""" if self.fd is None: return None try: data = os.read(self.fd, EVENT_STRUCT.size * 64) except OSError: return None self.buf += data result = None while len(self.buf) >= EVENT_STRUCT.size: chunk = self.buf[:EVENT_STRUCT.size] self.buf = self.buf[EVENT_STRUCT.size:] sec, usec, etype, code, value = EVENT_STRUCT.unpack(chunk) if etype != EV_KEY: continue if self.key is not None and code != self.key: continue if value == VALUE_REPEAT: continue if value not in (VALUE_RELEASE, VALUE_PRESS): continue ts = sec + usec / 1e6 result = (value, ts) return result # --------------------------------------------------------------------------- # sysfs 状态文件后端(SE9 的 IN1、所有 IN2) # --------------------------------------------------------------------------- class SysfsBackend(Backend): """基于 /sys/.../gpio_status 或 /dev/tv_gpioin_N,轮询采样 + 抗抖。""" can_poll = False def __init__(self, path, debounce=DEBOUNCE, interval=DEFAULT_INTERVAL): self.path = path self.debounce = max(1, int(debounce)) self.interval = interval self._cand = None self._cand_n = 0 @property def name(self): return f"sysfs:{self.path}" def open(self): if not os.access(self.path, os.R_OK): raise FileNotFoundError( errno.ENOENT, f"状态文件不可读:{self.path}", self.path, ) def close(self): pass # ---- 原始电平 ---- def read_raw(self): try: with open(self.path, "r") as f: v = f.read() except OSError: return None v = v.strip().strip("\t\r\n ") return v if v in ("0", "1") else None # ---- 带抗抖的读取 ---- def read(self): """返回 (level, ts) 或 None。 level 为抗抖确认后的稳定值;None 表示还没确认跳变。 """ v = self.read_raw() if v is None: return None ts = time.time() if self._cand is None or v == self._cand: self._cand = v self._cand_n += 1 else: self._cand = v self._cand_n = 1 if self._cand_n >= self.debounce: self._cand_n = 0 # 复位,避免重复上报 return (int(v), ts) return None # --------------------------------------------------------------------------- # 工厂 # --------------------------------------------------------------------------- def make_backend(kind: str, **kwargs): """kind = 'event' | 'sysfs'。""" if kind == "event": return EventBackend(**kwargs) if kind == "sysfs": return SysfsBackend(**kwargs) raise ValueError(f"未知后端类型:{kind}") def make_in1_backend(device=None, key=None, in_index=1): """自动为 IN1 选择后端,适配 SE5 / SE9。 优先级: 1) 显式 device(非 auto) -> EventBackend 2) 自动查找 gpio-keys 的 event 设备 -> EventBackend 3) 查找 gpio-in- 的状态文件 -> SysfsBackend 4) 都找不到 -> 返回 EventBackend(open 时抛出可诊断的错误) """ # 1) 显式指定 if device and device != "auto": return EventBackend(device, key=key) # 2) 自动查找 gpio-keys ev = EventBackend.find_event_device(GPIO_KEYS_NAME) if ev: return EventBackend(ev, key=key) # 3) 回退到 sysfs 状态文件 status = find_status_file(in_index) if status: return SysfsBackend(status) # 4) 都找不到,返回一个 open 时会报错的 EventBackend return EventBackend(DEFAULT_EVENT_DEVICE, key=key) def find_event_device(name=GPIO_KEYS_NAME): """模块级别别名,等价于 EventBackend.find_event_device.""" return EventBackend.find_event_device(name) __all__ = [ "Backend", "EventBackend", "SysfsBackend", "make_backend", "make_in1_backend", "find_status_file", "find_event_device", ]