autocal: 新增单位与换算页(默认锁定)及轴页报警与事件日志

- 设备设置新增“单位与换算”页:显示各轴 UNITS、读回值、脉冲/mm 换算与核对状态;
  默认锁定,点击“修改”后才可编辑,应用前弹窗确认,旋转轴偏离标定值时额外警告
- 轴调试页:脉冲当量改为只读并提供入口;新增报警与事件日志(报警、使能、
  回零、读取失败、单位不符等)及单位核对摘要
- 新增 motion/unit_report.py、motion/events.py 及对应测试
This commit is contained in:
ykcheungab
2026-10-19 10:14:55 +08:00
parent c6c16df753
commit 22658dbfc1
12 changed files with 1462 additions and 10 deletions
+95
View File
@@ -0,0 +1,95 @@
"""The axis event log: only changes, a baseline on first sight, nothing across a gap."""
from __future__ import annotations
from orisys_autocal.motion import AxisState, Snapshot
from orisys_autocal.motion import events as ev
from orisys_autocal.motion.models import AxisStatusBits
from orisys_autocal.motion.events import AxisEventTracker
def _axis(axis=0, *, alarm=False, enabled=True, read_ok=True, homing=False, home_status=0, units=10.0, error=""):
raw = (512 if alarm else 0) | (64 if homing else 0)
return AxisState(
axis=axis,
enabled=enabled,
units=units,
status=AxisStatusBits(raw),
home_status=home_status,
read_ok=read_ok,
error=error,
)
def _snap(*axes, connected=True) -> Snapshot:
return Snapshot(connected=connected, axes=tuple(axes))
def _kinds(events):
return [(event.axis, event.kind) for event in events]
def test_the_first_poll_is_a_baseline_and_a_steady_state_is_silent():
tracker = AxisEventTracker(clock=lambda: 1.0)
assert tracker.update(_snap(_axis(0), _axis(1))) == []
assert tracker.update(_snap(_axis(0), _axis(1))) == []
assert tracker.events == []
def test_an_alarm_that_is_already_up_when_first_seen_is_reported():
tracker = AxisEventTracker()
assert _kinds(tracker.update(_snap(_axis(2, alarm=True)))) == [(2, ev.ALARM_ON)]
def test_alarm_enable_and_homing_edges_are_reported_once():
tracker = AxisEventTracker()
tracker.update(_snap(_axis(0, enabled=False)))
assert _kinds(tracker.update(_snap(_axis(0, enabled=True)))) == [(0, ev.ENABLED)]
assert _kinds(tracker.update(_snap(_axis(0, alarm=True)))) == [(0, ev.ALARM_ON)]
assert tracker.update(_snap(_axis(0, alarm=True))) == []
assert _kinds(tracker.update(_snap(_axis(0)))) == [(0, ev.ALARM_OFF)]
assert _kinds(tracker.update(_snap(_axis(0, homing=True)))) == [(0, ev.HOME_STARTED)]
assert _kinds(tracker.update(_snap(_axis(0, home_status=1)))) == [(0, ev.HOME_DONE)]
tracker.update(_snap(_axis(0, homing=True)))
assert _kinds(tracker.update(_snap(_axis(0, home_status=0)))) == [(0, ev.HOME_INCOMPLETE)]
def test_a_read_failure_and_its_recovery_are_reported():
tracker = AxisEventTracker()
tracker.update(_snap(_axis(1)))
failed = tracker.update(_snap(_axis(1, read_ok=False, error="timeout")))
assert [(e.axis, e.kind, e.detail) for e in failed] == [(1, ev.READ_FAILED, "timeout")]
assert tracker.update(_snap(_axis(1, read_ok=False, error="timeout"))) == []
assert _kinds(tracker.update(_snap(_axis(1)))) == [(1, ev.READ_OK)]
def test_a_gap_in_the_connection_resets_the_baseline():
tracker = AxisEventTracker()
tracker.update(_snap(_axis(0)))
assert _kinds(tracker.update(_snap(connected=False))) == [(ev.NO_AXIS, ev.DISCONNECTED)]
# Back again with the axis alarmed: the connection edge, then the alarm as a
# first sight; nothing is compared with a state from before the gap.
back = tracker.update(_snap(_axis(0, alarm=True)))
assert _kinds(back) == [(ev.NO_AXIS, ev.CONNECTED), (0, ev.ALARM_ON)]
def test_units_disagreeing_with_the_configuration_is_an_event_both_ways():
tracker = AxisEventTracker()
expected = {0: 10.0}
assert tracker.update(_snap(_axis(0, units=10.0)), expected) == []
mismatch = tracker.update(_snap(_axis(0, units=5.0)), expected)
assert [(e.kind, e.detail) for e in mismatch] == [(ev.UNITS_MISMATCH, "10|5")]
assert tracker.update(_snap(_axis(0, units=5.0)), expected) == []
assert _kinds(tracker.update(_snap(_axis(0, units=10.0)), expected)) == [(0, ev.UNITS_OK)]
# With nothing to compare against no verdict is invented.
assert AxisEventTracker().update(_snap(_axis(0, units=5.0))) == []
def test_the_log_is_bounded_and_can_be_cleared():
tracker = AxisEventTracker(limit=3)
tracker.update(_snap(_axis(0)))
for index in range(10):
tracker.update(_snap(_axis(0, alarm=bool(index % 2 == 0))))
assert len(tracker.events) == 3
tracker.clear()
assert tracker.events == []