feat: 发布 XR 手柄摇杆与按键
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
import math
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from builtin_interfaces.msg import Time
|
||||||
|
from xr_rm_input.udp_controller_receiver import UdpControllerReceiver
|
||||||
|
from xr_rm_input.xrobotoolkit_to_udp_bridge import (
|
||||||
|
_buttons_payload,
|
||||||
|
_controller_payload,
|
||||||
|
_stop_controller_payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _receiver_without_socket() -> UdpControllerReceiver:
|
||||||
|
receiver = object.__new__(UdpControllerReceiver)
|
||||||
|
receiver._quat_order = "xyzw"
|
||||||
|
receiver.get_clock = lambda: SimpleNamespace(
|
||||||
|
now=lambda: SimpleNamespace(to_msg=lambda: Time())
|
||||||
|
)
|
||||||
|
return receiver
|
||||||
|
|
||||||
|
|
||||||
|
def test_bridge_payload_contains_only_selected_controller_inputs() -> None:
|
||||||
|
buttons = _buttons_payload(
|
||||||
|
primary=lambda: True,
|
||||||
|
secondary=lambda: False,
|
||||||
|
)
|
||||||
|
payload = _controller_payload(
|
||||||
|
hand="left",
|
||||||
|
pose=[1.0, 2.0, 3.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
axis=[2.0, -2.0],
|
||||||
|
buttons=buttons,
|
||||||
|
grip_pressed=True,
|
||||||
|
trigger_pressed=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload == {
|
||||||
|
"hand": "left",
|
||||||
|
"grip": True,
|
||||||
|
"trigger": 0.0,
|
||||||
|
"pos": [1.0, 2.0, 3.0],
|
||||||
|
"quat": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
"pose_valid": True,
|
||||||
|
"pose_source": "xrobotoolkit",
|
||||||
|
"axis": [1.0, -1.0],
|
||||||
|
"buttons": {
|
||||||
|
"primary": True,
|
||||||
|
"secondary": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop_payload_uses_neutral_selected_inputs() -> None:
|
||||||
|
payload = _stop_controller_payload("right")
|
||||||
|
|
||||||
|
assert payload["axis"] == [0.0, 0.0]
|
||||||
|
assert payload["buttons"] == {
|
||||||
|
"primary": False,
|
||||||
|
"secondary": False,
|
||||||
|
}
|
||||||
|
assert "grip_value" not in payload
|
||||||
|
assert "trigger_value" not in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_publishes_selected_controller_inputs() -> None:
|
||||||
|
msg = _receiver_without_socket()._payload_to_msg(
|
||||||
|
{
|
||||||
|
"grip": True,
|
||||||
|
"trigger": 1.0,
|
||||||
|
"axis": [2.0, -2.0],
|
||||||
|
"buttons": {
|
||||||
|
"primary": True,
|
||||||
|
"secondary": False,
|
||||||
|
},
|
||||||
|
"pos": [1.0, 2.0, 3.0],
|
||||||
|
"quat": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
},
|
||||||
|
"left",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert msg.primary is True
|
||||||
|
assert msg.secondary is False
|
||||||
|
assert list(msg.axis) == [1.0, -1.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_defaults_invalid_optional_inputs() -> None:
|
||||||
|
msg = _receiver_without_socket()._payload_to_msg(
|
||||||
|
{
|
||||||
|
"grip": True,
|
||||||
|
"trigger": 0.4,
|
||||||
|
"axis": [math.nan, 0.0],
|
||||||
|
"buttons": [],
|
||||||
|
"pos": [1.0, 2.0, 3.0],
|
||||||
|
"quat": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
},
|
||||||
|
"right",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert msg.primary is False
|
||||||
|
assert msg.secondary is False
|
||||||
|
assert list(msg.axis) == [0.0, 0.0]
|
||||||
|
assert msg.grip is True
|
||||||
|
assert abs(msg.trigger - 0.4) < 1e-6
|
||||||
|
assert msg.pose.position.x == 1.0
|
||||||
|
assert msg.pose.position.y == 2.0
|
||||||
|
assert msg.pose.position.z == 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_defaults_missing_legacy_optional_inputs() -> None:
|
||||||
|
msg = _receiver_without_socket()._payload_to_msg(
|
||||||
|
{
|
||||||
|
"grip": True,
|
||||||
|
"trigger": 0.0,
|
||||||
|
"pos": [0.0, 1.0, 0.0],
|
||||||
|
"quat": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
},
|
||||||
|
"left",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert msg.primary is False
|
||||||
|
assert msg.secondary is False
|
||||||
|
assert list(msg.axis) == [0.0, 0.0]
|
||||||
|
assert msg.grip is True
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
"""XR 手柄 UDP 接收节点。
|
"""XR 手柄 UDP 接收节点。
|
||||||
|
|
||||||
从 UDP JSON 数据包中解析左右手柄位姿、握持键和扳机值,并发布为
|
从 UDP JSON 数据包中解析左右手柄位姿、Grip、Trigger、摇杆和主副按键,
|
||||||
`xr_rm_interfaces/XrController` 消息,供遥操作和夹爪节点订阅。
|
并发布为 `xr_rm_interfaces/XrController` 消息,供遥操作和夹爪节点订阅。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import socket
|
import socket
|
||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -124,6 +125,8 @@ class UdpControllerReceiver(Node):
|
|||||||
pos, quat = self._extract_pose(payload)
|
pos, quat = self._extract_pose(payload)
|
||||||
if len(pos) != 3 or len(quat) != 4:
|
if len(pos) != 3 or len(quat) != 4:
|
||||||
raise ValueError("expected pos[3] and quat[4]")
|
raise ValueError("expected pos[3] and quat[4]")
|
||||||
|
axis = self._optional_axis(payload.get("axis"))
|
||||||
|
primary, secondary = self._optional_buttons(payload.get("buttons"))
|
||||||
|
|
||||||
msg = XrController()
|
msg = XrController()
|
||||||
msg.header.stamp = self.get_clock().now().to_msg()
|
msg.header.stamp = self.get_clock().now().to_msg()
|
||||||
@@ -144,6 +147,9 @@ class UdpControllerReceiver(Node):
|
|||||||
|
|
||||||
msg.grip = grip
|
msg.grip = grip
|
||||||
msg.trigger = self._clamp_float(payload.get("trigger", 0.0), 0.0, 1.0)
|
msg.trigger = self._clamp_float(payload.get("trigger", 0.0), 0.0, 1.0)
|
||||||
|
msg.primary = primary
|
||||||
|
msg.secondary = secondary
|
||||||
|
msg.axis = axis
|
||||||
msg.pose.position.x = float(pos[0])
|
msg.pose.position.x = float(pos[0])
|
||||||
msg.pose.position.y = float(pos[1])
|
msg.pose.position.y = float(pos[1])
|
||||||
msg.pose.position.z = float(pos[2])
|
msg.pose.position.z = float(pos[2])
|
||||||
@@ -213,6 +219,28 @@ class UdpControllerReceiver(Node):
|
|||||||
raise ValueError("expected 3D position")
|
raise ValueError("expected 3D position")
|
||||||
return [float(item) for item in vector]
|
return [float(item) for item in vector]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _optional_axis(value: Any) -> list[float]:
|
||||||
|
try:
|
||||||
|
axis = [float(item) for item in value]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return [0.0, 0.0]
|
||||||
|
if len(axis) != 2 or not all(math.isfinite(item) for item in axis):
|
||||||
|
return [0.0, 0.0]
|
||||||
|
return [
|
||||||
|
min(max(axis[0], -1.0), 1.0),
|
||||||
|
min(max(axis[1], -1.0), 1.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _optional_buttons(cls, value: Any) -> tuple[bool, bool]:
|
||||||
|
if not isinstance(value, Mapping):
|
||||||
|
return False, False
|
||||||
|
return (
|
||||||
|
cls._as_bool(value.get("primary", False)),
|
||||||
|
cls._as_bool(value.get("secondary", False)),
|
||||||
|
)
|
||||||
|
|
||||||
def _quaternion(self, value: Any) -> list[float]:
|
def _quaternion(self, value: Any) -> list[float]:
|
||||||
if isinstance(value, Mapping):
|
if isinstance(value, Mapping):
|
||||||
if self._quat_order == "wxyz":
|
if self._quat_order == "wxyz":
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""XRoboToolkit SDK 到当前 UDP controller JSON 协议的桥接脚本。
|
"""XRoboToolkit SDK 到当前 UDP controller JSON 协议的桥接脚本。
|
||||||
|
|
||||||
该脚本运行在安装了 `xrobotoolkit_sdk` 的 Python 环境中,从官方
|
该脚本运行在安装了 `xrobotoolkit_sdk` 的 Python 环境中,从官方
|
||||||
XRoboToolkit PC-Service SDK 读取 PICO 左右手柄 pose / grip / trigger,
|
XRoboToolkit PC-Service SDK 读取 PICO 左右手柄 pose、Grip、Trigger、
|
||||||
再发送现有 `udp_controller_receiver` 已兼容的 UDP JSON 包。
|
摇杆和主副按键,再发送 `udp_controller_receiver` 兼容的 UDP JSON 包。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -94,8 +94,6 @@ def _controller_payload(
|
|||||||
*,
|
*,
|
||||||
hand: str,
|
hand: str,
|
||||||
pose: Any,
|
pose: Any,
|
||||||
grip_value: Any,
|
|
||||||
trigger_value: Any,
|
|
||||||
axis: Any,
|
axis: Any,
|
||||||
buttons: dict[str, bool],
|
buttons: dict[str, bool],
|
||||||
grip_pressed: bool,
|
grip_pressed: bool,
|
||||||
@@ -103,8 +101,6 @@ def _controller_payload(
|
|||||||
pose_valid: bool = True,
|
pose_valid: bool = True,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
pos, quat = _pose_to_pos_quat(pose) if pose_valid else (ZERO_POS.copy(), IDENTITY_QUAT.copy())
|
pos, quat = _pose_to_pos_quat(pose) if pose_valid else (ZERO_POS.copy(), IDENTITY_QUAT.copy())
|
||||||
grip_float = _clamp_float(grip_value, 0.0, 1.0)
|
|
||||||
trigger_float = _clamp_float(trigger_value, 0.0, 1.0)
|
|
||||||
return {
|
return {
|
||||||
"hand": hand,
|
"hand": hand,
|
||||||
"grip": pose_valid and grip_pressed,
|
"grip": pose_valid and grip_pressed,
|
||||||
@@ -113,8 +109,6 @@ def _controller_payload(
|
|||||||
"quat": quat,
|
"quat": quat,
|
||||||
"pose_valid": pose_valid,
|
"pose_valid": pose_valid,
|
||||||
"pose_source": POSE_SOURCE,
|
"pose_source": POSE_SOURCE,
|
||||||
"grip_value": grip_float,
|
|
||||||
"trigger_value": trigger_float,
|
|
||||||
"axis": _safe_axis(axis),
|
"axis": _safe_axis(axis),
|
||||||
"buttons": buttons,
|
"buttons": buttons,
|
||||||
}
|
}
|
||||||
@@ -129,15 +123,10 @@ def _stop_controller_payload(hand: str) -> dict[str, Any]:
|
|||||||
"quat": IDENTITY_QUAT.copy(),
|
"quat": IDENTITY_QUAT.copy(),
|
||||||
"pose_valid": False,
|
"pose_valid": False,
|
||||||
"pose_source": POSE_SOURCE,
|
"pose_source": POSE_SOURCE,
|
||||||
"grip_value": 0.0,
|
|
||||||
"trigger_value": 0.0,
|
|
||||||
"axis": [0.0, 0.0],
|
"axis": [0.0, 0.0],
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"grip": False,
|
|
||||||
"primary": False,
|
"primary": False,
|
||||||
"secondary": False,
|
"secondary": False,
|
||||||
"menu": False,
|
|
||||||
"axis_click": False,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,18 +170,12 @@ def _send_stop_packets(
|
|||||||
|
|
||||||
def _buttons_payload(
|
def _buttons_payload(
|
||||||
*,
|
*,
|
||||||
grip: bool,
|
|
||||||
primary: Callable[[], Any],
|
primary: Callable[[], Any],
|
||||||
secondary: Callable[[], Any],
|
secondary: Callable[[], Any],
|
||||||
menu: Callable[[], Any],
|
|
||||||
axis_click: Callable[[], Any],
|
|
||||||
) -> dict[str, bool]:
|
) -> dict[str, bool]:
|
||||||
return {
|
return {
|
||||||
"grip": grip,
|
|
||||||
"primary": _safe_bool(primary),
|
"primary": _safe_bool(primary),
|
||||||
"secondary": _safe_bool(secondary),
|
"secondary": _safe_bool(secondary),
|
||||||
"menu": _safe_bool(menu),
|
|
||||||
"axis_click": _safe_bool(axis_click),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -321,15 +304,10 @@ def main(argv: Sequence[str] | None = None) -> None:
|
|||||||
"left": _controller_payload(
|
"left": _controller_payload(
|
||||||
hand="left",
|
hand="left",
|
||||||
pose=xrt.get_left_controller_pose(),
|
pose=xrt.get_left_controller_pose(),
|
||||||
grip_value=left_grip_value,
|
|
||||||
trigger_value=left_trigger_value,
|
|
||||||
axis=xrt.get_left_axis(),
|
axis=xrt.get_left_axis(),
|
||||||
buttons=_buttons_payload(
|
buttons=_buttons_payload(
|
||||||
grip=left_grip,
|
|
||||||
primary=xrt.get_X_button,
|
primary=xrt.get_X_button,
|
||||||
secondary=xrt.get_Y_button,
|
secondary=xrt.get_Y_button,
|
||||||
menu=xrt.get_left_menu_button,
|
|
||||||
axis_click=xrt.get_left_axis_click,
|
|
||||||
),
|
),
|
||||||
grip_pressed=left_grip,
|
grip_pressed=left_grip,
|
||||||
trigger_pressed=left_trigger,
|
trigger_pressed=left_trigger,
|
||||||
@@ -337,15 +315,10 @@ def main(argv: Sequence[str] | None = None) -> None:
|
|||||||
"right": _controller_payload(
|
"right": _controller_payload(
|
||||||
hand="right",
|
hand="right",
|
||||||
pose=xrt.get_right_controller_pose(),
|
pose=xrt.get_right_controller_pose(),
|
||||||
grip_value=right_grip_value,
|
|
||||||
trigger_value=right_trigger_value,
|
|
||||||
axis=xrt.get_right_axis(),
|
axis=xrt.get_right_axis(),
|
||||||
buttons=_buttons_payload(
|
buttons=_buttons_payload(
|
||||||
grip=right_grip,
|
|
||||||
primary=xrt.get_A_button,
|
primary=xrt.get_A_button,
|
||||||
secondary=xrt.get_B_button,
|
secondary=xrt.get_B_button,
|
||||||
menu=xrt.get_right_menu_button,
|
|
||||||
axis_click=xrt.get_right_axis_click,
|
|
||||||
),
|
),
|
||||||
grip_pressed=right_grip,
|
grip_pressed=right_grip,
|
||||||
trigger_pressed=right_trigger,
|
trigger_pressed=right_trigger,
|
||||||
|
|||||||
Reference in New Issue
Block a user