lyx@lyx-virtual-machine:~/Codroid_ROS2_WS/src/codroid_msgs/src$ python3 2.py
[INFO] [1761226713.090949430] [s360_robot_full_control]: === S3 60机器人控制节点启动 ===
[INFO] [1761226713.206587598] [s360_robot_full_control]: ✅ MoveIt2接口初始化完成
[INFO] [1761226713.207614623] [s360_robot_full_control]: 📡 Socket服务器监听: 192.168.101.111:8888
[INFO] [1761226713.208277726] [s360_robot_full_control]: ✅ 系统初始化完成: 192.168.101.111:8888
[INFO] [1761226713.209199884] [s360_robot_full_control]: 🚀 节点进入运行状态,等待指令...
[INFO] [1761226718.977626025] [s360_robot_full_control]: 📞 新客户端连接: ('192.168.101.112', 58502)
[INFO] [1761226718.994709784] [s360_robot_full_control]: 📥 接收指令: 类型=p2, 参数=[[0.4, 0.1, 0.2], [1.0, 0.0, 0.0, 0.0]]
[FATAL] [1761226719.002192131] [s360_robot_full_control]: ❌ 节点运行异常: Failed to get number of ready entities for action client: wait set index for result client is out of bounds, at ./src/rcl_action/action_client.c:635
Traceback (most recent call last):
File "/home/lyx/Codroid_ROS2_WS/src/codroid_msgs/src/2.py", line 491, in main
executor.spin()
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 294, in spin
self.spin_once()
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 806, in spin_once
self._spin_once_impl(timeout_sec)
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 787, in _spin_once_impl
handler, entity, node = self.wait_for_ready_callbacks(
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 723, in wait_for_ready_callbacks
return next(self._cb_iter)
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py", line 642, in _wait_for_ready_callbacks
if wt in waitables and wt.is_ready(wait_set):
File "/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/action/client.py", line 231, in is_ready
ready_entities = self._client_handle.is_ready(wait_set)
rclpy._rclpy_pybind11.RCLError: Failed to get number of ready entities for action client: wait set index for result client is out of bounds, at ./src/rcl_action/action_client.c:635
[INFO] [1761226719.002788258] [s360_robot_full_control]: 🛑 节点开始关闭,释放资源中...
[INFO] [1761226719.003273958] [s360_robot_full_control]: ⏳ 等待Socket线程退出...
[WARN] [1761226719.009362504] [s360_robot_full_control]: Joint states are not available yet!
[INFO] [1761226719.010012613] [s360_robot_full_control]: Joint states are available now
[INFO] [1761226719.072622565] [s360_robot_full_control]: 📋 收到规划轨迹: 37个点
[INFO] [1761226719.077731579] [s360_robot_full_control]: ⏳ 开始运动完成检测...
[INFO] [1761226719.078853332] [s360_robot_full_control]: 📤 指令执行结果: None(客户端:('192.168.101.112', 58502))
Exception in thread Thread-2 (handle_client):
Traceback (most recent call last):
File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
self.run()
File "/usr/lib/python3.10/threading.py", line 953, in run
self._target(*self._args, **self._kwargs)
File "/home/lyx/Codroid_ROS2_WS/src/codroid_msgs/src/2.py", line 453, in handle_client
self.get_logger().info(f"📞 客户端连接已断开: {client_socket.getpeername()}")
OSError: [Errno 9] Bad file descriptor
[INFO] [1761226719.981364921] [s360_robot_full_control]: 📡 Socket服务器已关闭
[INFO] [1761226719.982142669] [s360_robot_full_control]: 🗑️ 销毁MoveIt2资源...
[INFO] [1761226719.988285992] [s360_robot_full_control]: ✅ 所有资源已释放
[INFO] [1761226720.018866697] [s360_robot_full_control]: ✅ 节点已完全关闭
#!/usr/bin/env python3
"""
S3 60机器人控制节点 - 运动完成确认修复版
基于关节角度比较确认运动完成,到达目标后等待0.5s返回
修复:解决关节数据顺序问题、多线程资源竞争、节点关闭资源泄漏问题
"""
import socket
import json
import time
import traceback
from threading import Thread, Lock
import rclpy
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor
from sensor_msgs.msg import JointState
from moveit_msgs.msg import DisplayTrajectory
from pymoveit2 import MoveIt2
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
# -------------------------- 配置参数 --------------------------
# 请根据你的机器人硬件配置调整以下参数
JOINT_NAMES = ["Joint1", "Joint2", "Joint3", "Joint4", "Joint5", "Joint6"]
BASE_LINK_NAME = "base_link"
END_EFFECTOR_NAME = "link6"
MOVE_GROUP_ARM = "arm"
PLANNER_ID = "RRTConnectkConfigDefault"
SERVER_IP = "192.168.101.111" # 机器人控制节点IP
SERVER_PORT = 8888 # 通信端口
MAX_VELOCITY = 0.5 # 最大速度(单位:rad/s)
MAX_ACCELERATION = 0.5 # 最大加速度(单位:rad/s²)
JOINT_TOLERANCE = 0.03 # 关节角度容差(单位:rad)
MAX_POINTS = 5 # 轨迹最大采样点数
MOTION_TIMEOUT = 30.0 # 运动超时时间(单位:s)
class S360RobotControlNode(Node):
def __init__(self):
super().__init__("s360_robot_full_control")
self.get_logger().info("=== S3 60机器人控制节点启动 ===")
# 状态管理(新增:is_shutting_down用于关闭状态标记)
self.is_shutting_down = False
self.joint_state_received = False # 关节状态是否已接收
self.current_joint_positions = {name: 0.0 for name in JOINT_NAMES} # 关节位置字典
# 线程锁(新增:解决多线程资源竞争)
self.joints_lock = Lock() # 关节数据锁
self.trajectory_lock = Lock() # 轨迹数据锁
self.moveit_lock = Lock() # MoveIt2操作锁(核心修复)
# 轨迹与运动相关变量
self.planned_trajectory = None
# -------------------------- 子系统初始化 --------------------------
# 1. 轨迹发布器(控制机器人运动)
self.trajectory_pub = self.create_publisher(JointTrajectory, 'RobotMove', 10)
# 2. 轨迹监控器(接收MoveIt2规划结果)
self.trajectory_sub = self.create_subscription(
DisplayTrajectory,
'/display_planned_path',
self.trajectory_callback,
10
)
# 3. 关节状态订阅器(获取机器人当前关节位置)
self.joint_state_sub = self.create_subscription(
JointState,
'joint_states',
self.joint_state_callback,
10
)
# 4. MoveIt2初始化(运动规划核心)
self.moveit2 = None
self.init_moveit2()
# 5. Socket服务器(接收外部控制指令)
self.socket_thread = Thread(
target=self.start_socket_server,
daemon=True
)
self.socket_thread.start()
self.get_logger().info(f"✅ 系统初始化完成: {SERVER_IP}:{SERVER_PORT}")
# ======================= 核心初始化方法 =======================
def init_moveit2(self):
"""初始化MoveIt2接口,建立与运动规划器的连接"""
try:
self.moveit2 = MoveIt2(
node=self,
joint_names=JOINT_NAMES,
base_link_name=BASE_LINK_NAME,
end_effector_name=END_EFFECTOR_NAME,
group_name=MOVE_GROUP_ARM
)
self.moveit2.planner_id = PLANNER_ID
self.moveit2.max_velocity = MAX_VELOCITY
self.moveit2.max_acceleration = MAX_ACCELERATION
self.get_logger().info("✅ MoveIt2接口初始化完成")
except Exception as e:
self.get_logger().error(f"❌ MoveIt2初始化失败: {e}")
self.moveit2 = None
# ======================= 数据回调方法 =======================
def joint_state_callback(self, msg):
"""关节状态回调:更新当前关节位置(字典存储,解决顺序问题)"""
try:
# 建立“关节名-位置”映射,避免顺序错乱
joint_mapping = {name: position for name, position in zip(msg.name, msg.position)}
with self.joints_lock:
# 只更新已知关节的位置,过滤无效数据
for name in JOINT_NAMES:
if name in joint_mapping:
self.current_joint_positions[name] = joint_mapping[name]
self.joint_state_received = True # 标记关节数据已就绪
except Exception as e:
self.get_logger().error(f"❌ 关节状态处理异常: {e}\n{traceback.format_exc()}")
def trajectory_callback(self, msg):
"""轨迹回调:接收MoveIt2规划的轨迹点"""
try:
if msg.trajectory and msg.trajectory[0].joint_trajectory.points:
with self.trajectory_lock:
self.planned_trajectory = msg.trajectory[0].joint_trajectory.points
self.get_logger().info(f"📋 收到规划轨迹: {len(self.planned_trajectory)}个点")
except Exception as e:
self.get_logger().error(f"❌ 轨迹处理异常: {e}")
# ======================= 数据工具方法 =======================
def get_current_joints_in_standard_order(self):
"""获取标准顺序(JOINT_NAMES)的当前关节位置列表"""
with self.joints_lock:
return [self.current_joint_positions[name] for name in JOINT_NAMES]
def wait_for_planned_trajectory(self, timeout=5.0):
"""等待MoveIt2规划轨迹生成,超时返回None"""
start_time = time.time()
while rclpy.ok() and not self.is_shutting_down:
with self.trajectory_lock:
if self.planned_trajectory:
return self.planned_trajectory
if time.time() - start_time > timeout:
self.get_logger().warning("⚠️ 等待轨迹超时")
return None
time.sleep(0.1)
return None
# ======================= 轨迹发布方法 =======================
def publish_joint_trajectory(self, target_joints):
"""发布单目标点轨迹(用于简单关节运动)"""
try:
traj_msg = JointTrajectory()
traj_msg.joint_names = JOINT_NAMES
point = JointTrajectoryPoint()
point.positions = target_joints
point.time_from_start = rclpy.duration.Duration(seconds=2.0).to_msg()
traj_msg.points.append(point)
self.trajectory_pub.publish(traj_msg)
return True
except Exception as e:
self.get_logger().error(f"❌ 关节轨迹发布失败: {e}")
return False
def publish_planned_trajectory(self, trajectory_points):
"""发布MoveIt2规划的多段轨迹(采样后发布,避免点数过多)"""
try:
if not trajectory_points:
self.get_logger().warning("⚠️ 无轨迹点可发布")
return False
traj_msg = JointTrajectory()
traj_msg.joint_names = JOINT_NAMES
# 采样轨迹点(最多MAX_POINTS个,平衡平滑度与实时性)
if len(trajectory_points) > MAX_POINTS:
step = max(1, len(trajectory_points) // MAX_POINTS)
trajectory_points = [trajectory_points[i] for i in range(0, len(trajectory_points), step)][:MAX_POINTS]
# 分配时间戳,确保运动平滑
total_time = 5.0 # 总运动时长(可根据需求调整)
for i, point in enumerate(trajectory_points):
traj_point = JointTrajectoryPoint()
traj_point.positions = point.positions
traj_point.time_from_start = rclpy.duration.Duration(
seconds=total_time * (i / (len(trajectory_points) - 1))
).to_msg()
traj_msg.points.append(traj_point)
self.trajectory_pub.publish(traj_msg)
return True
except Exception as e:
self.get_logger().error(f"❌ 规划轨迹发布失败: {e}")
return False
def publish_smooth_trajectory(self, start_joints, target_joints, num_points=5):
"""发布平滑过渡轨迹(从起点到目标点的线性插值)"""
try:
traj_msg = JointTrajectory()
traj_msg.joint_names = JOINT_NAMES
num_points = min(num_points, MAX_POINTS) # 限制最大点数
total_time = 5.0 # 总运动时长
# 线性插值生成中间点
for i in range(num_points):
ratio = i / (num_points - 1) if num_points > 1 else 1.0
point = JointTrajectoryPoint()
point.positions = [
start + ratio * (target - start)
for start, target in zip(start_joints, target_joints)
]
point.time_from_start = rclpy.duration.Duration(seconds=total_time * ratio).to_msg()
traj_msg.points.append(point)
self.trajectory_pub.publish(traj_msg)
return True
except Exception as e:
self.get_logger().error(f"❌ 平滑轨迹发布失败: {e}")
return False
# ======================= 运动控制核心方法 =======================
def execute_joint_motion(self, target_joints):
"""执行关节空间运动:根据目标关节角度控制机器人"""
# 修复1:关闭状态下拒绝执行
if self.is_shutting_down:
self.get_logger().error("❌ 节点正在关闭,拒绝关节运动指令")
return False
# 修复2:检查关节数据是否就绪
if not self.joint_state_received:
self.get_logger().error("❌ 未收到关节状态数据,无法执行关节运动")
return False
# 基础参数校验
if not self.moveit2 or len(target_joints) != len(JOINT_NAMES):
self.get_logger().error("❌ 关节运动参数无效(MoveIt2未就绪或关节数不匹配)")
return False
try:
# 获取当前关节位置,用于生成平滑轨迹
current_joints = self.get_current_joints_in_standard_order()
self.publish_smooth_trajectory(current_joints, target_joints)
# 修复3:MoveIt2操作加锁,避免多线程竞争
with self.moveit_lock:
self.moveit2.move_to_configuration(target_joints)
# 等待运动完成(基于关节角度容差判断)
return self.wait_for_motion_completion(target_joints, timeout=MOTION_TIMEOUT)
except Exception as e:
self.get_logger().error(f"❌ 关节运动异常: {e}\n{traceback.format_exc()}")
return False
def execute_pose_motion(self, target_pose):
"""执行位姿空间运动:根据目标位姿(位置+姿态)控制机器人"""
# 修复1:关闭状态下拒绝执行
if self.is_shutting_down:
self.get_logger().error("❌ 节点正在关闭,拒绝位姿运动指令")
return False
# 修复2:检查关节数据是否就绪
if not self.joint_state_received:
self.get_logger().error("❌ 未收到关节状态数据,无法执行位姿运动")
return False
# 基础参数校验(target_pose格式:[[x,y,z], [x,y,z,w]])
if not self.moveit2 or len(target_pose) != 2 or len(target_pose[0]) != 3 or len(target_pose[1]) != 4:
self.get_logger().error("❌ 位姿运动参数无效(格式错误或MoveIt2未就绪)")
return False
try:
target_pos, target_quat = target_pose # 拆分位置和姿态
# 修复3:MoveIt2操作加锁,避免多线程竞争
with self.moveit_lock:
# 1. 计算逆运动学(位姿→关节角度)
ik_result = self.moveit2.compute_ik(target_pos, target_quat)
if not ik_result:
self.get_logger().error("❌ 逆运动学求解失败(目标位姿不可达)")
return False
# 2. 执行位姿运动
self.moveit2.move_to_pose(position=target_pos, quat_xyzw=target_quat, cartesian=False)
# 3. 提取目标关节角度(用于运动完成判断)
target_joints = self._extract_joint_positions_from_state(ik_result)
if not target_joints:
self.get_logger().error("❌ 无法提取目标关节角度,无法判断运动完成")
return False
# 4. 发布轨迹(优先用规划轨迹,其次用平滑轨迹)
planned_trajectory = self.wait_for_planned_trajectory(timeout=5.0)
if planned_trajectory:
self.publish_planned_trajectory(planned_trajectory)
else:
current_joints = self.get_current_joints_in_standard_order()
self.publish_smooth_trajectory(current_joints, target_joints)
# 5. 等待运动完成
return self.wait_for_motion_completion(target_joints, timeout=MOTION_TIMEOUT)
except Exception as e:
self.get_logger().error(f"❌ 位姿运动异常: {e}\n{traceback.format_exc()}")
return False
def _extract_joint_positions_from_state(self, joint_state):
"""从JointState消息中提取标准顺序的关节位置"""
if not joint_state or not hasattr(joint_state, 'position'):
self.get_logger().warning("⚠️ 无效的JointState消息,无法提取关节位置")
return None
try:
position_map = {name: pos for name, pos in zip(joint_state.name, joint_state.position)}
# 按JOINT_NAMES顺序提取,确保与目标顺序一致
return [position_map[name] for name in JOINT_NAMES if name in position_map]
except Exception as e:
self.get_logger().error(f"❌ 关节提取失败: {e}")
return None
# ======================= 运动完成判断方法 =======================
def wait_for_motion_completion(self, target_joints, timeout=30.0):
"""基于关节角度容差判断运动是否完成(稳定0.5s后确认)"""
start_time = time.time()
target_reached_time = None # 首次到达目标的时间
last_log_time = start_time # 上次打印日志的时间
stable_count = 0 # 稳定计数器(0.1s/次,5次=0.5s)
self.get_logger().info("⏳ 开始运动完成检测...")
while rclpy.ok() and not self.is_shutting_down:
# 1. 检查超时
if time.time() - start_time > timeout:
current_joints = self.get_current_joints_in_standard_order()
errors = [abs(c - t) for c, t in zip(current_joints, target_joints)]
self.get_logger().error(
f"⚠️ 运动超时! 最大误差: {max(errors):.4f}rad\n"
f"目标关节: {[round(t, 3) for t in target_joints]}\n"
f"当前关节: {[round(c, 3) for c in current_joints]}"
)
return False
# 2. 获取当前关节位置
current_joints = self.get_current_joints_in_standard_order()
# 3. 检查是否到达目标(所有关节在容差内)
if self._joints_reached_target(current_joints, target_joints):
if target_reached_time is None:
target_reached_time = time.time()
self.get_logger().info("🎯 首次到达目标位置,开始稳定检测...")
stable_count += 1
# 稳定0.5s(5个检测周期)后确认完成
if stable_count >= 5:
stable_time = time.time() - target_reached_time
self.get_logger().info(f"✅ 运动完成! 稳定时间: {stable_time:.2f}s")
return True
else:
# 未到达目标,重置稳定计数器
target_reached_time = None
stable_count = 0
# 4. 每秒打印一次运动状态(避免日志刷屏)
current_time = time.time()
if current_time - last_log_time >= 1.0:
last_log_time = current_time
elapsed = current_time - start_time
errors = [abs(c - t) for c, t in zip(current_joints, target_joints)]
self.get_logger().info(
f"⏱️ 运动中... 已耗时: {elapsed:.1f}s | "
f"最大误差: {max(errors):.4f}rad | "
f"稳定计数: {stable_count}/5"
)
time.sleep(0.1) # 100ms检测周期,平衡实时性与资源占用
def _joints_reached_target(self, current_joints, target_joints):
"""检查所有关节是否在容差范围内到达目标位置"""
return all(abs(c - t) <= JOINT_TOLERANCE for c, t in zip(current_joints, target_joints))
# ======================= 网络通信方法 =======================
def start_socket_server(self):
"""启动Socket服务器,接收外部控制指令(如p1/p2/p3)"""
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 允许端口复用
try:
server_socket.bind((SERVER_IP, SERVER_PORT))
server_socket.listen(5) # 最大同时连接数
server_socket.settimeout(1.0) # 超时避免阻塞关闭流程
self.get_logger().info(f"📡 Socket服务器监听: {SERVER_IP}:{SERVER_PORT}")
while rclpy.ok() and not self.is_shutting_down:
try:
# 接收客户端连接(非阻塞,超时后循环检查关闭状态)
client_socket, addr = server_socket.accept()
self.get_logger().info(f"📞 新客户端连接: {addr}")
# 启动线程处理客户端请求(避免阻塞其他连接)
Thread(target=self.handle_client, args=(client_socket,), daemon=True).start()
except socket.timeout:
continue # 超时无连接,继续循环检查关闭状态
except Exception as e:
self.get_logger().error(f"❌ Socket连接异常: {e}")
except Exception as e:
self.get_logger().fatal(f"❌ Socket服务器启动失败: {e}")
finally:
server_socket.close()
self.get_logger().info("📡 Socket服务器已关闭")
def handle_client(self, client_socket):
"""处理单个客户端的控制指令(指令格式:cmd_type:json_params)"""
try:
# 接收指令(最大1024字节,避免数据包过大)
data = client_socket.recv(1024).decode().strip()
if not data:
self.get_logger().warning("⚠️ 客户端发送空指令,断开连接")
return
# 解析指令格式(如"p2:[[0.4,0.1,0.2],[1.0,0.0,0.0,0.0]]")
if ':' not in data:
self.get_logger().error(f"❌ 无效指令格式: {data}(需符合'cmd:params')")
client_socket.sendall("False".encode())
return
cmd_type, cmd_json = data.split(':', 1)
cmd_params = json.loads(cmd_json)
self.get_logger().info(f"📥 接收指令: 类型={cmd_type}, 参数={cmd_params}")
# 执行对应指令
success = False
if cmd_type == "p1":
# p1指令:关节空间运动(参数:[joint1, joint2, ..., joint6])
success = self.execute_joint_motion(cmd_params)
elif cmd_type in ["p2", "p3"]:
# p2/p3指令:位姿空间运动(参数:[[x,y,z], [x,y,z,w]])
success = self.execute_pose_motion(cmd_params)
else:
self.get_logger().error(f"❌ 未知指令类型: {cmd_type}(支持p1/p2/p3)")
# 向客户端返回执行结果(True/False)
client_socket.sendall(str(success).encode())
self.get_logger().info(f"📤 指令执行结果: {success}(客户端:{client_socket.getpeername()})")
except json.JSONDecodeError:
self.get_logger().error(f"❌ JSON参数解析失败: {cmd_json}")
client_socket.sendall("False".encode())
except Exception as e:
self.get_logger().error(f"❌ 客户端处理异常: {e}\n{traceback.format_exc()}")
client_socket.sendall("False".encode())
finally:
client_socket.close()
self.get_logger().info(f"📞 客户端连接已断开: {client_socket.getpeername()}")
# ======================= 节点关闭方法 =======================
def shutdown(self):
"""优化关闭流程:确保线程退出、资源释放(核心修复)"""
self.is_shutting_down = True
self.get_logger().info("🛑 节点开始关闭,释放资源中...")
# 1. 等待Socket线程退出(避免通信中强制关闭)
if hasattr(self, 'socket_thread') and self.socket_thread.is_alive():
self.get_logger().info("⏳ 等待Socket线程退出...")
self.socket_thread.join(timeout=2.0) # 最多等待2秒
if self.socket_thread.is_alive():
self.get_logger().warning("⚠️ Socket线程未正常退出,强制终止")
# 2. 销毁MoveIt2资源(避免资源泄漏导致的"Destroyable"错误)
if hasattr(self, 'moveit2') and self.moveit2 is not None:
self.get_logger().info("🗑️ 销毁MoveIt2资源...")
# 置空MoveIt2实例,触发垃圾回收
self.moveit2 = None
# 3. 关闭发布器与订阅器(ROS2节点标准操作)
self.destroy_publisher(self.trajectory_pub)
self.destroy_subscription(self.trajectory_sub)
self.destroy_subscription(self.joint_state_sub)
self.get_logger().info("✅ 所有资源已释放")
def main():
"""节点主函数:初始化ROS2、启动执行器"""
rclpy.init()
node = S360RobotControlNode()
# 使用多线程执行器,支持同时处理回调与Socket通信
executor = MultiThreadedExecutor(num_threads=4)
executor.add_node(node)
try:
node.get_logger().info("🚀 节点进入运行状态,等待指令...")
executor.spin()
except KeyboardInterrupt:
# 捕获Ctrl+C中断信号
node.get_logger().info("🛑 收到键盘中断信号(Ctrl+C)")
except Exception as e:
# 捕获其他未知异常
node.get_logger().fatal(f"❌ 节点运行异常: {e}\n{traceback.format_exc()}")
finally:
# 确保关闭流程执行
node.shutdown()
node.destroy_node()
rclpy.shutdown()
node.get_logger().info("✅ 节点已完全关闭")
if __name__ == "__main__":
main()
怎么解决,给我完整的解决代码
最新发布