fix(autocal): 校验运动单位并等待命令成功后判断到位
阻止单位不一致的姿态命令,避免提前到达的位置快照误报成功或推进下一轴。 相关检查 164 项通过,2064 组目标及 8256 条运动调用与原基线完全一致。
This commit is contained in:
@@ -5,7 +5,15 @@ import math
|
||||
|
||||
import pytest
|
||||
|
||||
from orisys_autocal.motion.models import AxisParams, BusStatus, ConnectionSpec
|
||||
from orisys_autocal.motion.models import (
|
||||
AxisParams,
|
||||
AxisState,
|
||||
AxisStatusBits,
|
||||
BusStatus,
|
||||
ConnectionSpec,
|
||||
MotionResult,
|
||||
Snapshot,
|
||||
)
|
||||
from orisys_autocal.motion.pose_move import PoseMover, plan_pose_move
|
||||
from orisys_autocal.motion.service import MotionService
|
||||
from orisys_autocal.motion.transport import DemoTransport
|
||||
@@ -185,3 +193,86 @@ def test_raw_debug_moves_keep_the_existing_user_unit_contract():
|
||||
transport.set_units(2, 5.0)
|
||||
assert instance.move_abs(2, 90.0).ok
|
||||
assert transport.moves == [(2, 90.0)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("together", [False, True])
|
||||
@pytest.mark.parametrize("failure", ["refused", "exception"])
|
||||
def test_arrival_before_command_failure_never_reports_success(qt_app, together, failure):
|
||||
instance, transport = service()
|
||||
actions = []
|
||||
mover = PoseMover(instance, lambda action: actions.append(action) or len(actions))
|
||||
finished = []
|
||||
mover.finished.connect(finished.append)
|
||||
assert mover.start(plan_pose_move(instance, {"x": 0.0}, together=together)).ok
|
||||
mover.on_snapshot(Snapshot(axes=(instance._last_axes[0],)))
|
||||
assert not finished and mover.running
|
||||
|
||||
if failure == "refused":
|
||||
transport.set_units(0, 5.0)
|
||||
result = actions[0](instance)
|
||||
assert not result.ok and "UNITS" in result.message
|
||||
mover.on_command_finished(1, result)
|
||||
else:
|
||||
mover.on_command_failed(1, "read failed")
|
||||
assert mover.state.failure and not mover.state.finished
|
||||
assert not finished and not mover.running
|
||||
assert transport.moves == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("together", [False, True])
|
||||
def test_only_matching_success_and_a_later_snapshot_allow_arrival(qt_app, together):
|
||||
instance, _transport = service()
|
||||
actions = []
|
||||
mover = PoseMover(instance, lambda action: actions.append(action) or len(actions))
|
||||
assert mover.start(plan_pose_move(instance, {"x": 0.0}, together=together)).ok
|
||||
snapshot = Snapshot(axes=(instance._last_axes[0],))
|
||||
mover.on_command_finished(99, MotionResult.success())
|
||||
mover.on_snapshot(snapshot)
|
||||
assert mover.running and not mover.state.finished
|
||||
result = actions[0](instance)
|
||||
assert result.ok
|
||||
mover.on_command_finished(1, result)
|
||||
assert mover.running, "a pre-acknowledgement snapshot was reused"
|
||||
mover.on_snapshot(Snapshot(axes=(instance._last_axes[0],)))
|
||||
assert mover.state.finished and not mover.running
|
||||
|
||||
|
||||
def test_each_sequential_axis_waits_for_its_own_command_success(qt_app):
|
||||
instance, _transport = service()
|
||||
actions = []
|
||||
mover = PoseMover(instance, lambda action: actions.append(action) or len(actions))
|
||||
assert mover.start(plan_pose_move(instance, {"x": 0.0, "y": 0.0})).ok
|
||||
snapshot = Snapshot(axes=tuple(instance._last_axes[axis] for axis in (0, 1)))
|
||||
mover.on_snapshot(snapshot)
|
||||
assert len(actions) == 1 and mover.state.index == 0
|
||||
result = actions[0](instance)
|
||||
assert result.ok
|
||||
mover.on_command_finished(1, result)
|
||||
mover.on_snapshot(snapshot)
|
||||
assert len(actions) == 2 and mover.state.index == 1
|
||||
mover.on_command_finished(1, MotionResult.success())
|
||||
mover.on_snapshot(snapshot)
|
||||
assert mover.running and not mover.state.finished
|
||||
result = actions[1](instance)
|
||||
assert result.ok
|
||||
mover.on_command_finished(2, result)
|
||||
mover.on_snapshot(snapshot)
|
||||
assert mover.state.finished
|
||||
|
||||
|
||||
@pytest.mark.parametrize("together", [False, True])
|
||||
@pytest.mark.parametrize("fault", ["alarm", "read"])
|
||||
def test_axis_faults_remain_visible_while_command_is_pending(qt_app, together, fault):
|
||||
instance, _transport = service()
|
||||
mover = PoseMover(instance, lambda _action: 1)
|
||||
targets = {"x": 0.0, "y": 0.0} if together else {"y": 0.0}
|
||||
assert mover.start(plan_pose_move(instance, targets, together=together)).ok
|
||||
bad_axis = AxisState(
|
||||
axis=1,
|
||||
units=10.0,
|
||||
status=AxisStatusBits(raw=512 if fault == "alarm" else 0),
|
||||
read_ok=fault != "read",
|
||||
)
|
||||
mover.on_snapshot(Snapshot(axes=(AxisState(axis=0, units=10.0), bad_axis)))
|
||||
assert not mover.running and not mover.state.finished
|
||||
assert ("alarm" if fault == "alarm" else "read failed") in mover.state.failure
|
||||
|
||||
@@ -34,6 +34,8 @@ def _connected(make_window, *, confirm: bool = False):
|
||||
service = window.poller.service
|
||||
service._spec = object() # noqa: SLF001 - connected, without opening anything
|
||||
service._bus = BusStatus(bus_type="ecat", init_status=1, total_axes=5) # noqa: SLF001
|
||||
# A ready physical-unit move also has a successful unit readback.
|
||||
service._last_axes = {axis: service._read_axis(axis) for axis in range(5)}
|
||||
return window
|
||||
|
||||
|
||||
|
||||
@@ -54,11 +54,13 @@ def make_service(**kwargs) -> MotionService:
|
||||
)
|
||||
service._spec = object() # noqa: SLF001 - connected, without opening anything
|
||||
service._bus = BusStatus(bus_type="ecat", init_status=1, total_axes=5) # noqa: SLF001
|
||||
# Physical-unit planning requires observed units, just as the live poll supplies.
|
||||
service._last_axes = {axis: service._read_axis(axis) for axis in range(5)}
|
||||
return service
|
||||
|
||||
|
||||
class FakePoller:
|
||||
"""Records submissions and runs them, so a plan's commands can be inspected."""
|
||||
"""Records submissions and explicitly acknowledges simulated worker results."""
|
||||
|
||||
def __init__(self, service: MotionService) -> None:
|
||||
self.service = service
|
||||
@@ -71,6 +73,9 @@ class FakePoller:
|
||||
# this test exists to check.
|
||||
return len(self.commands)
|
||||
|
||||
def acknowledge(self, mover: PoseMover) -> None:
|
||||
mover.on_command_finished(len(self.commands), MotionResult.success())
|
||||
|
||||
|
||||
def reading(axis: int, dpos: float, *, idle: bool = True, alarm: bool = False) -> AxisState:
|
||||
return AxisState(
|
||||
@@ -284,6 +289,7 @@ def test_an_execute_plan_moves_the_four_axes_together_and_leaves_z():
|
||||
assert len(poller.commands) == 1
|
||||
poller.commands[0](service)
|
||||
assert [axis for axis, _value in called[0]] == [3, 2, 0, 1]
|
||||
poller.acknowledge(mover)
|
||||
|
||||
mover.on_snapshot(
|
||||
snapshot_of(service, {0: 1000.0, 1: 0.0, 2: 0.0, 3: 0.0}, idle=False)
|
||||
@@ -332,11 +338,13 @@ def test_the_mover_commands_one_axis_and_waits_for_it_to_arrive():
|
||||
plan = plan_pose_move(service, {"x": 2.0, "y": 1.0})
|
||||
assert mover.start(plan).ok
|
||||
assert len(poller.commands) == 1, "more than the first axis was commanded at once"
|
||||
poller.acknowledge(mover)
|
||||
|
||||
# x is at its target and idle -> the second axis goes out.
|
||||
mover.on_snapshot(snapshot_of(service, {0: 1000.0, 1: 0.0}))
|
||||
assert len(poller.commands) == 2
|
||||
assert mover.state.step.role == "y"
|
||||
poller.acknowledge(mover)
|
||||
|
||||
# y arrives -> the plan is done.
|
||||
mover.on_snapshot(snapshot_of(service, {0: 1000.0, 1: 500.0}))
|
||||
@@ -349,6 +357,7 @@ def test_the_mover_waits_while_an_axis_is_still_moving():
|
||||
poller = FakePoller(service)
|
||||
mover = PoseMover(service, poller.submit)
|
||||
mover.start(plan_pose_move(service, {"x": 2.0, "y": 1.0}))
|
||||
poller.acknowledge(mover)
|
||||
mover.on_snapshot(snapshot_of(service, {0: 990.0, 1: 0.0}, idle=False))
|
||||
assert len(poller.commands) == 1
|
||||
assert mover.state.index == 0
|
||||
@@ -370,6 +379,7 @@ def test_a_long_yaw_is_not_cut_off_while_it_is_still_moving(monkeypatch):
|
||||
poller = FakePoller(service)
|
||||
mover = PoseMover(service, poller.submit)
|
||||
assert mover.start(plan_pose_move(service, {"yaw": 180.0})).ok
|
||||
poller.acknowledge(mover)
|
||||
clock["t"] += 120.0
|
||||
mover.on_snapshot(snapshot_of(service, {3: 1_000.0}, idle=False))
|
||||
assert mover.running
|
||||
@@ -383,6 +393,7 @@ def test_the_mover_fails_when_an_axis_stops_short():
|
||||
poller = FakePoller(service)
|
||||
mover = PoseMover(service, poller.submit)
|
||||
mover.start(plan_pose_move(service, {"x": 2.0, "y": 1.0}))
|
||||
poller.acknowledge(mover)
|
||||
mover.on_snapshot(snapshot_of(service, {0: 10.0, 1: 0.0}))
|
||||
assert mover.running, "gave up before the stall grace elapsed"
|
||||
mover.state._idle_since = time.monotonic() - 10.0 # noqa: SLF001
|
||||
@@ -408,6 +419,7 @@ def test_a_tolerance_accepts_an_axis_that_reports_its_final_position_late():
|
||||
mover = PoseMover(service, poller.submit)
|
||||
plan = plan_pose_move(service, {"x": 2.0, "y": 1.0})
|
||||
mover.start(plan)
|
||||
poller.acknowledge(mover)
|
||||
# 1 unit inside SETTLE_MM of 1000 units for 2 mm.
|
||||
mover.on_snapshot(snapshot_of(service, {0: 1000.0 - 1.0, 1: 0.0}))
|
||||
assert len(poller.commands) == 2, "a step within tolerance was treated as a miss"
|
||||
|
||||
Reference in New Issue
Block a user