diff --git a/docs/ARM_CONTROL_README.md b/docs/ARM_CONTROL_README.md
new file mode 100644
index 0000000..38a1906
--- /dev/null
+++ b/docs/ARM_CONTROL_README.md
@@ -0,0 +1,319 @@
+# arm_control ROS 节点使用指南
+
+## 概述
+
+`arm_control` 是一个封装了机械臂控制功能的 ROS 2 节点,基于 `udp_control.py` 改造,提供服务接口进行机械臂控制。
+
+## 功能特性
+
+- ✅ 关节空间运动控制(带插值)
+- ✅ 笛卡尔空间运动控制(带逆运动学)
+- ✅ 正运动学查询
+- ✅ 夹爪控制
+- ✅ 状态发布(关节状态 + TCP 位姿)
+- ✅ 状态缓存(平滑运动)
+
+## 编译
+
+```bash
+cd ros2
+
+# 1. 编译消息包
+colcon build --packages-select arm_control_msgs
+
+# 2. Source 消息包
+source install/setup.bash
+
+# 3. 编译控制节点
+colcon build --packages-select udp_teleop
+
+# 4. Source 控制节点
+source install/setup.bash
+```
+
+## 运行
+
+### 启动控制节点
+
+```bash
+# 使用默认参数
+ros2 run udp_teleop arm_control
+
+# 使用配置文件
+ros2 run udp_teleop arm_control \
+ --ros-args --params-file src/udp_teleop/config/arm_control.yaml
+
+# 覆盖特定参数
+ros2 run udp_teleop arm_control \
+ --ros-args -p udp_ip:=192.168.233.67 -p udp_port:=8888
+```
+
+## 服务接口
+
+### 1. 关节空间运动
+
+```bash
+ros2 service call /arm_control/move_joints arm_control_msgs/srv/MoveJoints \
+ "{height: -100, j2: 10, j3: 20, j4: 30, j5: 81, j6: 30, duration: 2.0}"
+```
+
+### 2. 笛卡尔空间运动
+
+```bash
+# 基本运动
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -100.0, phi: 45.0, duration: 2.0}"
+
+# 带夹爪控制
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -100.0, phi: 45.0, grip: true, duration: 2.0}"
+```
+
+### 3. 查询当前位姿
+
+```bash
+ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
+```
+
+输出示例:
+```
+success: true
+message: ''
+x: 150.234
+y: 75.123
+z: -100.0
+phi: 45.678
+height: -100
+j2: 13
+j3: 27
+j4: 55
+j5: 81
+j6: 30
+```
+
+### 4. 夹爪控制
+
+```bash
+# 抓取
+ros2 service call /arm_control/set_gripper arm_control_msgs/srv/SetGripper \
+ "{grip: true}"
+
+# 释放
+ros2 service call /arm_control/set_gripper arm_control_msgs/srv/SetGripper \
+ "{release: true}"
+```
+
+## 话题订阅
+
+### 1. 关节状态
+
+```bash
+ros2 topic echo /arm_control/joint_states
+```
+
+输出:
+```yaml
+header:
+ stamp:
+ sec: 1234567890
+ nanosec: 123456789
+ frame_id: ''
+height: -100
+j2: 13
+j3: 27
+j4: 55
+j5: 81
+j6: 30
+```
+
+### 2. TCP 位姿
+
+```bash
+ros2 topic echo /arm_control/tcp_pose
+```
+
+输出:
+```yaml
+header:
+ stamp:
+ sec: 1234567890
+ nanosec: 123456789
+ frame_id: ''
+x: 150.234
+y: 75.123
+z: -100.0
+phi: 45.678
+```
+
+## Python 客户端示例
+
+```python
+#!/usr/bin/env python3
+import rclpy
+from rclpy.node import Node
+from arm_control_msgs.srv import MovePose
+
+class MyArmController(Node):
+ def __init__(self):
+ super().__init__('my_controller')
+ self.cli = self.create_client(MovePose, 'arm_control/move_pose')
+ self.cli.wait_for_service()
+
+ def move_to(self, x, y, z, phi):
+ req = MovePose.Request()
+ req.x = x
+ req.y = y
+ req.z = z
+ req.phi = phi
+ req.duration = 2.0
+
+ future = self.cli.call_async(req)
+ rclpy.spin_until_future_complete(self, future)
+ return future.result().success
+
+def main():
+ rclpy.init()
+ controller = MyArmController()
+
+ # 移动到目标位置
+ controller.move_to(200.0, 100.0, -100.0, 45.0)
+
+ controller.destroy_node()
+ rclpy.shutdown()
+
+if __name__ == '__main__':
+ main()
+```
+
+## 完整抓取流程示例
+
+```bash
+# 运行示例客户端(包含完整抓取流程)
+ros2 run udp_teleop arm_control_client
+```
+
+或手动调用:
+
+```bash
+# 1. 查询当前位姿
+ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
+
+# 2. 移动到物体上方
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -50.0, phi: 45.0, release: true, duration: 2.0}"
+
+# 3. 下降到抓取位置
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -150.0, phi: 45.0, release: true, duration: 1.0}"
+
+# 4. 抓取
+ros2 service call /arm_control/set_gripper arm_control_msgs/srv/SetGripper \
+ "{grip: true}"
+
+# 5. 提升
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -50.0, phi: 45.0, grip: true, duration: 1.0}"
+```
+
+## 参数配置
+
+编辑 `config/arm_control.yaml`:
+
+```yaml
+arm_control:
+ ros__parameters:
+ # UDP 配置
+ udp_ip: '192.168.4.1'
+ udp_port: 8888
+
+ # 机械臂几何参数
+ l1: 125.0
+ l2: 125.0
+ x4: 110.0
+ z4: 80.0
+
+ # 关节限位
+ height_min: -290
+ height_max: 0
+ j2_min: -110
+ j2_max: 115
+ # ... (更多参数见配置文件)
+```
+
+## 调试
+
+### 查看服务列表
+
+```bash
+ros2 service list | grep arm_control
+```
+
+### 查看话题列表
+
+```bash
+ros2 topic list | grep arm_control
+```
+
+### 查看服务接口定义
+
+```bash
+ros2 interface show arm_control_msgs/srv/MovePose
+```
+
+### 实时监控状态
+
+```bash
+# 终端 1: 查看关节状态
+ros2 topic echo /arm_control/joint_states
+
+# 终端 2: 查看 TCP 位姿
+ros2 topic echo /arm_control/tcp_pose
+
+# 终端 3: 发送控制命令
+ros2 service call /arm_control/move_pose ...
+```
+
+## 常见问题
+
+### Q1: 服务调用失败
+
+**检查**:
+1. 节点是否正在运行?`ros2 node list`
+2. UDP 连接是否正常?检查 `udp_ip` 参数
+3. 关节限位是否合理?查看错误消息
+
+### Q2: 运动不平滑
+
+**调整参数**:
+- 增加 `duration`(运动时长)
+- 增加 `default_rate`(插值频率)
+
+### Q3: 状态不更新
+
+**检查**:
+- `use_state_cache` 是否启用?
+- `tools/.udp_control_state.json` 是否可写?
+
+## 与原始 udp_control.py 对比
+
+| 功能 | udp_control.py | arm_control 节点 |
+|------|---------------|-----------------|
+| 接口 | 命令行 | ROS 服务 + 话题 |
+| 集成 | 独立脚本 | ROS 生态系统 |
+| 状态查询 | 文件缓存 | 服务调用 |
+| 多客户端 | 不支持 | 支持 |
+| 实时监控 | 不支持 | 话题订阅 |
+
+## 下一步
+
+- 集成视觉系统:创建视觉抓取节点,订阅相机话题,调用 arm_control 服务
+- 添加轨迹规划:创建轨迹规划器,生成平滑路径
+- 碰撞检测:添加工作空间限制和碰撞检测
+
+## 相关文件
+
+- 节点实现:`udp_teleop/arm_control.py`
+- 消息定义:`arm_control_msgs/msg/`
+- 服务定义:`arm_control_msgs/srv/`
+- 配置文件:`udp_teleop/config/arm_control.yaml`
+- 示例客户端:`udp_teleop/arm_control_client.py`
diff --git a/docs/BUILD_SUCCESS.md b/docs/BUILD_SUCCESS.md
new file mode 100644
index 0000000..ecacc20
--- /dev/null
+++ b/docs/BUILD_SUCCESS.md
@@ -0,0 +1,122 @@
+# 编译成功!🎉
+
+## ✅ 已完成
+
+1. **消息包编译** - arm_control_msgs ✓
+2. **控制节点编译** - udp_teleop ✓
+
+## 🚀 快速测试
+
+### 1. 启动控制节点
+
+```bash
+# 激活环境
+conda activate ros2_humble
+
+# Source 工作空间
+cd ros2
+source install/setup.bash
+
+# 启动节点(修改 IP 为你的 ESP32 IP)
+ros2 run udp_teleop arm_control \
+ --ros-args --params-file src/udp_teleop/config/arm_control.yaml
+```
+
+### 2. 测试服务(新终端)
+
+```bash
+# 激活环境
+conda activate ros2_humble
+cd ros2
+source install/setup.bash
+
+# 查询当前位姿
+ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
+
+# 移动到指定位置
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -100.0, phi: 45.0, duration: 2.0}"
+```
+
+### 3. 查看状态
+
+```bash
+# 查看关节状态
+ros2 topic echo /arm_control/joint_states
+
+# 查看 TCP 位姿
+ros2 topic echo /arm_control/tcp_pose
+
+# 查看所有服务
+ros2 service list | grep arm_control
+```
+
+## ⚠️ 重要提示
+
+### 编译说明
+
+由于 robostack 的 Python 配置问题,编译时需要显式指定 Python 路径:
+
+```bash
+# 已在 build_arm_control.sh 中自动处理
+export PYTHON_EXECUTABLE=$CONDA_PREFIX/bin/python
+export PYTHON_INCLUDE_DIR=$CONDA_PREFIX/include/python3.12
+export PYTHON_LIBRARY=$CONDA_PREFIX/lib/libpython3.12.so
+```
+
+### 修改配置
+
+编辑 `src/udp_teleop/config/arm_control.yaml` 修改参数:
+
+```yaml
+arm_control:
+ ros__parameters:
+ udp_ip: '192.168.4.1' # 修改为你的 ESP32 IP
+ udp_port: 8888
+```
+
+修改后直接重启节点即可,无需重新编译。
+
+## 📝 下一步
+
+1. **修改 ESP32 IP**: 编辑 `config/arm_control.yaml`
+2. **测试连接**: 启动节点,查看是否有错误
+3. **调用服务**: 使用上面的命令测试
+4. **运行示例**: `ros2 run udp_teleop arm_control_client`
+
+## 🐛 故障排查
+
+### 问题:找不到服务
+
+**解决**:
+```bash
+# 检查节点是否运行
+ros2 node list
+
+# 重新 source 环境
+source install/setup.bash
+```
+
+### 问题:UDP 发送失败
+
+**解决**:
+1. 检查 ESP32 IP 是否正确
+2. 测试网络连接:`ping 192.168.4.1`
+3. 测试 UDP:`echo 'XYW:0:0:0:XZHY' | nc -u 192.168.4.1 8888`
+
+### 问题:重新编译
+
+**解决**:
+```bash
+# 清理后重新编译
+rm -rf build/ install/ log/
+./build_arm_control.sh
+```
+
+## 📚 文档
+
+- 完整文档:[ARM_CONTROL_README.md](ARM_CONTROL_README.md)
+- 快速指南:[QUICKSTART.md](QUICKSTART.md)
+- 实现总结:[IMPLEMENTATION_SUMMARY.md](IMPLEMENTATION_SUMMARY.md)
+
+祝使用愉快!🎉
diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md
new file mode 100644
index 0000000..f43ae00
--- /dev/null
+++ b/docs/IMPLEMENTATION_SUMMARY.md
@@ -0,0 +1,252 @@
+# arm_control ROS 节点封装总结
+
+## ✅ 完成的工作
+
+### 1. 创建了消息和服务定义包 (`arm_control_msgs`)
+
+**消息类型**:
+- `TCPPose.msg` - TCP 位姿(x, y, z, phi)
+- `JointState.msg` - 关节状态(height, j2-j6)
+
+**服务类型**:
+- `MoveJoints.srv` - 关节空间运动控制
+- `MovePose.srv` - 笛卡尔空间运动控制(带逆运动学)
+- `GetPose.srv` - 查询当前位姿(正运动学)
+- `SetGripper.srv` - 夹爪控制
+
+### 2. 封装了控制节点 (`arm_control.py`)
+
+**核心功能**:
+- ✅ 关节空间插值运动
+- ✅ 笛卡尔空间逆运动学求解
+- ✅ 正运动学位姿计算
+- ✅ UDP 命令发送(与 ESP32 通信)
+- ✅ 状态缓存(平滑运动)
+- ✅ 参数化配置
+- ✅ 状态发布(10Hz)
+
+**服务接口**:
+- `/arm_control/move_joints` - 关节运动
+- `/arm_control/move_pose` - 位姿运动
+- `/arm_control/get_pose` - 查询位姿
+- `/arm_control/set_gripper` - 夹爪控制
+
+**话题发布**:
+- `/arm_control/joint_states` - 关节状态(10Hz)
+- `/arm_control/tcp_pose` - TCP 位姿(10Hz)
+
+### 3. 创建了示例客户端 (`arm_control_client.py`)
+
+**演示功能**:
+- 查询当前位姿
+- 完整抓取流程:
+ 1. 移动到物体上方
+ 2. 下降
+ 3. 抓取
+ 4. 提升
+ 5. 移动到目标位置
+ 6. 下降
+ 7. 释放
+ 8. 提升
+
+### 4. 配置和文档
+
+**配置文件**:
+- `config/arm_control.yaml` - 完整参数配置
+
+**文档**:
+- `ARM_CONTROL_README.md` - 完整使用文档
+- `QUICKSTART.md` - 快速开始指南
+
+**脚本**:
+- `build_arm_control.sh` - 一键编译脚本
+
+## 📁 文件清单
+
+```
+ros2/
+├── build_arm_control.sh # 编译脚本 ✨
+├── ARM_CONTROL_README.md # 完整文档 ✨
+├── QUICKSTART.md # 快速指南 ✨
+└── src/
+ ├── arm_control_msgs/ # 消息包 ✨
+ │ ├── CMakeLists.txt
+ │ ├── package.xml
+ │ ├── msg/
+ │ │ ├── TCPPose.msg
+ │ │ └── JointState.msg
+ │ └── srv/
+ │ ├── MoveJoints.srv
+ │ ├── MovePose.srv
+ │ ├── GetPose.srv
+ │ └── SetGripper.srv
+ └── udp_teleop/
+ ├── setup.py # 已更新 ✨
+ ├── package.xml # 已更新 ✨
+ ├── udp_teleop/
+ │ ├── keyboard_control.py # 原有
+ │ ├── arm_control.py # 新增 ✨
+ │ └── arm_control_client.py # 新增 ✨
+ └── config/
+ ├── params.yaml # 原有
+ └── arm_control.yaml # 新增 ✨
+```
+
+## 🚀 快速使用
+
+### 编译
+
+```bash
+cd ros2
+./build_arm_control.sh
+```
+
+### 运行节点
+
+```bash
+ros2 run udp_teleop arm_control \
+ --ros-args --params-file src/udp_teleop/config/arm_control.yaml
+```
+
+### 测试服务
+
+```bash
+# 查询位姿
+ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
+
+# 移动
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -100.0, phi: 45.0, duration: 2.0}"
+```
+
+### 运行示例
+
+```bash
+ros2 run udp_teleop arm_control_client
+```
+
+## 🎯 与原始 udp_control.py 对比
+
+| 特性 | udp_control.py | arm_control 节点 |
+|------|---------------|-----------------|
+| **接口方式** | 命令行参数 | ROS 服务调用 |
+| **状态查询** | 读取 JSON 文件 | 服务调用 + 话题订阅 |
+| **多客户端** | ❌ 不支持 | ✅ 支持 |
+| **实时监控** | ❌ 无 | ✅ 10Hz 状态发布 |
+| **参数配置** | 命令行参数 | YAML 配置文件 |
+| **集成度** | 独立工具 | ROS 生态集成 |
+| **可编程性** | Shell 脚本 | Python/C++ 客户端 |
+
+## 💡 优势
+
+### 1. **标准化接口**
+- 使用 ROS 服务和话题,符合 ROS 生态标准
+- 易于与其他 ROS 节点集成(如视觉、规划器)
+
+### 2. **多客户端支持**
+- 多个客户端可同时连接
+- 适合复杂系统(如视觉 + 手动控制)
+
+### 3. **实时状态监控**
+- 10Hz 状态发布
+- 可用于可视化、日志记录、故障诊断
+
+### 4. **灵活配置**
+- YAML 参数文件
+- 运行时参数覆盖
+- 无需重新编译
+
+### 5. **易于扩展**
+- 添加新服务:只需定义 .srv 文件
+- 添加新话题:只需定义 .msg 文件
+- 集成其他功能:订阅/发布话题即可
+
+## 🔧 使用场景
+
+### 场景 1:视觉抓取
+
+```python
+# 视觉节点订阅相机话题,检测物体
+# 调用 arm_control 服务控制机械臂
+class VisionGraspNode(Node):
+ def __init__(self):
+ self.arm_cli = self.create_client(MovePose, 'arm_control/move_pose')
+ self.sub = self.create_subscription(Image, '/camera/image', self.on_image, 10)
+
+ def on_image(self, msg):
+ # 检测物体
+ x, y, z = detect_object(msg)
+
+ # 控制机械臂抓取
+ self.move_to(x, y, z, phi=45.0)
+```
+
+### 场景 2:示教编程
+
+```python
+# 记录示教点位
+class TeachPendant(Node):
+ def __init__(self):
+ self.get_cli = self.create_client(GetPose, 'arm_control/get_pose')
+ self.move_cli = self.create_client(MovePose, 'arm_control/move_pose')
+ self.waypoints = []
+
+ def record_waypoint(self):
+ # 记录当前位置
+ pose = self.get_current_pose()
+ self.waypoints.append(pose)
+
+ def replay(self):
+ # 重放示教轨迹
+ for pose in self.waypoints:
+ self.move_to(pose.x, pose.y, pose.z, pose.phi)
+```
+
+### 场景 3:轨迹规划
+
+```python
+# 使用规划器生成轨迹
+class TrajectoryPlanner(Node):
+ def __init__(self):
+ self.move_cli = self.create_client(MovePose, 'arm_control/move_pose')
+
+ def execute_trajectory(self, waypoints):
+ # 执行轨迹点序列
+ for wp in waypoints:
+ self.move_to(wp.x, wp.y, wp.z, wp.phi, duration=0.5)
+```
+
+## 📚 下一步建议
+
+### 1. **视觉集成**
+创建视觉抓取节点,结合 `camera_to_base.py` 实现自动抓取
+
+### 2. **GUI 控制面板**
+使用 RQt 创建图形界面,实时显示状态和控制
+
+### 3. **轨迹记录与回放**
+实现示教编程功能
+
+### 4. **碰撞检测**
+添加工作空间限制和简单碰撞检测
+
+### 5. **MoveIt 集成**
+创建 URDF 和 MoveIt 配置,使用高级运动规划
+
+## 🎓 学习资源
+
+- ROS 2 服务教程:https://docs.ros.org/en/humble/Tutorials/Services.html
+- ROS 2 话题教程:https://docs.ros.org/en/humble/Tutorials/Topics.html
+- 自定义消息:https://docs.ros.org/en/humble/Tutorials/Custom-ROS2-Interfaces.html
+
+## ✨ 总结
+
+现在你有了一个完整的 ROS 节点化的机械臂控制系统:
+
+1. ✅ **功能完整** - 保留了 udp_control.py 的所有功能
+2. ✅ **接口标准** - 使用 ROS 服务和话题
+3. ✅ **易于集成** - 可与其他 ROS 节点无缝配合
+4. ✅ **文档齐全** - 提供了完整的文档和示例
+5. ✅ **开箱即用** - 一键编译,快速上手
+
+祝你使用愉快!🎉
diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md
new file mode 100644
index 0000000..96afcfc
--- /dev/null
+++ b/docs/QUICKSTART.md
@@ -0,0 +1,244 @@
+# 机械臂控制 ROS 节点 - 快速开始
+
+## 🚀 快速编译和运行
+
+### 1. 一键编译
+
+```bash
+cd ros2
+./build_arm_control.sh
+```
+
+### 2. 启动节点
+
+```bash
+# 方法 A: 使用配置文件(推荐)
+ros2 run udp_teleop arm_control \
+ --ros-args --params-file src/udp_teleop/config/arm_control.yaml
+
+# 方法 B: 使用默认参数
+ros2 run udp_teleop arm_control
+
+# 方法 C: 覆盖特定参数
+ros2 run udp_teleop arm_control \
+ --ros-args -p udp_ip:=192.168.233.67
+```
+
+### 3. 测试服务
+
+```bash
+# 查询当前位姿
+ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
+
+# 移动到指定位置
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -100.0, phi: 45.0, duration: 2.0}"
+```
+
+### 4. 运行完整示例
+
+```bash
+# 在新终端运行示例客户端(包含完整抓取流程)
+ros2 run udp_teleop arm_control_client
+```
+
+## 📁 文件结构
+
+```
+ros2/
+├── build_arm_control.sh # 一键编译脚本
+├── ARM_CONTROL_README.md # 完整使用文档
+├── QUICKSTART.md # 本文件
+└── src/
+ ├── arm_control_msgs/ # 消息和服务定义
+ │ ├── msg/
+ │ │ ├── TCPPose.msg # TCP 位姿消息
+ │ │ └── JointState.msg # 关节状态消息
+ │ └── srv/
+ │ ├── MoveJoints.srv # 关节运动服务
+ │ ├── MovePose.srv # 位姿运动服务
+ │ ├── GetPose.srv # 查询位姿服务
+ │ └── SetGripper.srv # 夹爪控制服务
+ └── udp_teleop/
+ ├── udp_teleop/
+ │ ├── arm_control.py # 控制节点实现
+ │ └── arm_control_client.py # 示例客户端
+ └── config/
+ └── arm_control.yaml # 参数配置
+```
+
+## 🎯 常用命令
+
+### 服务调用
+
+```bash
+# 1. 关节空间运动
+ros2 service call /arm_control/move_joints arm_control_msgs/srv/MoveJoints \
+ "{height: -100, j2: 10, j3: 20, j4: 30, j5: 81, j6: 30, duration: 2.0}"
+
+# 2. 笛卡尔空间运动
+ros2 service call /arm_control/move_pose arm_control_msgs/srv/MovePose \
+ "{x: 200.0, y: 100.0, z: -100.0, phi: 45.0, duration: 2.0}"
+
+# 3. 查询当前位姿
+ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
+
+# 4. 夹爪控制
+ros2 service call /arm_control/set_gripper arm_control_msgs/srv/SetGripper \
+ "{grip: true}"
+```
+
+### 话题订阅
+
+```bash
+# 查看关节状态(10Hz 发布)
+ros2 topic echo /arm_control/joint_states
+
+# 查看 TCP 位姿(10Hz 发布)
+ros2 topic echo /arm_control/tcp_pose
+```
+
+### 调试命令
+
+```bash
+# 查看所有服务
+ros2 service list | grep arm_control
+
+# 查看所有话题
+ros2 topic list | grep arm_control
+
+# 查看节点信息
+ros2 node info /arm_control
+
+# 查看服务接口定义
+ros2 interface show arm_control_msgs/srv/MovePose
+```
+
+## 📝 Python 客户端模板
+
+```python
+#!/usr/bin/env python3
+import rclpy
+from rclpy.node import Node
+from arm_control_msgs.srv import MovePose, GetPose
+
+class MyController(Node):
+ def __init__(self):
+ super().__init__('my_controller')
+
+ # 创建服务客户端
+ self.move_cli = self.create_client(MovePose, 'arm_control/move_pose')
+ self.get_cli = self.create_client(GetPose, 'arm_control/get_pose')
+
+ # 等待服务
+ self.move_cli.wait_for_service()
+ self.get_cli.wait_for_service()
+
+ def move_to(self, x, y, z, phi, duration=2.0):
+ """移动到指定位置"""
+ req = MovePose.Request()
+ req.x = x
+ req.y = y
+ req.z = z
+ req.phi = phi
+ req.duration = duration
+
+ future = self.move_cli.call_async(req)
+ rclpy.spin_until_future_complete(self, future)
+ return future.result().success
+
+ def get_pose(self):
+ """查询当前位姿"""
+ req = GetPose.Request()
+ future = self.get_cli.call_async(req)
+ rclpy.spin_until_future_complete(self, future)
+ return future.result()
+
+def main():
+ rclpy.init()
+ controller = MyController()
+
+ # 查询位姿
+ pose = controller.get_pose()
+ print(f"当前位置: ({pose.x}, {pose.y}, {pose.z})")
+
+ # 移动
+ controller.move_to(200.0, 100.0, -100.0, 45.0)
+
+ controller.destroy_node()
+ rclpy.shutdown()
+
+if __name__ == '__main__':
+ main()
+```
+
+## 🔧 配置修改
+
+编辑 `src/udp_teleop/config/arm_control.yaml`:
+
+```yaml
+arm_control:
+ ros__parameters:
+ # 修改 ESP32 IP
+ udp_ip: '192.168.4.1'
+
+ # 修改运动速度
+ default_duration: 1.0 # 更快:0.5,更慢:2.0
+
+ # 修改关节限位
+ height_min: -290
+ height_max: 0
+```
+
+修改后重新运行节点即可(无需重新编译)。
+
+## ⚠️ 常见问题
+
+### 编译失败
+
+```bash
+# 确保环境激活
+conda activate ros2_humble
+source install/setup.bash
+
+# 清理后重新编译
+rm -rf build/ install/ log/
+./build_arm_control.sh
+```
+
+### 服务不可用
+
+```bash
+# 检查节点是否运行
+ros2 node list
+
+# 检查服务是否存在
+ros2 service list | grep arm_control
+
+# 查看节点日志
+ros2 run udp_teleop arm_control --ros-args --log-level debug
+```
+
+### UDP 连接失败
+
+```bash
+# 测试 UDP 连接
+echo 'XYW:0:0:0:XZHY' | nc -u 192.168.4.1 8888
+
+# 修改 IP 配置
+ros2 run udp_teleop arm_control \
+ --ros-args -p udp_ip:=<你的IP> -p udp_port:=8888
+```
+
+## 📚 更多文档
+
+- 完整使用文档:[ARM_CONTROL_README.md](ARM_CONTROL_README.md)
+- 原始工具文档:[../tools/README.md](../tools/README.md)
+- ROS 2 包文档:[src/udp_teleop/README.md](src/udp_teleop/README.md)
+
+## 🎓 下一步
+
+1. **集成视觉**:创建视觉抓取节点,订阅相机话题,调用 arm_control 服务
+2. **添加规划**:使用 MoveIt 或自定义轨迹规划器
+3. **多机械臂**:启动多个 arm_control 节点控制多个机械臂
+4. **远程控制**:通过 ROS 2 的 DDS 实现跨机器控制
diff --git a/docs/VISION_GRASP_README.md b/docs/VISION_GRASP_README.md
new file mode 100644
index 0000000..043073f
--- /dev/null
+++ b/docs/VISION_GRASP_README.md
@@ -0,0 +1,359 @@
+# 视觉抓取节点使用指南
+
+## 概述
+
+`vision_grasp` 节点基于 `camera_to_base.py` 实现自动抓取和释放功能,将相机坐标系的检测结果转换为机械臂基坐标系,并自动执行抓取流程。
+
+## 功能
+
+### 1. 抓取功能
+
+**输入**:相机坐标系 `(x, y, z)`
+
+**流程**:
+1. 坐标转换:`(xc, yc, zc) = (x, -y, z)`(图像坐标到相机坐标)
+2. 转换到基坐标系
+3. 释放夹爪(duration=0)
+4. 移动到目标位置(duration=3s)
+5. 抓取(duration=1s)
+6. 回收到 (200, 0, 当前z)
+
+### 2. 释放功能
+
+**输入**:基坐标系 `(x, y, z)`
+
+**流程**:
+1. 移动到释放位置
+2. 释放夹爪(duration=0)
+3. 回收到 (200, 0, 当前z)
+
+## 编译
+
+```bash
+cd ros2
+colcon build --packages-select udp_teleop
+source install/setup.bash
+```
+
+## 运行
+
+### 启动节点
+
+**终端 1**:启动机械臂控制节点
+```bash
+ros2 run udp_teleop arm_control \
+ --ros-args --params-file src/udp_teleop/config/arm_control.yaml
+```
+
+**终端 2**:启动视觉抓取节点
+```bash
+ros2 run udp_teleop vision_grasp \
+ --ros-args --params-file src/udp_teleop/config/vision_grasp.yaml
+```
+
+## 使用
+
+### 方法 1:发布话题触发抓取
+
+```bash
+# 抓取:输入相机坐标
+ros2 topic pub --once /vision_grasp/grasp_target geometry_msgs/Point \
+ "{x: 10.0, y: 5.0, z: 250.0}"
+
+# 释放:输入基坐标
+ros2 topic pub --once /vision_grasp/release_target geometry_msgs/Point \
+ "{x: 100.0, y: 150.0, z: -100.0}"
+```
+
+### 方法 2:Python 脚本集成
+
+```python
+#!/usr/bin/env python3
+import rclpy
+from rclpy.node import Node
+from geometry_msgs.msg import Point
+
+class VisionDetector(Node):
+ def __init__(self):
+ super().__init__('vision_detector')
+
+ # 创建发布者
+ self.grasp_pub = self.create_publisher(
+ Point,
+ 'vision_grasp/grasp_target',
+ 10
+ )
+
+ def detect_and_grasp(self):
+ # 模拟检测结果(相机坐标系)
+ camera_x = 10.0 # 相机右侧 10mm
+ camera_y = 5.0 # 相机下方 5mm
+ camera_z = 250.0 # 前方 250mm
+
+ # 发布抓取目标
+ msg = Point()
+ msg.x = camera_x
+ msg.y = camera_y
+ msg.z = camera_z
+
+ self.grasp_pub.publish(msg)
+ self.get_logger().info(f'发送抓取目标: ({camera_x}, {camera_y}, {camera_z})')
+
+def main():
+ rclpy.init()
+ node = VisionDetector()
+
+ # 检测并抓取
+ node.detect_and_grasp()
+
+ node.destroy_node()
+ rclpy.shutdown()
+
+if __name__ == '__main__':
+ main()
+```
+
+### 方法 3:与检测节点集成
+
+```python
+#!/usr/bin/env python3
+"""完整的视觉检测+抓取示例"""
+
+import rclpy
+from rclpy.node import Node
+from sensor_msgs.msg import Image
+from geometry_msgs.msg import Point
+import cv2
+from cv_bridge import CvBridge
+
+class VisionPipeline(Node):
+ def __init__(self):
+ super().__init__('vision_pipeline')
+
+ # 订阅相机图像
+ self.image_sub = self.create_subscription(
+ Image,
+ '/camera/image_raw',
+ self.on_image,
+ 10
+ )
+
+ # 发布抓取目标
+ self.grasp_pub = self.create_publisher(
+ Point,
+ 'vision_grasp/grasp_target',
+ 10
+ )
+
+ self.bridge = CvBridge()
+
+ def on_image(self, msg):
+ # 转换 ROS 图像到 OpenCV
+ image = self.bridge.imgmsg_to_cv2(msg, 'bgr8')
+
+ # 检测物体(示例:使用轮廓检测)
+ detected = self.detect_object(image)
+
+ if detected:
+ camera_x, camera_y, camera_z = detected
+
+ # 发布抓取目标
+ target = Point()
+ target.x = camera_x
+ target.y = camera_y
+ target.z = camera_z
+
+ self.grasp_pub.publish(target)
+ self.get_logger().info(f'检测到物体,发送抓取指令')
+
+ def detect_object(self, image):
+ """检测物体并返回相机坐标"""
+ # TODO: 实现你的检测算法
+ # 1. 图像处理(阈值、轮廓等)
+ # 2. 获取像素坐标 (u, v) 和像素宽度
+ # 3. 使用相似三角形计算深度
+ # 4. 转换到相机坐标系
+
+ # 示例返回值
+ return (10.0, 5.0, 250.0) # (xc, yc, zc)
+
+def main():
+ rclpy.init()
+ node = VisionPipeline()
+ rclpy.spin(node)
+ node.destroy_node()
+ rclpy.shutdown()
+
+if __name__ == '__main__':
+ main()
+```
+
+## 参数配置
+
+编辑 `config/vision_grasp.yaml`:
+
+```yaml
+vision_grasp:
+ ros__parameters:
+ # 相机到 TCP 的变换(如果相机不在 TCP 中心)
+ cam_tx: 0.0 # X 偏移
+ cam_ty: 0.0 # Y 偏移(高度)
+ cam_tz: 0.0 # Z 偏移(前后)
+
+ # 回收位置
+ retract_position_x: 200.0
+ retract_position_y: 0.0
+
+ # 运动时长
+ grasp_duration: 3.0 # 抓取移动时长
+ release_duration: 2.0 # 释放移动时长
+```
+
+## 坐标系说明
+
+### 相机坐标系
+
+```
+ Yc (下)
+ |
+ |
+ o-----> Zc (前,水平)
+ /
+ /
+ Xc (右)
+```
+
+### 坐标转换
+
+检测结果 `(x, y, z)` 表示:
+- `x`: 图像列方向(右为正)
+- `y`: 图像行方向(下为正)
+- `z`: 深度方向(前为正)
+
+节点会自动转换:
+```
+(xc, yc, zc) = (x, -y, z)
+```
+
+这是因为:
+- 图像 Y 向下 → 相机 Y 向下(负号修正方向)
+- 然后再转换到基坐标系
+
+## 调试
+
+### 查看节点状态
+
+```bash
+# 查看节点列表
+ros2 node list
+
+# 查看话题列表
+ros2 topic list | grep vision_grasp
+
+# 监听抓取目标
+ros2 topic echo /vision_grasp/grasp_target
+```
+
+### 测试流程
+
+1. **启动节点**
+ ```bash
+ # 终端 1
+ ros2 run udp_teleop arm_control --ros-args --params-file src/udp_teleop/config/arm_control.yaml
+
+ # 终端 2
+ ros2 run udp_teleop vision_grasp --ros-args --params-file src/udp_teleop/config/vision_grasp.yaml
+ ```
+
+2. **发送测试抓取**
+ ```bash
+ # 终端 3
+ ros2 topic pub --once /vision_grasp/grasp_target geometry_msgs/Point \
+ "{x: 0.0, y: 0.0, z: 300.0}"
+ ```
+
+3. **观察日志**
+ - 终端 2 会显示详细的抓取流程日志
+ - 确认坐标转换和每一步动作
+
+## 常见问题
+
+### Q1: 坐标转换不正确
+
+**检查**:
+1. 相机内参是否准确标定
+2. 相机到 TCP 的变换参数是否正确
+3. 当前 TCP 位姿是否正确
+
+### Q2: 抓取位置偏移
+
+**可能原因**:
+1. 深度计算不准确
+2. 相机安装角度有偏差
+3. 坐标系定义理解错误
+
+**解决**:
+1. 调整 `cam_pitch` 参数(如果相机有俯仰角)
+2. 校准相机内参
+3. 使用已知位置物体验证
+
+### Q3: 夹爪动作失败
+
+**检查**:
+1. arm_control 节点是否正常运行
+2. UDP 连接是否正常
+3. 关节限位是否合理
+
+## 扩展功能
+
+### 添加安全检查
+
+```python
+def execute_grasp(self, x: float, y: float, z: float, phi: float):
+ # 检查目标是否在工作空间内
+ if not self.is_in_workspace(x, y, z):
+ self.get_logger().warn(f'目标超出工作空间: ({x}, {y}, {z})')
+ return
+
+ # 执行抓取...
+```
+
+### 添加碰撞检测
+
+```python
+def is_path_safe(self, start, end):
+ # 检查路径是否安全
+ # TODO: 实现碰撞检测逻辑
+ return True
+```
+
+### 多物体抓取
+
+```python
+# 订阅物体列表
+self.objects_sub = self.create_subscription(
+ PointArray, # 自定义消息类型
+ 'vision_grasp/object_list',
+ self.handle_objects,
+ 10
+)
+
+def handle_objects(self, msg):
+ for obj in msg.points:
+ self.execute_grasp(obj.x, obj.y, obj.z, self.current_phi)
+ # 等待完成...
+```
+
+## 相关文件
+
+- 节点实现:`udp_teleop/vision_grasp.py`
+- 配置文件:`udp_teleop/config/vision_grasp.yaml`
+- 坐标变换工具:`tools/camera_to_base.py`
+- 机械臂控制:`udp_teleop/arm_control.py`
+
+## 下一步
+
+1. 集成物体检测算法(YOLO、轮廓检测等)
+2. 添加深度估计(相似三角形、双目视觉等)
+3. 优化抓取策略(多物体排序、路径规划等)
+4. 添加可视化(RViz 显示检测结果和机械臂状态)
diff --git a/docs/VISION_GRASP_SUMMARY.md b/docs/VISION_GRASP_SUMMARY.md
new file mode 100644
index 0000000..89f1426
--- /dev/null
+++ b/docs/VISION_GRASP_SUMMARY.md
@@ -0,0 +1,286 @@
+# 视觉抓取节点 - 完成总结
+
+## ✅ 完成的工作
+
+### 1. 创建了视觉抓取 ROS 节点 (`vision_grasp.py`)
+
+**功能**:
+- ✅ 抓取功能:输入相机坐标 → 自动转换 → 执行抓取流程
+- ✅ 释放功能:输入基坐标 → 移动 → 释放物体
+- ✅ 坐标变换:集成 `camera_to_base.py` 的完整变换逻辑
+- ✅ 自动化流程:释放夹爪 → 移动 → 抓取 → 回收
+
+### 2. 抓取流程
+
+```
+输入相机坐标 (x, y, z)
+ ↓
+转换: (xc, yc, zc) = (x, -y, z)
+ ↓
+变换到基坐标系
+ ↓
+1. Release 夹爪 (duration=0)
+ ↓
+2. 移动到目标 (duration=3s)
+ ↓
+3. Grip 夹爪 (duration=1s)
+ ↓
+4. 回收到 (200, 0, 当前z)
+```
+
+### 3. 释放流程
+
+```
+输入基坐标 (x, y, z)
+ ↓
+1. 移动到释放位置
+ ↓
+2. Release 夹爪 (duration=0)
+ ↓
+3. 回收到 (200, 0, 当前z)
+```
+
+## 📁 创建的文件
+
+```
+ros2/
+├── src/udp_teleop/
+│ ├── udp_teleop/
+│ │ └── vision_grasp.py ✨ 视觉抓取节点
+│ └── config/
+│ └── vision_grasp.yaml ✨ 参数配置
+├── test_vision_grasp.py ✨ 测试脚本
+└── VISION_GRASP_README.md ✨ 完整文档
+```
+
+## 🚀 快速使用
+
+### 启动节点
+
+**终端 1**:arm_control 节点
+```bash
+cd ros2
+source install/setup.bash
+
+ros2 run udp_teleop arm_control \
+ --ros-args --params-file src/udp_teleop/config/arm_control.yaml
+```
+
+**终端 2**:vision_grasp 节点
+```bash
+cd ros2
+source install/setup.bash
+
+ros2 run udp_teleop vision_grasp \
+ --ros-args --params-file src/udp_teleop/config/vision_grasp.yaml
+```
+
+**终端 3**:测试
+```bash
+cd ros2
+source install/setup.bash
+
+# 测试抓取(相机正前方 300mm)
+python test_vision_grasp.py grasp 0 0 300
+
+# 测试抓取(相机右侧 50mm,前方 300mm)
+python test_vision_grasp.py grasp 50 0 300
+
+# 测试释放(基坐标)
+python test_vision_grasp.py release 100 150 -100
+```
+
+### 或使用话题发布
+
+```bash
+# 抓取
+ros2 topic pub --once /vision_grasp/grasp_target geometry_msgs/Point \
+ "{x: 0.0, y: 0.0, z: 300.0}"
+
+# 释放
+ros2 topic pub --once /vision_grasp/release_target geometry_msgs/Point \
+ "{x: 100.0, y: 150.0, z: -100.0}"
+```
+
+## 🎯 关键特性
+
+### 1. 自动坐标转换
+
+- **输入**:相机坐标系 `(x, y, z)`
+- **自动转换**:`(xc, yc, zc) = (x, -y, z)`(图像坐标修正)
+- **变换到基坐标系**:使用当前 TCP 位姿进行完整变换
+
+### 2. 参数化配置
+
+```yaml
+vision_grasp:
+ ros__parameters:
+ # 相机到 TCP 的变换
+ cam_tx: 0.0
+ cam_ty: 0.0
+ cam_tz: 0.0
+
+ # 回收位置
+ retract_position_x: 200.0
+ retract_position_y: 0.0
+
+ # 运动时长
+ grasp_duration: 3.0
+ release_duration: 2.0
+```
+
+### 3. 完整日志
+
+节点会输出详细的流程日志:
+```
+============================================================
+开始抓取流程
+============================================================
+1. 释放夹爪
+2. 移动到目标位置: (323.5, 229.6, -108.6)
+3. 抓取物体
+4. 移动到回收位置: (200.0, 0.0, -108.6)
+============================================================
+✓ 抓取完成!
+============================================================
+```
+
+## 🔗 集成示例
+
+### Python 脚本集成
+
+```python
+#!/usr/bin/env python3
+import rclpy
+from rclpy.node import Node
+from geometry_msgs.msg import Point
+
+class MyDetector(Node):
+ def __init__(self):
+ super().__init__('my_detector')
+ self.grasp_pub = self.create_publisher(
+ Point, 'vision_grasp/grasp_target', 10)
+
+ def on_detection(self, camera_x, camera_y, camera_z):
+ """检测到物体后触发抓取"""
+ msg = Point()
+ msg.x = camera_x
+ msg.y = camera_y
+ msg.z = camera_z
+ self.grasp_pub.publish(msg)
+
+def main():
+ rclpy.init()
+ node = MyDetector()
+
+ # 模拟检测结果
+ node.on_detection(10.0, 5.0, 250.0)
+
+ rclpy.spin(node)
+ node.destroy_node()
+ rclpy.shutdown()
+```
+
+## 📊 话题接口
+
+| 话题 | 类型 | 说明 |
+|------|------|------|
+| `/vision_grasp/grasp_target` | geometry_msgs/Point | 抓取目标(相机坐标) |
+| `/vision_grasp/release_target` | geometry_msgs/Point | 释放目标(基坐标) |
+
+## 🎓 下一步
+
+### 1. 集成物体检测
+
+```python
+# 订阅相机图像
+self.image_sub = self.create_subscription(
+ Image, '/camera/image_raw', self.on_image, 10)
+
+def on_image(self, msg):
+ # 检测物体
+ camera_x, camera_y, camera_z = detect_object(msg)
+
+ # 触发抓取
+ self.publish_grasp_target(camera_x, camera_y, camera_z)
+```
+
+### 2. 添加深度估计
+
+使用 `tools/vision_transform.py` 中的相似三角形方法:
+
+```python
+from tools.vision_transform import compute_depth_from_size
+
+# 从检测获得像素宽度
+pixel_width = 100 # px
+real_width = 50 # mm
+focal_length = 500 # px
+
+depth = compute_depth_from_size(pixel_width, real_width, focal_length)
+```
+
+### 3. 多物体抓取
+
+```python
+# 创建队列
+self.grasp_queue = []
+
+def on_multiple_detections(self, detections):
+ for det in detections:
+ self.grasp_queue.append(det)
+
+ # 逐个抓取
+ while self.grasp_queue:
+ target = self.grasp_queue.pop(0)
+ self.publish_grasp_target(target.x, target.y, target.z)
+ # 等待完成...
+```
+
+## 🐛 故障排查
+
+### Q1: 坐标转换不正确
+
+**检查**:
+1. TCP 位姿是否正确(`ros2 service call /arm_control/get_pose`)
+2. 相机到 TCP 的变换参数(`cam_tx/ty/tz`, `cam_roll/pitch/yaw`)
+3. 坐标系方向理解是否正确
+
+### Q2: 抓取位置偏移
+
+**解决**:
+1. 校准相机内参
+2. 验证深度计算准确性
+3. 调整 `cam_pitch`(如果相机有俯仰角)
+
+### Q3: 服务调用超时
+
+**检查**:
+1. arm_control 节点是否运行
+2. UDP 连接是否正常
+3. 机械臂是否在合理位置
+
+## 📚 相关文档
+
+- **完整文档**:`VISION_GRASP_README.md`
+- **坐标变换**:`tools/camera_to_base.py`
+- **机械臂控制**:`ARM_CONTROL_README.md`
+- **视觉变换**:`docs/vision_calibration_horizontal.md`
+
+## 🎉 总结
+
+现在你有了一个完整的视觉抓取系统:
+
+1. ✅ **独立的机械臂控制节点** - `arm_control`
+2. ✅ **自动化抓取节点** - `vision_grasp`
+3. ✅ **完整的坐标变换** - 相机 → 基坐标系
+4. ✅ **参数化配置** - 灵活调整参数
+5. ✅ **测试工具** - 快速验证功能
+6. ✅ **完整文档** - 使用指南和示例
+
+只需要:
+1. 添加物体检测算法
+2. 连接相机获取图像
+3. 发布检测结果到 `/vision_grasp/grasp_target`
+
+系统就会自动完成抓取!
diff --git a/docs/craic.md b/docs/arm.md
similarity index 100%
rename from docs/craic.md
rename to docs/arm.md
diff --git a/ros2/src/arm_control_msgs/CMakeLists.txt b/ros2/src/arm_control_msgs/CMakeLists.txt
new file mode 100644
index 0000000..4072dba
--- /dev/null
+++ b/ros2/src/arm_control_msgs/CMakeLists.txt
@@ -0,0 +1,25 @@
+cmake_minimum_required(VERSION 3.10)
+project(arm_control_msgs)
+
+if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ add_compile_options(-Wall -Wextra -Wpedantic)
+endif()
+
+# find dependencies
+find_package(ament_cmake REQUIRED)
+find_package(rosidl_default_generators REQUIRED)
+find_package(std_msgs REQUIRED)
+
+# Generate messages and services
+rosidl_generate_interfaces(${PROJECT_NAME}
+ "msg/TCPPose.msg"
+ "msg/JointState.msg"
+ "srv/MoveJoints.srv"
+ "srv/MovePose.srv"
+ "srv/GetPose.srv"
+ "srv/SetGripper.srv"
+ DEPENDENCIES std_msgs
+)
+
+ament_export_dependencies(rosidl_default_runtime)
+ament_package()
diff --git a/ros2/src/arm_control_msgs/msg/JointState.msg b/ros2/src/arm_control_msgs/msg/JointState.msg
new file mode 100644
index 0000000..69710c1
--- /dev/null
+++ b/ros2/src/arm_control_msgs/msg/JointState.msg
@@ -0,0 +1,8 @@
+# 关节状态消息
+std_msgs/Header header
+int32 height # 高度 (mm)
+int32 j2 # 关节 2 角度 (度)
+int32 j3 # 关节 3 角度 (度)
+int32 j4 # 关节 4 角度 (度)
+int32 j5 # 关节 5 角度 (度)
+int32 j6 # 关节 6 角度 (度)
diff --git a/ros2/src/arm_control_msgs/msg/TCPPose.msg b/ros2/src/arm_control_msgs/msg/TCPPose.msg
new file mode 100644
index 0000000..1eed2f7
--- /dev/null
+++ b/ros2/src/arm_control_msgs/msg/TCPPose.msg
@@ -0,0 +1,6 @@
+# TCP 位姿消息
+std_msgs/Header header
+float64 x # X 坐标 (mm)
+float64 y # Y 坐标 (mm)
+float64 z # Z 坐标 (mm)
+float64 phi # 偏航角 (度)
diff --git a/ros2/src/arm_control_msgs/package.xml b/ros2/src/arm_control_msgs/package.xml
new file mode 100644
index 0000000..ea88616
--- /dev/null
+++ b/ros2/src/arm_control_msgs/package.xml
@@ -0,0 +1,22 @@
+
+
+
+ arm_control_msgs
+ 0.0.1
+ Message and service definitions for arm control
+ fallensigh
+ MIT
+
+ ament_cmake
+ rosidl_default_generators
+
+ std_msgs
+
+ rosidl_default_runtime
+
+ rosidl_interface_packages
+
+
+ ament_cmake
+
+
diff --git a/ros2/src/arm_control_msgs/srv/GetPose.srv b/ros2/src/arm_control_msgs/srv/GetPose.srv
new file mode 100644
index 0000000..6cf1bfd
--- /dev/null
+++ b/ros2/src/arm_control_msgs/srv/GetPose.srv
@@ -0,0 +1,14 @@
+# 查询当前位姿服务
+---
+bool success # 是否成功
+string message # 返回信息
+float64 x # 当前 X 坐标 (mm)
+float64 y # 当前 Y 坐标 (mm)
+float64 z # 当前 Z 坐标 (mm)
+float64 phi # 当前偏航角 (度)
+int32 height # 当前高度 (mm)
+int32 j2 # 当前 J2 角度 (度)
+int32 j3 # 当前 J3 角度 (度)
+int32 j4 # 当前 J4 角度 (度)
+int32 j5 # 当前 J5 角度 (度)
+int32 j6 # 当前 J6 角度 (度)
diff --git a/ros2/src/arm_control_msgs/srv/MoveJoints.srv b/ros2/src/arm_control_msgs/srv/MoveJoints.srv
new file mode 100644
index 0000000..5f1909d
--- /dev/null
+++ b/ros2/src/arm_control_msgs/srv/MoveJoints.srv
@@ -0,0 +1,11 @@
+# 关节空间运动服务
+int32 height # 目标高度 (mm)
+int32 j2 # 目标 J2 角度 (度)
+int32 j3 # 目标 J3 角度 (度)
+int32 j4 # 目标 J4 角度 (度)
+int32 j5 # 目标 J5 角度 (度)
+int32 j6 # 目标 J6 角度 (度)
+float64 duration # 运动时长 (秒,0 表示使用默认值)
+---
+bool success # 是否成功
+string message # 返回信息
diff --git a/ros2/src/arm_control_msgs/srv/MovePose.srv b/ros2/src/arm_control_msgs/srv/MovePose.srv
new file mode 100644
index 0000000..4e0ec31
--- /dev/null
+++ b/ros2/src/arm_control_msgs/srv/MovePose.srv
@@ -0,0 +1,18 @@
+# 笛卡尔空间运动服务
+float64 x # 目标 X 坐标 (mm)
+float64 y # 目标 Y 坐标 (mm)
+float64 z # 目标 Z 坐标 (mm)
+float64 phi # 目标偏航角 (度)
+bool elbow_up # 是否使用肘部向上解
+uint8 gripper_state # 夹爪状态: 0=保持, 1=打开, 2=闭合
+bool grip # 是否抓取
+bool release # 是否释放
+float64 duration # 运动时长 (秒,0 表示使用默认值)
+
+# 夹爪状态常量
+uint8 GRIPPER_KEEP = 0
+uint8 GRIPPER_OPEN = 1
+uint8 GRIPPER_CLOSED = 2
+---
+bool success # 是否成功
+string message # 返回信息
diff --git a/ros2/src/arm_control_msgs/srv/SetGripper.srv b/ros2/src/arm_control_msgs/srv/SetGripper.srv
new file mode 100644
index 0000000..918bb07
--- /dev/null
+++ b/ros2/src/arm_control_msgs/srv/SetGripper.srv
@@ -0,0 +1,12 @@
+# 夹爪控制服务
+uint8 gripper_state # 夹爪状态: 0=保持, 1=打开, 2=闭合
+bool grip # 是否抓取
+bool release # 是否释放
+
+# 夹爪状态常量
+uint8 GRIPPER_KEEP = 0
+uint8 GRIPPER_OPEN = 1
+uint8 GRIPPER_CLOSED = 2
+---
+bool success # 是否成功
+string message # 返回信息
diff --git a/ros2/src/udp_teleop/config/arm_control.yaml b/ros2/src/udp_teleop/config/arm_control.yaml
new file mode 100644
index 0000000..fe3de57
--- /dev/null
+++ b/ros2/src/udp_teleop/config/arm_control.yaml
@@ -0,0 +1,38 @@
+# 机械臂控制节点参数配置
+
+arm_control:
+ ros__parameters:
+ # UDP 通信配置
+ udp_ip: '192.168.4.1'
+ udp_port: 8888
+
+ # 机械臂几何参数 (mm)
+ l1: 125.0 # J2-J3 连杆长度
+ l2: 125.0 # J3-J4 连杆长度
+ x4: 110.0 # J4-TCP 水平偏移
+ z4: 80.0 # J4-TCP 垂直偏移
+
+ # 关节限位 (mm 或 度)
+ height_min: -290
+ height_max: 0
+ j2_min: -110
+ j2_max: 115
+ j3_min: -120
+ j3_max: 145
+ j4_min: -90
+ j4_max: 130
+ joint_min: -180 # J5/J6 通用限位
+ joint_max: 180
+
+ # 零点偏移 (度)
+ zero_j2: 3
+ zero_j3: 7
+ zero_j4: 25
+
+ # 运动参数
+ default_duration: 3.0 # 默认运动时长 (秒)
+ default_rate: 100.0 # 插值频率 (Hz)
+ use_state_cache: true # 是否使用状态缓存
+
+ # 状态发布频率
+ publish_rate: 10.0 # Hz
diff --git a/ros2/src/udp_teleop/config/vision_grasp.yaml b/ros2/src/udp_teleop/config/vision_grasp.yaml
new file mode 100644
index 0000000..20ebc39
--- /dev/null
+++ b/ros2/src/udp_teleop/config/vision_grasp.yaml
@@ -0,0 +1,21 @@
+# 视觉抓取节点参数配置
+
+vision_grasp:
+ ros__parameters:
+ # 相机到 TCP 的变换参数
+ cam_tx: 0.0 # X 平移 (mm)
+ cam_ty: 0.0 # Y 平移 (mm)
+ cam_tz: 0.0 # Z 平移 (mm)
+ cam_roll: 0.0 # 绕 X 轴旋转 (度)
+ cam_pitch: 0.0 # 绕 Y 轴旋转 (度)
+ cam_yaw: 0.0 # 绕 Z 轴旋转 (度)
+
+ # 抓取参数
+ approach_height_offset: 50.0 # 接近高度偏移 (mm)
+ retract_position_x: 200.0 # 回收位置 X (mm)
+ retract_position_y: 0.0 # 回收位置 Y (mm)
+
+ # 运动时长
+ grasp_duration: 3.0 # 抓取移动时长 (秒)
+ release_duration: 2.0 # 释放移动时长 (秒)
+ gripper_duration: 1.0 # 夹爪动作时长 (秒)
diff --git a/ros2/src/udp_teleop/package.xml b/ros2/src/udp_teleop/package.xml
index 501df2c..9ede626 100644
--- a/ros2/src/udp_teleop/package.xml
+++ b/ros2/src/udp_teleop/package.xml
@@ -3,9 +3,13 @@
udp_teleop
0.0.0
- TODO: Package description
+ UDP teleoperation and arm control for CRAIC robot
fallensigh
- TODO: License declaration
+ MIT
+
+ rclpy
+ std_msgs
+ arm_control_msgs
ament_copyright
ament_flake8
diff --git a/ros2/src/udp_teleop/setup.py b/ros2/src/udp_teleop/setup.py
index f2a4c26..a3c5138 100644
--- a/ros2/src/udp_teleop/setup.py
+++ b/ros2/src/udp_teleop/setup.py
@@ -29,7 +29,9 @@ setup(
},
entry_points={
'console_scripts': [
- 'keyboard_control = udp_teleop.keyboard_control:main'
+ 'keyboard_control = udp_teleop.keyboard_control:main',
+ 'arm_control = udp_teleop.arm_control:main',
+ 'vision_grasp = udp_teleop.vision_grasp:main'
],
},
)
diff --git a/ros2/src/udp_teleop/udp_teleop/arm_control.py b/ros2/src/udp_teleop/udp_teleop/arm_control.py
new file mode 100644
index 0000000..03cf8dc
--- /dev/null
+++ b/ros2/src/udp_teleop/udp_teleop/arm_control.py
@@ -0,0 +1,743 @@
+#!/usr/bin/env python3
+"""机械臂控制 ROS 节点(独立版本)
+
+完全独立,不依赖 udp_control.py
+包含所有必要的运动学和控制代码
+"""
+
+import json
+import math
+import socket
+import time
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Optional, List
+
+import rclpy
+from rclpy.node import Node
+
+# 导入自定义消息
+from arm_control_msgs.srv import (
+ MoveJoints,
+ MovePose,
+ GetPose,
+ SetGripper
+)
+from arm_control_msgs.msg import (
+ JointState,
+ TCPPose
+)
+
+# ============================================================================
+# 常量定义
+# ============================================================================
+
+DEFAULT_UDP_IP = "192.168.4.1"
+DEFAULT_UDP_PORT = 8888
+
+DEFAULT_HEIGHT_MIN = -290
+DEFAULT_HEIGHT_MAX = 0
+DEFAULT_JOINT_MIN = -180
+DEFAULT_JOINT_MAX = 180
+DEFAULT_J2_MIN = -110
+DEFAULT_J2_MAX = 115
+DEFAULT_J3_MIN = -120
+DEFAULT_J3_MAX = 145
+DEFAULT_J4_MIN = -90
+DEFAULT_J4_MAX = 130
+
+J5_OPEN = 81
+J5_CLOSED = -100
+DEFAULT_FIXED_J5 = J5_OPEN
+
+GRIP_ANGLE = -5
+RELEASE_ANGLE = 80
+DEFAULT_FIXED_J6 = RELEASE_ANGLE
+
+DEFAULT_ZERO_J2 = 3
+DEFAULT_ZERO_J3 = 7
+DEFAULT_ZERO_J4 = 25
+
+DEFAULT_L1 = 125.0
+DEFAULT_L2 = 125.0
+DEFAULT_X4 = 110.0
+DEFAULT_Z4 = 80.0
+
+DEFAULT_INTERP_DURATION = 1.0
+DEFAULT_INTERP_RATE = 20.0
+
+STATE_FILE = Path.home() / ".ros" / "udp_control_state.json"
+
+# ============================================================================
+# 数据类定义
+# ============================================================================
+
+class ArmControlError(ValueError):
+ """机械臂控制错误"""
+ pass
+
+
+@dataclass(frozen=True)
+class ArmGeometry:
+ """机械臂几何参数"""
+ l1: float
+ l2: float
+ x4: float
+ z4: float
+
+
+@dataclass(frozen=True)
+class ArmLimits:
+ """关节限位"""
+ height_min: int = DEFAULT_HEIGHT_MIN
+ height_max: int = DEFAULT_HEIGHT_MAX
+ joint_min: int = DEFAULT_JOINT_MIN
+ joint_max: int = DEFAULT_JOINT_MAX
+ j2_min: int = DEFAULT_J2_MIN
+ j2_max: int = DEFAULT_J2_MAX
+ j3_min: int = DEFAULT_J3_MIN
+ j3_max: int = DEFAULT_J3_MAX
+ j4_min: int = DEFAULT_J4_MIN
+ j4_max: int = DEFAULT_J4_MAX
+
+
+@dataclass(frozen=True)
+class ArmZeroOffsets:
+ """零点偏移"""
+ j2: int = DEFAULT_ZERO_J2
+ j3: int = DEFAULT_ZERO_J3
+ j4: int = DEFAULT_ZERO_J4
+
+
+@dataclass(frozen=True)
+class ArmJointState:
+ """关节状态"""
+ height: int
+ j2: int
+ j3: int
+ j4: int
+ j5: int = DEFAULT_FIXED_J5
+ j6: int = DEFAULT_FIXED_J6
+
+ def to_udp_message(self) -> bytes:
+ """转换为 UDP 消息"""
+ return (
+ f"JXB:{self.height}:{self.j2}:{self.j3}:{self.j4}:"
+ f"{self.j5}:{self.j6}:0:0:EZHY\n"
+ ).encode("utf-8")
+
+
+@dataclass(frozen=True)
+class ArmPose:
+ """TCP 位姿"""
+ x: float
+ y: float
+ z: float
+ phi_deg: float
+
+
+@dataclass(frozen=True)
+class ArmMathState:
+ """数学坐标系的关节状态"""
+ d1: float
+ theta2_deg: float
+ theta3_deg: float
+ theta4_deg: float
+
+
+# ============================================================================
+# 运动学函数
+# ============================================================================
+
+def clamp_int(value: float, lower: int, upper: int, name: str) -> int:
+ """限幅并转换为整数"""
+ rounded = int(round(value))
+ if rounded < lower or rounded > upper:
+ raise ArmControlError(f"{name}={rounded} 超出范围 [{lower}, {upper}]")
+ return rounded
+
+
+def normalize_angle_deg(angle_deg: float) -> float:
+ """角度归一化到 [-180, 180)"""
+ normalized = (angle_deg + 180.0) % 360.0 - 180.0
+ if normalized == -180.0 and angle_deg > 0:
+ return 180.0
+ return normalized
+
+
+def forward_kinematics(geometry: ArmGeometry, state: ArmMathState) -> ArmPose:
+ """正运动学:关节角度 → TCP 位姿"""
+ theta2 = math.radians(state.theta2_deg)
+ theta3 = math.radians(state.theta3_deg)
+ theta4 = math.radians(state.theta4_deg)
+ phi = theta2 + theta3 + theta4
+
+ j4_center_x = (
+ geometry.l1 * math.cos(theta2) +
+ geometry.l2 * math.cos(theta2 + theta3)
+ )
+ j4_center_y = (
+ geometry.l1 * math.sin(theta2) +
+ geometry.l2 * math.sin(theta2 + theta3)
+ )
+
+ x = j4_center_x + geometry.x4 * math.cos(phi)
+ y = j4_center_y + geometry.x4 * math.sin(phi)
+ z = state.d1 - geometry.z4
+
+ return ArmPose(x=x, y=y, z=z, phi_deg=math.degrees(phi))
+
+
+def inverse_kinematics(
+ geometry: ArmGeometry,
+ pose: ArmPose,
+ limits: ArmLimits,
+ elbow_up: bool,
+ j5: int,
+ j6: int,
+) -> ArmMathState:
+ """逆运动学:TCP 位姿 → 关节角度"""
+ # 计算 J4 中心位置
+ phi = math.radians(pose.phi_deg)
+ j4_x = pose.x - geometry.x4 * math.cos(phi)
+ j4_y = pose.y - geometry.x4 * math.sin(phi)
+ j4_z = pose.z + geometry.z4
+
+ d1 = j4_z
+
+ # 计算平面距离
+ r2 = j4_x * j4_x + j4_y * j4_y
+ if r2 < 1e-9:
+ raise ArmControlError("目标点过于接近奇异点")
+
+ # 计算 theta3
+ denom = 2.0 * geometry.l1 * geometry.l2
+ if abs(denom) < 1e-9:
+ raise ArmControlError("几何参数无效")
+
+ c3 = (r2 - geometry.l1 * geometry.l1 - geometry.l2 * geometry.l2) / denom
+ if c3 < -1.0 - 1e-9 or c3 > 1.0 + 1e-9:
+ reach = math.sqrt(r2)
+ raise ArmControlError(f"目标超出工作空间,距离={reach:.3f}mm")
+ c3 = max(-1.0, min(1.0, c3))
+
+ s3_abs = math.sqrt(max(0.0, 1.0 - c3 * c3))
+ s3 = -s3_abs if elbow_up else s3_abs
+
+ theta3 = math.atan2(s3, c3)
+ theta2 = math.atan2(j4_y, j4_x) - math.atan2(
+ geometry.l2 * s3,
+ geometry.l1 + geometry.l2 * c3,
+ )
+ theta4 = phi - theta2 - theta3
+
+ return ArmMathState(
+ d1=d1,
+ theta2_deg=normalize_angle_deg(math.degrees(theta2)),
+ theta3_deg=normalize_angle_deg(math.degrees(theta3)),
+ theta4_deg=normalize_angle_deg(math.degrees(theta4)),
+ )
+
+
+def command_to_math_state(
+ command_state: ArmJointState,
+ zero_offsets: ArmZeroOffsets,
+) -> ArmMathState:
+ """命令状态 → 数学状态"""
+ return ArmMathState(
+ d1=command_state.height,
+ theta2_deg=command_state.j2 - zero_offsets.j2,
+ theta3_deg=command_state.j3 - zero_offsets.j3,
+ theta4_deg=command_state.j4 - zero_offsets.j4,
+ )
+
+
+def math_to_command_state(
+ math_state: ArmMathState,
+ zero_offsets: ArmZeroOffsets,
+ limits: ArmLimits,
+ j5: int,
+ j6: int,
+) -> ArmJointState:
+ """数学状态 → 命令状态"""
+ return ArmJointState(
+ height=clamp_int(
+ round(math_state.d1),
+ limits.height_min,
+ limits.height_max,
+ "height",
+ ),
+ j2=clamp_int(
+ math_state.theta2_deg + zero_offsets.j2,
+ limits.j2_min,
+ limits.j2_max,
+ "J2",
+ ),
+ j3=clamp_int(
+ math_state.theta3_deg + zero_offsets.j3,
+ limits.j3_min,
+ limits.j3_max,
+ "J3",
+ ),
+ j4=clamp_int(
+ math_state.theta4_deg + zero_offsets.j4,
+ limits.j4_min,
+ limits.j4_max,
+ "J4",
+ ),
+ j5=clamp_int(j5, limits.joint_min, limits.joint_max, "J5"),
+ j6=clamp_int(j6, limits.joint_min, limits.joint_max, "J6"),
+ )
+
+
+def interpolate_command_states(
+ start: ArmJointState,
+ end: ArmJointState,
+ steps: int,
+) -> List[ArmJointState]:
+ """关节空间插值"""
+ if steps <= 1:
+ return [end]
+
+ states = []
+ for step_index in range(1, steps + 1):
+ t = step_index / steps
+ states.append(
+ ArmJointState(
+ height=int(round(start.height + (end.height - start.height) * t)),
+ j2=int(round(start.j2 + (end.j2 - start.j2) * t)),
+ j3=int(round(start.j3 + (end.j3 - start.j3) * t)),
+ j4=int(round(start.j4 + (end.j4 - start.j4) * t)),
+ j5=int(round(start.j5 + (end.j5 - start.j5) * t)),
+ j6=int(round(start.j6 + (end.j6 - start.j6) * t)),
+ )
+ )
+ return states
+
+
+# ============================================================================
+# ROS 节点
+# ============================================================================
+
+class ArmControlNode(Node):
+ """机械臂控制 ROS 节点"""
+
+ def __init__(self):
+ super().__init__('arm_control')
+
+ # 声明参数
+ self.declare_parameters(
+ namespace='',
+ parameters=[
+ ('udp_ip', DEFAULT_UDP_IP),
+ ('udp_port', DEFAULT_UDP_PORT),
+ ('l1', DEFAULT_L1),
+ ('l2', DEFAULT_L2),
+ ('x4', DEFAULT_X4),
+ ('z4', DEFAULT_Z4),
+ ('height_min', DEFAULT_HEIGHT_MIN),
+ ('height_max', DEFAULT_HEIGHT_MAX),
+ ('j2_min', DEFAULT_J2_MIN),
+ ('j2_max', DEFAULT_J2_MAX),
+ ('j3_min', DEFAULT_J3_MIN),
+ ('j3_max', DEFAULT_J3_MAX),
+ ('j4_min', DEFAULT_J4_MIN),
+ ('j4_max', DEFAULT_J4_MAX),
+ ('joint_min', DEFAULT_JOINT_MIN),
+ ('joint_max', DEFAULT_JOINT_MAX),
+ ('zero_j2', DEFAULT_ZERO_J2),
+ ('zero_j3', DEFAULT_ZERO_J3),
+ ('zero_j4', DEFAULT_ZERO_J4),
+ ('default_duration', DEFAULT_INTERP_DURATION),
+ ('default_rate', DEFAULT_INTERP_RATE),
+ ('use_state_cache', True),
+ ('publish_rate', 10.0),
+ ]
+ )
+
+ # 获取参数
+ self.udp_ip = self.get_parameter('udp_ip').value
+ self.udp_port = self.get_parameter('udp_port').value
+ self.publish_rate = self.get_parameter('publish_rate').value
+
+ # 机械臂几何参数
+ self.geometry = ArmGeometry(
+ l1=self.get_parameter('l1').value,
+ l2=self.get_parameter('l2').value,
+ x4=self.get_parameter('x4').value,
+ z4=self.get_parameter('z4').value,
+ )
+
+ # 关节限位
+ self.limits = ArmLimits(
+ height_min=self.get_parameter('height_min').value,
+ height_max=self.get_parameter('height_max').value,
+ j2_min=self.get_parameter('j2_min').value,
+ j2_max=self.get_parameter('j2_max').value,
+ j3_min=self.get_parameter('j3_min').value,
+ j3_max=self.get_parameter('j3_max').value,
+ j4_min=self.get_parameter('j4_min').value,
+ j4_max=self.get_parameter('j4_max').value,
+ joint_min=self.get_parameter('joint_min').value,
+ joint_max=self.get_parameter('joint_max').value,
+ )
+
+ # 零点偏移
+ self.zero_offsets = ArmZeroOffsets(
+ j2=self.get_parameter('zero_j2').value,
+ j3=self.get_parameter('zero_j3').value,
+ j4=self.get_parameter('zero_j4').value,
+ )
+
+ # 默认插值参数
+ self.default_duration = self.get_parameter('default_duration').value
+ self.default_rate = self.get_parameter('default_rate').value
+ self.use_state_cache = self.get_parameter('use_state_cache').value
+
+ # 当前状态
+ self.current_state: Optional[ArmJointState] = None
+ self.load_state()
+
+ # UDP socket
+ self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ # 创建服务
+ self.srv_move_joints = self.create_service(
+ MoveJoints,
+ 'arm_control/move_joints',
+ self.handle_move_joints
+ )
+ self.srv_move_pose = self.create_service(
+ MovePose,
+ 'arm_control/move_pose',
+ self.handle_move_pose
+ )
+ self.srv_get_pose = self.create_service(
+ GetPose,
+ 'arm_control/get_pose',
+ self.handle_get_pose
+ )
+ self.srv_set_gripper = self.create_service(
+ SetGripper,
+ 'arm_control/set_gripper',
+ self.handle_set_gripper
+ )
+
+ # 创建发布者
+ self.pub_joint_states = self.create_publisher(
+ JointState,
+ 'arm_control/joint_states',
+ 10
+ )
+ self.pub_tcp_pose = self.create_publisher(
+ TCPPose,
+ 'arm_control/tcp_pose',
+ 10
+ )
+
+ # 创建定时器发布状态
+ self.timer = self.create_timer(
+ 1.0 / self.publish_rate,
+ self.publish_state
+ )
+
+ self.get_logger().info(f'机械臂控制节点已启动')
+ self.get_logger().info(f'UDP 目标: {self.udp_ip}:{self.udp_port}')
+
+ def load_state(self):
+ """从缓存加载状态"""
+ if not self.use_state_cache or not STATE_FILE.exists():
+ self.current_state = ArmJointState(
+ height=0,
+ j2=self.zero_offsets.j2,
+ j3=self.zero_offsets.j3,
+ j4=self.zero_offsets.j4,
+ j5=DEFAULT_FIXED_J5,
+ j6=DEFAULT_FIXED_J6,
+ )
+ return
+
+ try:
+ data = json.loads(STATE_FILE.read_text())
+ self.current_state = ArmJointState(
+ height=data['height'],
+ j2=data['j2'],
+ j3=data['j3'],
+ j4=data['j4'],
+ j5=data.get('j5', DEFAULT_FIXED_J5),
+ j6=data.get('j6', DEFAULT_FIXED_J6),
+ )
+ self.get_logger().info(f'从缓存加载状态')
+ except Exception as e:
+ self.get_logger().warn(f'加载状态失败: {e},使用默认状态')
+ self.current_state = ArmJointState(
+ height=0,
+ j2=self.zero_offsets.j2,
+ j3=self.zero_offsets.j3,
+ j4=self.zero_offsets.j4,
+ j5=DEFAULT_FIXED_J5,
+ j6=DEFAULT_FIXED_J6,
+ )
+
+ def save_state(self):
+ """保存状态到缓存"""
+ if not self.use_state_cache or self.current_state is None:
+ return
+
+ try:
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
+ data = {
+ 'height': self.current_state.height,
+ 'j2': self.current_state.j2,
+ 'j3': self.current_state.j3,
+ 'j4': self.current_state.j4,
+ 'j5': self.current_state.j5,
+ 'j6': self.current_state.j6,
+ }
+ STATE_FILE.write_text(json.dumps(data, indent=2))
+ except Exception as e:
+ self.get_logger().warn(f'保存状态失败: {e}')
+
+ def send_udp_commands(
+ self,
+ states: List[ArmJointState],
+ duration: float
+ ) -> bool:
+ """发送 UDP 命令序列"""
+ if not states:
+ return True
+
+ delay = duration / len(states) if len(states) > 1 and duration > 0.0 else 0.0
+
+ try:
+ for i, state in enumerate(states):
+ msg = state.to_udp_message()
+ self.udp_socket.sendto(msg, (self.udp_ip, self.udp_port))
+
+ if delay > 0.0 and i < len(states) - 1:
+ time.sleep(delay)
+
+ self.current_state = states[-1]
+ self.save_state()
+ return True
+
+ except Exception as e:
+ self.get_logger().error(f'发送 UDP 命令失败: {e}')
+ return False
+
+
+ def handle_move_joints(self, request, response):
+ """处理关节空间运动服务"""
+ try:
+ target_state = ArmJointState(
+ height=clamp_int(request.height, self.limits.height_min, self.limits.height_max, 'height'),
+ j2=clamp_int(request.j2, self.limits.j2_min, self.limits.j2_max, 'j2'),
+ j3=clamp_int(request.j3, self.limits.j3_min, self.limits.j3_max, 'j3'),
+ j4=clamp_int(request.j4, self.limits.j4_min, self.limits.j4_max, 'j4'),
+ j5=clamp_int(request.j5, self.limits.joint_min, self.limits.joint_max, 'j5'),
+ j6=clamp_int(request.j6, self.limits.joint_min, self.limits.joint_max, 'j6'),
+ )
+
+ duration = request.duration if request.duration > 0 else self.default_duration
+ steps = max(1, int(math.ceil(duration * self.default_rate)))
+
+ path = interpolate_command_states(self.current_state, target_state, steps)
+ success = self.send_udp_commands(path, duration)
+
+ response.success = success
+ response.message = "运动完成" if success else "运动失败"
+ self.get_logger().info(f'关节运动 -> {response.message}')
+
+ except Exception as e:
+ response.success = False
+ response.message = f'错误: {str(e)}'
+ self.get_logger().error(f'关节运动失败: {e}')
+
+ return response
+
+ def handle_move_pose(self, request, response):
+ """处理笛卡尔空间运动服务"""
+ try:
+ target_pose = ArmPose(
+ x=request.x,
+ y=request.y,
+ z=request.z,
+ phi_deg=request.phi
+ )
+
+ # 解析夹爪状态
+ if request.gripper_state == SetGripper.Request.GRIPPER_OPEN:
+ j5 = J5_OPEN
+ elif request.gripper_state == SetGripper.Request.GRIPPER_CLOSED:
+ j5 = J5_CLOSED
+ else:
+ j5 = self.current_state.j5
+
+ if request.grip:
+ j6 = GRIP_ANGLE
+ elif request.release:
+ j6 = RELEASE_ANGLE
+ else:
+ j6 = self.current_state.j6
+
+ # 逆运动学
+ math_state = inverse_kinematics(
+ geometry=self.geometry,
+ pose=target_pose,
+ limits=self.limits,
+ elbow_up=request.elbow_up,
+ j5=j5,
+ j6=j6,
+ )
+
+ # 转换为命令状态
+ target_state = math_to_command_state(
+ math_state,
+ self.zero_offsets,
+ self.limits,
+ j5=j5,
+ j6=j6,
+ )
+
+ duration = request.duration if request.duration > 0 else self.default_duration
+ steps = max(1, int(math.ceil(duration * self.default_rate)))
+
+ path = interpolate_command_states(self.current_state, target_state, steps)
+ success = self.send_udp_commands(path, duration)
+
+ response.success = success
+ response.message = "运动完成" if success else "运动失败"
+ self.get_logger().info(
+ f'位姿运动: ({request.x:.1f}, {request.y:.1f}, {request.z:.1f}, {request.phi:.1f}) '
+ f'-> {response.message}'
+ )
+
+ except Exception as e:
+ response.success = False
+ response.message = f'错误: {str(e)}'
+ self.get_logger().error(f'位姿运动失败: {e}')
+
+ return response
+
+ def handle_get_pose(self, request, response):
+ """处理查询位姿服务"""
+ try:
+ if self.current_state is None:
+ response.success = False
+ response.message = "当前状态未初始化"
+ return response
+
+ math_state = command_to_math_state(self.current_state, self.zero_offsets)
+ pose = forward_kinematics(self.geometry, math_state)
+
+ response.success = True
+ response.x = pose.x
+ response.y = pose.y
+ response.z = pose.z
+ response.phi = pose.phi_deg
+ response.height = self.current_state.height
+ response.j2 = self.current_state.j2
+ response.j3 = self.current_state.j3
+ response.j4 = self.current_state.j4
+ response.j5 = self.current_state.j5
+ response.j6 = self.current_state.j6
+
+ except Exception as e:
+ response.success = False
+ response.message = f'错误: {str(e)}'
+ self.get_logger().error(f'查询位姿失败: {e}')
+
+ return response
+
+ def handle_set_gripper(self, request, response):
+ """处理夹爪控制服务"""
+ try:
+ if request.gripper_state == SetGripper.Request.GRIPPER_OPEN:
+ j5 = J5_OPEN
+ elif request.gripper_state == SetGripper.Request.GRIPPER_CLOSED:
+ j5 = J5_CLOSED
+ else:
+ j5 = self.current_state.j5
+
+ if request.grip:
+ j6 = GRIP_ANGLE
+ elif request.release:
+ j6 = RELEASE_ANGLE
+ else:
+ j6 = self.current_state.j6
+
+ target_state = ArmJointState(
+ height=self.current_state.height,
+ j2=self.current_state.j2,
+ j3=self.current_state.j3,
+ j4=self.current_state.j4,
+ j5=j5,
+ j6=j6,
+ )
+
+ success = self.send_udp_commands([target_state], 0.0)
+
+ response.success = success
+ response.message = "夹爪控制完成" if success else "夹爪控制失败"
+ self.get_logger().info(f'夹爪控制: j5={j5}, j6={j6} -> {response.message}')
+
+ except Exception as e:
+ response.success = False
+ response.message = f'错误: {str(e)}'
+ self.get_logger().error(f'夹爪控制失败: {e}')
+
+ return response
+
+ def publish_state(self):
+ """定时发布状态"""
+ if self.current_state is None:
+ return
+
+ try:
+ # 发布关节状态
+ joint_msg = JointState()
+ joint_msg.header.stamp = self.get_clock().now().to_msg()
+ joint_msg.height = self.current_state.height
+ joint_msg.j2 = self.current_state.j2
+ joint_msg.j3 = self.current_state.j3
+ joint_msg.j4 = self.current_state.j4
+ joint_msg.j5 = self.current_state.j5
+ joint_msg.j6 = self.current_state.j6
+ self.pub_joint_states.publish(joint_msg)
+
+ # 计算并发布 TCP 位姿
+ math_state = command_to_math_state(self.current_state, self.zero_offsets)
+ pose = forward_kinematics(self.geometry, math_state)
+
+ pose_msg = TCPPose()
+ pose_msg.header.stamp = self.get_clock().now().to_msg()
+ pose_msg.x = pose.x
+ pose_msg.y = pose.y
+ pose_msg.z = pose.z
+ pose_msg.phi = pose.phi_deg
+ self.pub_tcp_pose.publish(pose_msg)
+
+ except Exception as e:
+ self.get_logger().error(f'发布状态失败: {e}')
+
+ def destroy_node(self):
+ """节点销毁时的清理"""
+ self.udp_socket.close()
+ super().destroy_node()
+
+
+def main(args=None):
+ rclpy.init(args=args)
+ node = ArmControlNode()
+
+ try:
+ rclpy.spin(node)
+ except KeyboardInterrupt:
+ pass
+ finally:
+ node.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ros2/src/udp_teleop/udp_teleop/arm_control_client.py b/ros2/src/udp_teleop/udp_teleop/arm_control_client.py
new file mode 100644
index 0000000..eebb4ce
--- /dev/null
+++ b/ros2/src/udp_teleop/udp_teleop/arm_control_client.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+"""机械臂控制客户端示例
+
+演示如何调用机械臂控制服务
+"""
+
+import rclpy
+from rclpy.node import Node
+from arm_control_msgs.srv import MoveJoints, MovePose, GetPose, SetGripper
+
+
+class ArmControlClient(Node):
+ """机械臂控制客户端"""
+
+ def __init__(self):
+ super().__init__('arm_control_client')
+
+ # 创建服务客户端
+ self.cli_move_joints = self.create_client(MoveJoints, 'arm_control/move_joints')
+ self.cli_move_pose = self.create_client(MovePose, 'arm_control/move_pose')
+ self.cli_get_pose = self.create_client(GetPose, 'arm_control/get_pose')
+ self.cli_set_gripper = self.create_client(SetGripper, 'arm_control/set_gripper')
+
+ # 等待服务可用
+ self.get_logger().info('等待服务...')
+ self.cli_move_joints.wait_for_service()
+ self.cli_move_pose.wait_for_service()
+ self.cli_get_pose.wait_for_service()
+ self.cli_set_gripper.wait_for_service()
+ self.get_logger().info('服务已连接')
+
+ def get_current_pose(self):
+ """查询当前位姿"""
+ req = GetPose.Request()
+ future = self.cli_get_pose.call_async(req)
+ rclpy.spin_until_future_complete(self, future)
+
+ if future.result().success:
+ result = future.result()
+ self.get_logger().info(
+ f'当前位姿: x={result.x:.1f}, y={result.y:.1f}, '
+ f'z={result.z:.1f}, phi={result.phi:.1f}°'
+ )
+ self.get_logger().info(
+ f'关节角度: height={result.height}, j2={result.j2}, '
+ f'j3={result.j3}, j4={result.j4}, j5={result.j5}, j6={result.j6}'
+ )
+ return result
+ else:
+ self.get_logger().error(f'查询失败: {future.result().message}')
+ return None
+
+ def move_to_joints(self, height, j2, j3, j4, j5=81, j6=30, duration=2.0):
+ """关节空间运动"""
+ req = MoveJoints.Request()
+ req.height = height
+ req.j2 = j2
+ req.j3 = j3
+ req.j4 = j4
+ req.j5 = j5
+ req.j6 = j6
+ req.duration = duration
+
+ self.get_logger().info(f'关节运动: height={height}, j2={j2}, j3={j3}, j4={j4}')
+ future = self.cli_move_joints.call_async(req)
+ rclpy.spin_until_future_complete(self, future)
+
+ if future.result().success:
+ self.get_logger().info('运动完成')
+ return True
+ else:
+ self.get_logger().error(f'运动失败: {future.result().message}')
+ return False
+
+ def move_to_pose(self, x, y, z, phi, duration=2.0, grip=False, release=False):
+ """笛卡尔空间运动"""
+ req = MovePose.Request()
+ req.x = x
+ req.y = y
+ req.z = z
+ req.phi = phi
+ req.duration = duration
+ req.grip = grip
+ req.release = release
+ req.gripper_state = MovePose.Request.GRIPPER_KEEP
+ req.elbow_up = False
+
+ self.get_logger().info(f'位姿运动: ({x:.1f}, {y:.1f}, {z:.1f}), phi={phi:.1f}°')
+ future = self.cli_move_pose.call_async(req)
+ rclpy.spin_until_future_complete(self, future)
+
+ if future.result().success:
+ self.get_logger().info('运动完成')
+ return True
+ else:
+ self.get_logger().error(f'运动失败: {future.result().message}')
+ return False
+
+ def set_gripper(self, grip=False, release=False):
+ """夹爪控制"""
+ req = SetGripper.Request()
+ req.grip = grip
+ req.release = release
+ req.gripper_state = SetGripper.Request.GRIPPER_KEEP
+
+ action = "抓取" if grip else ("释放" if release else "保持")
+ self.get_logger().info(f'夹爪控制: {action}')
+ future = self.cli_set_gripper.call_async(req)
+ rclpy.spin_until_future_complete(self, future)
+
+ if future.result().success:
+ self.get_logger().info('夹爪控制完成')
+ return True
+ else:
+ self.get_logger().error(f'夹爪控制失败: {future.result().message}')
+ return False
+
+
+def demo_sequence(client):
+ """演示抓取流程"""
+ print("\n" + "="*60)
+ print("演示序列:查询 → 移动 → 抓取")
+ print("="*60 + "\n")
+
+ # 1. 查询当前位姿
+ print("1. 查询当前位姿...")
+ client.get_current_pose()
+
+ # 2. 移动到上方
+ print("\n2. 移动到物体上方...")
+ client.move_to_pose(x=200.0, y=100.0, z=-50.0, phi=45.0, duration=2.0, release=True)
+
+ # 3. 下降到抓取位置
+ print("\n3. 下降到抓取位置...")
+ client.move_to_pose(x=200.0, y=100.0, z=-150.0, phi=45.0, duration=1.0, release=True)
+
+ # 4. 抓取
+ print("\n4. 执行抓取...")
+ client.set_gripper(grip=True)
+
+ # 5. 提升
+ print("\n5. 提升物体...")
+ client.move_to_pose(x=200.0, y=100.0, z=-50.0, phi=45.0, duration=1.0, grip=True)
+
+ # 6. 移动到目标位置
+ print("\n6. 移动到目标位置...")
+ client.move_to_pose(x=100.0, y=200.0, z=-50.0, phi=90.0, duration=2.0, grip=True)
+
+ # 7. 下降
+ print("\n7. 下降...")
+ client.move_to_pose(x=100.0, y=200.0, z=-150.0, phi=90.0, duration=1.0, grip=True)
+
+ # 8. 释放
+ print("\n8. 释放物体...")
+ client.set_gripper(release=True)
+
+ # 9. 提升
+ print("\n9. 提升...")
+ client.move_to_pose(x=100.0, y=200.0, z=-50.0, phi=90.0, duration=1.0, release=True)
+
+ print("\n" + "="*60)
+ print("演示完成!")
+ print("="*60 + "\n")
+
+
+def main():
+ rclpy.init()
+ client = ArmControlClient()
+
+ try:
+ # 运行演示序列
+ demo_sequence(client)
+
+ except KeyboardInterrupt:
+ pass
+ finally:
+ client.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ros2/src/udp_teleop/udp_teleop/vision_grasp.py b/ros2/src/udp_teleop/udp_teleop/vision_grasp.py
new file mode 100644
index 0000000..b6c72ca
--- /dev/null
+++ b/ros2/src/udp_teleop/udp_teleop/vision_grasp.py
@@ -0,0 +1,428 @@
+#!/usr/bin/env python3
+"""视觉抓取 ROS 节点
+
+基于相机坐标系到基坐标系的变换,实现自动抓取和释放功能。
+
+功能:
+1. 抓取服务:输入相机坐标 (x, y, z),自动转换并执行抓取
+2. 释放服务:输入基坐标系位置,移动并释放物体
+"""
+
+import math
+from typing import Tuple
+
+import numpy as np
+import rclpy
+from rclpy.node import Node
+
+from arm_control_msgs.srv import MovePose, GetPose, SetGripper
+from std_srvs.srv import Trigger
+from geometry_msgs.msg import Point
+
+
+# ============================================================================
+# 坐标变换函数(从 camera_to_base.py 复制)
+# ============================================================================
+
+def euler_to_rotation_matrix(roll_deg: float, pitch_deg: float, yaw_deg: float) -> np.ndarray:
+ """欧拉角转旋转矩阵(ZYX顺序)"""
+ roll = math.radians(roll_deg)
+ pitch = math.radians(pitch_deg)
+ yaw = math.radians(yaw_deg)
+
+ Rx = np.array([
+ [1, 0, 0],
+ [0, math.cos(roll), -math.sin(roll)],
+ [0, math.sin(roll), math.cos(roll)]
+ ])
+
+ Ry = np.array([
+ [math.cos(pitch), 0, math.sin(pitch)],
+ [0, 1, 0],
+ [-math.sin(pitch), 0, math.cos(pitch)]
+ ])
+
+ Rz = np.array([
+ [math.cos(yaw), -math.sin(yaw), 0],
+ [math.sin(yaw), math.cos(yaw), 0],
+ [0, 0, 1]
+ ])
+
+ return Rz @ Ry @ Rx
+
+
+def camera_to_tcp(
+ xc: float, yc: float, zc: float,
+ tx: float = 0.0, ty: float = 0.0, tz: float = 0.0,
+ roll: float = 0.0, pitch: float = 0.0, yaw: float = 0.0
+) -> Tuple[float, float, float]:
+ """相机坐标系 → TCP 坐标系"""
+ R = euler_to_rotation_matrix(roll, pitch, yaw)
+ T = np.array([tx, ty, tz])
+ P_cam = np.array([xc, yc, zc])
+ P_tcp = R @ P_cam + T
+ return float(P_tcp[0]), float(P_tcp[1]), float(P_tcp[2])
+
+
+def tcp_to_base(
+ xt: float, yt: float, zt: float,
+ tcp_x: float, tcp_y: float, tcp_z: float, tcp_phi_deg: float
+) -> Tuple[float, float, float]:
+ """TCP 坐标系 → 机械臂基坐标系(水平相机版本)"""
+ phi = math.radians(tcp_phi_deg)
+
+ R_tcp_to_base = np.array([
+ [-math.sin(phi), 0, math.cos(phi)],
+ [ math.cos(phi), 0, math.sin(phi)],
+ [0, -1, 0]
+ ])
+
+ P_tcp = np.array([xt, yt, zt])
+ P_base_relative = R_tcp_to_base @ P_tcp
+ P_base = P_base_relative + np.array([tcp_x, tcp_y, tcp_z])
+
+ return float(P_base[0]), float(P_base[1]), float(P_base[2])
+
+
+def camera_to_base(
+ xc: float, yc: float, zc: float,
+ tcp_x: float, tcp_y: float, tcp_z: float, tcp_phi_deg: float,
+ cam_tx: float = 0.0, cam_ty: float = 0.0, cam_tz: float = 0.0,
+ cam_roll: float = 0.0, cam_pitch: float = 0.0, cam_yaw: float = 0.0
+) -> Tuple[float, float, float]:
+ """完整变换:相机坐标系 → 基坐标系"""
+ xt, yt, zt = camera_to_tcp(xc, yc, zc, cam_tx, cam_ty, cam_tz,
+ cam_roll, cam_pitch, cam_yaw)
+ xb, yb, zb = tcp_to_base(xt, yt, zt, tcp_x, tcp_y, tcp_z, tcp_phi_deg)
+ return xb, yb, zb
+
+
+# ============================================================================
+# 自定义服务定义
+# ============================================================================
+
+# 由于没有预定义服务,我们使用简化的接口
+# 实际使用时可以创建自定义 .srv 文件
+
+
+# ============================================================================
+# 视觉抓取节点
+# ============================================================================
+
+class VisionGraspNode(Node):
+ """视觉抓取 ROS 节点"""
+
+ def __init__(self):
+ super().__init__('vision_grasp')
+
+ # 声明参数
+ self.declare_parameters(
+ namespace='',
+ parameters=[
+ # 相机到 TCP 的变换参数
+ ('cam_tx', 0.0),
+ ('cam_ty', 0.0),
+ ('cam_tz', 0.0),
+ ('cam_roll', 0.0),
+ ('cam_pitch', 0.0),
+ ('cam_yaw', 0.0),
+ # 抓取参数
+ ('approach_height_offset', 50.0), # 接近高度偏移 (mm)
+ ('retract_position_x', 200.0), # 回收位置 X
+ ('retract_position_y', 0.0), # 回收位置 Y
+ ('grasp_duration', 3.0), # 抓取移动时长 (秒)
+ ('release_duration', 2.0), # 释放移动时长 (秒)
+ ('gripper_duration', 1.0), # 夹爪动作时长 (秒)
+ ]
+ )
+
+ # 获取参数
+ self.cam_tx = self.get_parameter('cam_tx').value
+ self.cam_ty = self.get_parameter('cam_ty').value
+ self.cam_tz = self.get_parameter('cam_tz').value
+ self.cam_roll = self.get_parameter('cam_roll').value
+ self.cam_pitch = self.get_parameter('cam_pitch').value
+ self.cam_yaw = self.get_parameter('cam_yaw').value
+
+ self.approach_offset = self.get_parameter('approach_height_offset').value
+ self.retract_x = self.get_parameter('retract_position_x').value
+ self.retract_y = self.get_parameter('retract_position_y').value
+ self.grasp_duration = self.get_parameter('grasp_duration').value
+ self.release_duration = self.get_parameter('release_duration').value
+ self.gripper_duration = self.get_parameter('gripper_duration').value
+
+ # 创建服务客户端(连接到 arm_control 节点)
+ self.move_cli = self.create_client(MovePose, 'arm_control/move_pose')
+ self.get_pose_cli = self.create_client(GetPose, 'arm_control/get_pose')
+ self.gripper_cli = self.create_client(SetGripper, 'arm_control/set_gripper')
+
+ # 等待服务可用
+ self.get_logger().info('等待 arm_control 服务...')
+ self.move_cli.wait_for_service(timeout_sec=5.0)
+ self.get_pose_cli.wait_for_service(timeout_sec=5.0)
+ self.gripper_cli.wait_for_service(timeout_sec=5.0)
+ self.get_logger().info('arm_control 服务已连接')
+
+ # 创建订阅者:接收检测结果
+ self.grasp_sub = self.create_subscription(
+ Point,
+ 'vision_grasp/grasp_target',
+ self.handle_grasp_target,
+ 10
+ )
+
+ self.release_sub = self.create_subscription(
+ Point,
+ 'vision_grasp/release_target',
+ self.handle_release_target,
+ 10
+ )
+
+ self.get_logger().info('视觉抓取节点已启动')
+ self.get_logger().info('订阅话题:')
+ self.get_logger().info(' - /vision_grasp/grasp_target (geometry_msgs/Point)')
+ self.get_logger().info(' - /vision_grasp/release_target (geometry_msgs/Point)')
+
+ def get_current_tcp_pose(self) -> Tuple[float, float, float, float]:
+ """查询当前 TCP 位姿"""
+ req = GetPose.Request()
+ future = self.get_pose_cli.call_async(req)
+
+ # 等待结果(不阻塞其他回调)
+ import time
+ start_time = time.time()
+ timeout = 5.0
+
+ while not future.done():
+ if time.time() - start_time > timeout:
+ raise RuntimeError("获取 TCP 位姿超时")
+ time.sleep(0.01)
+
+ result = future.result()
+ if not result.success:
+ raise RuntimeError(f"获取 TCP 位姿失败: {result.message}")
+
+ return result.x, result.y, result.z, result.phi
+
+ def move_to(self, x: float, y: float, z: float, phi: float,
+ duration: float, grip: bool = False, release: bool = False) -> bool:
+ """移动到指定位置"""
+ req = MovePose.Request()
+ req.x = x
+ req.y = y
+ req.z = z
+ req.phi = phi
+ req.duration = duration
+ req.grip = grip
+ req.release = release
+ req.gripper_state = MovePose.Request.GRIPPER_KEEP
+ req.elbow_up = False
+
+ future = self.move_cli.call_async(req)
+
+ # 等待结果
+ import time
+ start_time = time.time()
+ timeout = duration + 5.0
+
+ while not future.done():
+ if time.time() - start_time > timeout:
+ self.get_logger().error("移动超时")
+ return False
+ time.sleep(0.01)
+
+ result = future.result()
+ if result is None:
+ self.get_logger().error("移动失败:无响应")
+ return False
+
+ return result.success
+
+ def set_gripper(self, grip: bool = False, release: bool = False) -> bool:
+ """控制夹爪"""
+ req = SetGripper.Request()
+ req.grip = grip
+ req.release = release
+ req.gripper_state = SetGripper.Request.GRIPPER_KEEP
+
+ future = self.gripper_cli.call_async(req)
+
+ # 等待结果
+ import time
+ start_time = time.time()
+ timeout = 3.0
+
+ while not future.done():
+ if time.time() - start_time > timeout:
+ self.get_logger().error("夹爪控制超时")
+ return False
+ time.sleep(0.01)
+
+ result = future.result()
+ if result is None:
+ self.get_logger().error("夹爪控制失败:无响应")
+ return False
+
+ return result.success
+
+ def handle_grasp_target(self, msg: Point):
+ """处理抓取目标"""
+ self.get_logger().info(f'收到抓取目标: 相机坐标 ({msg.x:.1f}, {msg.y:.1f}, {msg.z:.1f})')
+
+ # 在单独的线程中执行抓取,避免阻塞回调
+ import threading
+ thread = threading.Thread(
+ target=self._execute_grasp_thread,
+ args=(msg.x, msg.y, msg.z)
+ )
+ thread.start()
+
+ def _execute_grasp_thread(self, x: float, y: float, z: float):
+ """在独立线程中执行抓取流程"""
+ try:
+ # 转换坐标:(x, y, z) -> (xc, yc, zc) = (x, -y, z)
+ xc = x
+ yc = -y
+ zc = z
+
+ self.get_logger().info(f'转换后相机坐标: ({xc:.1f}, {yc:.1f}, {zc:.1f})')
+
+ # 获取当前 TCP 位姿
+ tcp_x, tcp_y, tcp_z, tcp_phi = self.get_current_tcp_pose()
+ self.get_logger().info(f'当前 TCP: ({tcp_x:.1f}, {tcp_y:.1f}, {tcp_z:.1f}), phi={tcp_phi:.1f}°')
+
+ # 坐标变换
+ target_x, target_y, target_z = camera_to_base(
+ xc, yc, zc,
+ tcp_x, tcp_y, tcp_z, tcp_phi,
+ self.cam_tx, self.cam_ty, self.cam_tz,
+ self.cam_roll, self.cam_pitch, self.cam_yaw
+ )
+
+ self.get_logger().info(f'目标基坐标: ({target_x:.1f}, {target_y:.1f}, {target_z:.1f})')
+
+ # 执行抓取流程
+ self.execute_grasp(target_x, target_y, target_z, tcp_phi)
+
+ except Exception as e:
+ self.get_logger().error(f'抓取失败: {e}')
+
+ def handle_release_target(self, msg: Point):
+ """处理释放目标"""
+ self.get_logger().info(f'收到释放目标: 基坐标 ({msg.x:.1f}, {msg.y:.1f}, {msg.z:.1f})')
+
+ # 在单独的线程中执行释放,避免阻塞回调
+ import threading
+ thread = threading.Thread(
+ target=self._execute_release_thread,
+ args=(msg.x, msg.y, msg.z)
+ )
+ thread.start()
+
+ def _execute_release_thread(self, x: float, y: float, z: float):
+ """在独立线程中执行释放流程"""
+ try:
+ # 获取当前 TCP 位姿(用于获取 phi)
+ _, _, _, tcp_phi = self.get_current_tcp_pose()
+
+ # 执行释放流程
+ self.execute_release(x, y, z, tcp_phi)
+
+ except Exception as e:
+ self.get_logger().error(f'释放失败: {e}')
+
+ def execute_grasp(self, x: float, y: float, z: float, phi: float):
+ """执行抓取流程
+
+ 1. release 夹爪 (duration=0)
+ 2. 移动到目标位置 (duration=3)
+ 3. grip 夹爪 (duration=1)
+ 4. 移动到回收位置 (200, 0, 当前z)
+ """
+ self.get_logger().info('=' * 60)
+ self.get_logger().info('开始抓取流程')
+ self.get_logger().info('=' * 60)
+
+ # 步骤 1: 释放夹爪
+ self.get_logger().info('1. 释放夹爪')
+ if not self.set_gripper(release=True):
+ self.get_logger().error('释放夹爪失败')
+ return
+
+ # 步骤 2: 移动到目标位置
+ self.get_logger().info(f'2. 移动到目标位置: ({x:.1f}, {y:.1f}, {z:.1f})')
+ if not self.move_to(x, y, z, phi, self.grasp_duration, release=True):
+ self.get_logger().error('移动到目标位置失败')
+ return
+
+ # 步骤 3: 抓取
+ self.get_logger().info('3. 抓取物体')
+ if not self.move_to(x, y, z, phi, self.gripper_duration, grip=True):
+ self.get_logger().error('抓取失败')
+ return
+
+ # 步骤 4: 移动到回收位置
+ self.get_logger().info(f'4. 移动到回收位置: ({self.retract_x:.1f}, {self.retract_y:.1f}, {z:.1f})')
+ if not self.move_to(self.retract_x, self.retract_y, z, phi, self.grasp_duration, grip=True):
+ self.get_logger().error('移动到回收位置失败')
+ return
+
+ self.get_logger().info('=' * 60)
+ self.get_logger().info('✓ 抓取完成!')
+ self.get_logger().info('=' * 60)
+
+ def execute_release(self, x: float, y: float, z: float, phi: float):
+ """执行释放流程
+
+ 1. 移动到指定位置
+ 2. release 夹爪 (duration=0)
+ 3. 回收到 (200, 0, 当前z)
+ """
+ self.get_logger().info('=' * 60)
+ self.get_logger().info('开始释放流程')
+ self.get_logger().info('=' * 60)
+
+ # 步骤 1: 移动到释放位置
+ self.get_logger().info(f'1. 移动到释放位置: ({x:.1f}, {y:.1f}, {z:.1f})')
+ if not self.move_to(x, y, z, phi, self.release_duration, grip=True):
+ self.get_logger().error('移动到释放位置失败')
+ return
+
+ # 步骤 2: 释放夹爪
+ self.get_logger().info('2. 释放物体')
+ if not self.set_gripper(release=True):
+ self.get_logger().error('释放夹爪失败')
+ return
+
+ # 步骤 3: 回收到初始位置
+ self.get_logger().info(f'3. 回收到初始位置: ({self.retract_x:.1f}, {self.retract_y:.1f}, {z:.1f})')
+ if not self.move_to(self.retract_x, self.retract_y, z, phi, self.release_duration, release=True):
+ self.get_logger().error('回收失败')
+ return
+
+ self.get_logger().info('=' * 60)
+ self.get_logger().info('✓ 释放完成!')
+ self.get_logger().info('=' * 60)
+
+
+def main(args=None):
+ rclpy.init(args=args)
+
+ # 使用多线程执行器以支持在回调中调用服务
+ from rclpy.executors import MultiThreadedExecutor
+
+ node = VisionGraspNode()
+ executor = MultiThreadedExecutor()
+ executor.add_node(node)
+
+ try:
+ executor.spin()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ node.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == '__main__':
+ main()