#!/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, }