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

456 lines
14 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
"""
watch_in2.py — 监听数字输入 IN2 的电平变化(Python 版)
原 Bash 脚本 watch_in2.sh 的功能等价实现:
- IN2 编号:Linux 全局 GPIO 483
= gpiochip480(label "27010000.gpio",bank-name "porta")的第 3 线
- 推导依据可用 --check 现场复算
- 由于 483 被平台驱动占用,sysfs export 会 EBUSY,
唯一可读通道是 gpio_status,不支持 poll/select,只能定时采样。
- 该文件权限 0666,所以本脚本不需要 root。
用法:
./watch_in2.py [轮询间隔秒] 默认 0.02
./watch_in2.py 0.005 5ms 细粒度
./watch_in2.py --check 只做自检:复算编号 + 打印当前电平,然后退出
停止: Ctrl-C 打印汇总
"""
import os
import sys
import time
import signal
import datetime
import argparse
from pathlib import Path
# ---------------------------------------------------------------------------
# 常量
# ---------------------------------------------------------------------------
IN_INDEX = 2
GPIO_NUM = 483
GPIOCHIP = 480
DEBOUNCE = 2 # 连续多少次采样一致才确认跳变
DEFAULT_INTERVAL = 0.02
LOG_DIR_ENV = "DI_LOG_DIR"
STATUS_FILE_ENV = "DI_STATUS_FILE" # 仅用于测试
# 候选状态文件路径
STATUS_CANDIDATES = [
f"/sys/bus/platform/devices/gpio-in-{IN_INDEX}/gpio_status",
f"/dev/tv_gpioin_{IN_INDEX}",
]
IN1_CANDIDATES = [
"/sys/bus/platform/devices/gpio-in-1/gpio_status",
"/dev/tv_gpioin_1",
]
DEVICETREE_BASE = Path("/sys/firmware/devicetree/base")
# ---------------------------------------------------------------------------
# 工具函数
# ---------------------------------------------------------------------------
def usage() -> str:
"""返回脚本头部注释作为用法说明。"""
return __doc__ or ""
def read_level_of(path: str):
"""读取状态文件,返回 '0' / '1',失败返回 None。"""
try:
with open(path, "r") as f:
v = f.read()
except (OSError, IOError):
return None
v = v.strip().strip("\t\r\n ")
if v in ("0", "1"):
return v
return None
def ts_short() -> str:
"""HH:MM:SS.mmm"""
now = datetime.datetime.now()
return now.strftime("%H:%M:%S.") + f"{now.microsecond // 1000:03d}"
def fmt_dur(ms: int) -> str:
"""毫秒格式化。"""
if ms < 1000:
return f"{ms}ms"
return f"{ms / 1000:.3f}s"
def probe_writable(path: Path) -> bool:
"""检查目录是否可写,必要时创建。"""
try:
path.mkdir(parents=True, exist_ok=True)
except OSError:
return False
test_file = path / f".wtest.{os.getpid()}"
try:
with open(test_file, "w"):
pass
test_file.unlink()
return True
except OSError:
try:
test_file.unlink()
except OSError:
pass
return False
# ---------------------------------------------------------------------------
# 设备树复算(等价于 Bash 的 derive_gpio)
# ---------------------------------------------------------------------------
def derive_gpio(n: int):
"""从设备树复算 IN<n> 的全局 GPIO 号;失败返回 None。"""
f = DEVICETREE_BASE / f"gpio-in-{n}" / "control-gpios"
if not f.exists():
return None
try:
data = f.read_bytes()
except OSError:
return None
if len(data) < 8:
return None
# 前 4 字节 phandle,后 4 字节 pin(大端)
ph = int.from_bytes(data[0:4], "big")
pin = int.from_bytes(data[4:8], "big")
# phandle -> gpio-controller 节点
ctrl = None
for c in DEVICETREE_BASE.glob("gpio@*/gpio-controller@*"):
ph_file = c / "phandle"
if not ph_file.exists():
continue
try:
cph = int.from_bytes(ph_file.read_bytes()[:4], "big")
except OSError:
continue
if cph == ph:
ctrl = c
break
if ctrl is None:
return None
# gpio@<地址>/gpio-controller@N -> gpiochip label "<地址>.gpio" -> base
bank = ctrl.parent.name # gpio@27010000
bank = bank[len("gpio@"):] if bank.startswith("gpio@") else bank
label = f"{bank}.gpio"
base = None
gpio_class = Path("/sys/class/gpio")
if gpio_class.exists():
for g in gpio_class.glob("gpiochip*"):
label_file = g / "label"
base_file = g / "base"
if not label_file.exists() or not base_file.exists():
continue
try:
if label_file.read_text().strip() == label:
base = int(base_file.read_text().strip())
break
except (OSError, ValueError):
continue
if base is None:
return None
return base + pin
# ---------------------------------------------------------------------------
# 主监听器
# ---------------------------------------------------------------------------
class Watcher:
def __init__(self, interval: float, dev: str, dev_in1: str, log_dir: Path):
self.interval = interval
self.dev = dev
self.dev_in1 = dev_in1
self.log_dir = log_dir
self.log_path = None
self.log_fh = None
self.state = None
self.count = 0
self.ms_low = 0
self.ms_high = 0
self.state_since = 0
self._stop = False
# ---------- 日志 ----------
def open_log(self):
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_path = self.log_dir / f"in2_{ts}.log"
try:
self.log_fh = open(self.log_path, "a", encoding="utf-8")
except OSError:
self.log_fh = None
def say(self, msg: str):
print(msg)
if self.log_fh:
try:
self.log_fh.write(msg + "\n")
self.log_fh.flush()
except OSError:
pass
# ---------- 电平读取 ----------
def read_level(self):
return read_level_of(self.dev)
def peer(self) -> str:
if self.dev_in1:
v = read_level_of(self.dev_in1)
if v is not None:
return f"IN1={v}"
return "IN1=-"
# ---------- 信号处理 ----------
def request_stop(self, *_args):
self._stop = True
# ---------- 汇总 ----------
def print_summary(self):
now_ms = int(time.time() * 1000)
dur = now_ms - self.state_since
if self.state == "0":
self.ms_low += dur
elif self.state == "1":
self.ms_high += dur
self.say("")
self.say("============================================================")
self.say(f"汇总 {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self.say(f" 跳变次数:{self.count}")
self.say(f" 低电平累计:{fmt_dur(self.ms_low)}")
self.say(f" 高电平累计:{fmt_dur(self.ms_high)}")
self.say(f" 结束电平:{self.state}")
if self.count == 0:
self.say("")
self.say(f" 整段时间内 IN{IN_INDEX} 没有任何变化。")
self.say(f" 若此时确实短接过 IN{IN_INDEX} 与 COM,说明激励没真正加到端子上;")
self.say(f" 请确认接的是 IN{IN_INDEX}(不是 IN1)且 COM 已共地,必要时上 12V 再试。")
self.say(f" 日志已保存:{self.log_path}")
self.say("============================================================")
# ---------- 主循环 ----------
def run(self):
# 首次采样
state = self.read_level()
if state is None:
print(f"错误:读不出 {self.dev} 的有效电平(期望 0/1)。", file=sys.stderr)
try:
content = Path(self.dev).read_text()[:64]
except OSError:
content = "<无法读取>"
print(f" 内容:{content}", file=sys.stderr)
return 1
self.state = state
self.state_since = int(time.time() * 1000)
self.open_log()
self.say("============================================================")
self.say(f"IN{IN_INDEX} 监听开始 {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self.say(f" IN{IN_INDEX} = GPIO {GPIO_NUM} = gpiochip{GPIOCHIP} 第 {GPIO_NUM - GPIOCHIP} 线")
self.say(f" 状态文件:{self.dev}")
self.say(f" 轮询间隔:{self.interval}s 抗抖:连续 {DEBOUNCE} 次采样一致")
self.say(f" 日志:{self.log_path}")
self.say("============================================================")
self.say(f"初始电平:{self.state}")
self.say("")
self.say("把 IN1 与 IN2 短接约 2 秒再松开(实测这根接法才有效;单接 IN2-GND 无效)。")
self.say("下面每行会同时给出 IN1 和 IN2 的电平。Ctrl-C 结束。")
self.say("")
cand = None
cand_n = 0
# 注册信号
signal.signal(signal.SIGINT, self.request_stop)
signal.signal(signal.SIGTERM, self.request_stop)
try:
while not self._stop:
v = self.read_level()
if v is None:
self.say(f"{ts_short()} 读取失败,跳过本次采样")
time.sleep(self.interval)
continue
now_ms = int(time.time() * 1000)
if v == self.state:
cand = None
cand_n = 0
else:
if v == cand:
cand_n += 1
else:
cand = v
cand_n = 1
if cand_n >= DEBOUNCE:
dur = now_ms - self.state_since
if self.state == "0":
self.ms_low += dur
else:
self.ms_high += dur
self.count += 1
self.say(
f"{ts_short()} 变化 IN{IN_INDEX}: {self.state} -> {v}"
f" [{self.peer()}] 上一状态持续 {fmt_dur(dur)}"
)
self.state = v
self.state_since = now_ms
cand = None
cand_n = 0
time.sleep(self.interval)
finally:
self.print_summary()
if self.log_fh:
self.log_fh.close()
return 0
# ---------------------------------------------------------------------------
# --check 模式
# ---------------------------------------------------------------------------
def do_check(dev: str, interval: float):
print(f"=== IN{IN_INDEX} 编号自检 ===")
print()
print(f"状态文件:{dev}")
print(f"轮询间隔:{interval}s(本模式不轮询)")
print()
print("--- 从设备树现场复算 ---")
ok = True
for n in (1, 2):
got = derive_gpio(n)
if got is None:
print(f" IN{n}:复算失败(设备树 gpio-in-{n} 或对应 gpiochip 不可读)")
ok = False
continue
expect = 481 + n # IN1 -> 482, IN2 -> 483
if got == expect:
print(f" IN{n} -> GPIO {got} ✓ 与预期一致")
else:
print(f" IN{n} -> GPIO {got} ✗ 预期 {expect},需人工核对")
ok = False
print()
if ok:
print(f"结论:IN{IN_INDEX} = GPIO {GPIO_NUM}"
f"(gpiochip{GPIOCHIP} 第 {GPIO_NUM - GPIOCHIP} 线),推导成立。")
else:
print("结论:推导与实测不符,请把上面的输出贴出来再排查。")
print()
print("--- 当前电平 ---")
v = read_level_of(dev)
if v is not None:
print(f" gpio-in-{IN_INDEX}/gpio_status = {v} (对应的全局 GPIO 为 {GPIO_NUM})")
print(" 注:待机时读到 0 是正常的,激励到来时才会翻转。")
else:
print(f" 读取失败:{dev} 内容不是 0/1。")
return 0
# ---------------------------------------------------------------------------
# 入口
# ---------------------------------------------------------------------------
def find_status_file() -> str:
"""定位 IN2 状态文件。"""
# 测试用环境变量
test_file = os.environ.get(STATUS_FILE_ENV)
if test_file and os.access(test_file, os.R_OK):
return test_file
for c in STATUS_CANDIDATES:
if os.access(c, os.R_OK):
return c
return ""
def find_in1_file() -> str:
"""定位 IN1 状态文件(可选)。"""
for c in IN1_CANDIDATES:
if os.access(c, os.R_OK):
return c
return ""
def main(argv=None):
parser = argparse.ArgumentParser(
description=usage(),
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=True,
)
parser.add_argument(
"interval", nargs="?", default=str(DEFAULT_INTERVAL),
help=f"轮询间隔秒(默认 {DEFAULT_INTERVAL})"
)
parser.add_argument(
"--check", action="store_true",
help="只做自检:复算编号 + 打印当前电平,然后退出"
)
args = parser.parse_args(argv)
# 解析 interval
try:
interval = float(args.interval)
except ValueError:
print(f"错误:轮询间隔 '{args.interval}' 不是正数。", file=sys.stderr)
return 1
if interval <= 0:
print(f"错误:轮询间隔 '{args.interval}' 不是正数。", file=sys.stderr)
return 1
# 定位状态文件
dev = find_status_file()
if not dev:
print(f"错误:找不到 gpio-in-{IN_INDEX} 的状态文件。", file=sys.stderr)
print(f" 已尝试:{STATUS_CANDIDATES[0]}", file=sys.stderr)
print(f" {STATUS_CANDIDATES[1]}", file=sys.stderr)
print(" 排查:ls /sys/bus/platform/devices/ | grep gpio-in", file=sys.stderr)
return 1
# --check 模式
if args.check:
return do_check(dev, interval)
# 准备日志目录
log_dir = Path(os.environ.get(LOG_DIR_ENV, str(Path.home() / "di_logs")))
if not probe_writable(log_dir):
print(f"提示:{log_dir} 不可写(属主可能是 root),本次日志改写到 {Path.home()}",
file=sys.stderr)
print(f" 想恢复到 {log_dir}:sudo chown {os.environ.get('USER', '')} {log_dir}",
file=sys.stderr)
log_dir = Path.home()
# IN1 一并盯着
dev_in1 = find_in1_file()
watcher = Watcher(interval, dev, dev_in1, log_dir)
return watcher.run()
if __name__ == "__main__":
sys.exit(main())