From 1ca1bb739a13d0fb6714926dc9cf4e5c855033f0 Mon Sep 17 00:00:00 2001 From: yijian Date: Wed, 14 Oct 2026 19:40:22 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BC=A0=E6=84=9F=E5=99=A8?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/autocal/config/locales/en_US.json | 6 +- apps/autocal/config/locales/zh_CN.json | 6 +- .../src/orisys_autocal/motion/pose_move.py | 11 +++- .../src/orisys_autocal/motion/service.py | 17 +++--- .../autocal/src/orisys_autocal/preferences.py | 60 ++++++++++++++----- apps/autocal/src/orisys_autocal/ui/window.py | 19 ++++-- apps/autocal/tests/test_pose_move.py | 25 ++++++++ 7 files changed, 107 insertions(+), 37 deletions(-) diff --git a/apps/autocal/config/locales/en_US.json b/apps/autocal/config/locales/en_US.json index 021179f..4bbdd1c 100644 --- a/apps/autocal/config/locales/en_US.json +++ b/apps/autocal/config/locales/en_US.json @@ -52,7 +52,7 @@ "stop": "Stop", "stop_hint": "Stop every axis now (the same command as 全部停止 in the strip).", "return_initial": "Return to zero", - "return_hint": "Yaw, pitch, table X/Y and press axis Z return to zero together.", + "return_hint": "Yaw, pitch and press axis Z return to 0. The table parks at its initial position: X at -1.0 mm, Y 15.0 mm forward.", "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 these axes back to zero together:\n\n{steps}", @@ -405,10 +405,10 @@ "row_residual": "Alignment residual", "row_frames": "Frame mapping", "tool_xy": "Tool X / Y", - "tool_hint": "Where the tool is in the table frame. With Y at 3 mm, every point's table Y is the solved move plus 3 mm, so the sensor shell tail stays on the platform centre axis.", + "tool_hint": "Where the tool is in the table frame. Each point's table move is measured from this position.", "sensor_xy": "Sensor X / Y", "sensor_zy": "Sensor Z / yaw", - "setup_hint": "How the sensor actually sits on the stack with the machine at zero: X/Y/Z translation and a yaw about Z. Z is the mounting plate, shims included — there is no separate plate field. Measure it on the real setup.", + "setup_hint": "How the sensor actually sits on the stack with the machine at zero: X/Y/Z translation and a yaw about Z. Z is the mounting-plate thickness. The plate on this bench is 8 mm.", "clear_points": "Clear points", "remove_selected": "Remove selected", "move_up": "Move up", diff --git a/apps/autocal/config/locales/zh_CN.json b/apps/autocal/config/locales/zh_CN.json index fa61f9e..3b82ff8 100644 --- a/apps/autocal/config/locales/zh_CN.json +++ b/apps/autocal/config/locales/zh_CN.json @@ -52,7 +52,7 @@ "stop": "停止", "stop_hint": "立即停止所有轴(与顶栏「全部停止」相同)。", "return_initial": "回初始位", - "return_hint": "回转、俯仰、工作台 X/Y、压轴 Z 一起回到零点。", + "return_hint": "回转、俯仰、压轴 Z 回到 0。工作台停在初始位置:X 为 -1.0 mm,Y 为向前 15.0 mm。", "confirm_title": "确认运动", "confirm_execute": "将同时移动:\n\n{steps}\n\n请确认仿真与机床一致。", "confirm_return": "将同时移回零点:\n\n{steps}\n\n请确认机床上没有干涉。", @@ -405,10 +405,10 @@ "row_residual": "对齐残差", "row_frames": "坐标映射", "tool_xy": "刀具 X / Y", - "tool_hint": "刀具在工作台坐标系中的位置。Y 为 3 mm 时,每个测点的工作台 Y 都会在解算结果上再加 3 mm,使传感器壳体尾部与平台原中心同轴。", + "tool_hint": "刀具在工作台坐标系中的位置。每个测点的工作台移动都相对这个位置计算。", "sensor_xy": "传感器 X / Y", "sensor_zy": "传感器 Z / yaw", - "setup_hint": "传感器在转台零位时相对工作台原点的实际安装位姿:X/Y/Z 平移与绕 Z 的偏摆。Z 即安装板厚度(含垫片),无需另填。请按实际安装测量填写。", + "setup_hint": "传感器在转台零位时相对工作台原点的实际安装位姿:X/Y/Z 平移与绕 Z 的偏摆。Z 为安装板厚度,当前安装台是 8 mm。", "clear_points": "清空标定点", "remove_selected": "删除所选", "move_up": "上移", diff --git a/apps/autocal/src/orisys_autocal/motion/pose_move.py b/apps/autocal/src/orisys_autocal/motion/pose_move.py index 56b8e68..ab0bb1e 100644 --- a/apps/autocal/src/orisys_autocal/motion/pose_move.py +++ b/apps/autocal/src/orisys_autocal/motion/pose_move.py @@ -148,11 +148,16 @@ def plan_pose_move( return MovePlan(steps=tuple(steps), title=title, held=tuple(hold), together=together) -def plan_return_to_initial(service: MotionService) -> MovePlan: - """Yaw, pitch, table X/Y and the press axis back to zero together.""" +def plan_return_to_initial( + service: MotionService, *, x_mm: float = 0.0, y_mm: float = 0.0 +) -> MovePlan: + """Yaw, pitch and the press axis back to zero. Table X/Y park at ``x_mm`` and ``y_mm``.""" + targets = {role: 0.0 for role in ALIGN_ORDER} + targets["x"] = float(x_mm) + targets["y"] = float(y_mm) return plan_pose_move( service, - {role: 0.0 for role in ALIGN_ORDER}, + targets, order=ALIGN_ORDER, together=True, ) diff --git a/apps/autocal/src/orisys_autocal/motion/service.py b/apps/autocal/src/orisys_autocal/motion/service.py index ecb7278..2cac258 100644 --- a/apps/autocal/src/orisys_autocal/motion/service.py +++ b/apps/autocal/src/orisys_autocal/motion/service.py @@ -64,16 +64,16 @@ AXIS_ROLES: tuple[str, ...] = ("x", "y", "z", "yaw", "pitch") LINEAR_ROLES: tuple[str, ...] = ("x", "y", "z") ROTARY_ROLES: tuple[str, ...] = ("yaw", "pitch") -#: Default mapping, from the bench: drives 0-4 are x, y, pitch, yaw, z. +#: Default mapping, from the bench: drives 0-4 are x, y, yaw, pitch, z. #: -#: Deliberately *not* the same as the model's own order. It is written the way the -#: machine is wired, so reading it against the terminal strip is a check rather than -#: a translation. +#: Pitch and yaw were first wired the other way around. Jogging the machine +#: showed drive 2 is yaw (回转) and drive 3 is pitch (俯仰). The degree scales +#: stay on the drive numbers in ``units.py``. DEFAULT_AXIS_ROLES: dict[str, int] = { "x": 0, "y": 1, - "pitch": 2, - "yaw": 3, + "pitch": 3, + "yaw": 2, "z": 4, } @@ -905,10 +905,11 @@ class MotionService: ), ) try: + sent = float(value) if absolute: - self._transport.move_abs(axis, float(value)) + self._transport.move_abs(axis, sent) else: - self._transport.move_rel(axis, float(value)) + self._transport.move_rel(axis, sent) except (ZAuxError, OSError) as exc: code = getattr(exc, "code", 0) return self._logged( diff --git a/apps/autocal/src/orisys_autocal/preferences.py b/apps/autocal/src/orisys_autocal/preferences.py index 3e22502..3f9603a 100644 --- a/apps/autocal/src/orisys_autocal/preferences.py +++ b/apps/autocal/src/orisys_autocal/preferences.py @@ -99,11 +99,10 @@ def _default_safety_limits() -> dict[str, float]: #: How far the axis may descend looking for contact before the run is #: aborted (workflow P7, no-contact abort). "contact_search_mm": 3.0, - #: Stage envelope, from the confirmed bench values: the yaw stage turns a - #: half-turn either way, the pitch stage a full turn. These are the limits a - #: pose move is checked against before any axis is commanded. + #: Stage envelope. Yaw turns ±180. Pitch is limited to ±90 on this bench: + #: past that the fixture interferes, so a pose move is refused. "max_yaw_deg": 180.0, - "max_pitch_deg": 360.0, + "max_pitch_deg": 90.0, } @@ -220,21 +219,20 @@ def default_preferences() -> dict[str, Any]: #: x/y/z translation along the table's axes and a yaw about its z. **The z #: is the mounting plate**, so the plate is not a setting of its own — it #: lifts the sensing face and does nothing else, and two fields for one - #: number drift apart. - "finger_sensor_offset_mm": [0.0, 0.0, 0.0], + #: number drift apart. The bench plate under the finger is 8 mm thick. + "finger_sensor_offset_mm": [0.0, 0.0, 8.0], "finger_sensor_yaw_deg": 0.0, - #: Where the tool is, in the table frame, mm. Straight above the table - #: origin unless the real machine says otherwise; every XY move is measured - #: against it, so it is what makes the reported move match the bench. A tool - #: tip offset belongs here — it used to be a separate field, and having two - #: of them is what let the picture and the numbers disagree. + #: Where the tool is, in the table frame, mm. A tool-tip offset belongs + #: here. The bench X shift is the initial position, not this number. "finger_tool_xy_mm": [0.0, 3.0], - #: Stage travel, degrees. Confirmed at the bench: the vertical stage turns - #: a full half-turn either way (±180), and the horizontal stage turns - #: continuously. An earlier value of 90 was wrong and made half the patch - #: look unreachable when it is not. + #: Where 「回初始位」 parks the table, mm. X is 1 mm toward the negative + #: direction. Y is 15.0 mm forward of the old zero. + "table_initial_x_mm": -1.0, + "table_initial_y_mm": 15.0, + #: Stage travel, degrees. Yaw turns ±180. Pitch is held to ±90 because the + #: fixture interferes outside that. "finger_yaw_limit_deg": 180.0, - "finger_pitch_limit_deg": 360.0, + "finger_pitch_limit_deg": 90.0, } @@ -277,6 +275,10 @@ def load_preferences() -> dict[str, Any]: offset.append(0.0) if plate and not offset[2]: offset[2] = plate + # A session started before the plate was entered saves Z as 0 and would put + # that 0 back over the 8 mm bench plate. Keep a stored non-zero thickness. + if not offset[2]: + offset[2] = float(defaults["finger_sensor_offset_mm"][2]) prefs["finger_sensor_offset_mm"] = offset # The tool tip offset used to be a vector added to the reported coordinates @@ -299,6 +301,27 @@ def load_preferences() -> dict[str, Any]: if value and not tool[index]: tool[index] = value prefs["finger_tool_xy_mm"] = tool + # Closing AutoCal writes the whole in-memory file back. A session that started + # on the previous initial position would otherwise put that old Y over this one. + prefs["table_initial_x_mm"] = defaults["table_initial_x_mm"] + prefs["table_initial_y_mm"] = defaults["table_initial_y_mm"] + # 360° was the old pitch envelope. This fixture interferes outside ±90, and a + # session started on 360 would write that envelope back on close. + limits = prefs.get("safety_limits") + if isinstance(limits, dict): + try: + if float(limits.get("max_pitch_deg")) == 360.0: + limits["max_pitch_deg"] = 90.0 + except (TypeError, ValueError): + limits["max_pitch_deg"] = 90.0 + try: + if float(prefs.get("finger_pitch_limit_deg")) == 360.0: + prefs["finger_pitch_limit_deg"] = 90.0 + except (TypeError, ValueError): + prefs["finger_pitch_limit_deg"] = 90.0 + # Yaw travel is ±180. The same-sign pitch solution sits near ±150° of yaw, + # and a stored ±90 drops it, leaving the opposite pitch sign on one side. + prefs["finger_yaw_limit_deg"] = float(defaults["finger_yaw_limit_deg"]) # A hand-edited task number must not reach the controller as typed. prefs["bas_task"] = clamp_bas_task(prefs.get("bas_task")) prefs = _migrate_axis_roles(prefs) @@ -333,6 +356,11 @@ def _migrate_axis_roles(prefs: dict[str, Any]) -> dict[str, Any]: merged[role] = int(value) except (TypeError, ValueError): continue + # The first bench note had these two drives swapped. A session that still + # has that pair would otherwise put it back on close. + if merged.get("pitch") == 2 and merged.get("yaw") == 3: + merged["pitch"] = 3 + merged["yaw"] = 2 prefs["axis_roles"] = merged return prefs diff --git a/apps/autocal/src/orisys_autocal/ui/window.py b/apps/autocal/src/orisys_autocal/ui/window.py index fc83718..b5f8b34 100644 --- a/apps/autocal/src/orisys_autocal/ui/window.py +++ b/apps/autocal/src/orisys_autocal/ui/window.py @@ -278,8 +278,8 @@ class AutoCalMainWindow(QMainWindow): # platform connected. Its stack carries the bench's stage travel limits, # because a pose outside them is not a pose. self.finger_stack = PlatformStack( - yaw_limit_deg=float(self.prefs.get("finger_yaw_limit_deg") or 90.0), - pitch_limit_deg=float(self.prefs.get("finger_pitch_limit_deg") or 360.0), + yaw_limit_deg=float(self.prefs.get("finger_yaw_limit_deg") or 180.0), + pitch_limit_deg=float(self.prefs.get("finger_pitch_limit_deg") or 90.0), ) self.finger_panel = FingerPanel( self.finger_stack, @@ -1104,8 +1104,19 @@ class AutoCalMainWindow(QMainWindow): ) def _on_return_to_initial(self) -> None: - """Yaw, pitch, table X/Y and the press axis back to zero together.""" - plan = plan_return_to_initial(self.poller.service) + """Yaw, pitch and Z back to zero. Table X/Y park at the stored initial.""" + + def initial(key: str) -> float: + try: + return float(self.prefs.get(key) or 0.0) + except (TypeError, ValueError): + return 0.0 + + plan = plan_return_to_initial( + self.poller.service, + x_mm=initial("table_initial_x_mm"), + y_mm=initial("table_initial_y_mm"), + ) self._start_move(plan, t("move.return_initial")) def _start_move(self, plan, title: str, *, report=None) -> None: diff --git a/apps/autocal/tests/test_pose_move.py b/apps/autocal/tests/test_pose_move.py index 6708771..b5831df 100644 --- a/apps/autocal/tests/test_pose_move.py +++ b/apps/autocal/tests/test_pose_move.py @@ -136,6 +136,31 @@ def test_returning_to_zero_moves_the_five_axes_together(): assert all(step.target == 0.0 for step in plan.steps) +def test_return_to_initial_can_park_y_forward(): + service = make_service() + plan = plan_return_to_initial(service, x_mm=-1.0, y_mm=15.0) + targets = {step.role: step.target for step in plan.steps} + assert targets == {"yaw": 0.0, "pitch": 0.0, "x": -1.0, "y": 15.0, "z": 0.0} + + +def test_pitch_positive_direction_matches_the_drive(): + service = make_service() + pitch = ROLES["pitch"] + yaw = ROLES["yaw"] + seen: list[bool] = [] + service.transport.jog = lambda axis, forward: seen.append((axis, forward)) # type: ignore[method-assign] + + assert service.jog(pitch, True).ok + assert seen == [(pitch, True)] + assert service.move_rel(pitch, 90.0).ok + assert service.move_rel(yaw, 90.0).ok + assert service.transport.get_dpos(pitch) == pytest.approx(90.0) + assert service.transport.get_dpos(yaw) == pytest.approx(90.0) + snapshot = service.poll() + assert snapshot.axis(pitch).dpos == pytest.approx(90.0) + assert snapshot.axis(yaw).dpos == pytest.approx(90.0) + + # ----------------------------------------------------------------------- guards