公司动态
基于Jetson与ROS2的Reachy Mini机器人集群舞蹈控制系统设计与实现
1. 项目缘起从单机到集群的机器人编排挑战最近在折腾一个挺有意思的项目核心目标是想用一块Jetson开发板通过一个统一的控制台界面去同时指挥好几台Reachy Mini机器人让它们能协同完成一些动作比如跳个简单的集体舞。听起来有点像给机器人当编舞导演对吧这个想法的源头其实是在做单台Reachy Mini开发时遇到的瓶颈。当你只控制一台机器人时通过ROS2的节点发布指令一切都很清晰。但一旦数量增加到两台、三台甚至更多问题就来了指令如何同步下发状态如何统一监控动作的时序和一致性怎么保证总不能给每台机器人配一台电脑然后靠人肉喊“1、2、3走”吧。这就引出了“集群控制”的需求。这里的“集群”并非指Hadoop、Kubernetes那种用于数据计算或服务编排的IT集群而是特指多台实体机器人为了完成协同任务而组成的群体。它们的核心诉求是集中管理、统一调度和协同作业。Jetson作为边缘AI计算平台其强大的算力和对ROS等机器人框架的良好支持让它成为充当这个“集群大脑”或“控制台”的理想选择。而“控制台”在这里就是一个运行在Jetson上的软件应用它提供了一个图形化或命令行的界面允许我们一次性编排所有机器人的动作序列。Reachy Mini是一款开源、模块化的仿人机器人平台基于Python和ROS2非常适合研究和教育。让它跳舞本质上就是按照时间线精确控制其多个关节电机的角度。单台控制是基础多台同步则是工程上的深化。这个项目就是要把这个“深化”的过程实现出来构建一个从硬件连接、软件框架到上层应用的小型机器人集群控制系统。2. 系统架构设计Jetson如何扮演“指挥家”要实现多台Reachy Mini的集群控制我们需要一个清晰、可靠且易于扩展的系统架构。整个系统的核心是运行在Jetson上的“集群舞蹈控制台”它负责高层逻辑而每台Reachy Mini则是独立的“执行单元”通过ROS2网络接收指令。2.1 硬件连接与网络拓扑首先得把所有硬件连起来。假设我们有三台Reachy Mini机器人R1, R2, R3和一台Jetson Orin Nano作为控制主机。网络连接这是集群通信的基石。最稳定可靠的方式是使用一个千兆交换机将所有设备Jetson和三台Reachy Mini的主控板通常是树莓派或类似的单板机通过网线连接到同一个局域网LAN中。确保它们处于同一网段例如192.168.1.0/24。无线网络Wi-Fi虽然方便但在多机器人实时控制中可能因延迟和抖动导致动作不同步因此强烈推荐有线网络。Jetson的角色Jetson在这里不直接驱动Reachy Mini的电机。它的核心任务是运行动作编排算法计算或读取预先设计好的舞蹈动作序列。运行集群控制台应用提供用户界面UI或API用于启动、停止、监控集群任务。作为ROS2 Master在ROS2网络中需要一个“主节点”来协调所有其他节点之间的发现与通信。我们将Jetson配置为ROS2 Master所有Reachy Mini都将其ROS_DOMAIN_ID设置为与Jetson相同并指向Jetson的IP地址作为ROS Master的发现地址通过设置ROS_MASTER_URI环境变量在ROS2中更常见的是设置ROS_DOMAIN_ID和确保组播连通。Reachy Mini的准备每台Reachy Mini需要预先刷好系统安装好Reachy SDK、ROS2 Humble或Foxy版本并确保其电机驱动、传感器等底层功能正常。每台机器人的ROS2节点需要能够被Jetson上的控制台发现和通信。网络拓扑示意图逻辑上[ 集群舞蹈控制台 (GUI/CLI) ] | v [ 动作序列管理器 ROS2 节点 (运行于Jetson) ] | (ROS2 Topic/Service/Action) ------------------ | | | v v v [Reachy Mini 1] [Reachy Mini 2] [Reachy Mini 3]2.2 软件栈选型与核心组件软件层面我们基于ROS2来构建通信中间件这是机器人领域的标准选择。通信中间件ROS2 (推荐Humble版本)为什么是ROS2ROS1的通信机制对网络要求苛刻在分布式系统中有时不够稳定。ROS2基于DDS天生支持真正的分布式、跨平台通信更适合多机集群场景。其/tf、/clock如果使用仿真时间等工具对多机器人系统也很友好。关键概念利用Topic: 用于流式数据例如控制台向所有机器人广播同步时钟信号或全局状态。Service: 用于一对一的请求-响应例如控制台查询某台机器人的当前关节状态。Action: 用于执行可抢占、有反馈的长时任务这是控制舞蹈动作的核心。我们可以为“执行一段舞蹈”定义一个Action控制台作为Action Client每台机器人运行一个Action Server。这样控制台可以同时向多台机器人发送目标并接收各自的执行进度反馈。集群控制台应用开发语言Python。因为Reachy SDK和ROS2的Python客户端rclpy生态丰富开发效率高。框架选择方案A轻量CLI直接使用Python脚本通过argparse库处理命令行参数。适合快速测试和自动化。方案B图形界面GUI使用PyQt5或Tkinter。这对于演示和教学非常直观可以显示每台机器人的状态、提供动作序列的可视化编辑、一键启动/停止等。考虑到易用性本项目更倾向于开发一个简单的PyQt5 GUI。核心功能模块机器人管理模块维护一个机器人列表包含每台机器人的名称、ROS2节点名称、IP地址、状态在线/离线/忙碌/空闲。动作序列加载与解析模块从YAML或JSON文件加载预先设计好的舞蹈动作。动作文件应定义每个时间点、每个机器人、每个关节的目标角度或位姿。同步调度器模块这是最关键的部件。它需要解决“同时开始”和“节奏一致”的问题。一个简单有效的策略是控制台向所有在线机器人发送一个“准备”指令通过ROS2 Service。收到所有机器人的“准备就绪”应答后控制台发布一个“开始”信号到某个同步Topic例如/cluster_sync_start。所有机器人的Action Server订阅这个Topic收到“开始”信号后立刻开始执行本地存储或流式接收的动作序列。使用ROS2的/clock话题发布仿真时间可以让所有机器人在统一的时间线上运行这对于复杂编排尤其有用但会引入仿真时间管理的复杂度。对于舞蹈这种对绝对时间戳要求高的场景依赖系统时钟和精确的网络延时估计可能更简单。Reachy Mini端节点程序每台机器人上需要运行一个常驻的ROS2节点Python程序这个程序主要做两件事暴露Action Server提供一个名为/execute_dance的Action接口接收来自控制台的动作目标可能是动作序列ID或直接的动作点列表。执行与反馈调用Reachy SDK的API将接收到的关节角度目标依次发送给机器人电机并实时将执行进度例如“已完成第5个动作点/共100个”反馈给控制台。3. 核心实现从动作设计到同步执行有了架构我们来拆解具体的实现步骤。这个过程可以分为离线的“动作设计”和在线的“集群执行”两大部分。3.1 舞蹈动作序列的设计与生成让机器人跳舞首先得有“舞谱”。我们不能实时手动遥操作必须预先设计好动作。单机器人动作录制可选但推荐 最直观的方式是先用Reachy提供的“示教”功能手动摆弄一台机器人让它完成一套你想要的舞蹈动作同时用程序记录下每个时刻所有关节的角度。Reachy SDK通常有相关工具。这能得到非常自然流畅的动作数据。动作序列文件格式 我们将录制的或手动设计的数据保存为结构化的文件。YAML是个好选择因为它易读易写。# dance_routine_v1.yaml name: SimpleGroupWave duration: 10.0 # 总时长秒 robots: # 定义参与此套动作的机器人 - id: reachy_01 joints: [l_shoulder_pitch, l_shoulder_roll, l_arm_yaw, l_elbow_pitch, l_wrist_roll, l_wrist_pitch, l_wrist_yaw, r_shoulder_pitch, r_shoulder_roll, r_arm_yaw, r_elbow_pitch, r_wrist_roll, r_wrist_pitch, r_wrist_yaw, head_pan, head_tilt] keyframes: - time: 0.0 positions: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # 初始位置单位弧度 - time: 2.0 positions: [0.5, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # reachy_01 左臂抬起 - time: 4.0 positions: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0] # 左臂放下右臂抬起 - id: reachy_02 joints: [...] # 关节名列表同上 keyframes: - time: 0.0 positions: [...] # 初始位置 - time: 2.0 positions: [...] # reachy_02 可能做不同的动作例如向右看 - time: 4.0 positions: [...] # 头转回注意这里每个机器人的keyframes里的time是相对于该套动作开始时刻的绝对时间。这意味着reachy_01在2.0秒时抬左臂reachy_02在2.0秒时向右看它们是同时发生的。这就是集群同步的基础。动作插值 记录的关键帧Keyframes可能不够密集。为了让动作平滑需要在执行时进行插值例如线性插值或五次多项式插值。这个插值逻辑可以放在控制台的调度器里集中计算后下发也可以放在每个机器人的Action Server里分布式计算。考虑到网络带宽和计算负载分散推荐将插值任务下放到每个机器人。控制台只需要下发关键帧数据机器人端根据当前时间和关键帧数据实时计算目标角度。3.2 集群控制台的关键代码剖析控制台的核心是同步调度。下面用伪代码展示一个简化版的同步启动逻辑# cluster_console.py (部分核心代码) import rclpy from rclpy.node import Node from rclpy.action import ActionClient from your_robot_interfaces.action import ExecuteDance import threading import time class DanceClusterController(Node): def __init__(self, robot_names): super().__init__(dance_cluster_controller) self.robot_names robot_names self.action_clients {} self.ready_flags {name: False for name in robot_names} # 为每个机器人创建Action Client for name in robot_names: self.action_clients[name] ActionClient(self, ExecuteDance, f/{name}/execute_dance) # 同时可以创建一个Service Client来查询或准备状态此处略 # 创建一个Publisher用于发布同步开始信号 from std_msgs.msg import Bool self.sync_start_pub self.create_publisher(Bool, /cluster_sync_start, 10) def prepare_all_robots(self): 发送准备指令并等待所有机器人回应 # 这里简化处理实际应用中应该用Service调用并设置超时 print(发送准备指令...) # 模拟准备过程 time.sleep(1) for name in self.robot_names: self.ready_flags[name] True # 假设都成功了 print(所有机器人准备就绪。) def send_dance_goal(self, robot_name, dance_routine_data): 向单个机器人发送舞蹈动作目标 goal_msg ExecuteDance.Goal() goal_msg.routine_id dance_routine_data[id] goal_msg.keyframes dance_routine_data[keyframes] # 传递关键帧数据 self.action_clients[robot_name].wait_for_server() future self.action_clients[robot_name].send_goal_async(goal_msg) # 可以添加反馈和结果回调 return future def start_synchronized_dance(self, dance_data_dict): dance_data_dict: 字典key为机器人名value为该机器人的动作数据 # 1. 准备阶段 self.prepare_all_robots() # 2. 发送动作目标但告诉机器人等待开始信号 futures {} for name, data in dance_data_dict.items(): # 在动作数据中标记“等待信号” data[wait_for_sync] True future self.send_dance_goal(name, data) futures[name] future # 等待一小段时间确保所有机器人都收到了目标并进入等待状态 time.sleep(0.5) # 3. 发布同步开始信号 sync_msg Bool() sync_msg.data True self.sync_start_pub.publish(sync_msg) self.get_logger().info(*** 同步开始信号已发布 ***) # 4. 等待所有动作执行完成 for name, future in futures.items(): # 这里需要处理future的结果 pass def main(): rclpy.init() robot_list [reachy_01, reachy_02, reachy_03] controller DanceClusterController(robot_list) # 加载舞蹈数据 dance_data load_dance_from_yaml(dance_routine_v1.yaml) # dance_data 需要按机器人名组织成字典 # 启动同步舞蹈 controller.start_synchronized_dance(dance_data) rclpy.spin(controller) controller.destroy_node() rclpy.shutdown()3.3 Reachy Mini端Action Server的实现机器人端的节点需要响应控制台的调用。# robot_dance_server.py import rclpy from rclpy.node import Node from rclpy.action import ActionServer from your_robot_interfaces.action import ExecuteDance from reachy_sdk import ReachySDK import numpy as np class DanceActionServer(Node): def __init__(self, robot_name): super().__init__(f{robot_name}_dance_server) self.robot_name robot_name self.reachy ReachySDK(hostlocalhost) # 连接本地Reachy服务 self.sync_started False # 创建Action Server self._action_server ActionServer( self, ExecuteDance, execute_dance, self.execute_callback) # 订阅同步开始信号 from std_msgs.msg import Bool self.sync_sub self.create_subscription( Bool, /cluster_sync_start, self.sync_callback, 10) def sync_callback(self, msg): if msg.data: self.get_logger().info(f{self.robot_name} 收到同步开始信号) self.sync_started True def execute_callback(self, goal_handle): Action Server的回调函数执行舞蹈 goal goal_handle.request routine_id goal.routine_id keyframes goal.keyframes # 假设是列表每个元素是(time, positions) wait_for_sync goal.wait_for_sync feedback_msg ExecuteDance.Feedback() result_msg ExecuteDance.Result() # 检查动作数据有效性 if not keyframes: goal_handle.abort() result_msg.success False result_msg.message 动作数据为空 return result_msg # 如果需要等待同步信号 if wait_for_sync: self.get_logger().info(f{self.robot_name} 等待同步开始信号...) while not self.sync_started and rclpy.ok(): time.sleep(0.01) # 短暂休眠避免空转耗CPU if not self.sync_started: goal_handle.abort() result_msg.success False result_msg.message 等待同步信号超时 return result_msg # 开始执行动作序列 self.get_logger().info(f{self.robot_name} 开始执行舞蹈 {routine_id}) start_time time.time() keyframes_sorted sorted(keyframes, keylambda x: x.time) for i in range(len(keyframes_sorted)-1): if not goal_handle.is_active: self.get_logger().info(动作被取消) return result_msg frame_start keyframes_sorted[i] frame_end keyframes_sorted[i1] duration frame_end.time - frame_start.time # 线性插值 num_steps int(duration / 0.02) # 假设控制周期20ms if num_steps 0: for step in range(num_steps): alpha step / num_steps interp_positions (1-alpha)*np.array(frame_start.positions) alpha*np.array(frame_end.positions) # 使用Reachy SDK设置关节位置 self.reachy.set_joint_positions(joint_names, interp_positions.tolist()) # 发送反馈 feedback_msg.progress (frame_start.time step*0.02) / keyframes_sorted[-1].time goal_handle.publish_feedback(feedback_msg) time.sleep(0.02) # 控制周期 # 执行最后一个关键帧 self.reachy.set_joint_positions(joint_names, keyframes_sorted[-1].positions) time.sleep(0.1) goal_handle.succeed() result_msg.success True result_msg.message 舞蹈执行完成 return result_msg def main(argsNone): rclpy.init(argsargs) # 机器人名称可以从参数或环境变量获取 robot_name os.getenv(ROBOT_NAME, reachy_01) dance_server DanceActionServer(robot_name) rclpy.spin(dance_server) dance_server.destroy_node() rclpy.shutdown()4. 部署、调试与实战避坑指南将代码部署到真实的硬件上并跑通整个流程会遇到许多在仿真或单机测试中遇不到的问题。以下是基于实际操作的详细步骤和避坑点。4.1 系统环境部署与配置Jetson系统准备在Jetson Orin Nano上安装Ubuntu 20.04或22.04 LTS。推荐使用NVIDIA官方提供的SDK Manager进行刷机它会自动安装CUDA、TensorRT等基础AI堆栈。安装ROS2 Humble按照ROS官网指引安装Desktop版本。务必记得source /opt/ros/humble/setup.bash并将其加入~/.bashrc。安装Python3依赖pip3 install pyyaml numpy pyqt5如果做GUI。对于ROS2的Python包通常通过apt安装如sudo apt install ros-humble-rclpy ros-humble-std-msgs。Reachy Mini系统准备每台Reachy Mini的主控板如树莓派需要安装与Jetson兼容的ROS2版本同为Humble。确保网络连通能ping通Jetson。安装Reachy SDK按照Pollen Robotics官方文档通过pip安装reachy-sdk。这一步可能会涉及一些硬件特定驱动务必仔细阅读文档。配置ROS2环境变量这是多机通信的关键。在所有设备Jetson和所有Reachy的~/.bashrc中设置相同的ROS_DOMAIN_ID一个0-232之间的数字并确保ROS_MASTER_URI指向Jetson的IPROS2中此变量作用已变主要依赖组播但设置ROS_DOMAIN_ID和确保防火墙允许组播流量是关键。# 在 ~/.bashrc 末尾添加 export ROS_DOMAIN_ID42 export ROS_IP本机IP地址 # 有助于节点发现 source /opt/ros/humble/setup.bash验证网络发现在Jetson上运行ros2 topic list然后在任意一台Reachy上运行ros2 topic list两者应该能看到相同的topic列表可能为空但说明发现机制正常。如果看不到检查防火墙sudo ufw disable临时关闭测试和组播路由。4.2 同步性问题的排查与优化这是集群控制中最棘手的部分。动作不同步可能由以下原因导致网络延迟与抖动现象机器人动作开始时间有肉眼可见的先后顺序。排查使用ping命令测试Jetson到各Reachy的往返延迟RTT。理想情况应小于1ms。如果延迟过大或有丢包检查网线、交换机端口。优化使用有线网络重申无线网络不适合实时控制。优化交换机使用非管理型千兆交换机避免复杂的QoS策略引入不确定性。时间同步协议NTP在所有设备上安装并配置NTP客户端指向同一个时间服务器。虽然ROS2 Action的启动命令下发是即时的但机器人的本地时钟如果偏差大可能会影响日志分析和高级调度。运行sudo apt install chrony并配置。动作序列加载与解析耗时不同现象机器人收到“开始”信号后到真正开始运动的时间点不一致。原因如果动作数据较大每台机器人在收到目标后需要解析YAML/JSON数据这个耗时可能因CPU负载而异。解决在准备阶段完成所有耗时操作。在prepare_all_robots阶段不仅发送“准备”指令还可以将完整的动作序列数据预先发送给各机器人通过Service或一个非实时的Topic让机器人在后台提前解析好只等开始信号。这样开始信号到来时所有机器人都已“蓄势待发”。控制循环周期不一致现象动作开始同步但做着做着就逐渐错位。原因代码中用于插值和控制周期的time.sleep(0.02)20ms并不精确且受系统调度影响。多台设备上的time.sleep累积误差会导致不同步。解决使用ROS2 Timer在机器人端的Action Server中使用ROS2的create_timer来替代time.sleep。ROS2的Timer基于节点的事件循环精度和稳定性更好。# 替代 time.sleep 循环 self.timer self.create_timer(0.02, self._control_cycle_callback) # 20ms周期依赖绝对时间戳在动作执行循环中不依赖循环次数而是依赖从动作开始经过的绝对时间current_time - start_time来查询目标位置。这样即使某次循环被延迟下次循环也会努力“追上”正确的位置但可能会造成动作抖动。更高级的做法是使用轨迹插值库根据时间戳生成平滑的轨迹。4.3 可视化监控与故障处理一个健壮的控制台需要有状态监控能力。在GUI中集成状态监控使用PyQt5的QTableWidget或QLabel列表来显示每台机器人的状态在线、离线、执行中、错误。订阅每个机器人发布的特定状态Topic例如/reachy_01/robot_status在回调函数中更新UI。为每行状态设置颜色绿色-在线空闲黄色-执行中红色-错误/离线。实现简单的故障处理心跳机制让每个机器人节点定期如每秒发布一个“心跳”消息到/heartbeat/robot_name话题。控制台订阅所有心跳话题如果某个机器人的心跳超时比如3秒未收到则在UI上将其标记为“离线”并尝试重新连接或通知用户。动作执行异常处理在Action的反馈中除了进度还可以包含错误码。如果机器人端在执行过程中遇到关节错误、碰撞检测等应立即通过反馈通知控制台控制台可以决定是暂停所有机器人还是仅停止出问题的那个。日志记录在Jetson上使用ros2 bag record录制所有相关的Topic如同步信号、动作目标、机器人状态这对于后期分析不同步问题至关重要。5. 项目总结与扩展思考通过以上步骤我们搭建了一个基于Jetson和ROS2的Reachy Mini机器人集群舞蹈控制系统。从架构设计、通信协议、同步策略到具体的代码实现和调试技巧整个流程覆盖了多机器人协同中的核心问题。我个人在实测中的几点深刻体会第一网络是基石稳定压倒一切。在项目初期我们尝试过用Wi-Fi结果同步性惨不忍睹动作看起来像“群魔乱舞”。换上千兆交换机后问题立刻解决了80%。所以在机器人集群项目中对网络的投入绝对不能省。第二“准备阶段”的设计至关重要。最初我们图省事把动作数据解析放在开始信号之后结果就是开始信号发出后有的机器人秒动有的要卡顿半秒同步无从谈起。后来把数据预加载、解析、甚至电机上电自检都放到准备阶段让所有机器人在起跑线前就位同步启动的效果就好多了。第三可视化监控不是锦上添花而是雪中送炭。当你有三台以上的机器人时光靠看它们的物理动作很难快速定位问题。一个能实时显示每台机器人状态、日志和简单数据曲线的控制台能极大提升调试效率。我们后来在控制台里加了一个简单的时序图显示每台机器人收到关键命令的时间点一下子就把网络延迟的问题可视化出来了。这个项目还可以向多个方向扩展动作编排可视化开发一个图形化的动作编辑器像音乐制作软件一样用时间轴来为每个机器人编排动作并实时预览。引入感知反馈为Jetson连接摄像头利用YOLO等模型识别机器人的实际姿态实现基于视觉的闭环控制让舞蹈动作更精准或者实现“人机共舞”。动态角色分配不预先固定每台机器人的动作而是由控制台根据实时情况如某台机器人故障动态分配动作角色提高系统的鲁棒性。规模扩展当前的架构对于10台以内的机器人是合适的。如果规模进一步扩大可能需要引入更专业的集群管理中间件如基于ROS2的Supervisor节点或借鉴K8s的理念进行分组管理和负载均衡。从单台机器人的控制到多台机器人的协同这一步跨越带来的挑战和乐趣是成倍增长的。希望这个详细的实现指南和踩坑记录能为你开启自己的机器人集群项目提供扎实的参考。