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

205 lines
5.9 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 -*-
"""DIChannel:统一的数字输入通道。
把不同后端(event / sysfs)归一成同一套 API:
ch = DIChannel("IN1", backend=make_in1_backend(), active_low=True)
ch.on_change = lambda old, new, ts: ...
ch.run() # 阻塞
ch.run(duration=10) # 跑 10 秒
电平语义:
对外统一用 0/1,0 = 低/断电,1 = 高/有电。
active_low=True 时,把底层原始值取反(适配光耦反相)。
"""
import datetime
import select
import time
def now_str():
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
def fmt_dur(ms):
if ms < 1000:
return f"{ms}ms"
return f"{ms / 1000:.3f}s"
class DIChannel:
"""单个数字输入通道。"""
def __init__(self, name, backend, active_low=False,
on_change=None, on_press=None, on_release=None,
on_event=None, log=None):
"""
name : 通道名,如 "IN1"
backend : Backend 实例
active_low : True 时把原始值取反(0->1, 1->0)
on_change : (old, new, ts) -> None
on_press : (ts) -> None # new=1
on_release : (ts) -> None # new=0
on_event : (ts, raw) -> None # 原始事件
log : (msg) -> None
"""
self.name = name
self.backend = backend
self.active_low = active_low
self.on_change = on_change
self.on_press = on_press
self.on_release = on_release
self.on_event = on_event
self.log = log or (lambda m: print(m, flush=True))
self._level = None
self._level_since = None
self._stop = False
self._count = 0
self._ms_low = 0
self._ms_high = 0
# ---- 上下文管理 ----
def __enter__(self):
self.open()
return self
def __exit__(self, *exc):
self.close()
def open(self):
self.backend.open()
def close(self):
self.backend.close()
# ---- 电平换算 ----
def _to_level(self, raw):
if raw is None:
return None
v = 1 if raw else 0
if self.active_low:
v = 1 - v
return v
# ---- 回调分发 ----
def _emit(self, new, ts):
old = self._level
if old == new:
return
self._level = new
now_ms = int(time.time() * 1000)
if self._level_since is not None:
dur = now_ms - self._level_since
if old == 0:
self._ms_low += dur
elif old == 1:
self._ms_high += dur
self._level_since = now_ms
self._count += 1
self.log(f"{now_str()} {self.name} {'有电' if new else '断电'}")
if self.on_change:
self.on_change(old, new, ts)
if new == 1 and self.on_press:
self.on_press(ts)
elif new == 0 and self.on_release:
self.on_release(ts)
# ---- 单次读取(用于初始化)----
def read_now(self):
"""立即读一次并返回归一化后的电平;失败返回 None。
仅对支持 read_raw 的后端(sysfs)有效;event 后端返回 None。
"""
raw = None
if hasattr(self.backend, "read_raw"):
raw = self.backend.read_raw()
if raw is None:
return None
lvl = self._to_level(int(raw))
self._level = lvl
self._level_since = int(time.time() * 1000)
return lvl
# ---- 主循环 ----
def run(self, duration=None):
"""阻塞监听。duration 秒后返回;None = 一直跑到 Ctrl-C。"""
self.open()
# 初始化当前电平
if self._level is None:
lvl = self.read_now()
if lvl is not None:
self.log(f"{self.name} 初始电平:{lvl}")
else:
# event 后端:置为 0,等第一个事件再校正
self._level = 0
self._level_since = int(time.time() * 1000)
deadline = time.time() + duration if duration else None
try:
if self.backend.can_poll:
self._run_poll(deadline)
else:
self._run_sample(deadline)
finally:
self.close()
def _run_poll(self, deadline):
poller = select.poll()
poller.register(self.backend.fileno(), select.POLLIN)
while not self._stop:
timeout = 500
if deadline:
remain = deadline - time.time()
if remain <= 0:
break
timeout = int(min(remain, 0.5) * 1000)
poller.poll(timeout)
got = self.backend.read()
if got:
raw, ts = got
if self.on_event:
self.on_event(ts, raw)
self._emit(self._to_level(raw), ts)
def _run_sample(self, deadline):
interval = getattr(self.backend, "interval", 0.02)
while not self._stop:
if deadline and time.time() >= deadline:
break
got = self.backend.read()
if got:
raw, ts = got
lvl = self._to_level(raw)
if self.on_event:
self.on_event(ts, raw)
self._emit(lvl, ts)
time.sleep(interval)
def request_stop(self):
self._stop = True
# ---- 汇总 ----
def summary(self):
now_ms = int(time.time() * 1000)
if self._level_since is not None:
dur = now_ms - self._level_since
if self._level == 0:
self._ms_low += dur
elif self._level == 1:
self._ms_high += dur
self._level_since = now_ms
return {
"name": self.name,
"count": self._count,
"ms_low": self._ms_low,
"ms_high": self._ms_high,
"level": self._level,
}