343 lines
11 KiB
Python
343 lines
11 KiB
Python
#!/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())
|