feat: 记录夹爪逻辑执行状态
This commit is contained in:
@@ -27,3 +27,4 @@ arms:
|
|||||||
scissorgripper: 0
|
scissorgripper: 0
|
||||||
right:
|
right:
|
||||||
scissorgripper: 1
|
scissorgripper: 1
|
||||||
|
set_initial_tool_state: true
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ from types import ModuleType, SimpleNamespace
|
|||||||
import pytest
|
import pytest
|
||||||
import yaml
|
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 RealManAdapter
|
||||||
from xr_rm_teleop.realman_adapter import MockRealManAdapter
|
from xr_rm_teleop.realman_adapter import MockRealManAdapter
|
||||||
from xr_rm_teleop.fun_peripheral import (
|
from xr_rm_teleop.fun_peripheral import (
|
||||||
PeripheralConfig,
|
PeripheralConfig,
|
||||||
_configure_tool_frame,
|
_configure_tool_frame,
|
||||||
load_peripheral_config,
|
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]
|
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(
|
@pytest.mark.parametrize(
|
||||||
("config_name", "node_names"),
|
("config_name", "node_names"),
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import math
|
import math
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
@@ -42,6 +43,69 @@ class FakePublisher:
|
|||||||
self.messages.append(message)
|
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:
|
def _joint_publishing_teleop() -> SingleArmVelocityTeleop:
|
||||||
names = [f"omnipic_joint_{index}" for index in range(1, 8)]
|
names = [f"omnipic_joint_{index}" for index in range(1, 8)]
|
||||||
teleop = object.__new__(SingleArmVelocityTeleop)
|
teleop = object.__new__(SingleArmVelocityTeleop)
|
||||||
|
|||||||
@@ -225,9 +225,12 @@ def peripheral_cfg(
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
if set_initial_tool_state:
|
if set_initial_tool_state:
|
||||||
set_tool_position(robot, percent=0.75, device=1, scissorgripper=scissorgripper)
|
set_tool_position(
|
||||||
time.sleep(1.5)
|
robot,
|
||||||
set_tool_position(robot, percent=0.15, device=1, scissorgripper=scissorgripper)
|
percent=1.0,
|
||||||
|
device=1,
|
||||||
|
scissorgripper=scissorgripper,
|
||||||
|
)
|
||||||
|
|
||||||
elif scissorgripper == 2:
|
elif scissorgripper == 2:
|
||||||
# 小型剪刀夹爪通过控制器 DO3/DO4 控制。
|
# 小型剪刀夹爪通过控制器 DO3/DO4 控制。
|
||||||
|
|||||||
@@ -297,6 +297,11 @@ class SingleArmVelocityTeleop(Node):
|
|||||||
self._control_fault_latched = False
|
self._control_fault_latched = False
|
||||||
self._stop_sent = True
|
self._stop_sent = True
|
||||||
self._trigger_tool_open = 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_primary_pressed: bool | None = None
|
||||||
self._last_trigger_pressed: bool | None = None
|
self._last_trigger_pressed: bool | None = None
|
||||||
self._tool_command_queue: queue.Queue[tuple[bool, str] | None] | 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:
|
def _setup_tool_control(self) -> None:
|
||||||
peripheral_arm = self._peripheral_arm_name()
|
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._adapter.configure_peripheral(
|
||||||
self._peripheral_config,
|
self._peripheral_config,
|
||||||
peripheral_arm,
|
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 not self._enable_tool_control:
|
||||||
if self._enable_trigger_gripper_control:
|
if self._enable_trigger_gripper_control:
|
||||||
@@ -484,6 +498,10 @@ class SingleArmVelocityTeleop(Node):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
with self._tool_state_lock:
|
||||||
|
self._tool_target_open = open_tool
|
||||||
|
self._tool_command_pending = True
|
||||||
|
|
||||||
item = (open_tool, source)
|
item = (open_tool, source)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -512,15 +530,34 @@ class SingleArmVelocityTeleop(Node):
|
|||||||
try:
|
try:
|
||||||
self._adapter.set_tool_enabled(open_tool)
|
self._adapter.set_tool_enabled(open_tool)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
with self._tool_state_lock:
|
||||||
|
self._tool_command_failed = True
|
||||||
self.get_logger().error(
|
self.get_logger().error(
|
||||||
f"{self._arm_name} tool {action} failed from {source}: {exc}"
|
f"{self._arm_name} tool {action} failed from {source}: {exc}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
with self._tool_state_lock:
|
||||||
|
self._tool_state_open = open_tool
|
||||||
|
self._tool_command_failed = False
|
||||||
self.get_logger().info(
|
self.get_logger().info(
|
||||||
f"{self._arm_name} tool {action} command sent from {source}"
|
f"{self._arm_name} tool {action} command sent from {source}"
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self._tool_command_queue.task_done()
|
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:
|
def _peripheral_arm_name(self) -> str:
|
||||||
configured = str(self.get_parameter("peripheral_arm").value).strip().lower()
|
configured = str(self.get_parameter("peripheral_arm").value).strip().lower()
|
||||||
|
|||||||
Reference in New Issue
Block a user