modify: DI 输入监听
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
#!/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,
|
||||
}
|
||||
Reference in New Issue
Block a user