feat: Implement RM75 joint feedback and fault recovery design

This commit is contained in:
2026-07-29 19:39:12 +08:00
parent f795c06d44
commit 6d22d5600a
12 changed files with 1800 additions and 108 deletions
+60 -1
View File
@@ -202,6 +202,60 @@ def _install_fake_sdk(monkeypatch, *, push_return=0, send_feedback=True):
return SimpleNamespace(RoboticArm=FakeArm)
def test_joint_degree_query_returns_validated_radians() -> None:
class FakeArm:
def rm_get_joint_degree(self):
return 0, [0.0, 10.0, -20.0, 30.0, -40.0, 50.0, -60.0]
adapter = RealManAdapter(
"127.0.0.1",
8080,
0,
"127.0.0.1",
8090,
)
adapter._arm = FakeArm()
snapshot = adapter.read_joint_state()
assert snapshot.positions == pytest.approx(
[math.radians(value) for value in [0, 10, -20, 30, -40, 50, -60]]
)
assert snapshot.read_duration_ms is not None
@pytest.mark.parametrize(
"result",
[
(7, [0.0] * 7),
(0, [0.0] * 6),
(0, [0.0, 0.0, 0.0, math.nan, 0.0, 0.0, 0.0]),
],
)
def test_joint_degree_query_rejects_sdk_errors_and_invalid_values(result) -> None:
adapter = RealManAdapter(
"127.0.0.1",
8080,
0,
"127.0.0.1",
8090,
)
adapter._arm = SimpleNamespace(rm_get_joint_degree=lambda: result)
with pytest.raises((RuntimeError, ValueError)):
adapter.read_joint_state()
def test_mock_joint_query_uses_current_mock_positions() -> None:
adapter = MockRealManAdapter([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0])
snapshot = adapter.read_joint_state()
assert snapshot.positions == pytest.approx(
[math.radians(value) for value in [1, 2, 3, 4, 5, 6, 7]]
)
def test_udp_feedback_is_cached_in_radians(monkeypatch) -> None:
monotonic = iter([10.0, 10.005])
monkeypatch.setattr(realman_adapter.time, "monotonic", lambda: next(monotonic))
@@ -253,7 +307,12 @@ def test_invalid_udp_feedback_does_not_replace_snapshot(state) -> None:
adapter._on_realtime_arm_state(state)
assert adapter.get_latest_joint_state() == before
after = adapter.get_latest_joint_state()
assert after is not None
assert before is not None
assert after.positions == before.positions
assert after.received_at == before.received_at
assert after.motion_ready is False
def test_udp_joint_fault_marks_snapshot_unready() -> None:
+201 -66
View File
@@ -30,34 +30,177 @@ class FakeTime:
return SimpleNamespace(nanoseconds=0)
def test_missing_or_stale_feedback_does_not_enable_qp() -> None:
def test_startup_joint_query_initializes_qp_and_command_history() -> None:
positions = [0.1] * 7
pose = np.eye(4)
teleop = object.__new__(SingleArmVelocityTeleop)
teleop._command_timeout_sec = 0.12
teleop._adapter = SimpleNamespace(get_latest_joint_state=lambda: None)
assert teleop._fresh_joint_state() is None
teleop._arm_name = "right_rm75"
teleop._adapter = SimpleNamespace(
get_latest_joint_state=lambda: JointStateSnapshot(
[0.0] * 7,
time.monotonic() - 1.0,
read_joint_state=lambda: JointStateSnapshot(
positions,
time.monotonic(),
)
)
assert teleop._fresh_joint_state() is None
teleop._ik_solver = SimpleNamespace(
update_joint_state=lambda joints: pose
)
teleop.get_logger = lambda: FakeLogger()
teleop._initialize_joint_state()
assert teleop._latest_joint_positions == positions
assert teleop._last_valid_joint_target == positions
assert teleop._last_joint_command_target == positions
assert teleop._last_joint_command_velocity == [0.0] * 7
assert teleop._last_current_pose is pose
def test_disabled_joint_feedback_does_not_enable_qp() -> None:
def test_startup_joint_query_failure_closes_adapter() -> None:
class FailingAdapter:
def __init__(self):
self.close_calls = 0
def read_joint_state(self):
raise RuntimeError("rm_get_joint_degree failed with code 7")
def close(self):
self.close_calls += 1
errors = []
teleop = object.__new__(SingleArmVelocityTeleop)
teleop._command_timeout_sec = 0.12
teleop._adapter = SimpleNamespace(
get_latest_joint_state=lambda: JointStateSnapshot(
teleop._arm_name = "left_rm75"
teleop._adapter = FailingAdapter()
teleop.get_logger = lambda: SimpleNamespace(
error=lambda message: errors.append(message)
)
with pytest.raises(RuntimeError, match="code 7"):
teleop._initialize_joint_state()
assert teleop._adapter.close_calls == 1
assert "left_rm75" in errors[0]
assert "启动关节同步失败" in errors[0]
def _timeout_teleop(adapter) -> SingleArmVelocityTeleop:
teleop = object.__new__(SingleArmVelocityTeleop)
teleop._adapter = adapter
teleop._arm_name = "right_rm75"
teleop._follow = False
teleop._active = True
teleop._joint_feedback_ready = True
teleop._grip_rearm_required = False
teleop._feedback_resync_attempted = False
teleop._control_fault_latched = False
teleop._last_joint_command_target = [0.1] * 7
teleop._last_joint_command_velocity = [0.0] * 7
teleop._latest_joint_positions = [0.1] * 7
teleop._last_valid_joint_target = [0.1] * 7
teleop._last_current_pose = np.eye(4)
teleop._controller_start = None
teleop._controller_orientation_start = None
teleop._robot_start_transform = None
teleop._filtered_target = None
teleop._filtered_orientation_target = None
teleop._last_sent_target = None
teleop._last_sent_orientation = None
teleop._last_command_time = None
teleop._ik_solver = SimpleNamespace(
update_joint_state=lambda joints: np.eye(4)
)
teleop._stop_sent = False
teleop._feedback_resync_timeout_sec = 0.5
teleop._publish_stop_debug = lambda: None
teleop.get_logger = lambda: FakeLogger()
return teleop
def test_missing_or_disabled_joint_snapshot_is_not_motion_ready() -> None:
assert not SingleArmVelocityTeleop._joint_snapshot_is_motion_ready(None)
assert not SingleArmVelocityTeleop._joint_snapshot_is_motion_ready(
JointStateSnapshot(
[0.0] * 7,
time.monotonic(),
motion_ready=False,
)
)
assert teleop._fresh_joint_state() is None
def test_short_udp_timeout_repeats_last_limited_target_without_query() -> None:
sends = []
adapter = SimpleNamespace(
send_joint_target=lambda joints, follow: sends.append(
(list(joints), follow)
),
read_joint_state=lambda: pytest.fail("query must not run"),
stop=lambda: pytest.fail("stop must not run"),
)
teleop = _timeout_teleop(adapter)
teleop._handle_stale_joint_feedback(0.2)
assert sends == [([0.1] * 7, False)]
assert teleop._last_joint_command_target == [0.1] * 7
assert teleop._grip_rearm_required
def test_short_udp_timeout_without_active_target_stays_stopped() -> None:
stop_calls = []
adapter = SimpleNamespace(
send_joint_target=lambda joints, follow: pytest.fail(
"inactive control must not start CANFD output"
),
read_joint_state=lambda: pytest.fail("query must not run"),
stop=lambda: stop_calls.append(True),
)
teleop = _timeout_teleop(adapter)
teleop._active = False
teleop._handle_stale_joint_feedback(0.2)
assert len(stop_calls) == 1
def test_persistent_udp_timeout_queries_once_and_holds_actual_position() -> None:
sends = []
query_calls = []
adapter = SimpleNamespace(
send_joint_target=lambda joints, follow: sends.append(list(joints)),
read_joint_state=lambda: (
query_calls.append(True)
or JointStateSnapshot([0.2] * 7, time.monotonic())
),
stop=lambda: None,
)
teleop = _timeout_teleop(adapter)
teleop._handle_stale_joint_feedback(0.5)
teleop._handle_stale_joint_feedback(0.6)
assert len(query_calls) == 1
assert sends == [[0.2] * 7, [0.2] * 7]
assert teleop._last_valid_joint_target == [0.2] * 7
assert teleop._last_joint_command_velocity == [0.0] * 7
def test_persistent_udp_timeout_query_failure_latches_control() -> None:
stop_calls = []
adapter = SimpleNamespace(
send_joint_target=lambda joints, follow: pytest.fail(
"CANFD must stop after query failure"
),
read_joint_state=lambda: (_ for _ in ()).throw(
RuntimeError("rm_get_joint_degree failed with code 7")
),
stop=lambda: stop_calls.append(True),
)
teleop = _timeout_teleop(adapter)
teleop._handle_stale_joint_feedback(0.5)
teleop._handle_stale_joint_feedback(0.6)
assert teleop._control_fault_latched
assert len(stop_calls) == 1
def test_joint_command_step_limits_acceleration_from_rest() -> None:
@@ -106,6 +249,8 @@ def test_feedback_fault_blocks_grip_until_release() -> None:
update_joint_state=lambda joints: np.eye(4)
)
teleop._grip_rearm_required = True
teleop._control_fault_latched = False
teleop._feedback_resync_attempted = False
teleop.get_clock = lambda: FakeClock()
teleop.get_logger = lambda: FakeLogger()
stopped = []
@@ -125,40 +270,6 @@ def test_feedback_fault_blocks_grip_until_release() -> None:
assert len(entered) == 1
def test_stale_feedback_stops_before_active_control() -> None:
stopped = []
entered = []
teleop = object.__new__(SingleArmVelocityTeleop)
teleop._adapter = SimpleNamespace(
get_latest_joint_state=lambda: JointStateSnapshot(
[0.0] * 7,
time.monotonic() - 1.0,
)
)
teleop._command_timeout_sec = 0.12
teleop._joint_feedback_ready = True
teleop._arm_name = "right_rm75"
teleop._last_msg = SimpleNamespace(
grip=True,
pose=SimpleNamespace(
position=SimpleNamespace(x=0.0, y=0.0, z=0.0),
orientation=SimpleNamespace(x=0.0, y=0.0, z=0.0, w=1.0),
),
)
teleop._last_msg_time = FakeTime()
teleop._active = False
teleop._enable_orientation_control = False
teleop.get_clock = lambda: SimpleNamespace(now=lambda: FakeTime())
teleop.get_logger = lambda: FakeLogger()
teleop._safe_stop = lambda reset_active: stopped.append(reset_active)
teleop._enter_active_control = lambda *args: entered.append(args)
teleop._control_tick()
assert stopped == [True]
assert entered == []
def test_first_feedback_initializes_last_valid_target_without_solving() -> None:
class FakeSolver:
def __init__(self) -> None:
@@ -318,34 +429,58 @@ def test_timing_stats_logs_summary_and_clears_window() -> None:
assert all(not samples for samples in teleop._timing_samples.values())
def test_joint_send_failure_requests_slow_stop_and_resets_control() -> None:
class FailingAdapter:
def __init__(self) -> None:
def test_canfd_error_stops_queries_and_requires_grip_rearm() -> None:
class RecoveringAdapter:
def __init__(self):
self.stop_calls = 0
self.read_calls = 0
def send_joint_target(self, joints, follow):
del joints, follow
raise RuntimeError("send failed")
raise RuntimeError("rm_movej_canfd failed with code 9")
def stop(self):
self.stop_calls += 1
reset_calls = []
teleop = object.__new__(SingleArmVelocityTeleop)
teleop._adapter = FailingAdapter()
teleop._follow = False
teleop._arm_name = "left_rm75"
teleop._stop_sent = False
teleop._last_joint_command_target = [0.0] * 7
teleop._last_joint_command_velocity = [0.0] * 7
def read_joint_state(self):
self.read_calls += 1
return JointStateSnapshot([0.2] * 7, time.monotonic())
teleop = _timeout_teleop(RecoveringAdapter())
teleop._joint_command_max_speed = math.radians(180.0)
teleop._joint_command_max_acceleration = math.radians(300.0)
teleop._dt = 1.0 / 125.0
teleop.get_logger = lambda: FakeLogger()
teleop._safe_stop = lambda reset_active: reset_calls.append(reset_active)
teleop._dt = 1.0 / 90.0
sent = teleop._send_joint_target([0.1] * 7)
sent = teleop._send_joint_target([0.3] * 7)
assert not sent
assert teleop._adapter.stop_calls == 1
assert reset_calls == [True]
assert teleop._adapter.read_calls == 1
assert not teleop._control_fault_latched
assert teleop._grip_rearm_required
assert teleop._last_joint_command_target == [0.2] * 7
def test_canfd_error_latches_when_joint_query_also_fails() -> None:
class FailingAdapter:
def __init__(self):
self.stop_calls = 0
def send_joint_target(self, joints, follow):
del joints, follow
raise RuntimeError("rm_movej_canfd failed with code 9")
def stop(self):
self.stop_calls += 1
def read_joint_state(self):
raise RuntimeError("rm_get_joint_degree failed with code 7")
teleop = _timeout_teleop(FailingAdapter())
teleop._joint_command_max_speed = math.radians(180.0)
teleop._joint_command_max_acceleration = math.radians(300.0)
teleop._dt = 1.0 / 90.0
assert not teleop._send_joint_target([0.3] * 7)
assert teleop._control_fault_latched
assert teleop._adapter.stop_calls == 1
@@ -179,6 +179,8 @@ def test_invalid_controller_quaternion_stops_current_tick() -> None:
teleop._last_valid_joint_target = None
teleop._last_current_pose = None
teleop._joint_feedback_ready = True
teleop._control_fault_latched = False
teleop._feedback_resync_attempted = False
stopped = []
teleop.get_clock = lambda: FakeClock()
teleop.get_logger = lambda: FakeLogger()
+58 -11
View File
@@ -59,6 +59,9 @@ class MockRealManAdapter:
time.monotonic(),
)
def read_joint_state(self) -> JointStateSnapshot:
return self.get_latest_joint_state()
def send_joint_target(self, joints: list[float], follow: bool) -> None:
del follow
if len(joints) != 7 or not all(math.isfinite(value) for value in joints):
@@ -228,6 +231,25 @@ class RealManAdapter:
self._latest_joint_state.motion_ready,
)
def read_joint_state(self) -> JointStateSnapshot:
self._require_arm()
started_at = time.monotonic()
result = self._arm.rm_get_joint_degree()
finished_at = time.monotonic()
if not isinstance(result, tuple) or len(result) != 2:
raise RuntimeError(
f"rm_get_joint_degree returned invalid result: {result!r}"
)
self._check_return(result, "rm_get_joint_degree")
return JointStateSnapshot(
self._joint_positions_from_degrees(
result[1],
"rm_get_joint_degree",
),
finished_at,
(finished_at - started_at) * 1000.0,
)
def send_joint_target(self, joints: list[float], follow: bool) -> None:
self._require_arm()
if len(joints) != 7 or not all(math.isfinite(value) for value in joints):
@@ -305,17 +327,10 @@ class RealManAdapter:
raise ValueError(
f"unexpected realtime feedback source: {arm_ip}"
)
degrees = list(data.joint_status.joint_position)
if (
len(degrees) != 7
or not all(isinstance(value, Number) for value in degrees)
):
raise ValueError(
"RM75 UDP feedback must contain 7 numeric joints"
)
positions = [math.radians(float(value)) for value in degrees]
if not all(math.isfinite(value) for value in positions):
raise ValueError("RM75 UDP feedback contains NaN/Inf")
positions = self._joint_positions_from_degrees(
data.joint_status.joint_position,
"RM75 UDP feedback",
)
joint_enabled = [
bool(value) for value in data.joint_status.joint_en_flag
]
@@ -367,12 +382,44 @@ class RealManAdapter:
self._feedback_fault_logged = False
self._feedback_ready.set()
except Exception as exc:
with self._joint_state_lock:
if self._latest_joint_state is not None:
current = self._latest_joint_state
self._latest_joint_state = JointStateSnapshot(
list(current.positions),
current.received_at,
current.read_duration_ms,
current.update_interval_ms,
False,
)
if not self._feedback_fault_logged:
self._log_warn(
f"RealMan UDP realtime feedback invalid: {exc}"
)
self._feedback_fault_logged = True
@staticmethod
def _joint_positions_from_degrees(
values: Any,
source: str,
) -> list[float]:
try:
degrees = list(values)
except TypeError as exc:
raise ValueError(
f"{source} must contain 7 numeric joints"
) from exc
if len(degrees) != 7 or not all(
isinstance(value, Number) for value in degrees
):
raise ValueError(
f"{source} must contain 7 numeric joints"
)
positions = [math.radians(float(value)) for value in degrees]
if not all(math.isfinite(value) for value in positions):
raise ValueError(f"{source} contains NaN/Inf")
return positions
def _log_motion_status_transition(
self,
status: tuple[Any, ...],
@@ -177,8 +177,9 @@ class SingleArmVelocityTeleop(Node):
self.declare_parameter("arm_name", "rm75")
self.declare_parameter("controller_topic", "/xr/right_controller")
self.declare_parameter("control_rate_hz", 125.0)
self.declare_parameter("control_rate_hz", 90.0)
self.declare_parameter("command_timeout_sec", 0.12)
self.declare_parameter("feedback_resync_timeout_sec", 0.5)
self.declare_parameter("scale", 1.0)
self.declare_parameter("deadband_m", 0.001)
self.declare_parameter("target_filter_alpha", 0.65)
@@ -234,6 +235,9 @@ class SingleArmVelocityTeleop(Node):
raise ValueError("control_rate_hz must be > 0")
self._dt = 1.0 / control_rate_hz
self._command_timeout_sec = float(self.get_parameter("command_timeout_sec").value)
self._feedback_resync_timeout_sec = float(
self.get_parameter("feedback_resync_timeout_sec").value
)
self._scale = float(self.get_parameter("scale").value)
self._deadband_m = float(self.get_parameter("deadband_m").value)
self._target_filter_alpha = float(self.get_parameter("target_filter_alpha").value)
@@ -287,6 +291,8 @@ class SingleArmVelocityTeleop(Node):
self._last_joint_command_velocity: list[float] | None = None
self._joint_feedback_ready = False
self._grip_rearm_required = False
self._feedback_resync_attempted = False
self._control_fault_latched = False
self._stop_sent = True
self._trigger_tool_open = True
self._last_trigger_pressed: bool | None = None
@@ -321,6 +327,7 @@ class SingleArmVelocityTeleop(Node):
)
self._adapter = self._make_adapter()
self._adapter.connect()
self._initialize_joint_state()
self._setup_tool_control()
debug_ns = f"{self._debug_topic_prefix}/{self._arm_name}"
@@ -374,6 +381,31 @@ class SingleArmVelocityTeleop(Node):
canfd_radio=int(self.get_parameter("canfd_radio").value),
)
def _initialize_joint_state(self) -> None:
try:
self._reset_joint_state(self._adapter.read_joint_state())
except Exception as exc:
self.get_logger().error(
f"{self._arm_name} 启动关节同步失败:{exc}"
)
self._adapter.close()
raise
def _reset_joint_state(
self,
snapshot: JointStateSnapshot,
) -> np.ndarray:
current_pose = self._ik_solver.update_joint_state(
snapshot.positions
)
positions = list(snapshot.positions)
self._latest_joint_positions = positions
self._last_current_pose = current_pose
self._last_valid_joint_target = list(positions)
self._last_joint_command_target = list(positions)
self._last_joint_command_velocity = [0.0] * 7
return current_pose
def _setup_tool_control(self) -> None:
peripheral_arm = self._peripheral_arm_name()
if self._bool_parameter("configure_peripheral_on_connect"):
@@ -515,17 +547,32 @@ class SingleArmVelocityTeleop(Node):
else (tick_started_ns - last_tick_started_ns) * 1e-6
)
now = self.get_clock().now()
snapshot = self._fresh_joint_state()
if snapshot is None:
if self._control_fault_latched:
return
snapshot = self._adapter.get_latest_joint_state()
if not self._joint_snapshot_is_motion_ready(snapshot):
self._grip_rearm_required = True
if self._joint_feedback_ready:
self.get_logger().warn(
f"{self._arm_name} 关节反馈缺失、过期或机械臂未就绪,机械臂停止。",
f"{self._arm_name} 关节反馈无效或机械臂未就绪,机械臂停止。",
throttle_duration_sec=1.0,
)
self._joint_feedback_ready = False
self._safe_stop(reset_active=True)
return
assert snapshot is not None
feedback_age = time.monotonic() - snapshot.received_at
if feedback_age < 0.0:
self._grip_rearm_required = True
self._joint_feedback_ready = False
self._safe_stop(reset_active=True)
return
if feedback_age > self._command_timeout_sec:
self._handle_stale_joint_feedback(feedback_age)
return
self._feedback_resync_attempted = False
try:
current_pose = self._sync_joint_feedback(snapshot)
except Exception as exc:
@@ -538,9 +585,17 @@ class SingleArmVelocityTeleop(Node):
self._safe_stop(reset_active=True)
return
if not self._joint_feedback_ready:
self.get_logger().info(
f"{self._arm_name} 已收到首帧有效关节反馈,QP 可以启用。"
)
if self._grip_rearm_required:
message = (
f"{self._arm_name} UDP关节反馈已恢复,"
"等待Grip松开后重新使能。"
)
else:
message = (
f"{self._arm_name} 已收到首帧有效关节反馈,"
"QP可以启用。"
)
self.get_logger().info(message)
self._joint_feedback_ready = True
if self._last_msg is None or self._last_msg_time is None:
@@ -592,7 +647,7 @@ class SingleArmVelocityTeleop(Node):
)
return
feedback_age_ms = (time.monotonic() - snapshot.received_at) * 1000.0
feedback_age_ms = feedback_age * 1000.0
assert self._controller_start is not None
assert self._robot_start_transform is not None
@@ -978,20 +1033,102 @@ class SingleArmVelocityTeleop(Node):
samples.clear()
self.get_logger().info(message)
def _fresh_joint_state(self) -> JointStateSnapshot | None:
snapshot = self._adapter.get_latest_joint_state()
if snapshot is None:
return None
age = time.monotonic() - snapshot.received_at
if age < 0.0 or age > self._command_timeout_sec:
return None
def _handle_stale_joint_feedback(self, age: float) -> None:
if self._control_fault_latched:
return
self._grip_rearm_required = True
if self._joint_feedback_ready:
self.get_logger().warn(
f"{self._arm_name} UDP关节反馈超时,保持最后安全目标。"
)
self._joint_feedback_ready = False
if (
len(snapshot.positions) != 7
or not all(math.isfinite(value) for value in snapshot.positions)
or not snapshot.motion_ready
age >= self._feedback_resync_timeout_sec
and not self._feedback_resync_attempted
):
return None
return snapshot
self._feedback_resync_attempted = True
self.get_logger().warn(
f"{self._arm_name} UDP关节反馈持续超时,"
"尝试rm_get_joint_degree重新同步。"
)
try:
self._reset_joint_state(
self._adapter.read_joint_state()
)
except Exception as exc:
self._latch_control_fault(
f"UDP关节反馈持续超时且重新同步失败:{exc}"
)
return
self.get_logger().info(
f"{self._arm_name} 已通过rm_get_joint_degree重新同步,"
"继续保持并等待UDP恢复。"
)
if self._active and self._last_joint_command_target is not None:
self._repeat_last_joint_target()
else:
self._safe_stop(reset_active=True)
def _repeat_last_joint_target(self) -> None:
target = self._last_joint_command_target
if target is None:
return
try:
self._adapter.send_joint_target(list(target), self._follow)
self._stop_sent = False
except Exception as exc:
self._recover_from_canfd_error(exc)
def _recover_from_canfd_error(
self,
send_error: Exception,
) -> None:
self.get_logger().error(
f"{self._arm_name} rm_movej_canfd发送失败:{send_error}"
)
self._grip_rearm_required = True
self._send_stop_once()
self._safe_stop(reset_active=True)
try:
self._reset_joint_state(
self._adapter.read_joint_state()
)
except Exception as query_error:
self._latch_control_fault(
"CANFD错误后关节同步失败:"
f"send={send_error}; query={query_error}"
)
return
self.get_logger().info(
f"{self._arm_name} CANFD错误后已同步实际关节角,"
"等待UDP恢复及Grip重新使能。"
)
def _latch_control_fault(self, message: str) -> None:
if self._control_fault_latched:
return
self._control_fault_latched = True
self._grip_rearm_required = True
self.get_logger().error(
f"{self._arm_name} 控制故障已锁存:{message}"
)
self._safe_stop(reset_active=True)
@staticmethod
def _joint_snapshot_is_motion_ready(
snapshot: JointStateSnapshot | None,
) -> bool:
return (
snapshot is not None
and len(snapshot.positions) == 7
and all(
math.isfinite(value)
for value in snapshot.positions
)
and snapshot.motion_ready
)
def _sync_joint_feedback(
self,
@@ -1086,12 +1223,7 @@ class SingleArmVelocityTeleop(Node):
try:
self._adapter.send_joint_target(limited_target, self._follow)
except Exception as exc:
self.get_logger().error(
f"{self._arm_name} 发送关节透传命令失败:{exc}",
throttle_duration_sec=1.0,
)
self._send_stop_once()
self._safe_stop(reset_active=True)
self._recover_from_canfd_error(exc)
return False
self._last_joint_command_target = limited_target
self._last_joint_command_velocity = limited_velocity
@@ -1205,6 +1337,11 @@ class SingleArmVelocityTeleop(Node):
def _validate_parameters(self) -> None:
if self._command_timeout_sec <= 0.0:
raise ValueError("command_timeout_sec must be > 0")
if self._feedback_resync_timeout_sec <= self._command_timeout_sec:
raise ValueError(
"feedback_resync_timeout_sec must be greater than "
"command_timeout_sec"
)
if self._deadband_m < 0.0:
raise ValueError("deadband_m must be >= 0")
if not 0.0 <= self._target_filter_alpha <= 1.0: