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