Merge branch 'main' of ssh://892887cvmk08.aicp.io:24928/orisys-tech/orisys-app

This commit is contained in:
ykcheungab
2026-10-10 17:17:43 +08:00
8 changed files with 58 additions and 49 deletions
+3 -3
View File
@@ -48,14 +48,14 @@
},
"move": {
"execute": "Execute",
"execute_hint": "Send the selected point's solved pose to the platform: yaw, pitch and table X/Y move together. The press axis Z is not moved.",
"execute_hint": "Send the selected point's solved pose to the platform: yaw, pitch, table X/Y and press axis Z move together.",
"stop": "Stop",
"stop_hint": "Stop every axis now (the same command as 全部停止 in the strip).",
"return_initial": "Return to zero",
"return_hint": "Every axis back to the initial state, press axis Z first.",
"return_hint": "Yaw, pitch, table X/Y and press axis Z return to zero together.",
"confirm_title": "Confirm motion",
"confirm_execute": "Will move these axes together:\n\n{steps}\n\nCheck this against the machine before continuing.",
"confirm_return": "Will move every axis back to zero (press axis Z first):\n\n{steps}",
"confirm_return": "Will move these axes back to zero together:\n\n{steps}",
"hold_note": "Not moved: {roles} (the press axis is P7's business — contact detection and press depth)",
"cancelled": "Cancelled; nothing was commanded",
"confirm_check": "Confirm before moving",
+3 -3
View File
@@ -48,14 +48,14 @@
},
"move": {
"execute": "执行动作",
"execute_hint": "把选中点解算出的位姿送到平台:回转、俯仰、工作台 X/Y 同时移动。压轴 Z 不动作。",
"execute_hint": "把选中点解算出的位姿送到平台:回转、俯仰、工作台 X/Y、压轴 Z 同时移动。",
"stop": "停止",
"stop_hint": "立即停止所有轴(与顶栏「全部停止」相同)。",
"return_initial": "回初始位",
"return_hint": "五个轴全部回到零点,先退压轴 Z。",
"return_hint": "回转、俯仰、工作台 X/Y、压轴 Z 一起回到零点。",
"confirm_title": "确认运动",
"confirm_execute": "将同时移动:\n\n{steps}\n\n请确认仿真与机床一致。",
"confirm_return": "将把五个轴全部移回零点(先退压轴 Z):\n\n{steps}",
"confirm_return": "将同时移回零点:\n\n{steps}\n\n请确认机床上没有干涉。",
"hold_note": "不动作:{roles}(压轴由 P7 的接触检测与压深另外管)",
"cancelled": "已取消,未发出任何运动",
"confirm_check": "移动前确认",
@@ -10,10 +10,10 @@ matters:
:class:`~orisys_autocal.motion.poller.MotionPoller` is built, and it is why a stop
is possible at all: a mover that sat in a loop inside one command could not be
interrupted, because the stop would have to wait for the loop it is meant to abort.
* **This module** owns the waiting. An execute plan starts yaw, pitch and table X/Y
in one command so they move together; a return plan still retracts one axis at a
time. It watches the snapshots the poller already produces and gives up on its
own if an axis stops short of its target.
* **This module** owns the waiting. Execute and return-to-zero both start yaw, pitch,
table X/Y and the press axis in one command so the five axes move together. The
mover watches the snapshots the poller already produces and gives up on its own
if an axis stops short of its target.
That shape is the whole reason for the class: ``stop`` is submitted as an ordinary
command, so it runs at the next tick and stops the axis that is moving *now*.
@@ -51,11 +51,8 @@ STALL_GRACE_S = 2.0
#: The order a role move is issued in, for the reason in the module docstring.
MOVE_ORDER: tuple[str, ...] = ("yaw", "pitch", "x", "y", "z")
#: The order "return to the initial state" retracts in: the press axis first, so the
#: tool is clear of the sensor before anything rotates, then the stages, then the
#: table. Returning in one coordinated swoop would move five axes through poses
#: nobody has simulated.
RETURN_ORDER: tuple[str, ...] = ("z", "pitch", "yaw", "x", "y")
#: Axes a pose execute and a return-to-zero command together, including the press axis.
ALIGN_ORDER: tuple[str, ...] = ("yaw", "pitch", "x", "y", "z")
@dataclass(frozen=True)
@@ -82,7 +79,6 @@ class MovePlan:
#: information the operator needs, not an omission.
held: tuple[str, ...] = ()
#: When set, every step is commanded in one call and they travel together.
#: Return-to-zero leaves this off so the press axis clears before anything turns.
together: bool = False
def describe(self) -> str:
@@ -153,11 +149,12 @@ def plan_pose_move(
def plan_return_to_initial(service: MotionService) -> MovePlan:
"""Every axis to zero, press axis first, in :data:`RETURN_ORDER`."""
"""Yaw, pitch, table X/Y and the press axis back to zero together."""
return plan_pose_move(
service,
{role: 0.0 for role in RETURN_ORDER},
order=RETURN_ORDER,
{role: 0.0 for role in ALIGN_ORDER},
order=ALIGN_ORDER,
together=True,
)
@@ -242,7 +239,7 @@ class PoseMover(QObject):
self.stepStarted.emit(step)
def _issue_together(self, plan: MovePlan) -> None:
"""Command every step in one worker call. Z is absent from the plan."""
"""Command every step in one worker call."""
moves: list[tuple[int, float]] = []
for step in plan.steps:
if not step.units == step.units:
@@ -317,7 +314,7 @@ class PoseMover(QObject):
)
def _on_snapshot_together(self, snapshot: Snapshot) -> None:
"""Finish when every commanded axis has arrived. Z is not in the plan."""
"""Finish when every commanded axis has arrived."""
plan = self.state.plan
if plan is None:
return
@@ -750,13 +750,8 @@ class FingerPanel(QGroupBox):
The two numbers that matter are the *table's* move and the stage angles: the
table translation is what the stages are resolved against, and it is what the
preview drew as the cyan path's end.
z is returned as *held*: executing a simulated pose moves the stages and the
table, it does not press. Pressing is P7 and has its own guards — contact
detection, press depth, the sensor's second opinion — none of which exist
here, so a move that reached the contact height would be a press with none of
them.
preview drew as the cyan path's end. Z is the contact height, commanded with
the other four axes in the same move.
"""
pose = self._last_pose
if pose is None:
@@ -767,8 +762,9 @@ class FingerPanel(QGroupBox):
"y": float(table_y),
"yaw": float(pose.yaw_deg),
"pitch": float(pose.pitch_deg),
"z": float(pose.contact_z_mm),
}
return targets, ("z",)
return targets, ()
def _sync_sim_button(self) -> None:
self.sim_button.setText(
+4 -5
View File
@@ -1045,10 +1045,9 @@ class AutoCalMainWindow(QMainWindow):
def _on_execute_pose(self) -> None:
"""Move the platform to the solved pose of the finger screen's selection.
Yaw, pitch and table X/Y start together. The press axis is deliberately
not commanded, so this is workflow P6's dry run for one point — it cannot
reach the sensor. The numbers shown in the confirmation are the ones the
preview drew, so the operator checks the simulation against the machine.
Yaw, pitch, table X/Y and the press axis start together. The Z target is
the solved contact height. The numbers in the confirmation are the ones
the preview drew.
"""
solve = self.finger_panel.solved_pose()
if solve is None:
@@ -1065,7 +1064,7 @@ class AutoCalMainWindow(QMainWindow):
self._start_move(plan, t("move.execute"))
def _on_return_to_initial(self) -> None:
"""Every drive back to zero, press axis first."""
"""Yaw, pitch, table X/Y and the press axis back to zero together."""
plan = plan_return_to_initial(self.poller.service)
self._start_move(plan, t("move.return_initial"))
+8 -2
View File
@@ -202,8 +202,14 @@ def test_accepting_the_prompt_starts_the_move(make_window, monkeypatch):
)
window._on_return_to_initial() # noqa: SLF001
assert window.mover.running, "accepting the prompt did not start the move"
assert [step.role for step in window.mover.state.plan.steps][0] == "z"
# The status line carries the plan, so the operator sees what is moving.
assert [step.role for step in window.mover.state.plan.steps] == [
"yaw",
"pitch",
"x",
"y",
"z",
]
assert window.mover.state.plan.together
assert "z" in window.finger_panel.move_label.text()
finally:
window.mover.cancel("test")
+15 -7
View File
@@ -31,7 +31,6 @@ from orisys_autocal.motion import (
from orisys_autocal.motion.models import AxisState, AxisStatusBits, BusStatus
from orisys_autocal.motion.pose_move import (
MOVE_ORDER,
RETURN_ORDER,
PoseMover,
SETTLE_DEG,
SETTLE_MM,
@@ -128,11 +127,12 @@ def test_the_plan_holds_back_what_it_is_not_given():
assert "z" in plan.describe()
def test_returning_to_zero_retracts_the_press_axis_first():
def test_returning_to_zero_moves_the_five_axes_together():
service = make_service()
plan = plan_return_to_initial(service)
assert [step.role for step in plan.steps] == list(RETURN_ORDER)
assert plan.steps[0].role == "z"
assert plan.together
assert [step.role for step in plan.steps] == ["yaw", "pitch", "x", "y", "z"]
assert plan.held == ()
assert all(step.target == 0.0 for step in plan.steps)
@@ -262,15 +262,23 @@ def test_an_execute_plan_moves_the_four_axes_together_and_leaves_z():
assert mover.state.finished
def test_return_to_zero_stays_one_axis_at_a_time():
def test_return_to_zero_is_one_command_for_the_five_axes():
service = make_service()
called: list[list[tuple[int, float]]] = []
def record(moves):
called.append(list(moves))
return MotionResult.success()
service.move_abs_many = record # type: ignore[method-assign]
poller = FakePoller(service)
mover = PoseMover(service, poller.submit)
plan = plan_return_to_initial(service)
assert not plan.together
assert mover.start(plan).ok
assert len(poller.commands) == 1
assert mover.state.step.role == "z"
poller.commands[0](service)
assert [axis for axis, _value in called[0]] == [3, 2, 0, 1, 4]
assert mover.progress_text().startswith("同时")
def test_the_mover_commands_one_axis_and_waits_for_it_to_arrive():
+9 -6
View File
@@ -539,13 +539,16 @@ def main() -> int:
if solve is not None:
targets, held = solve
checker.that(
"z" in held and "z" not in targets,
f"execute does not hold the press axis back: "
"z" in targets and not held,
f"execute does not command the press axis: "
f"{sorted(targets)} held={held}",
)
plan = plan_pose_move(window.poller.service, targets, hold=held)
plan = plan_pose_move(
window.poller.service, targets, hold=held, together=True
)
checker.that(
[step.role for step in plan.steps] == ["yaw", "pitch", "x", "y"],
[step.role for step in plan.steps]
== ["yaw", "pitch", "x", "y", "z"],
f"the move order is {[step.role for step in plan.steps]}",
)
checker.that(
@@ -581,11 +584,11 @@ def main() -> int:
+ " reached"
)
# Returning to the initial state, press axis first.
# All five axes back to zero together, including the press axis.
back = plan_return_to_initial(window.poller.service)
checker.that(
[step.role for step in back.steps]
== ["z", "pitch", "yaw", "x", "y"],
== ["yaw", "pitch", "x", "y", "z"],
f"the return order is {[step.role for step in back.steps]}",
)
result = window.mover.start(back)