Files
chongming-plugins/aibox_sophon_adapter/diwatch/backends.py
T

269 lines
7.9 KiB
Python
Raw Normal View History

2026-09-11 16:55:54 +08:00
#!/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-<in_index> 的状态文件 -> 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",
]