仿真机器人MetaEngine
本文面向工程用户,详细说明如何将基于MetaEngine + ROS2 Gem的仿真机器人端到端接入R2C,包括:
- 概念与总体流程。
- 配置文件逐项解读(r2c_ur5e.yaml为范本)。
- 配置项含义与取值建议。
- 端到端接入步骤(环境 > MetaEngine仿真侧 > R2C配置 > 启动 > 联调验证)。
一、为什么仿真机器人也能接入R2C
R2C并不直接对接某个仿真器,而是对接标准ROS2接口。MetaEngine启用ROS2 Gem后,仿真中的机器人会把运动学、传感器、控制器以标准ROS2 Topics/Services/Actions对外暴露。
| 仿真侧(ROS2 Gem) | ROS2接口 | R2C侧消费方式 |
|---|---|---|
| ROS2RobotControl关节状态 | /joint_states(sensor_msgs/JointState) | 订阅 |
| ROS2CameraSensor相机 | /xxx/camera_image_color(sensor_msgs/Image) | 订阅+编码 |
| 关节位置控制器 | /arm_position_controller/commands(Float64MultiArray) | 发布 |
| 夹爪控制器 | /hand_position_controller/commands(Float64MultiArray) | 发布 |
| 轨迹Action Server | FollowJointTrajectory Action | 动作客户端 |
| TF广播 | /tf、/tf_static | 可选,预留 |
因此R2C的Ros2HardwareAdapter对真实机器人与仿真机器人完全一视同仁:它只负责“订阅ROS2话题、按配置翻译成R2C观测;把R2C动作翻译回ROS2命令”。仿真与真实的差别只是对端是谁,配置范式完全相同。
相关参考:
二、端到端数据流总览
- 观测上行:订阅ROS2 > streams > device_to_r2c > Observations > session.publish_observations() > Zenoh > 云端。
- 控制下行:云端Actions > Zenoh > r2c_to_device > 命令dict > adapter.send_action() > 路由到ROS2发布器/服务/动作 > 仿真机器人执行。
三、端到端接入步骤
需要进行如下准备环境、MetaEngine(O3DE)仿真侧准备、编写R2C客户端通信配置、编写仿真机器人硬件配置以及启动并验证。
准备环境
| 配置项 | 要求 |
|---|---|
| 操作系统 | Linux(推荐Ubuntu 22.04 / 24.04) |
| ROS2 | Humble (22.04) / Jazzy (24.04) |
| Python | ≥ 3.10 |
| rclpy | 随ROS2安装 |
| message_filters | 可选:pip install message_filters(多路观测时间同步) |
执行如下命令,确认ROS2环境:
source /opt/ros/${ROS_DISTRO}/setup.bash
python3 -c "import rclpy; print('rclpy OK')"
R2C端侧进程与MetaEngine仿真进程需在同一ROS2域(ROS_DOMAIN_ID 一致)才能互相发现话题。
MetaEngine(O3DE)仿真侧准备
- 在O3DE工程中启用ROS2 Gem(Project Manager > Gems > ROS2)。
- 导入机器人URDF:使用ROS2RobotImporter导入机器人模型,生成物理模型与关节。
- 为机器人挂载:
- ROS2RobotControl/控制器 > 发布/joint_states、暴露关节命令话题。
- ROS2CameraSensor(手/头/外置)> 发布sensor_msgs/Image。
- 夹爪等执行器 > Float64MultiArray 命令话题。
- 运行仿真并验证话题可用:
ros2 topic list ros2 topic echo /joint_states --once ros2 topic hz /joint_states
记录下仿真侧实际发布的话题名与消息类型,这是后续配置subscriptions / command_publishers的依据。
编写R2C客户端通信配置
config/client_config.yaml(Zenoh通信):
project_id: "test-tenant" # 与云端一致 device_id: "sim-ur5e-001" # 设备标识 client_id: "sim-edge-001" endpoints: [] # 留空自动发现,或填 Router 地址 mode: "peer" # peer 或 client protocol: "zenoh"
编写仿真机器人硬件配置
hardware.type: "ros2",配置主体为r2c_ur5e.yaml(详见四、配置文件逐项详解)。按仿真侧实际话题名/消息类型调整。
启动并验证
source /opt/ros/${ROS_DISTRO}/setup.bash
# 保险起见先用 dry_run 验证链路,再真正驱动
python -m r2c_sdk.cloudroboclient \
--client-config config/client_config.yaml \
--robot-config r2c_ur5e.yaml \
--log-level INFO 启动后应看到日志:
- Connecting ROS2 hardware adapter / ROS2 adapter connected (subscriptions=.., publishers=..)。
- 观测正常发布、被云端接收并回推Actions。
- init_joints将机械臂/夹爪摆到初始位。
runtime:运行/控制循环
| 配置项 | 说明 | 默认/建议 |
|---|---|---|
| publish_hz | 动作执行频率,也决定观测发布检查频率。 | 10–30 Hz |
| max_duration_s | 最大运行时长(秒),0表示无限运行(Ctrl+C停止)。 | 0 |
| dry_run | true时不执行Actions,但仍发观测,用于安全联调。 | false |
| action_response_timeout_s | 发布观测后等待Action的超时,超时则重发观测。 | ≥ 30(弱网/大模型) |
| max_enqueue_actions_per_chunk | 每个chunk最多入队的Action步数,多余丢弃;-1全部采用。 | -1 |
| enable_action_chunk_alignment | 是否启用chunk对齐,避免轨迹跳变。 | false |
| skip_initial_observations | 启动时跳过的观测数(让动作队列先填满)。 | 1 |
| async_request.enabled | 异步请求融合开关(一般false)。 | false |
| keyboard_control.enabled | 启用键盘控制(空格键:暂停、h键:回home、e键:退出) | true |
hardware:ROS2硬件适配器
hardware:
type: "ros2"
config:
node_name: "r2c_ur5e_ros" # ROS2 节点名(与其他节点不重名)
spin_timeout_sec: 0.05 # spin_once 超时(秒)
auto_spin: True # 是否自动启动 spin 线程 | 配置项 | 说明 |
|---|---|
| node_name | ROS2节点名。多台仿真机器人应使用不同节点名,避免冲突。 |
| spin_timeout_sec | spin_once的处理超时,默认0.05s。 |
| auto_spin | True时SDK自动启动后台spin线程处理回调。 |
hardware.config.init_joints:启动自摆位
连接成功、发布器就绪后,主动下发初始位(把机械臂/夹爪摆到安全初位)。_publish_init_joints 在 connect() 末尾调用。
init_joints:
enabled: true # 是否启用
delay_sec: 3.0 # 连接后延迟多少秒再下发
entries:
- publisher: arm # 对应 command_publishers 的一个发布器
message: # 直接构造该发布器类型消息
data: [0.0, -1.9373155, 1.204277, -0.9075712, 4.694936, -0.0872665]
- publisher: hand
message:
data: [0.0, 0.0] | 配置项 | 说明 |
|---|---|
| enabled | 是否在启动时执行初始摆位。 |
| delay_sec | 连接后延迟秒数,等待对端控制话题就绪。 |
| entries[].publisher | 指向command_publishers中的发布器名。 |
| entries[].message | 直接以dict构造该发布器消息(此处为 std_msgs/Float64MultiArray.data)。 |
| entries[].joints(替代 message) | 也可提供joints+joint_names,SDK自动构造 trajectory_msgs/JointTrajectory 单点。 |
hardware.config.subscriptions:观测订阅(上行)
每个订阅项把一个ROS2 Topic拉进streams。一个典型订阅项如下所示:
joint_states: # 订阅流名称(任意起名,用于 store_as)
topic: /joint_states
msg_type: sensor_msgs.msg.JointState
qos: 10
max_update_hz: 10 # 限频,避免高频洪泛
store_as: joint_states # 在 streams 中的键名
include_fields: ["name", "position", "velocity", "effort"]
field_aliases:
name: names # 字段重命名 name > names
transforms: # 订阅级转换链(可选)
- select_joints_by_name:
names: [... 8 个关节名 ...] | 配置项 | 说明 | 是否必需 |
|---|---|---|
| topic | ROS2话题名。 | 是 |
| msg_type | ROS2消息类型(点分路径,如 sensor_msgs.msg.Image)。 | 是 |
| qos | QoS 队列深度。 | 否(默认 10) |
| store_as | 写入streams的键名(默认用流名)。 | 否 |
| max_update_hz | 最大更新频率,用于降采样。 | 否 |
| include_fields | 只提取指定字段。 | 否 |
| field_aliases | 字段重命名映射(name>names)。 | 否 |
| store_raw_message | true保留原始ROS消息(相机图像需用它)。 | 否 |
| transforms | 订阅级转换链(如 select_joints_by_name)。 | 否 |
相机订阅示例(UR5e):
head: topic: /head_camera/camera_image_color msg_type: sensor_msgs.msg.Image qos: 10 store_as: head store_raw_message: true # 保留 Image 对象,供下游 ros_image_to_jpeg 使用
hardware.config.command_publishers:命令发布(下行)
每个发布项把一个R2C命令映射到ROS2Topic:
command_publishers:
arm: # 发布器名称(供 target/init_joints 引用)
topic: "/arm_position_controller/commands"
msg_type: std_msgs.msg.Float64MultiArray
qos: 10
hand:
topic: "/hand_position_controller/commands"
msg_type: std_msgs.msg.Float64MultiArray
qos: 10
default_command_publisher: arm # 未显式指定时的默认发布器
action_targets: # 语义 target > 发布器名
arm: arm
hand: hand | 配置项 | 说明 |
|---|---|
| topic / msg_type / qos | 目标话题、消息类型、QoS。 |
| default_command_publisher | 命令未携带publisher/target时的默认发布器。 |
| action_targets | 逻辑目标名 > 发布器名的映射,供r2c_to_device的target解析。 |
下行通道:
Topic 发布器(推荐,低延迟):command_publishers。
translator:数据翻译层
type: "configurable",由device_to_r2c(ROS2 > R2C)与r2c_to_device(R2C > ROS2)两组映射构成。
device_to_r2c: 组R2C观测
关节位置:
- target_key: "joint_states.names" # 目标标准字段
default: ["shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint",
"wrist_1_joint", "wrist_2_joint", "wrist_3_joint", "gripper"]
- target_key: "joint_states.position" # 来自订阅 streams
source: streams.joint_states.position
transforms:
- array_to_list
- slice: { start: 0, end: 6 } # 前 6 维 = 机械臂
- target_key: "joint_states.position" # 第 7 维 = 夹爪行程
target_index: 6
source: streams.joint_states.position
transforms:
- array_to_list
- slice: { start: 6, end: 8 } # 夹爪两指
- my_gripper_fingers_to_stroke # 两指求和 > 行程(米) 相机图像:
- target_key: "images.color.head" source_path: "streams.head" # 原始 Image 对象(store_raw_message) transforms: ros_image_to_jpeg # 编码为 JPEG 字节 - target_key: "images.color.external" source_path: "streams.external" transforms: ros_image_to_jpeg
device_to_r2c映射项的字段:
| 字段 | 说明 |
|---|---|
| target_key | R2C观测标准字段(joint_states.names/.position、images.color.xxx等)。 |
| source / source_path | 取数来源。streams.<store_as>.字段或streams.<store_as> |
| source_index / target_index | 源/目标数组下标。 |
| default | 固定值(无source时用,如关节名表)。 |
| transforms | 转换链(array_to_list、slice、ros_image_to_jpeg、my_gripper_fingers_to_stroke等)。 |
target_key与task一起构成最终Observations(云端策略所需字段详见R2C观测模型)。
r2c_to_device:生成ROS2命令
机械臂(前6维 > arm发布器):
- target: "commands_by_publisher.arm.data" # 写入哪个发布器的哪个字段
source: "joint_states.position"
use_assign_dotted: true # 用点号路径赋值
required: true
transforms:
- slice: { start: 0, end: 6 } # 6 维机械臂
- list_to_ros_float64_multi_array # 组装 Float64MultiArray 夹爪(第7维行程 > 展开两指 > hand发布器):
- target: "commands_by_publisher.hand.data"
source: "joint_states.position"
source_index: 6
use_assign_dotted: true
required: true
transforms:
- my_gripper_stroke_to_fingers # 行程 > [S/2, S/2] 两指
- list_to_ros_float64_multi_array r2c_to_device映射项字段:
| 字段 | 说明 |
|---|---|
| target | 命令写入目标,如commands_by_publisher.<name>.data。 |
| source | 取自云端Action的字段,如joint_states.position。 |
| source_index | 源下标(取第7维夹爪行程)。 |
| required | 该字段是否必需。 |
| use_assign_dotted | 是否用点号路径完成嵌套赋值。 |
| transforms | 反向转换链(slice, my_gripper_stroke_to_fingers, list_to_ros_float64_multi_array …)。 |
最终翻译器输出commands_by_publisher(或commands_by_service/commands)给adapter.send_action(),由适配器按发布器名路由并发布到对应 Topic。
自定义转换器:r2c_to_device里可引用自定义转换器(如SO101的so101_gripper_value_to_joint)。参考Transformer使用指导。
五、常见ROS转换器速查
来自core/transformers.py注册表,可在subscriptions.transforms / device_to_r2c.transforms / r2c_to_device.transforms中引用:
| 转换器 | 作用 |
|---|---|
| array_to_list | numpy或数组转换到list |
| slice {start,end} | 截取数组片段。 |
| ros_message_to_mapping | ROS消息转换到dict。 |
| ros_image_to_jpeg/png/webp | sensor_msgs/Image转换到编码字节。 |
| ros_compressed_image_to_jpeg/... | 压缩图像转换到编码字节。 |
| select_joints_by_name {names} | 按关节名过滤出指定关节子集。 |
| list_to_ros_float64_multi_array | list转换到std_msgs/Float64MultiArray。 |
| list_to_ros_joint_state {names} | list转换到sensor_msgs/JointState。 |
| list_to_ros_move_request | 6维list转换到JAKA Move服务请求。 |
| scalar_to_ros_float64 | 标量转换到std_msgs/Float64。 |
| parallel_gripper_fingers_to_stroke / *_stroke_to_fingers | 两指位置与行程换算(0/对称)。 |
| my_gripper_fingers_to_stroke / my_gripper_stroke_to_fingers | UR5e夹爪行程换算变体。 |
六. 调试与验证
- 先配置dry_run验证链路,如下所示。
runtime: dry_run: true # 只发观测、不真正驱动仿真机器人
确认日志显示:ROS2订阅有数据、device_to_r2c成功、观测已发布、云端能收到。无误后再改false。
- 观测数据诊断。
查看ROS2侧话题是否有数据:ros2 topic echo /joint_states、ros2 topic hz /head_camera/camera_image_color。
如果translator.device_to_r2c因缺字段跳过:日志会打印缺失的source_path,对照store_as 与 include_fields检查。
- 执行诊断命令,确认command_publishers的话题名与仿真侧控制器话题完全一致(大小写、前导/)。
ros2 topic echo /arm_position_controller/commands
观察是否收到Float64MultiArray。
多台仿真机器人保证node_name不同、话题带命名空间。
- 执行如下命令,联动Policy Server端到端。
# 终端 1:云端推理 python -m r2c_sdk.inference.r2c_lerobot_policy_server \ --client-config config/client_config.yaml \ --cloud-config config/cloud_ur5e_act.yaml \ --policy-type act --device cpu # 终端 2:仿真机器人边缘端 python -m r2c_sdk.cloudroboclient \ --client-config config/client_config.yaml \ --robot-config r2c_ur5e.yaml
七、常见问题处理
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| ROS2 adapter connected ... 但没有观测 | 话题名/消息类型不对,或DDS域不一致。 | ros2 topic list;export ROS_DOMAIN_ID=x两端一致。 |
| ModuleNotFoundError: No module named 'rclpy' | 未执行ROS2的setup.bash脚本。 | source /opt/ros/${ROS_DISTRO}/setup.bash |
| No ROS2 command publisher found for 'arm' | commands_by_publisher.<name> 与发布器名不一致。 | 对齐r2c_to_device.target 与 command_publishers键名 |
| KeyError: 'joint_states.position' | 订阅数据未就绪或字段被过滤。 | 检查include_fields是否含position,订阅是否取到数。 |
| 动作不执行 | dry_run: true /发布器话题不对。 | 改false;核对话题名与切片/转换。 |
| 图像为空 | 未store_raw_message: true或编码转换缺失。 | 相机订阅加 store_raw_message: true,device_to_r2c 用 ros_image_to_jpeg |
| 多流时间不对齐 | 未开启观测同步。 | 启用observation_sync + 安装message_filters。 |