feat: 记录夹爪逻辑执行状态

This commit is contained in:
2026-08-10 17:57:07 +08:00
parent 7b2caf3012
commit 043d3d0533
5 changed files with 183 additions and 5 deletions
@@ -27,3 +27,4 @@ arms:
scissorgripper: 0
right:
scissorgripper: 1
set_initial_tool_state: true
+74 -1
View File
@@ -6,13 +6,14 @@ from types import ModuleType, SimpleNamespace
import pytest
import yaml
from xr_rm_teleop import realman_adapter
from xr_rm_teleop import fun_peripheral, realman_adapter
from xr_rm_teleop.realman_adapter import RealManAdapter
from xr_rm_teleop.realman_adapter import MockRealManAdapter
from xr_rm_teleop.fun_peripheral import (
PeripheralConfig,
_configure_tool_frame,
load_peripheral_config,
peripheral_cfg,
)
@@ -82,6 +83,78 @@ def test_deployed_peripheral_config_selects_left_and_right_tools() -> None:
assert right.tool_pose == [0.0, 0.0, 0.14, 0.0, 0.0, 0.0, 1.0]
def test_right_tool_initializes_open_only_for_right_arm() -> None:
config_file = CONFIG_DIR / "peripherals_rm75.yaml"
left = load_peripheral_config(str(config_file), "left")
right = load_peripheral_config(str(config_file), "right")
assert not left.set_initial_tool_state
assert right.set_initial_tool_state
def test_omnipic_initial_state_opens_fully(monkeypatch) -> None:
calls = []
class FakeArm:
def rm_set_voltage(self, *args):
del args
def rm_set_io_mode(self, *args):
del args
def rm_algo_quaternion2euler(self, quaternion):
del quaternion
return [0.0, 0.0, 0.0]
def rm_get_total_tool_frame(self):
return {"return_code": 0, "tool_names": []}
def rm_set_manual_tool_frame(self, *, frame):
del frame
return 0
def rm_change_tool_frame(self, tool_name):
del tool_name
return 0
def rm_set_modbus_mode(self, **kwargs):
del kwargs
return 0
def rm_write_single_register(self, params, value):
del params, value
return 0
sdk = ModuleType("Robotic_Arm.rm_robot_interface")
sdk.rm_frame_t = lambda *args: object()
sdk.rm_peripheral_read_write_params_t = lambda *args: object()
package = ModuleType("Robotic_Arm")
package.rm_robot_interface = sdk
monkeypatch.setitem(sys.modules, "Robotic_Arm", package)
monkeypatch.setitem(sys.modules, "Robotic_Arm.rm_robot_interface", sdk)
monkeypatch.setattr(fun_peripheral.time, "sleep", lambda seconds: None)
monkeypatch.setattr(
fun_peripheral,
"set_tool_position",
lambda robot, percent, device, scissorgripper: calls.append(
(percent, device, scissorgripper)
),
)
tools = {
"scissor": [[0.0] * 7, [0.0] * 7],
"omnipic": [[0.0] * 7, [0.0] * 7],
}
peripheral_cfg(
FakeArm(),
1,
tools,
set_initial_tool_state=True,
)
assert calls == [(1.0, 1, 1)]
@pytest.mark.parametrize(
("config_name", "node_names"),
[
+64
View File
@@ -1,4 +1,5 @@
import math
import threading
import time
from types import SimpleNamespace
@@ -42,6 +43,69 @@ class FakePublisher:
self.messages.append(message)
def _tool_state_teleop(*, command_error=None):
started = threading.Event()
release = threading.Event()
class Adapter:
def set_tool_enabled(self, open_tool):
del open_tool
started.set()
assert release.wait(timeout=1.0)
if command_error is not None:
raise command_error
teleop = object.__new__(SingleArmVelocityTeleop)
teleop._arm_name = "right_rm75"
teleop._adapter = Adapter()
teleop._tool_command_queue = None
teleop._tool_worker_stop = threading.Event()
teleop._tool_worker_thread = None
teleop._tool_state_lock = threading.Lock()
teleop._tool_target_open = True
teleop._tool_state_open = True
teleop._tool_command_pending = False
teleop._tool_command_failed = False
teleop.get_logger = lambda: FakeLogger()
teleop._start_tool_worker()
return teleop, started, release
def test_tool_state_changes_only_after_command_succeeds() -> None:
teleop, started, release = _tool_state_teleop()
try:
teleop._enqueue_tool_command(False, "test")
assert started.wait(timeout=1.0)
assert teleop._tool_state_snapshot() == (False, True, True, False)
release.set()
assert teleop._tool_command_queue is not None
teleop._tool_command_queue.join()
assert teleop._tool_state_snapshot() == (False, False, False, False)
finally:
release.set()
teleop._shutdown_tool_worker()
def test_tool_failure_keeps_previous_state_and_is_reported() -> None:
teleop, started, release = _tool_state_teleop(
command_error=RuntimeError("modbus failed")
)
try:
teleop._enqueue_tool_command(False, "test")
assert started.wait(timeout=1.0)
release.set()
assert teleop._tool_command_queue is not None
teleop._tool_command_queue.join()
assert teleop._tool_state_snapshot() == (False, True, False, True)
finally:
release.set()
teleop._shutdown_tool_worker()
def _joint_publishing_teleop() -> SingleArmVelocityTeleop:
names = [f"omnipic_joint_{index}" for index in range(1, 8)]
teleop = object.__new__(SingleArmVelocityTeleop)
+6 -3
View File
@@ -225,9 +225,12 @@ def peripheral_cfg(
time.sleep(0.5)
if set_initial_tool_state:
set_tool_position(robot, percent=0.75, device=1, scissorgripper=scissorgripper)
time.sleep(1.5)
set_tool_position(robot, percent=0.15, device=1, scissorgripper=scissorgripper)
set_tool_position(
robot,
percent=1.0,
device=1,
scissorgripper=scissorgripper,
)
elif scissorgripper == 2:
# 小型剪刀夹爪通过控制器 DO3/DO4 控制。
@@ -297,6 +297,11 @@ class SingleArmVelocityTeleop(Node):
self._control_fault_latched = False
self._stop_sent = True
self._trigger_tool_open = True
self._tool_state_lock = threading.Lock()
self._tool_target_open = True
self._tool_state_open: bool | None = None
self._tool_command_pending = False
self._tool_command_failed = False
self._last_primary_pressed: bool | None = None
self._last_trigger_pressed: bool | None = None
self._tool_command_queue: queue.Queue[tuple[bool, str] | None] | None = None
@@ -430,11 +435,20 @@ class SingleArmVelocityTeleop(Node):
def _setup_tool_control(self) -> None:
peripheral_arm = self._peripheral_arm_name()
if self._bool_parameter("configure_peripheral_on_connect"):
configure_on_connect = self._bool_parameter(
"configure_peripheral_on_connect"
)
if configure_on_connect:
self._adapter.configure_peripheral(
self._peripheral_config,
peripheral_arm,
)
if self._peripheral_config.set_initial_tool_state:
with self._tool_state_lock:
self._tool_target_open = True
self._tool_state_open = True
self._tool_command_pending = False
self._tool_command_failed = False
if not self._enable_tool_control:
if self._enable_trigger_gripper_control:
@@ -484,6 +498,10 @@ class SingleArmVelocityTeleop(Node):
)
return
with self._tool_state_lock:
self._tool_target_open = open_tool
self._tool_command_pending = True
item = (open_tool, source)
while True:
try:
@@ -512,15 +530,34 @@ class SingleArmVelocityTeleop(Node):
try:
self._adapter.set_tool_enabled(open_tool)
except Exception as exc:
with self._tool_state_lock:
self._tool_command_failed = True
self.get_logger().error(
f"{self._arm_name} tool {action} failed from {source}: {exc}"
)
continue
with self._tool_state_lock:
self._tool_state_open = open_tool
self._tool_command_failed = False
self.get_logger().info(
f"{self._arm_name} tool {action} command sent from {source}"
)
finally:
self._tool_command_queue.task_done()
if self._tool_command_queue.empty():
with self._tool_state_lock:
self._tool_command_pending = False
def _tool_state_snapshot(
self,
) -> tuple[bool, bool | None, bool, bool]:
with self._tool_state_lock:
return (
self._tool_target_open,
self._tool_state_open,
self._tool_command_pending,
self._tool_command_failed,
)
def _peripheral_arm_name(self) -> str:
configured = str(self.get_parameter("peripheral_arm").value).strip().lower()