dual arm control from sample_udp_sender.py

This commit is contained in:
2026-05-25 17:02:37 +08:00
parent 340bd9138d
commit 7551f1e8ea
18 changed files with 976 additions and 74 deletions
+101 -13
View File
@@ -1,5 +1,12 @@
"""RM75 机械臂适配层。
对上提供统一的当前位姿读取、笛卡尔速度发送和停止接口;对下根据配置
选择 mock 积分模拟器或睿尔曼 Python API2 真机通信。
"""
from __future__ import annotations
import time
from dataclasses import dataclass
from numbers import Number
from typing import Any
@@ -72,6 +79,9 @@ class RealManAdapter:
initial_joint_pose: list[float] | None = None,
initial_tcp_pose: list[float] | None = None,
init_move_speed: int = 20,
command_mode: str = "velocity",
canfd_trajectory_mode: int = 2,
canfd_radio: int = 0,
) -> None:
self._robot_ip = robot_ip
self._robot_port = robot_port
@@ -90,7 +100,12 @@ class RealManAdapter:
self._initial_joint_pose = initial_joint_pose
self._initial_tcp_pose = initial_tcp_pose
self._init_move_speed = init_move_speed
self._command_mode = command_mode
self._canfd_trajectory_mode = canfd_trajectory_mode
self._canfd_radio = canfd_radio
self._arm: Any | None = None
if self._command_mode not in ("velocity", "pose_canfd"):
raise ValueError("command_mode must be one of: velocity, pose_canfd")
def connect(self) -> None:
try:
@@ -103,17 +118,21 @@ class RealManAdapter:
self._arm = RoboticArm(rm_thread_mode_e.RM_TRIPLE_MODE_E)
handle = self._arm.rm_create_robot_arm(self._robot_ip, self._robot_port)
self._check_robot_handle(handle)
self._log_info(
"RealMan connected: "
f"ip={self._robot_ip}, port={self._robot_port}, "
f"avoid_singularity={self._avoid_singularity}, "
f"frame_type={self._frame_type}, dt_ms={self._dt_ms}, "
f"command_mode={self._command_mode}"
)
if self._configure_safety_limits:
self._apply_safety_limits()
if self._move_to_initial_pose_on_connect:
self._move_to_initial_pose()
# 速度透传初始化必须和控制循环周期一致,避免真实机械臂出现周期不稳定。
ret = self._arm.rm_set_movev_canfd_init(
self._avoid_singularity,
self._frame_type,
self._dt_ms,
)
self._check_return(ret, "rm_set_movev_canfd_init")
if self._command_mode == "velocity":
self._init_movev_canfd()
else:
self._log_info("command_mode=pose_canfd,跳过 rm_set_movev_canfd_init。")
def get_current_pose(self) -> ArmPose:
self._require_arm()
@@ -125,14 +144,32 @@ class RealManAdapter:
def send_cartesian_velocity(self, velocity: list[float], follow: bool) -> None:
self._require_arm()
if self._command_mode == "pose_canfd":
return
ret = self._arm.rm_movev_canfd(velocity, follow, 0, 0)
self._check_return(ret, "rm_movev_canfd")
def send_cartesian_target(self, pose: ArmPose, follow: bool) -> None:
self._require_arm()
if self._command_mode != "pose_canfd":
raise RuntimeError("send_cartesian_target requires command_mode=pose_canfd")
ret = self._arm.rm_movep_canfd(
[pose.x, pose.y, pose.z, pose.rx, pose.ry, pose.rz],
follow,
self._canfd_trajectory_mode,
self._canfd_radio,
)
self._check_return(ret, "rm_movep_canfd")
def uses_pose_targets(self) -> bool:
return self._command_mode == "pose_canfd"
def stop(self) -> None:
if self._arm is None:
return
try:
self._arm.rm_movev_canfd([0.0] * 6, False, 0, 0)
if self._command_mode == "velocity":
self._arm.rm_movev_canfd([0.0] * 6, False, 0, 0)
self._arm.rm_set_arm_slow_stop()
except Exception:
pass
@@ -151,7 +188,8 @@ class RealManAdapter:
raise RuntimeError("睿尔曼机械臂尚未连接")
def _apply_safety_limits(self) -> None:
self._try_call("rm_set_avoid_singularity_mode", True)
# 真机安全限幅尽量下发到控制器;不支持的 SDK 接口会在 _try_call 中降级为警告。
self._try_call("rm_set_avoid_singularity_mode", int(self._avoid_singularity))
self._try_call("rm_set_arm_max_line_speed", self._max_line_speed)
self._try_call("rm_set_arm_max_angular_speed", self._max_angular_speed)
self._try_call("rm_set_arm_max_line_acc", self._max_line_acc)
@@ -169,6 +207,46 @@ class RealManAdapter:
ret = self._arm.rm_movel(self._initial_tcp_pose, self._init_move_speed, 0, 0, 1)
self._check_return(ret, "rm_movel(initial_tcp_pose)")
def _init_movev_canfd(self) -> None:
attempts = 3
candidates = [self._avoid_singularity]
if self._avoid_singularity != 0:
candidates.append(0)
# 某些现场控制器初始化避奇异模式会超时,失败后自动降级到 0 再重试。
last_ret: Any = None
for avoid_singularity in candidates:
if avoid_singularity != self._avoid_singularity:
self._log_warn("rm_set_movev_canfd_init 降级为 avoid_singularity=0 后重试。")
for attempt in range(1, attempts + 1):
ret = self._arm.rm_set_movev_canfd_init(
avoid_singularity,
self._frame_type,
self._dt_ms,
)
last_ret = ret
code = self._return_code(ret)
if code == 0:
self._avoid_singularity = avoid_singularity
self._log_info(
"rm_set_movev_canfd_init 成功:"
f"avoid_singularity={avoid_singularity}, "
f"frame_type={self._frame_type}, dt_ms={self._dt_ms}"
)
return
self._log_warn(
"rm_set_movev_canfd_init 失败:"
f"avoid_singularity={avoid_singularity}, "
f"frame_type={self._frame_type}, dt_ms={self._dt_ms}, "
f"attempt={attempt}/{attempts}, ret={ret!r}"
)
if attempt < attempts:
time.sleep(0.2)
self._check_return(last_ret, "rm_set_movev_canfd_init")
def _try_call(self, name: str, *args: Any) -> None:
func = getattr(self._arm, name, None)
if func is None:
@@ -185,18 +263,28 @@ class RealManAdapter:
if self._logger is not None:
self._logger.warn(message)
@staticmethod
def _check_robot_handle(handle: Any) -> None:
def _log_info(self, message: str) -> None:
if self._logger is not None:
self._logger.info(message)
def _check_robot_handle(self, handle: Any) -> None:
handle_id = getattr(handle, "id", None)
if handle_id == -1:
raise RuntimeError("rm_create_robot_arm failed: socket error or robot unreachable")
raise RuntimeError(
"rm_create_robot_arm failed: TCP may be reachable, but the RealMan "
f"controller did not return robot info from {self._robot_ip}:{self._robot_port}"
)
@staticmethod
def _check_return(ret: Any, name: str) -> None:
code = ret[0] if isinstance(ret, tuple) and ret else ret
code = RealManAdapter._return_code(ret)
if isinstance(code, int) and code != 0:
raise RuntimeError(f"{name} failed with code {code}: {ret!r}")
@staticmethod
def _return_code(ret: Any) -> Any:
return ret[0] if isinstance(ret, tuple) and ret else ret
@classmethod
def _find_pose(cls, obj: Any) -> list[float] | None:
# 不同 SDK 版本返回字段可能略有差异,因此递归查找常见 TCP 位姿字段。