O12 gui
This commit is contained in:
@@ -9,6 +9,10 @@ class HandConfig:
|
|||||||
joint_names_en: Optional[List[str]] = None
|
joint_names_en: Optional[List[str]] = None
|
||||||
init_pos: List[int] = field(default_factory=list)
|
init_pos: List[int] = field(default_factory=list)
|
||||||
preset_actions: Optional[Dict[str, List[int]]] = None
|
preset_actions: Optional[Dict[str, List[int]]] = None
|
||||||
|
# Integer GUI values are divided by this scale before publishing.
|
||||||
|
# Legacy hands use raw u8 values (scale=1); O12 uses milliradians.
|
||||||
|
position_scale: int = 1
|
||||||
|
position_unit: str = "u8"
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 常量字典(仅构建一次)
|
# 常量字典(仅构建一次)
|
||||||
@@ -258,5 +262,33 @@ _HAND_CONFIGS: Dict[str, HandConfig] = {
|
|||||||
"拇指压感准备2": [139, 18, 130, 0, 0, 0]
|
"拇指压感准备2": [139, 18, 130, 0, 0, 0]
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
"O12": HandConfig(
|
||||||
|
# O12 active-angle API order. Keep this independent from the
|
||||||
|
# physical motor order exposed by some SDK metadata.
|
||||||
|
joint_names_en=[
|
||||||
|
"thumb_roll", "thumb_abad", "thumb_mcp", "thumb_pip",
|
||||||
|
"index_abad", "index_mcp", "index_pip",
|
||||||
|
"middle_abad", "middle_mcp", "middle_pip",
|
||||||
|
"ring_mcp", "pinky_mcp",
|
||||||
|
],
|
||||||
|
joint_names=[
|
||||||
|
"拇指旋转", "拇指侧摆", "拇指根部", "拇指中部",
|
||||||
|
"食指侧摆", "食指根部", "食指中部",
|
||||||
|
"中指侧摆", "中指根部", "中指中部",
|
||||||
|
"无名指根部", "小指根部",
|
||||||
|
],
|
||||||
|
# Values are milliradians so QSlider can retain 0.001 rad resolution.
|
||||||
|
init_pos=[0] * 12,
|
||||||
|
preset_actions={
|
||||||
|
"展开": [0] * 12,
|
||||||
|
"食指轻弯": [0, 0, 0, 0, 0, 300, 400, 0, 0, 0, 0, 0],
|
||||||
|
"半握": [
|
||||||
|
300, -200, -200, -500,
|
||||||
|
0, 650, 700, 0, 650, 800, 700, 700,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
position_scale=1000,
|
||||||
|
position_unit="rad",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
HAND_CONFIGS = MappingProxyType(_HAND_CONFIGS)
|
HAND_CONFIGS = MappingProxyType(_HAND_CONFIGS)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import time, json
|
import time, json
|
||||||
|
import math
|
||||||
import threading
|
import threading
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
@@ -37,6 +38,12 @@ _CANONICAL_COMMAND_NAMES = {
|
|||||||
"thumb_cmc_pitch", "thumb_cmc_roll", "index_mcp_pitch",
|
"thumb_cmc_pitch", "thumb_cmc_roll", "index_mcp_pitch",
|
||||||
"middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch",
|
"middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch",
|
||||||
],
|
],
|
||||||
|
"O12": [
|
||||||
|
"thumb_roll", "thumb_abad", "thumb_mcp", "thumb_pip",
|
||||||
|
"index_abad", "index_mcp", "index_pip",
|
||||||
|
"middle_abad", "middle_mcp", "middle_pip",
|
||||||
|
"ring_mcp", "pinky_mcp",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
_CANONICAL_COMMAND_BOUNDS = {
|
_CANONICAL_COMMAND_BOUNDS = {
|
||||||
@@ -50,15 +57,35 @@ _CANONICAL_COMMAND_BOUNDS = {
|
|||||||
"L6": [(0, 255)] * 6,
|
"L6": [(0, 255)] * 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Integer slider bounds in milliradians, derived from the O12 active-angle
|
||||||
|
# limits. Right and left thumb signs differ; all other limits are shared.
|
||||||
|
_O12_COMMAND_BOUNDS_MRAD = {
|
||||||
|
"right": [
|
||||||
|
(0, 942), (-1387, 0), (-827, 0), (-1291, 0),
|
||||||
|
(-261, 261), (0, 1352), (0, 1530), (-261, 261),
|
||||||
|
(0, 1357), (0, 1815), (0, 1535), (0, 1535),
|
||||||
|
],
|
||||||
|
"left": [
|
||||||
|
(-942, 0), (0, 1387), (-827, 0), (-1291, 0),
|
||||||
|
(-261, 261), (0, 1352), (0, 1530), (-261, 261),
|
||||||
|
(0, 1357), (0, 1815), (0, 1535), (0, 1535),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ROS2NodeManager(QObject):
|
class ROS2NodeManager(QObject):
|
||||||
"""ROS2节点管理器,处理ROS通信"""
|
"""ROS2节点管理器,处理ROS通信"""
|
||||||
status_updated = pyqtSignal(str, str) # 状态类型, 消息内容
|
status_updated = pyqtSignal(str, str) # 状态类型, 消息内容
|
||||||
|
feedback_updated = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, node_name: str = "hand_control_node"):
|
def __init__(self, node_name: str = "hand_control_node"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.node = None
|
self.node = None
|
||||||
self.publisher = None
|
self.publisher = None
|
||||||
|
self.publisher_arc = None
|
||||||
|
self.state_subscription = None
|
||||||
|
self.speed_pub = None
|
||||||
|
self.torque_pub = None
|
||||||
self.joint_state = JointState()
|
self.joint_state = JointState()
|
||||||
self.joint_state.header = Header()
|
self.joint_state.header = Header()
|
||||||
|
|
||||||
@@ -88,27 +115,40 @@ class ROS2NodeManager(QObject):
|
|||||||
self.node.get_parameter('topic_prefix').value
|
self.node.get_parameter('topic_prefix').value
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.is_arc == True:
|
if self.hand_joint == "O12":
|
||||||
|
topic_base = f'/o12/{self.hand_type}'
|
||||||
|
self.publisher = self.node.create_publisher(
|
||||||
|
JointState, f'{topic_base}/joint_cmd', 10
|
||||||
|
)
|
||||||
|
self.state_subscription = self.node.create_subscription(
|
||||||
|
JointState,
|
||||||
|
f'{topic_base}/joint_states',
|
||||||
|
self._o12_feedback_callback,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
elif self.is_arc == True:
|
||||||
# 创建发布者
|
# 创建发布者
|
||||||
self.publisher_arc = self.node.create_publisher(
|
self.publisher_arc = self.node.create_publisher(
|
||||||
JointState,
|
JointState,
|
||||||
self.topic(f'/cb_{self.hand_type}_hand_control_cmd_arc'),
|
self.topic(f'/cb_{self.hand_type}_hand_control_cmd_arc'),
|
||||||
10
|
10
|
||||||
)
|
)
|
||||||
# 创建发布者
|
# 创建旧型号发布者
|
||||||
self.publisher = self.node.create_publisher(
|
if self.hand_joint != "O12":
|
||||||
JointState,
|
self.publisher = self.node.create_publisher(
|
||||||
self.topic(f'/cb_{self.hand_type}_hand_control_cmd'),
|
JointState,
|
||||||
10
|
self.topic(f'/cb_{self.hand_type}_hand_control_cmd'),
|
||||||
)
|
10
|
||||||
|
)
|
||||||
self.snapshot_publisher = self.node.create_publisher(
|
self.snapshot_publisher = self.node.create_publisher(
|
||||||
JointState, self.topic('/calibration_pose_snapshot'), 10
|
JointState, self.topic('/calibration_pose_snapshot'), 10
|
||||||
)
|
)
|
||||||
# 新增 speed / torque 发布者
|
# 新增 speed / torque 发布者
|
||||||
self.speed_pub = self.node.create_publisher(
|
if self.hand_joint != "O12":
|
||||||
String, self.topic('/cb_hand_setting_cmd'), 10)
|
self.speed_pub = self.node.create_publisher(
|
||||||
self.torque_pub = self.node.create_publisher(
|
String, self.topic('/cb_hand_setting_cmd'), 10)
|
||||||
String, self.topic('/cb_hand_setting_cmd'), 10)
|
self.torque_pub = self.node.create_publisher(
|
||||||
|
String, self.topic('/cb_hand_setting_cmd'), 10)
|
||||||
self.status_updated.emit(
|
self.status_updated.emit(
|
||||||
"info",
|
"info",
|
||||||
f"ROS2节点初始化成功: {self.topic_prefix or '/'} "
|
f"ROS2节点初始化成功: {self.topic_prefix or '/'} "
|
||||||
@@ -141,15 +181,36 @@ class ROS2NodeManager(QObject):
|
|||||||
while rclpy.ok() and self.node:
|
while rclpy.ok() and self.node:
|
||||||
rclpy.spin_once(self.node, timeout_sec=0.1)
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
|
||||||
|
def _o12_feedback_callback(self, msg: JointState):
|
||||||
|
"""Forward 12-channel O12 radian feedback safely into the Qt thread."""
|
||||||
|
if len(msg.position) != 12:
|
||||||
|
self.status_updated.emit(
|
||||||
|
"error", f"O12位置反馈长度错误: {len(msg.position)},应为12"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.feedback_updated.emit([float(value) for value in msg.position])
|
||||||
|
|
||||||
|
def command_bounds(self, count: int):
|
||||||
|
if self.hand_joint == "O12":
|
||||||
|
bounds = _O12_COMMAND_BOUNDS_MRAD.get(self.hand_type)
|
||||||
|
else:
|
||||||
|
bounds = _CANONICAL_COMMAND_BOUNDS.get(self.hand_joint)
|
||||||
|
if not bounds or len(bounds) != count:
|
||||||
|
return [(0, 255)] * count
|
||||||
|
return bounds
|
||||||
|
|
||||||
def bound_positions(self, positions: List[int]) -> List[int]:
|
def bound_positions(self, positions: List[int]) -> List[int]:
|
||||||
bounds = _CANONICAL_COMMAND_BOUNDS.get(self.hand_joint)
|
bounds = self.command_bounds(len(positions))
|
||||||
if not bounds or len(bounds) != len(positions):
|
|
||||||
return list(positions)
|
|
||||||
return [
|
return [
|
||||||
max(minimum, min(maximum, int(value)))
|
max(minimum, min(maximum, int(value)))
|
||||||
for value, (minimum, maximum) in zip(positions, bounds)
|
for value, (minimum, maximum) in zip(positions, bounds)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def wire_positions(self, positions: List[int]) -> List[float]:
|
||||||
|
positions = self.bound_positions(positions)
|
||||||
|
scale = _HAND_CONFIGS[self.hand_joint].position_scale
|
||||||
|
return [float(value) / float(scale) for value in positions]
|
||||||
|
|
||||||
def publish_joint_state(self, positions: List[int]):
|
def publish_joint_state(self, positions: List[int]):
|
||||||
"""发布关节状态消息"""
|
"""发布关节状态消息"""
|
||||||
if not self.publisher or not self.node:
|
if not self.publisher or not self.node:
|
||||||
@@ -158,15 +219,21 @@ class ROS2NodeManager(QObject):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
positions = self.bound_positions(positions)
|
positions = self.bound_positions(positions)
|
||||||
|
wire_positions = self.wire_positions(positions)
|
||||||
self.joint_state.header.stamp = self.node.get_clock().now().to_msg()
|
self.joint_state.header.stamp = self.node.get_clock().now().to_msg()
|
||||||
self.joint_state.position = [float(pos) for pos in positions]
|
self.joint_state.position = wire_positions
|
||||||
# self.joint_state.velocity = [0.1] * len(positions)
|
# self.joint_state.velocity = [0.1] * len(positions)
|
||||||
# self.joint_state.effort = [0.01] * len(positions)
|
# self.joint_state.effort = [0.01] * len(positions)
|
||||||
# 如果有关节名称,添加到消息中
|
# 如果有关节名称,添加到消息中
|
||||||
#hand_config = HandConfig.from_hand_type(self.hand_joint)
|
#hand_config = HandConfig.from_hand_type(self.hand_joint)
|
||||||
hand_config = _HAND_CONFIGS[self.hand_joint]
|
hand_config = _HAND_CONFIGS[self.hand_joint]
|
||||||
canonical_names = _CANONICAL_COMMAND_NAMES.get(self.hand_joint)
|
canonical_names = _CANONICAL_COMMAND_NAMES.get(self.hand_joint)
|
||||||
if canonical_names and len(canonical_names) == len(positions):
|
# O12 SDK 1.1.8 may expose names in physical motor order while the
|
||||||
|
# position array uses active-angle order. An empty name array makes
|
||||||
|
# the documented position order unambiguous to the driver.
|
||||||
|
if self.hand_joint == "O12":
|
||||||
|
self.joint_state.name = []
|
||||||
|
elif canonical_names and len(canonical_names) == len(positions):
|
||||||
self.joint_state.name = canonical_names
|
self.joint_state.name = canonical_names
|
||||||
elif len(hand_config.joint_names) == len(positions):
|
elif len(hand_config.joint_names) == len(positions):
|
||||||
if hand_config.joint_names_en != None:
|
if hand_config.joint_names_en != None:
|
||||||
@@ -175,7 +242,7 @@ class ROS2NodeManager(QObject):
|
|||||||
self.joint_state.name = hand_config.joint_names
|
self.joint_state.name = hand_config.joint_names
|
||||||
|
|
||||||
self.publisher.publish(self.joint_state)
|
self.publisher.publish(self.joint_state)
|
||||||
if self.is_arc == True:
|
if self.is_arc == True and self.hand_joint != "O12":
|
||||||
if self.hand_joint == "O6":
|
if self.hand_joint == "O6":
|
||||||
if self.hand_type == "left":
|
if self.hand_type == "left":
|
||||||
pose = range_to_arc_left(positions,self.hand_joint)
|
pose = range_to_arc_left(positions,self.hand_joint)
|
||||||
@@ -210,14 +277,21 @@ class ROS2NodeManager(QObject):
|
|||||||
positions = self.bound_positions(positions)
|
positions = self.bound_positions(positions)
|
||||||
self.publish_joint_state(positions)
|
self.publish_joint_state(positions)
|
||||||
self.joint_state.header.stamp = self.node.get_clock().now().to_msg()
|
self.joint_state.header.stamp = self.node.get_clock().now().to_msg()
|
||||||
self.joint_state.position = [float(pos) for pos in positions]
|
self.joint_state.position = self.wire_positions(positions)
|
||||||
canonical_names = _CANONICAL_COMMAND_NAMES.get(self.hand_joint)
|
canonical_names = _CANONICAL_COMMAND_NAMES.get(self.hand_joint)
|
||||||
if canonical_names:
|
if canonical_names and self.hand_joint != "O12":
|
||||||
self.joint_state.name = canonical_names
|
self.joint_state.name = canonical_names
|
||||||
|
elif self.hand_joint == "O12":
|
||||||
|
self.joint_state.name = []
|
||||||
self.snapshot_publisher.publish(self.joint_state)
|
self.snapshot_publisher.publish(self.joint_state)
|
||||||
self.status_updated.emit("info", "当前标定姿势快照已发布")
|
self.status_updated.emit("info", "当前标定姿势快照已发布")
|
||||||
|
|
||||||
def publish_speed(self, val: int):
|
def publish_speed(self, val: int):
|
||||||
|
if self.hand_joint == "O12":
|
||||||
|
self.status_updated.emit(
|
||||||
|
"warning", "O12不使用旧版u8速度接口;请通过角度轨迹限制速度"
|
||||||
|
)
|
||||||
|
return
|
||||||
if self.hand_joint.upper() in ("O6", "L6"):
|
if self.hand_joint.upper() in ("O6", "L6"):
|
||||||
joint_len = 6
|
joint_len = 6
|
||||||
elif self.hand_joint == "L7":
|
elif self.hand_joint == "L7":
|
||||||
@@ -237,6 +311,11 @@ class ROS2NodeManager(QObject):
|
|||||||
self.speed_pub.publish(msg)
|
self.speed_pub.publish(msg)
|
||||||
|
|
||||||
def publish_torque(self, val: int):
|
def publish_torque(self, val: int):
|
||||||
|
if self.hand_joint == "O12":
|
||||||
|
self.status_updated.emit(
|
||||||
|
"warning", "O12不使用旧版u8扭矩接口;位置GUI已禁用该设置"
|
||||||
|
)
|
||||||
|
return
|
||||||
if self.hand_joint.upper() in ("O6", "L6"):
|
if self.hand_joint.upper() in ("O6", "L6"):
|
||||||
joint_len = 6
|
joint_len = 6
|
||||||
elif self.hand_joint == "L7":
|
elif self.hand_joint == "L7":
|
||||||
@@ -278,11 +357,16 @@ class HandControlGUI(QWidget):
|
|||||||
# 设置ROS管理器
|
# 设置ROS管理器
|
||||||
self.ros_manager = ros_manager
|
self.ros_manager = ros_manager
|
||||||
self.ros_manager.status_updated.connect(self.update_status)
|
self.ros_manager.status_updated.connect(self.update_status)
|
||||||
|
self.ros_manager.feedback_updated.connect(self.on_feedback_updated)
|
||||||
|
|
||||||
# 获取手部配置
|
# 获取手部配置
|
||||||
self.hand_joint = self.ros_manager.hand_joint
|
self.hand_joint = self.ros_manager.hand_joint
|
||||||
self.hand_type = self.ros_manager.hand_type
|
self.hand_type = self.ros_manager.hand_type
|
||||||
self.hand_config = _HAND_CONFIGS[self.hand_joint]
|
self.hand_config = _HAND_CONFIGS[self.hand_joint]
|
||||||
|
self.feedback_positions = None
|
||||||
|
# O12 only publishes after an explicit slider/preset action. This
|
||||||
|
# prevents opening or closing the real hand merely by launching GUI.
|
||||||
|
self.command_dirty = self.hand_joint != "O12"
|
||||||
|
|
||||||
# 初始化UI
|
# 初始化UI
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
@@ -293,6 +377,20 @@ class HandControlGUI(QWidget):
|
|||||||
self.publish_timer.timeout.connect(self.publish_joint_state)
|
self.publish_timer.timeout.connect(self.publish_joint_state)
|
||||||
self.publish_timer.start()
|
self.publish_timer.start()
|
||||||
|
|
||||||
|
def format_position(self, value: int) -> str:
|
||||||
|
"""Format an integer GUI value in the hand's actual command unit."""
|
||||||
|
scale = self.hand_config.position_scale
|
||||||
|
if self.hand_config.position_unit == "rad":
|
||||||
|
radians = float(value) / float(scale)
|
||||||
|
return f"{radians:.3f} rad / {math.degrees(radians):.1f}°"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def command_positions(self) -> List[float]:
|
||||||
|
"""Return current slider values converted to wire units."""
|
||||||
|
return self.ros_manager.wire_positions(
|
||||||
|
[slider.value() for slider in self.sliders]
|
||||||
|
)
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
"""初始化用户界面"""
|
"""初始化用户界面"""
|
||||||
# 设置窗口属性
|
# 设置窗口属性
|
||||||
@@ -470,13 +568,11 @@ class HandControlGUI(QWidget):
|
|||||||
for i, (name, value) in enumerate(zip(
|
for i, (name, value) in enumerate(zip(
|
||||||
self.hand_config.joint_names, self.hand_config.init_pos
|
self.hand_config.joint_names, self.hand_config.init_pos
|
||||||
)):
|
)):
|
||||||
bounds = _CANONICAL_COMMAND_BOUNDS.get(self.hand_joint)
|
bounds = self.ros_manager.command_bounds(len(self.hand_config.init_pos))
|
||||||
minimum, maximum = (
|
minimum, maximum = bounds[i]
|
||||||
bounds[i] if bounds and i < len(bounds) else (0, 255)
|
|
||||||
)
|
|
||||||
value = max(minimum, min(maximum, int(value)))
|
value = max(minimum, min(maximum, int(value)))
|
||||||
# 创建标签
|
# 创建标签
|
||||||
label = QLabel(f"{name}: {value}")
|
label = QLabel(f"{name}: {self.format_position(value)}")
|
||||||
label.setMinimumWidth(120)
|
label.setMinimumWidth(120)
|
||||||
|
|
||||||
# 创建滑动条
|
# 创建滑动条
|
||||||
@@ -569,6 +665,9 @@ class HandControlGUI(QWidget):
|
|||||||
|
|
||||||
# —— 2. 新增:速度与扭矩设置(每行一个)——
|
# —— 2. 新增:速度与扭矩设置(每行一个)——
|
||||||
quick_set_gb = QGroupBox("快速设置")
|
quick_set_gb = QGroupBox("快速设置")
|
||||||
|
if self.hand_joint == "O12":
|
||||||
|
quick_set_gb.setEnabled(False)
|
||||||
|
quick_set_gb.setToolTip("O12不使用旧版0-255速度/扭矩设置接口")
|
||||||
qv_layout = QVBoxLayout(quick_set_gb)
|
qv_layout = QVBoxLayout(quick_set_gb)
|
||||||
|
|
||||||
# 速度行
|
# 速度行
|
||||||
@@ -697,7 +796,10 @@ class HandControlGUI(QWidget):
|
|||||||
"""滑动条值改变事件处理"""
|
"""滑动条值改变事件处理"""
|
||||||
if 0 <= index < len(self.slider_labels):
|
if 0 <= index < len(self.slider_labels):
|
||||||
joint_name = self.hand_config.joint_names[index]
|
joint_name = self.hand_config.joint_names[index]
|
||||||
self.slider_labels[index].setText(f"{joint_name}: {value}")
|
self.slider_labels[index].setText(
|
||||||
|
f"{joint_name}: {self.format_position(value)}"
|
||||||
|
)
|
||||||
|
self.command_dirty = True
|
||||||
|
|
||||||
# 更新数值显示
|
# 更新数值显示
|
||||||
self.update_value_display()
|
self.update_value_display()
|
||||||
@@ -705,10 +807,23 @@ class HandControlGUI(QWidget):
|
|||||||
def update_value_display(self):
|
def update_value_display(self):
|
||||||
"""更新数值显示面板内容"""
|
"""更新数值显示面板内容"""
|
||||||
# 获取所有滑动条的当前值
|
# 获取所有滑动条的当前值
|
||||||
values = [slider.value() for slider in self.sliders]
|
command = self.command_positions()
|
||||||
|
command_text = ", ".join(f"{value:.3f}" for value in command)
|
||||||
|
lines = [f"目标({self.hand_config.position_unit}): [{command_text}]"]
|
||||||
|
if self.feedback_positions is not None:
|
||||||
|
feedback_text = ", ".join(
|
||||||
|
f"{value:.3f}" for value in self.feedback_positions
|
||||||
|
)
|
||||||
|
lines.append(f"反馈(rad): [{feedback_text}]")
|
||||||
|
self.value_display.setText("\n".join(lines))
|
||||||
|
|
||||||
# 格式化显示为列表形式
|
def on_feedback_updated(self, positions):
|
||||||
self.value_display.setText(f"{values}")
|
"""Display actual O12 joint feedback without moving command sliders."""
|
||||||
|
self.feedback_positions = list(positions)
|
||||||
|
self.update_value_display()
|
||||||
|
if hasattr(self, "connection_status"):
|
||||||
|
self.connection_status.setText("O12右手已连接并收到位置反馈")
|
||||||
|
self.connection_status.setObjectName("StatusInfo")
|
||||||
|
|
||||||
def on_preset_action_clicked(self, positions: List[int]):
|
def on_preset_action_clicked(self, positions: List[int]):
|
||||||
"""预设动作按钮点击事件处理"""
|
"""预设动作按钮点击事件处理"""
|
||||||
@@ -747,7 +862,25 @@ class HandControlGUI(QWidget):
|
|||||||
self.cycle_button.setText("循环运行预设动作")
|
self.cycle_button.setText("循环运行预设动作")
|
||||||
self.reset_preset_buttons_color()
|
self.reset_preset_buttons_color()
|
||||||
|
|
||||||
self.status_updated.emit("warning", "已停止所有动作")
|
if self.hand_joint == "O12" and self.feedback_positions is not None:
|
||||||
|
# Hold the latest measured pose instead of merely stopping topic
|
||||||
|
# publication while a previous position target is still active.
|
||||||
|
scale = self.hand_config.position_scale
|
||||||
|
hold_values = [
|
||||||
|
int(round(value * scale)) for value in self.feedback_positions
|
||||||
|
]
|
||||||
|
hold_values = self.ros_manager.bound_positions(hold_values)
|
||||||
|
for slider, value in zip(self.sliders, hold_values):
|
||||||
|
slider.blockSignals(True)
|
||||||
|
slider.setValue(value)
|
||||||
|
slider.blockSignals(False)
|
||||||
|
self.command_dirty = True
|
||||||
|
self.publish_joint_state()
|
||||||
|
self.update_value_display()
|
||||||
|
self.status_updated.emit("warning", "已下发当前反馈位置并保持")
|
||||||
|
else:
|
||||||
|
self.command_dirty = False
|
||||||
|
self.status_updated.emit("warning", "已停止连续动作和新的位置发布")
|
||||||
|
|
||||||
def on_save_pose_clicked(self):
|
def on_save_pose_clicked(self):
|
||||||
"""发布当前滑块姿势,供hand_pose_capture写入profile。"""
|
"""发布当前滑块姿势,供hand_pose_capture写入profile。"""
|
||||||
@@ -834,8 +967,11 @@ class HandControlGUI(QWidget):
|
|||||||
|
|
||||||
def publish_joint_state(self):
|
def publish_joint_state(self):
|
||||||
"""发布当前关节状态"""
|
"""发布当前关节状态"""
|
||||||
|
if self.hand_joint == "O12" and not self.command_dirty:
|
||||||
|
return
|
||||||
positions = [slider.value() for slider in self.sliders]
|
positions = [slider.value() for slider in self.sliders]
|
||||||
self.ros_manager.publish_joint_state(positions)
|
self.ros_manager.publish_joint_state(positions)
|
||||||
|
self.command_dirty = False
|
||||||
|
|
||||||
def update_status(self, status_type: str, message: str):
|
def update_status(self, status_type: str, message: str):
|
||||||
"""更新状态显示"""
|
"""更新状态显示"""
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Start the HCAN-connected O12 right hand and its safe position GUI."""
|
||||||
|
|
||||||
|
from launch import LaunchDescription
|
||||||
|
from launch.actions import IncludeLaunchDescription, TimerAction
|
||||||
|
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||||
|
from launch.substitutions import PathJoinSubstitution
|
||||||
|
from launch_ros.actions import Node
|
||||||
|
from launch_ros.substitutions import FindPackageShare
|
||||||
|
|
||||||
|
|
||||||
|
def generate_launch_description():
|
||||||
|
o12_driver = IncludeLaunchDescription(
|
||||||
|
PythonLaunchDescriptionSource(
|
||||||
|
PathJoinSubstitution([
|
||||||
|
FindPackageShare("omnihand_node"),
|
||||||
|
"launch",
|
||||||
|
"omnihand_pro_2025_node.launch.py",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
o12_gui = Node(
|
||||||
|
package="gui_control",
|
||||||
|
executable="gui_control",
|
||||||
|
name="o12_right_gui_control",
|
||||||
|
output="screen",
|
||||||
|
parameters=[{
|
||||||
|
"hand_type": "right",
|
||||||
|
"hand_joint": "O12",
|
||||||
|
# Commands are coalesced, so dragging a slider sends at most 10 Hz.
|
||||||
|
"topic_hz": 10,
|
||||||
|
"is_arc": False,
|
||||||
|
"topic_prefix": "",
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
|
||||||
|
return LaunchDescription([
|
||||||
|
o12_driver,
|
||||||
|
# Give the CANFD driver a moment to claim and initialise the adapter.
|
||||||
|
TimerAction(period=1.5, actions=[o12_gui]),
|
||||||
|
])
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<test_depend>python3-pytest</test_depend>
|
<test_depend>python3-pytest</test_depend>
|
||||||
|
|
||||||
<exec_depend>linker_hand_ros2_sdk</exec_depend>
|
<exec_depend>linker_hand_ros2_sdk</exec_depend>
|
||||||
|
<exec_depend>omnihand_node</exec_depend>
|
||||||
|
|
||||||
<export>
|
<export>
|
||||||
<build_type>ament_python</build_type>
|
<build_type>ament_python</build_type>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from gui_control.config.constants import HAND_CONFIGS
|
||||||
|
|
||||||
|
|
||||||
|
def test_o12_gui_uses_active_angle_order_and_milliradians() -> None:
|
||||||
|
config = HAND_CONFIGS["O12"]
|
||||||
|
|
||||||
|
assert config.joint_names_en == [
|
||||||
|
"thumb_roll", "thumb_abad", "thumb_mcp", "thumb_pip",
|
||||||
|
"index_abad", "index_mcp", "index_pip",
|
||||||
|
"middle_abad", "middle_mcp", "middle_pip",
|
||||||
|
"ring_mcp", "pinky_mcp",
|
||||||
|
]
|
||||||
|
assert config.position_scale == 1000
|
||||||
|
assert config.position_unit == "rad"
|
||||||
|
assert len(config.init_pos) == 12
|
||||||
|
assert all(len(pose) == 12 for pose in config.preset_actions.values())
|
||||||
Reference in New Issue
Block a user