Files
chongming-plugins/aibox_sophon_adapter/diwatch/cli.py
T
2026-09-11 16:55:54 +08:00

343 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""diwatch 统一命令行入口,适配 SE5 / SE9。
用法示例:
# IN1(自动选后端:SE5 走 event,SE9 走 sysfs)
python3 -m diwatch in1
python3 -m diwatch in1 -t 10
python3 -m diwatch in1 --press-command 'logger "IN1 on"' \
--release-command 'logger "IN1 off"'
python3 -m diwatch in1 -d /dev/input/event0 # 手动指定 event
python3 -m diwatch in1 --no-active-low # 不翻转极性
# IN2(sysfs 轮询)
python3 -m diwatch in2
python3 -m diwatch in2 0.005
python3 -m diwatch in2 --check
# 同时监听
python3 -m diwatch both
"""
import argparse
import datetime
import os
import signal
import sys
import time
from pathlib import Path
from .backends import (
EventBackend, SysfsBackend,
make_in1_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,
)
# ---------------------------------------------------------------------------
# 构造通道
# ---------------------------------------------------------------------------
def build_in1(args, log):
backend = make_in1_backend(
device=args.device,
key=args.key,
in_index=IN1_INDEX,
)
ch = DIChannel(
"IN1", backend,
active_low=args.active_low,
log=log,
)
if args.press_command:
ch.on_press = lambda ts: run_cmd(args.press_command)
if args.release_command:
ch.on_release = lambda ts: run_cmd(args.release_command)
return ch
def build_in2(args, log):
dev = find_status_file(IN2_INDEX)
if not dev:
return None
backend = SysfsBackend(
dev,
interval=args.interval or DEFAULT_INTERVAL,
)
return DIChannel("IN2", backend, active_low=False, log=log)
# ---------------------------------------------------------------------------
# 子命令
# ---------------------------------------------------------------------------
def cmd_in1(args):
log = make_logger("IN1")
ch = build_in1(args, log)
install_signal(ch)
# 手动 open,便于给出友好错误
try:
ch.open()
except FileNotFoundError as e:
_print_open_error("IN1", e)
return 1
except OSError as e:
print(f"错误:打开 IN1 失败:{e}", file=sys.stderr)
return 1
try:
print(f"监听 {ch.backend.name} key={args.key} "
f"active_low={args.active_low},Ctrl-C 退出", flush=True)
ch.run(duration=args.duration or None)
except KeyboardInterrupt:
pass
finally:
ch.close()
print_summary([ch])
return 0
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)
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)
return 1
if args.check:
return do_check_in2(ch)
install_signal(ch)
try:
ch.open()
except FileNotFoundError as e:
_print_open_error("IN2", e)
return 1
except OSError as e:
print(f"错误:打开 IN2 失败:{e}", 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} 次采样一致")
print("=" * 60)
ch.run(duration=args.duration or None)
except KeyboardInterrupt:
pass
finally:
ch.close()
print_summary([ch])
return 0
def cmd_both(args):
log = make_logger("BOTH")
ch1 = build_in1(args, log)
ch2 = build_in2(args, log)
if ch2 is None:
print(f"错误:找不到 IN{IN2_INDEX} 状态文件。", file=sys.stderr)
return 1
install_signal(ch1, ch2)
# 逐个 open,便于报错
for name, ch in (("IN1", ch1), ("IN2", ch2)):
try:
ch.open()
except FileNotFoundError as e:
_print_open_error(name, e)
return 1
except OSError as e:
print(f"错误:打开 {name} 失败:{e}", file=sys.stderr)
return 1
try:
print("=" * 60)
print(f"同时监听 IN1 + IN2 {datetime.datetime.now():%Y-%m-%d %H:%M:%S}")
print(f" IN1 -> {ch1.backend.name}")
print(f" IN2 -> {ch2.backend.name}")
print("=" * 60)
import threading
duration = args.duration or None
t1 = threading.Thread(target=ch1.run, kwargs={"duration": duration})
t2 = threading.Thread(target=ch2.run, kwargs={"duration": duration})
t1.start(); t2.start()
while t1.is_alive() or t2.is_alive():
t1.join(0.5); t2.join(0.5)
except KeyboardInterrupt:
ch1.request_stop(); ch2.request_stop()
finally:
ch1.close(); ch2.close()
print_summary([ch1, ch2])
return 0
# ---------------------------------------------------------------------------
# 辅助
# ---------------------------------------------------------------------------
def make_logger(tag):
def _log(msg):
print(f"[{tag}] {msg}", flush=True)
return _log
def run_cmd(cmd):
import subprocess
subprocess.Popen(cmd, shell=True)
def install_signal(*channels):
def _handler(*_a):
for c in channels:
c.request_stop()
signal.signal(signal.SIGINT, _handler)
signal.signal(signal.SIGTERM, _handler)
def _print_open_error(name, exc):
print(f"错误:{name} 不可用:{getattr(exc, 'filename', exc)}",
file=sys.stderr)
print("", file=sys.stderr)
print("排查步骤:", file=sys.stderr)
print(" 1) 查看 event 设备及其名字:", file=sys.stderr)
print(" for f in /dev/input/event*; do \\", file=sys.stderr)
print(" echo \"$f -> $(cat /sys/class/input/$(basename $f)/device/name 2>/dev/null)\"; \\",
file=sys.stderr)
print(" done", file=sys.stderr)
print(" 2) 查看 sysfs 状态文件:", file=sys.stderr)
print(" ls /sys/bus/platform/devices/ | grep gpio-in", file=sys.stderr)
print(" 3) 手动指定 event 设备:", file=sys.stderr)
print(" python3 -m diwatch in1 -d /dev/input/eventN", file=sys.stderr)
print(" 4) 若 SE9 无 /dev/input/,本工具会自动回退到 "
"gpio-in-1/gpio_status", file=sys.stderr)
def print_summary(channels):
print()
print("=" * 60)
print(f"汇总 {datetime.datetime.now():%Y-%m-%d %H:%M:%S}")
for ch in channels:
s = ch.summary()
print(f" [{s['name']}] 跳变 {s['count']} 次 "
f"低 {fmt_dur(s['ms_low'])} 高 {fmt_dur(s['ms_high'])} "
f"结束电平 {s['level']}")
print("=" * 60)
# ---------------------------------------------------------------------------
# --check for IN2
# ---------------------------------------------------------------------------
def do_check_in2(ch):
print(f"=== IN{IN2_INDEX} 编号自检 ===")
print()
print(f"状态文件:{ch.backend.path}")
print()
print("--- 从设备树现场复算 ---")
ok = True
for n in (1, 2):
got = derive_gpio(n)
if got is None:
print(f" IN{n}:复算失败(设备树不可读或节点缺失)")
ok = False
continue
gpio_num, label, pin = got
expect = 481 + n
if gpio_num == expect:
print(f" IN{n} -> GPIO {gpio_num} ✓ 与预期一致")
else:
print(f" IN{n} -> GPIO {gpio_num} ✗ 预期 {expect}")
ok = False
print()
if ok:
print(f"结论:IN{IN2_INDEX} = GPIO {IN2_GPIO_NUM},推导成立。")
else:
print("结论:推导与实测不符,请人工核对。")
print()
print("--- 当前电平 ---")
raw = ch.backend.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。")
return 0
# ---------------------------------------------------------------------------
# 入口
# ---------------------------------------------------------------------------
def build_parser():
p = argparse.ArgumentParser(
prog="diwatch",
description="统一 DI 输入监听(适配 SE5 / SE9)",
)
sub = p.add_subparsers(dest="cmd", required=True)
# ---- in1 ----
p1 = sub.add_parser("in1", help="监听 IN1(自动适配 event / sysfs)")
p1.add_argument("-d", "--device", default="auto",
help="事件设备路径;auto=自动查找(默认)。"
"SE5 用 /dev/input/eventN,SE9 自动回退到 sysfs。")
p1.add_argument("-k", "--key", type=lambda s: int(s, 0), default=KEY_UP,
help="键码,默认 103 (KEY_UP)")
p1.add_argument("-t", "--duration", type=float, default=0,
help="监听秒数,0=一直跑")
p1.add_argument("--press-command", help="有电时执行的 shell 命令")
p1.add_argument("--release-command", help="断电时执行的 shell 命令")
p1.add_argument("--active-low", dest="active_low", action="store_true",
default=True, help="12V 接通上报为“有电”(默认)")
p1.add_argument("--no-active-low", dest="active_low", action="store_false",
help="不翻转,按驱动/文件原始语义")
p1.set_defaults(func=cmd_in1, interval=None)
# ---- in2 ----
p2 = sub.add_parser("in2", help="监听 IN2(sysfs gpio_status)")
p2.add_argument("interval", nargs="?", type=float, default=DEFAULT_INTERVAL,
help=f"轮询间隔秒(默认 {DEFAULT_INTERVAL})")
p2.add_argument("--check", action="store_true",
help="只自检:复算编号 + 打印当前电平")
p2.add_argument("-t", "--duration", type=float, default=0,
help="监听秒数,0=一直跑")
p2.set_defaults(func=cmd_in2)
# ---- both ----
p3 = sub.add_parser("both", help="同时监听 IN1 + IN2")
p3.add_argument("-d", "--device", default="auto",
help="IN1 事件设备路径;auto=自动(默认)")
p3.add_argument("-k", "--key", type=lambda s: int(s, 0), default=KEY_UP)
p3.add_argument("--active-low", dest="active_low", action="store_true",
default=True)
p3.add_argument("--no-active-low", dest="active_low", action="store_false")
p3.add_argument("-t", "--duration", type=float, default=0)
p3.add_argument("--interval", type=float, default=DEFAULT_INTERVAL,
help="IN2 轮询间隔秒")
p3.set_defaults(func=cmd_both)
return p
def main(argv=None):
args = build_parser().parse_args(argv)
return args.func(args)
if __name__ == "__main__":
sys.exit(main())