diff --git a/apps/autocal/src/orisys_autocal/motion/pose_move.py b/apps/autocal/src/orisys_autocal/motion/pose_move.py index 619a1b5..88ad6dd 100644 --- a/apps/autocal/src/orisys_autocal/motion/pose_move.py +++ b/apps/autocal/src/orisys_autocal/motion/pose_move.py @@ -27,7 +27,7 @@ from __future__ import annotations import time from collections.abc import Mapping, Sequence -from dataclasses import dataclass, field +from dataclasses import dataclass from PySide6.QtCore import QObject, Signal @@ -43,10 +43,6 @@ from .units import user_units_to_deg SETTLE_MM = 0.01 SETTLE_DEG = 0.01 -#: Per-step patience. A step that is neither arriving nor moving this long is -#: treated as stuck: reported and abandoned, with no further axes commanded. -STEP_TIMEOUT_S = 20.0 - #: How long a step may report itself idle while still short of its target before it #: counts as failed — a drive that has stopped without arriving will never arrive. STALL_GRACE_S = 2.0 @@ -103,7 +99,6 @@ class MoveState: finished: bool = False failure: str = "" cancelled: bool = False - _step_started_at: float = field(default=0.0) _idle_since: float | None = None @property @@ -205,7 +200,7 @@ class PoseMover(QObject): return MotionResult.refused("没有要移动的轴 / nothing to move") stamp = time.monotonic() if now is None else now - self.state = MoveState(plan=plan, started_at=stamp, _step_started_at=stamp) + self.state = MoveState(plan=plan, started_at=stamp) self.started.emit(plan) self._issue_current() return MotionResult.success(plan.describe()) @@ -229,7 +224,6 @@ class PoseMover(QObject): if step is None: self._complete() return - self.state._step_started_at = time.monotonic() self.state._idle_since = None if not step.units == step.units: # NaN: no calibration for this role self._fail(f"{step.role} 无法换算成用户单位 / cannot convert {step.role}") @@ -258,12 +252,6 @@ class PoseMover(QObject): if step is None: self._complete() return - now = time.monotonic() - if now - self.state._step_started_at > STEP_TIMEOUT_S: - self._fail( - f"{step.role} 等待超时 / timed out waiting for {step.role}" - ) - return state = snapshot.axis(step.axis) if state is None: @@ -292,6 +280,7 @@ class PoseMover(QObject): return # Idle, but not there. A drive that has stopped short will not resume, so # this is a failure rather than something to keep waiting for. + now = time.monotonic() if self.state._idle_since is None: self.state._idle_since = now elif now - self.state._idle_since > STALL_GRACE_S: diff --git a/apps/autocal/tests/test_pose_move.py b/apps/autocal/tests/test_pose_move.py index a4ccd7b..ab1ec6a 100644 --- a/apps/autocal/tests/test_pose_move.py +++ b/apps/autocal/tests/test_pose_move.py @@ -245,6 +245,29 @@ def test_the_mover_waits_while_an_axis_is_still_moving(): assert mover.state.index == 0 +def test_a_long_yaw_is_not_cut_off_while_it_is_still_moving(monkeypatch): + """A yaw past 90° can take longer than a minute. Keep it while the axis moves. + + The old step cap abandoned the plan after 20s and the window then stopped + every axis, so a 180° yaw never arrived. + """ + clock = {"t": 1_000.0} + monkeypatch.setattr( + "orisys_autocal.motion.pose_move.time.monotonic", lambda: clock["t"] + ) + service = make_service( + limits={**LIMITS, "max_yaw_deg": 180.0, "max_pitch_deg": 360.0} + ) + poller = FakePoller(service) + mover = PoseMover(service, poller.submit) + assert mover.start(plan_pose_move(service, {"yaw": 180.0})).ok + clock["t"] += 120.0 + mover.on_snapshot(snapshot_of(service, {3: 1_000.0}, idle=False)) + assert mover.running + assert mover.state.failure == "" + assert len(poller.commands) == 1 + + def test_the_mover_fails_when_an_axis_stops_short(): """A drive that has stopped is not going to arrive: say so, do not wait.""" service = make_service()