diff --git a/apps/autocal/config/locales/en_US.json b/apps/autocal/config/locales/en_US.json index 77bd784..e9a608a 100644 --- a/apps/autocal/config/locales/en_US.json +++ b/apps/autocal/config/locales/en_US.json @@ -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", diff --git a/apps/autocal/config/locales/zh_CN.json b/apps/autocal/config/locales/zh_CN.json index 0612ff6..bd71992 100644 --- a/apps/autocal/config/locales/zh_CN.json +++ b/apps/autocal/config/locales/zh_CN.json @@ -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": "移动前确认", diff --git a/apps/autocal/src/orisys_autocal/motion/pose_move.py b/apps/autocal/src/orisys_autocal/motion/pose_move.py index 210db8d..56b8e68 100644 --- a/apps/autocal/src/orisys_autocal/motion/pose_move.py +++ b/apps/autocal/src/orisys_autocal/motion/pose_move.py @@ -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 diff --git a/apps/autocal/src/orisys_autocal/ui/sensor_panel.py b/apps/autocal/src/orisys_autocal/ui/sensor_panel.py index 8d890bc..5495d11 100644 --- a/apps/autocal/src/orisys_autocal/ui/sensor_panel.py +++ b/apps/autocal/src/orisys_autocal/ui/sensor_panel.py @@ -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( diff --git a/apps/autocal/src/orisys_autocal/ui/window.py b/apps/autocal/src/orisys_autocal/ui/window.py index e04efff..175a60f 100644 --- a/apps/autocal/src/orisys_autocal/ui/window.py +++ b/apps/autocal/src/orisys_autocal/ui/window.py @@ -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")) diff --git a/apps/autocal/tests/test_move_buttons.py b/apps/autocal/tests/test_move_buttons.py index 94362c9..5cb03a7 100644 --- a/apps/autocal/tests/test_move_buttons.py +++ b/apps/autocal/tests/test_move_buttons.py @@ -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") diff --git a/apps/autocal/tests/test_pose_move.py b/apps/autocal/tests/test_pose_move.py index 69e872f..6708771 100644 --- a/apps/autocal/tests/test_pose_move.py +++ b/apps/autocal/tests/test_pose_move.py @@ -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(): diff --git a/apps/autocal/tools/panel_check.py b/apps/autocal/tools/panel_check.py index 9fb0164..5041b48 100644 --- a/apps/autocal/tools/panel_check.py +++ b/apps/autocal/tools/panel_check.py @@ -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)