From e0211105bd93251f72bc1fa4e9233c9b786cdd63 Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Thu, 17 Sep 2026 15:34:34 +0800 Subject: [PATCH] =?UTF-8?q?modify=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E9=85=8D=E7=BD=AE=E4=B8=8E=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E9=85=8D=E7=BD=AE=E4=B8=8D=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aibox_sophon_adapter/README.md | 17 ++ aibox_sophon_adapter/__init__.py | 2 + aibox_sophon_adapter/diwatch/backends.py | 33 +++- aibox_sophon_adapter/diwatch/cli.py | 198 +++++++++++++++++++---- aibox_sophon_adapter/diwatch/config.py | 6 + aibox_sophon_adapter/task_switch.py | 45 ++++-- 6 files changed, 261 insertions(+), 40 deletions(-) diff --git a/aibox_sophon_adapter/README.md b/aibox_sophon_adapter/README.md index 774641b..c69344c 100644 --- a/aibox_sophon_adapter/README.md +++ b/aibox_sophon_adapter/README.md @@ -361,6 +361,23 @@ tags = ["aibox_sophon_adapter", "sophon", "adapter"] 控制流程:`POST {api_base}/api/alg_task_fetch`(查全部任务的 `AlgTaskSession`)→ 逐个 `POST {api_base}/api/alg_task_control`(启停控制,按返回 `Result.Code` 判定成败并记日志)。 +### IN2 后端自适应(SE5 / SE9) + +由 `diwatch.make_in2_backend` 自动选择,优先级:显式 `device` → sysfs 状态文件(SE9)→ gpio-keys event 设备(SE5)→ 放弃启动。 + +| 板卡 | 后端 | 说明 | +| ---- | ---- | ---- | +| SE9 | `SysfsBackend` | 轮询 `/sys/bus/platform/devices/gpio-in-2/gpio_status`,`poll_interval` / `debounce` 生效 | +| SE5 | `EventBackend` | 无 gpio-in-2 节点,IN2 与 IN1 同走 gpio-keys 事件设备(`event1`),键码 `key`(默认 `KEY_DOWN=108`) | + +SE5 首次部署需确认两件事: + +1. **event 读权限**:`linaro` 用户运行 diwatch 报 `Permission denied: '/dev/input/event1'` 时, + 执行 `sudo usermod -aG input linaro` 并重新登录(服务以 root 运行则不受影响)。 +2. **IN2 实际键码**:操作 IN2 的同时运行 `python3 -m diwatch detect -t 5`,输出里的 `code=` 即键码; + 写入 `task_switch.key`(或 CLI `in2 -k <键码>`)。若 detect 无任何输出,说明该 SE5 的 IN2 + 未接入 gpio-keys,需查 `sudo cat /sys/kernel/debug/gpio` 确认接线。 + 行为约定: 1. **只响应电平跳变**,不按插件启动时的初始电平动作(避免插件重启时误触发全量启停)。 diff --git a/aibox_sophon_adapter/__init__.py b/aibox_sophon_adapter/__init__.py index 33697ca..64777f9 100644 --- a/aibox_sophon_adapter/__init__.py +++ b/aibox_sophon_adapter/__init__.py @@ -191,6 +191,8 @@ class AiboxSophonAdapterPlugin(Plugin): poll_interval=switch_cfg.get("poll_interval", 0.05), debounce=switch_cfg.get("debounce", 3), active_low=switch_cfg.get("active_low", False), + device=switch_cfg.get("device"), + key=switch_cfg.get("key"), ) if not task_switch.start(asyncio.get_running_loop()): task_switch = None diff --git a/aibox_sophon_adapter/diwatch/backends.py b/aibox_sophon_adapter/diwatch/backends.py index 283332c..10e6a94 100644 --- a/aibox_sophon_adapter/diwatch/backends.py +++ b/aibox_sophon_adapter/diwatch/backends.py @@ -19,7 +19,7 @@ import time from .config import ( EV_KEY, VALUE_RELEASE, VALUE_PRESS, VALUE_REPEAT, - DEFAULT_EVENT_DEVICE, GPIO_KEYS_NAME, + DEFAULT_EVENT_DEVICE, GPIO_KEYS_NAME, DEFAULT_IN2_KEY, DEBOUNCE, DEFAULT_INTERVAL, find_status_file, ) @@ -259,6 +259,36 @@ def make_in1_backend(device=None, key=None, in_index=1): # 4) 都找不到,返回一个 open 时会报错的 EventBackend return EventBackend(DEFAULT_EVENT_DEVICE, key=key) + +def make_in2_backend(device=None, key=None, in_index=2, + interval=DEFAULT_INTERVAL, debounce=DEBOUNCE): + """自动为 IN2 选择后端:优先 sysfs(SE9),回退 gpio-keys event(SE5)。 + + SE5 无 gpio-in-2 状态文件,IN2 与 IN1 同走 gpio-keys 事件设备(不同键码)。 + + 优先级: + 1) 显式 device(event 设备路径) -> EventBackend + 2) 查找 gpio-in- 的状态文件 -> SysfsBackend + 3) 自动查找 gpio-keys 的 event 设备 -> EventBackend(key 缺省 DEFAULT_IN2_KEY) + 4) 都找不到 -> 返回 None(由调用方给出可诊断错误) + """ + # 1) 显式指定 event 设备 + if device and device != "auto": + return EventBackend(device, key=key if key is not None else DEFAULT_IN2_KEY) + + # 2) sysfs 状态文件(SE9) + status = find_status_file(in_index) + if status: + return SysfsBackend(status, debounce=debounce, interval=interval) + + # 3) 回退到 gpio-keys 事件设备(SE5) + ev = EventBackend.find_event_device(GPIO_KEYS_NAME) + if ev: + return EventBackend(ev, key=key if key is not None else DEFAULT_IN2_KEY) + + # 4) 都找不到 + return None + def find_event_device(name=GPIO_KEYS_NAME): """模块级别别名,等价于 EventBackend.find_event_device.""" return EventBackend.find_event_device(name) @@ -269,6 +299,7 @@ __all__ = [ "SysfsBackend", "make_backend", "make_in1_backend", + "make_in2_backend", "find_status_file", "find_event_device", ] diff --git a/aibox_sophon_adapter/diwatch/cli.py b/aibox_sophon_adapter/diwatch/cli.py index 6b5f284..1eb9c93 100644 --- a/aibox_sophon_adapter/diwatch/cli.py +++ b/aibox_sophon_adapter/diwatch/cli.py @@ -11,10 +11,12 @@ python3 -m diwatch in1 -d /dev/input/event0 # 手动指定 event python3 -m diwatch in1 --no-active-low # 不翻转极性 - # IN2(sysfs 轮询) + # IN2(优先 sysfs/SE9;SE5 无 gpio-in-2 时回退 gpio-keys event) python3 -m diwatch in2 python3 -m diwatch in2 0.005 python3 -m diwatch in2 --check + python3 -m diwatch in2 -k 108 # 指定 event 键码 + python3 -m diwatch detect # 扫描全部 event 设备,查 IN1/IN2 实际键码 # 同时监听 python3 -m diwatch both @@ -29,15 +31,16 @@ import time from pathlib import Path from .backends import ( - EventBackend, SysfsBackend, - make_in1_backend, find_status_file, find_event_device, + EventBackend, SysfsBackend, EVENT_STRUCT, + make_in1_backend, make_in2_backend, + find_status_file, find_event_device, ) from .core import DIChannel, fmt_dur from .devicetree import derive_gpio from .config import ( DEFAULT_EVENT_DEVICE, KEY_UP, DEFAULT_INTERVAL, IN1_INDEX, IN2_INDEX, IN2_GPIO_NUM, IN2_GPIOCHIP, - STATUS_CANDIDATES, + STATUS_CANDIDATES, DEFAULT_IN2_KEY, ) @@ -63,14 +66,24 @@ def build_in1(args, log): def build_in2(args, log): - dev = find_status_file(IN2_INDEX) - if not dev: - return None - backend = SysfsBackend( - dev, + backend = make_in2_backend( + device=getattr(args, "device", None), + key=getattr(args, "key", None), + in_index=IN2_INDEX, interval=args.interval or DEFAULT_INTERVAL, ) - return DIChannel("IN2", backend, active_low=False, log=log) + if backend is None: + return None + ch = DIChannel( + "IN2", backend, + active_low=getattr(args, "active_low", False), + log=log, + ) + if getattr(args, "press_command", None): + ch.on_press = lambda ts: run_cmd(args.press_command) + if getattr(args, "release_command", None): + ch.on_release = lambda ts: run_cmd(args.release_command) + return ch # --------------------------------------------------------------------------- @@ -108,11 +121,13 @@ def cmd_in2(args): log = make_logger("IN2") ch = build_in2(args, log) if ch is None: - print(f"错误:找不到 IN{IN2_INDEX} 状态文件。已尝试:", file=sys.stderr) + print(f"错误:找不到 IN{IN2_INDEX} 的可用后端。已尝试:", file=sys.stderr) for c in STATUS_CANDIDATES[IN2_INDEX]: - print(f" {c}", file=sys.stderr) - print("排查:ls /sys/bus/platform/devices/ | grep gpio-in", - file=sys.stderr) + print(f" sysfs: {c}", file=sys.stderr) + print(f" event: 自动查找 gpio-keys(默认键码 {DEFAULT_IN2_KEY})", file=sys.stderr) + print("排查:ls /sys/bus/platform/devices/ | grep gpio-in", file=sys.stderr) + print(" ls -l /dev/input/event*", file=sys.stderr) + print(f" python3 -m diwatch detect # 查 IN2 实际键码", file=sys.stderr) return 1 if args.check: @@ -127,16 +142,22 @@ def cmd_in2(args): return 1 except OSError as e: print(f"错误:打开 IN2 失败:{e}", file=sys.stderr) + print("若为 Permission denied,把当前用户加入 input 组:" + "sudo usermod -aG input $USER 后重新登录", file=sys.stderr) return 1 try: print("=" * 60) print(f"IN{IN2_INDEX} 监听开始 {datetime.datetime.now():%Y-%m-%d %H:%M:%S}") - print(f" IN{IN2_INDEX} = GPIO {IN2_GPIO_NUM} = " - f"gpiochip{IN2_GPIOCHIP} 第 {IN2_GPIO_NUM - IN2_GPIOCHIP} 线") - print(f" 状态文件:{ch.backend.path}") - print(f" 轮询间隔:{ch.backend.interval}s " - f"抗抖:连续 {ch.backend.debounce} 次采样一致") + if isinstance(ch.backend, SysfsBackend): + print(f" IN{IN2_INDEX} = GPIO {IN2_GPIO_NUM} = " + f"gpiochip{IN2_GPIOCHIP} 第 {IN2_GPIO_NUM - IN2_GPIOCHIP} 线") + print(f" 状态文件:{ch.backend.path}") + print(f" 轮询间隔:{ch.backend.interval}s " + f"抗抖:连续 {ch.backend.debounce} 次采样一致") + else: + print(f" 后端:{ch.backend.name} 键码:{ch.backend.key}") + print(" 提示:若操作 IN2 无事件输出,运行 python3 -m diwatch detect 查实际键码") print("=" * 60) ch.run(duration=args.duration or None) except KeyboardInterrupt: @@ -246,16 +267,23 @@ def print_summary(channels): # --check for IN2 # --------------------------------------------------------------------------- def do_check_in2(ch): - print(f"=== IN{IN2_INDEX} 编号自检 ===") + print(f"=== IN{IN2_INDEX} 自检 ===") print() - print(f"状态文件:{ch.backend.path}") + be = ch.backend + if not isinstance(be, SysfsBackend): + print(f"后端:{be.name} 键码:{be.key}") + print("event 后端无初始电平可读;操作一次 IN2 应出现 value=1(按下/有电)与 0(释放/断电)。") + print(f"若无任何输出,说明键码不是 {be.key},用 python3 -m diwatch detect 查实际键码。") + return 0 + + print(f"状态文件:{be.path}") print() print("--- 从设备树现场复算 ---") ok = True for n in (1, 2): got = derive_gpio(n) if got is None: - print(f" IN{n}:复算失败(设备树不可读或节点缺失)") + print(f" IN{n}:复算失败(设备树不可读或节点缺失,SE5 无此节点属正常)") ok = False continue gpio_num, label, pin = got @@ -269,15 +297,114 @@ def do_check_in2(ch): if ok: print(f"结论:IN{IN2_INDEX} = GPIO {IN2_GPIO_NUM},推导成立。") else: - print("结论:推导与实测不符,请人工核对。") + print("结论:推导与实测不符(SE9 正常成立;SE5 走 event 后端,此节可忽略)。") print() print("--- 当前电平 ---") - raw = ch.backend.read_raw() + raw = be.read_raw() if raw is not None: print(f" gpio_status = {raw} (全局 GPIO {IN2_GPIO_NUM})") print(" 注:待机时读到 0 是正常的。") else: - print(f" 读取失败:{ch.backend.path} 内容不是 0/1。") + print(f" 读取失败:{be.path} 内容不是 0/1。") + return 0 + + +# --------------------------------------------------------------------------- +# detect:扫描全部 event 设备,实时打印按键事件(确认 IN1/IN2 键码) +# --------------------------------------------------------------------------- +KEY_NAMES = { + 1: "KEY_ESC", 28: "KEY_ENTER", 103: "KEY_UP", 108: "KEY_DOWN", + 105: "KEY_LEFT", 106: "KEY_RIGHT", 114: "KEY_VOLUMEDOWN", + 115: "KEY_VOLUMEUP", 116: "KEY_POWER", +} + + +def cmd_detect(args): + """扫描可读的 /dev/input/event* 设备,监听 duration 秒并打印所有 EV_KEY 事件。""" + import glob + import select + + devices = sorted(glob.glob("/dev/input/event*")) + if not devices: + print("错误:/dev/input/ 下没有 event 设备", file=sys.stderr) + return 1 + + names = {} # fd -> (device, 名称) + poller = select.poll() + for dev in devices: + try: + fd = os.open(dev, os.O_RDONLY | os.O_NONBLOCK) + except PermissionError: + print(f" 跳过 {dev}(权限不足;用 sudo 运行,或把用户加入 input 组)", + file=sys.stderr) + continue + except OSError as e: + print(f" 跳过 {dev}({e})", file=sys.stderr) + continue + name = "" + try: + with open(f"/sys/class/input/{os.path.basename(dev)}/device/name") as f: + name = f.read().strip() + except OSError: + pass + names[fd] = (dev, name) + poller.register(fd, select.POLLIN) + + if not names: + print("错误:没有可读的 event 设备(Permission denied 时用 sudo 运行)", file=sys.stderr) + return 1 + + duration = args.duration or 5.0 + print("=" * 60) + print(f"监听 {len(names)} 个 event 设备 {duration:g} 秒,请现在操作 IN1 / IN2 ...") + for fd, (dev, name) in names.items(): + print(f" {dev} [{name}]") + print("=" * 60) + + deadline = time.time() + duration + count = 0 + try: + while True: + remain_ms = int((deadline - time.time()) * 1000) + if remain_ms <= 0: + break + if not poller.poll(min(remain_ms, 200)): + continue + for fd in list(names): + try: + data = os.read(fd, EVENT_STRUCT.size * 64) + except OSError: + continue + if not data: + continue + dev, _name = names[fd] + for off in range(0, len(data) - EVENT_STRUCT.size + 1, EVENT_STRUCT.size): + _sec, _usec, etype, code, value = EVENT_STRUCT.unpack_from(data, off) + if etype != 0x01: # 只看 EV_KEY + continue + if value == 2: # 长按 repeat 太吵 + continue + count += 1 + key = KEY_NAMES.get(code, "?") + val = "按下" if value == 1 else "释放" + print(f" {datetime.datetime.now():%H:%M:%S.%f} {dev} " + f"code={code:<4} ({key}) {val}") + except KeyboardInterrupt: + pass + finally: + for fd in names: + try: + os.close(fd) + except OSError: + pass + + print() + if count == 0: + print("未捕获任何按键事件。若已操作 IN2 仍无输出,说明 SE5 的 IN2 不走 event 设备,") + print("请检查:ls /sys/bus/platform/devices/ | grep -i gpio;" + "sudo cat /sys/kernel/debug/gpio") + else: + print(f"共捕获 {count} 个按键事件。IN2 对应的 code 即为 -k/--key(或 task_switch.key)应填的键码。") return 0 @@ -309,15 +436,32 @@ def build_parser(): p1.set_defaults(func=cmd_in1, interval=None) # ---- in2 ---- - p2 = sub.add_parser("in2", help="监听 IN2(sysfs gpio_status)") + p2 = sub.add_parser("in2", help="监听 IN2(优先 sysfs/SE9,回退 gpio-keys event/SE5)") p2.add_argument("interval", nargs="?", type=float, default=DEFAULT_INTERVAL, - help=f"轮询间隔秒(默认 {DEFAULT_INTERVAL})") + help=f"sysfs 轮询间隔秒(默认 {DEFAULT_INTERVAL})") p2.add_argument("--check", action="store_true", help="只自检:复算编号 + 打印当前电平") + p2.add_argument("-d", "--device", default=None, + help="显式指定 event 设备(回退 event 模式用);默认自动选择") + p2.add_argument("-k", "--key", type=lambda s: int(s, 0), default=None, + help=f"event 模式键码(默认 {DEFAULT_IN2_KEY},用 detect 查实际键码)") + p2.add_argument("--active-low", dest="active_low", action="store_true", + default=False, help="电平取反(默认不取反)") + p2.add_argument("--no-active-low", dest="active_low", action="store_false", + help="不翻转(默认)") + p2.add_argument("--press-command", help="有电时执行的 shell 命令") + p2.add_argument("--release-command", help="断电时执行的 shell 命令") p2.add_argument("-t", "--duration", type=float, default=0, help="监听秒数,0=一直跑") p2.set_defaults(func=cmd_in2) + # ---- detect ---- + pd = sub.add_parser("detect", + help="扫描全部 event 设备并打印按键事件(确认 IN1/IN2 键码)") + pd.add_argument("-t", "--duration", type=float, default=5.0, + help="监听秒数(默认 5)") + pd.set_defaults(func=cmd_detect) + # ---- both ---- p3 = sub.add_parser("both", help="同时监听 IN1 + IN2") p3.add_argument("-d", "--device", default="auto", diff --git a/aibox_sophon_adapter/diwatch/config.py b/aibox_sophon_adapter/diwatch/config.py index 7cd9a07..e532eb1 100644 --- a/aibox_sophon_adapter/diwatch/config.py +++ b/aibox_sophon_adapter/diwatch/config.py @@ -7,6 +7,7 @@ import os # ---- input 事件协议(IN1/event 后端用)---- EV_KEY = 0x01 KEY_UP = 103 +KEY_DOWN = 108 VALUE_RELEASE = 0 VALUE_PRESS = 1 VALUE_REPEAT = 2 @@ -14,6 +15,11 @@ VALUE_REPEAT = 2 DEFAULT_EVENT_DEVICE = "/dev/input/event1" GPIO_KEYS_NAME = "gpio-keys" +# SE5 的 IN2 与 IN1 同走 gpio-keys 事件设备(event1),仅键码不同; +# 默认取 KEY_DOWN,实测不符时用 `diwatch in2 -k <键码>` 覆盖, +# 键码可用 `diwatch detect` 现场确认。 +DEFAULT_IN2_KEY = KEY_DOWN + # ---- IN 通道编号 ---- IN1_INDEX = 1 IN2_INDEX = 2 diff --git a/aibox_sophon_adapter/task_switch.py b/aibox_sophon_adapter/task_switch.py index e5b840c..9d3744a 100644 --- a/aibox_sophon_adapter/task_switch.py +++ b/aibox_sophon_adapter/task_switch.py @@ -23,15 +23,19 @@ import threading import httpx -from .diwatch.backends import SysfsBackend -from .diwatch.config import IN2_INDEX, find_status_file +from .diwatch.backends import EventBackend, make_in2_backend from .diwatch.core import DIChannel logger = logging.getLogger(__name__) class In2TaskSwitch: - """IN2 数字输入 → 全部算法任务启停开关。""" + """IN2 数字输入 → 全部算法任务启停开关。 + + 后端自适应(make_in2_backend):SE9 走 sysfs gpio_status 轮询; + SE5 无 gpio-in-2 时回退 gpio-keys event 设备(默认键码 KEY_DOWN=108, + 可用 device/key 参数覆盖,键码用 diwatch detect 查询)。 + """ def __init__( self, @@ -41,6 +45,8 @@ class In2TaskSwitch: poll_interval: float = 0.05, debounce: int = 3, active_low: bool = False, + device: str | None = None, + key: int | None = None, timeout: float = 10.0, ): self.api_base = api_base.rstrip("/") @@ -49,6 +55,8 @@ class In2TaskSwitch: self.poll_interval = poll_interval self.debounce = debounce self.active_low = active_low + self.device = device + self.key = key self.timeout = timeout self._channel: DIChannel | None = None @@ -58,16 +66,18 @@ class In2TaskSwitch: # ---- 生命周期 ---- def start(self, loop: asyncio.AbstractEventLoop) -> bool: - """启动监听线程;找不到 IN2 状态文件时返回 False(非盒子环境属正常)。""" - status = find_status_file(IN2_INDEX) - if not status: + """启动监听线程;找不到 IN2 可用后端时返回 False(非盒子环境属正常)。""" + backend = make_in2_backend( + device=self.device, key=self.key, + interval=self.poll_interval, debounce=self.debounce, + ) + if backend is None: logger.warning( - "[TaskSwitch] 找不到 IN2 状态文件,任务启停开关未启动" - "(非盒子环境属正常现象)" + "[TaskSwitch] 找不到 IN2 可用后端(sysfs 状态文件与 gpio-keys " + "event 设备均不存在),任务启停开关未启动(非盒子环境属正常现象)" ) return False - backend = SysfsBackend(status, debounce=self.debounce, interval=self.poll_interval) self._channel = DIChannel( "IN2", backend, active_low=self.active_low, @@ -79,11 +89,21 @@ class In2TaskSwitch: target=self._run_channel, name="in2-task-switch", daemon=True, ) self._thread.start() + backend_desc = ( + f"event:{getattr(backend, 'device', '?')}, 键码={backend.key}" + if isinstance(backend, EventBackend) + else f"sysfs:{backend.path}" + ) logger.info( - f"[TaskSwitch] IN2 任务启停开关已启动: 状态文件={status}, " + f"[TaskSwitch] IN2 任务启停开关已启动: 后端={backend_desc}, " f"api_base={self.api_base}, " f"启动命令={self.start_command}, 停止命令={self.stop_command}" ) + if isinstance(backend, EventBackend): + logger.info( + "[TaskSwitch] event 后端无初始电平,以首个事件为准;" + "若操作 IN2 无反应,用 diwatch detect 查实际键码并配置 task_switch.key" + ) return True def stop(self) -> None: @@ -96,7 +116,7 @@ class In2TaskSwitch: def _run_channel(self) -> None: try: - self._channel.run() + self._channel.run() # type: ignore except Exception as e: logger.error(f"[TaskSwitch] IN2 监听线程异常退出: {type(e).__name__}: {e}") @@ -156,7 +176,8 @@ class In2TaskSwitch: """查询全部任务的 AlgTaskSession 列表;失败返回空列表。""" try: async with httpx.AsyncClient(timeout=self.timeout) as client: - resp = await client.post(f"{self.api_base}/api/alg_task_fetch", json={}) + # resp = await client.post(f"{self.api_base}/api/alg_task_fetch", json={}) + resp = await client.get(f"{self.api_base}/api/alg_task_fetch") if not resp.is_success: logger.warning( f"[TaskSwitch] 查询任务失败: Status={resp.status_code}, "