Compare commits
1 Commits
main
...
ad3e823908
| Author | SHA1 | Date | |
|---|---|---|---|
| ad3e823908 |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -44,16 +44,3 @@ Thumbs.db
|
|||||||
# ====================
|
# ====================
|
||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
# ====================
|
|
||||||
# Project Specific
|
|
||||||
# ====================
|
|
||||||
dataset/
|
|
||||||
dataset.zip
|
|
||||||
ros2.zip
|
|
||||||
|
|
||||||
# 运行时状态文件
|
|
||||||
tools/.udp_control_state.json
|
|
||||||
.ros/udp_control_state.json
|
|
||||||
|
|
||||||
.omo/
|
|
||||||
242
CLAUDE.md
242
CLAUDE.md
@@ -1,242 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
CRAIC (Camera-Robot AI Control System) — Competition code for the China Robot and Artificial Intelligence Competition (中国机器人及人工智能大赛), Robot Task Challenge (Small Desktop Level).
|
|
||||||
|
|
||||||
**Hardware**: ESP32-S3-WROOM-1-N16R8 with OV2640 camera, 6-DOF mechanical arm with Feettech SCS/STS serial servos.
|
|
||||||
|
|
||||||
**Architecture**: Three-tier system communicating via UDP port 8888:
|
|
||||||
1. **ESP32-S3 firmware** (`jxbeye/`) — Dual-core camera streaming + UDP command receiver
|
|
||||||
2. **ROS 2 teleop** (`ros2/src/udp_teleop/`) — Keyboard control node for chassis and arm
|
|
||||||
3. **Python tools** (`tools/`) — Standalone control scripts with inverse kinematics
|
|
||||||
|
|
||||||
## Build & Run Commands
|
|
||||||
|
|
||||||
### ESP32-S3 Firmware
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd jxbeye
|
|
||||||
pio run -t upload # Build and flash firmware
|
|
||||||
pio device monitor # Serial monitor (1000000 baud)
|
|
||||||
```
|
|
||||||
|
|
||||||
**First boot**: ESP32 creates AP `ESP32-S3-Camera` (password `12345678`), access web UI at `http://192.168.4.1`.
|
|
||||||
|
|
||||||
**WiFi configuration via serial**: Send `WIFI:SSID:PASSWORD` to configure station mode.
|
|
||||||
|
|
||||||
### ROS 2 Teleop
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build (from ros2/ directory)
|
|
||||||
conda activate ros2_humble
|
|
||||||
colcon build --symlink-install --packages-select udp_teleop
|
|
||||||
source install/setup.bash
|
|
||||||
|
|
||||||
# Run keyboard control
|
|
||||||
ros2 run udp_teleop keyboard_control \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/params.yaml
|
|
||||||
|
|
||||||
# Override target IP
|
|
||||||
ros2 run udp_teleop keyboard_control \
|
|
||||||
--ros-args -p udp_ip:=192.168.4.1 -p udp_port:=8888
|
|
||||||
```
|
|
||||||
|
|
||||||
**Keyboard mappings**:
|
|
||||||
- Chassis: W/S (forward/back), A/D (strafe), Q/E (rotate)
|
|
||||||
- Arm: ↑/↓ (height), 2-6 (select joint), ←/→ (adjust angle)
|
|
||||||
|
|
||||||
**Important**: Must use `ros2 run`, not `ros2 launch` — the `stdin` keyboard backend requires an interactive terminal.
|
|
||||||
|
|
||||||
### Python Arm Control Tools
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Direct joint command with interpolation
|
|
||||||
python tools/udp_control.py joints \
|
|
||||||
--height -100 --j2 10 --j3 20 --j4 30 \
|
|
||||||
--duration 1.0 --rate 20
|
|
||||||
|
|
||||||
# Cartesian pose mode (uses inverse kinematics)
|
|
||||||
python tools/udp_control.py pose \
|
|
||||||
--x 150 --y 50 --z -100 --phi 45 \
|
|
||||||
--duration 1.0
|
|
||||||
|
|
||||||
# Dry run (print commands without sending)
|
|
||||||
python tools/udp_control.py pose --x 200 --y 0 --z -50 --phi 0 --dry-run
|
|
||||||
|
|
||||||
# Camera frame capture
|
|
||||||
python tools/camera_capture.py --ip 192.168.4.1
|
|
||||||
python tools/camera_capture.py --scan # Auto-detect camera on subnet
|
|
||||||
```
|
|
||||||
|
|
||||||
### UDP Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start echo server
|
|
||||||
python tools/udp_server.py
|
|
||||||
|
|
||||||
# Send test commands
|
|
||||||
echo 'XYW:100:0:0:XZHY' | nc -u 192.168.4.1 8888
|
|
||||||
echo 'JXB:-100:10:20:30:0:0:0:0:EZHY' | nc -u 192.168.4.1 8888
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Details
|
|
||||||
|
|
||||||
### ESP32-S3 Dual-Core Design
|
|
||||||
|
|
||||||
- **Core 0**: Camera capture loop (OV2640 → JPEG)
|
|
||||||
- **Core 1**: WiFi streaming (MJPEG HTTP server on port 80)
|
|
||||||
- **AsyncUDP**: Non-blocking UDP command receiver runs on Core 1, handled via interrupt callbacks
|
|
||||||
|
|
||||||
The dual-core split ensures camera capture never blocks on WiFi transmission. UDP commands are processed asynchronously and do not interfere with streaming.
|
|
||||||
|
|
||||||
### UDP Protocol
|
|
||||||
|
|
||||||
All commands are ASCII text ending with terminator (varies by command type):
|
|
||||||
|
|
||||||
```
|
|
||||||
# Chassis control (XYZ cartesian velocity)
|
|
||||||
XYW:<X_speed>:<Y_speed>:<W_angular>:XZHY\n
|
|
||||||
|
|
||||||
# Arm control (6 motors: height, J2-J6)
|
|
||||||
JXB:<height>:<J2>:<J3>:<J4>:<J5>:<J6>:0:0:EZHY\n
|
|
||||||
|
|
||||||
# Laser control
|
|
||||||
LASERON\n
|
|
||||||
LASEROFF\n
|
|
||||||
|
|
||||||
# Serial passthrough (any payload with ZHY or \n terminator)
|
|
||||||
<payload>ZHY\n
|
|
||||||
```
|
|
||||||
|
|
||||||
**Critical**: The ESP32 firmware parses based on terminator suffix, not command prefix.
|
|
||||||
|
|
||||||
### Mechanical Arm Coordinate System & Kinematics
|
|
||||||
|
|
||||||
**Coordinate frame**: Base frame with Z-axis pointing UP (not down). Origin at the bottom of the J1 linear slide.
|
|
||||||
|
|
||||||
**Height coordinate (d1)**:
|
|
||||||
- User-facing coordinate: `-290 mm` (bottom) to `0 mm` (top)
|
|
||||||
- Physical meaning: vertical position of J2 relative to base origin
|
|
||||||
- **Z-up convention**: positive d1 = higher position
|
|
||||||
|
|
||||||
**Planar joints (J2, J3, J4)**:
|
|
||||||
- All three rotate around vertical Z-axis in the XY plane
|
|
||||||
- J2 is base rotation, J3/J4 are elbow/wrist
|
|
||||||
- TCP yaw angle `phi = J2 + J3 + J4` (additive)
|
|
||||||
|
|
||||||
**Geometry parameters** (see `docs/arm.md` for full derivation):
|
|
||||||
- `L1 = 125 mm`: J2-J3 link length
|
|
||||||
- `L2 = 125 mm`: J3-J4 link length
|
|
||||||
- `x4 = 110 mm`: J4-to-TCP horizontal offset
|
|
||||||
- `z4 = 80 mm`: J4-to-TCP vertical offset (variable: 55mm when gripper down, -100mm when up)
|
|
||||||
|
|
||||||
**Zero offsets**: Physical mechanical zero does not align with math zero (straight line):
|
|
||||||
- `J2_zero = 3°`, `J3_zero = 7°`, `J4_zero = 25°`
|
|
||||||
- UDP command angles = math angles + zero offsets
|
|
||||||
|
|
||||||
**Inverse kinematics** (`tools/udp_control.py`):
|
|
||||||
- Solves for joint angles given TCP pose `(x, y, z, phi)`
|
|
||||||
- Uses standard 2-link planar arm solution (atan2-based)
|
|
||||||
- Two solutions: `--elbow-up` vs default elbow-down
|
|
||||||
- Validates workspace limits and singularity checks
|
|
||||||
- See `docs/arm.md` for full mathematical derivation
|
|
||||||
|
|
||||||
**State persistence**: `tools/.udp_control_state.json` caches last sent joint command. This enables smooth interpolated motion from previous position without re-homing. Use `--no-state-cache` to disable.
|
|
||||||
|
|
||||||
### ROS 2 Keyboard Backend Selection
|
|
||||||
|
|
||||||
The `udp_teleop` node supports three keyboard input backends:
|
|
||||||
|
|
||||||
- `stdin` (default on Linux/macOS): Terminal raw mode, zero dependencies, **requires interactive terminal**
|
|
||||||
- `pynput`: Cross-platform library, works in background
|
|
||||||
- `win_poll`: Windows-specific Win32 API polling
|
|
||||||
|
|
||||||
Backend auto-selected by platform. Override with `keyboard_backend` parameter in `config/params.yaml`.
|
|
||||||
|
|
||||||
**Limitation**: `stdin` backend fails when launched via `ros2 launch` because child processes lack TTY. Always use `ros2 run` for interactive keyboard control.
|
|
||||||
|
|
||||||
### Camera Capture Tool Auto-Detection
|
|
||||||
|
|
||||||
`camera_capture.py` implements subnet scanning and ESP32 identification:
|
|
||||||
|
|
||||||
1. Probes common DHCP IPs on local subnet (`x.x.x.1`, `x.x.x.100-110`, etc.)
|
|
||||||
2. Verifies ESP32 by checking `/status` endpoint for JSON keys `capture_fps` and `has_client`
|
|
||||||
3. Falls back to full subnet scan if not found in common range
|
|
||||||
4. Connects to `/stream` MJPEG endpoint, parses multipart frames, extracts first valid JPEG (SOI `0xFFD8` to EOI `0xFFD9`)
|
|
||||||
|
|
||||||
Use `--scan` to force full subnet scan, or `--ip` to skip detection.
|
|
||||||
|
|
||||||
## Environment Setup
|
|
||||||
|
|
||||||
### ROS 2 Humble via Conda (robostack)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create environment (one-time)
|
|
||||||
conda create -n ros2_humble -c robostack-staging -c conda-forge ros-humble-desktop
|
|
||||||
conda activate ros2_humble
|
|
||||||
conda install -c robostack-staging -c conda-forge colcon-common-extensions
|
|
||||||
pip install pynput
|
|
||||||
|
|
||||||
# Every session
|
|
||||||
conda activate ros2_humble
|
|
||||||
cd ros2
|
|
||||||
source install/setup.bash # After first colcon build
|
|
||||||
```
|
|
||||||
|
|
||||||
**Alternative**: Native apt installation on Ubuntu 22.04 — see `ros2/README.md`.
|
|
||||||
|
|
||||||
### PlatformIO ESP32
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install platformio
|
|
||||||
cd jxbeye
|
|
||||||
pio pkg install # Install dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
**Board configuration** (`platformio.ini`):
|
|
||||||
- Custom board definition: `esp32-s3-wroom-1-n16r8`
|
|
||||||
- PSRAM: Octal mode (`board_build.psram_type = octal`)
|
|
||||||
- Flash: 16MB QIO mode at 80MHz
|
|
||||||
- Partition table: `default_16MB.csv`
|
|
||||||
|
|
||||||
## Important Files
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
- `jxbeye/platformio.ini` — ESP32 build config (PSRAM settings critical)
|
|
||||||
- `ros2/src/udp_teleop/config/params.yaml` — ROS node parameters (IP, speeds, steps)
|
|
||||||
- `tools/.udp_control_state.json` — Cached arm joint state for interpolation
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- `docs/arm.md` — Full inverse kinematics derivation with LaTeX equations
|
|
||||||
- `README.md` — Project overview and quick start
|
|
||||||
- `ros2/src/udp_teleop/README.md` — ROS package details and keyboard mappings
|
|
||||||
|
|
||||||
### Core Implementations
|
|
||||||
|
|
||||||
- `jxbeye/src/main.cpp` — ESP32 dual-core firmware entry point
|
|
||||||
- `ros2/src/udp_teleop/udp_teleop/keyboard_control.py` — ROS keyboard node
|
|
||||||
- `tools/udp_control.py` — Standalone arm controller with full IK solver
|
|
||||||
- `tools/camera_capture.py` — MJPEG stream frame extractor
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
**Change arm geometry parameters**: Edit constants in `tools/udp_control.py` (`DEFAULT_L1`, `DEFAULT_L2`, `DEFAULT_X4`, `DEFAULT_Z4`) or pass as CLI args.
|
|
||||||
|
|
||||||
**Modify joint limits**: Edit `DEFAULT_*_MIN/MAX` in `tools/udp_control.py` or use `--height-min`, `--j2-max`, etc.
|
|
||||||
|
|
||||||
**Adjust interpolation smoothness**: Change `--duration` (total time) and `--rate` (Hz) in `udp_control.py`. Default is 1.0s at 20Hz = 20 steps.
|
|
||||||
|
|
||||||
**Debug UDP protocol**: Use `tools/udp_server.py` as echo server, point ROS/tools at `127.0.0.1:8888` to inspect raw commands.
|
|
||||||
|
|
||||||
**Test kinematics without hardware**: Use `--dry-run` flag with `udp_control.py` to print UDP commands without sending.
|
|
||||||
|
|
||||||
**Verify IK correctness**: Use `--show-fk` flag to compute forward kinematics of the solved joint angles and compare to target pose.
|
|
||||||
|
|
||||||
## Dataset
|
|
||||||
|
|
||||||
The `dataset.zip` and `dataset/` directory contain competition-specific training data (exact format unknown from structure alone).
|
|
||||||
319
README.md
319
README.md
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
> 本仓库为 **[中国机器人及人工智能大赛](https://www.caairobot.com)**(CRAIC)**机器人任务挑战赛(小型桌面级)** 参赛代码。
|
> 本仓库为 **[中国机器人及人工智能大赛](https://www.caairobot.com)**(CRAIC)**机器人任务挑战赛(小型桌面级)** 参赛代码。
|
||||||
|
|
||||||
ESP32-S3 双核摄像头 + ROS 2 机械臂控制 + 视觉抓取 + 激光 SLAM 定位导航的一体化机器人系统。
|
ESP32-S3 双核摄像头 + ROS 2 键盘遥控 + UDP 实时通讯的一体化机器人控制系统。
|
||||||
|
|
||||||
## 赛事信息
|
## 赛事
|
||||||
|
|
||||||
**中国机器人及人工智能大赛**(**C**hina **R**obot and **A**rtificial **I**ntelligence **C**ompetition,简称 **CRAIC**)是由 [中国人工智能学会](https://www.caai.cn)(CAAI)主办的全国性学科竞赛,始于 1999 年,已列入**全国普通高等学校学科竞赛排行榜**及**教育部 A 类竞赛名单**。
|
**中国机器人及人工智能大赛**(**C**hina **R**obot and **A**rtificial **I**ntelligence **C**ompetition,简称 **CRAIC**)是由 [中国人工智能学会](https://www.caai.cn)(CAAI)主办的全国性学科竞赛,始于 1999 年,已列入**全国普通高等学校学科竞赛排行榜**及**教育部 A 类竞赛名单**。
|
||||||
|
|
||||||
@@ -19,111 +19,72 @@ ESP32-S3 双核摄像头 + ROS 2 机械臂控制 + 视觉抓取 + 激光 SLAM
|
|||||||
| 参赛赛项 | 机器人任务挑战赛(小型桌面级) |
|
| 参赛赛项 | 机器人任务挑战赛(小型桌面级) |
|
||||||
| 官方网站 | [www.caairobot.com](https://www.caairobot.com) |
|
| 官方网站 | [www.caairobot.com](https://www.caairobot.com) |
|
||||||
|
|
||||||
## 硬件
|
|
||||||
|
|
||||||
| 组件 | 型号 |
|
|
||||||
|------|------|
|
|
||||||
| 主控 | ESP32-S3-WROOM-1-N16R8 (16MB Flash, 8MB PSRAM) |
|
|
||||||
| 摄像头 | OV2640 (XGA 1024×768, JPEG) |
|
|
||||||
| 机械臂 | 6-DOF,飞特 SCS/STS 串行舵机 |
|
|
||||||
| 激光雷达 | YDLiDAR TminiPro (360°, 10Hz) |
|
|
||||||
| 底盘 | 麦克纳姆轮底盘,LZUCAR 底盘 MCU(串口里程计) |
|
|
||||||
| 激光 | 激光模块(LASERON / LASEROFF) |
|
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
craic/
|
craic/
|
||||||
├── jxbeye/ # ESP32-S3 固件 (PlatformIO)
|
├── jxbeye/ # ESP32-S3 摄像头固件 (PlatformIO)
|
||||||
│ ├── src/main.cpp # 双核:Core 0 摄像头采集,Core 1 WiFi推流 + UDP控制
|
│ ├── src/main.cpp # 双核并行推流 + AsyncUDP + ESP-NOW
|
||||||
│ ├── platformio.ini # 开发板与 PSRAM 配置
|
│ ├── platformio.ini # 构建配置 (ESP32-S3-WROOM-1-N16R8)
|
||||||
│ └── lib/FTServo/ # 飞特串行舵机库
|
│ ├── boards/ # 自定义板级定义
|
||||||
├── ros2/ # ROS 2 控制系统
|
│ ├── lib/FTServo/ # 飞特舵机控制库
|
||||||
│ ├── build.sh # 一键编译脚本
|
│ └── include/ / test/
|
||||||
│ ├── src/arm_control_msgs/ # 机械臂消息与服务定义 (ament_cmake)
|
├── ros2/ # ROS 2 Humble 键盘遥控
|
||||||
│ │ ├── msg/ # JointState, TCPPose
|
│ └── src/udp_teleop/ # 底盘 + 6轴机械臂 UDP 遥控节点
|
||||||
│ │ └── srv/ # MoveJoints, MovePose, GetPose, SetGripper
|
├── tools/ # 调试工具
|
||||||
│ ├── src/udp_teleop/ # UDP 遥控与机械臂控制 (ament_python)
|
│ └── udp_server.py # UDP 回显测试服务器
|
||||||
│ │ ├── udp_teleop/ # arm_control, vision_grasp, keyboard_control, box_detection_grasp
|
└── .gitignore
|
||||||
│ │ ├── launch/ # vision_grasp.launch.py(统一启动)
|
|
||||||
│ │ ├── config/ # arm / vision / keyboard / box_detection 参数
|
|
||||||
│ │ └── models/ # YOLO 模型权重 (box_detection.pt)
|
|
||||||
│ ├── src/craic_localization/ # 定位与导航 (ament_python)
|
|
||||||
│ │ ├── launch/ # mapping / localization / lidar / bringup
|
|
||||||
│ │ ├── config/ # amcl / gmapping / lidar 参数
|
|
||||||
│ │ └── rviz/ # 定位专用 RViz 配置
|
|
||||||
│ ├── src/rf2o_laser_odometry/ # 激光里程计 (ament_cmake, C++14)
|
|
||||||
│ └── src/ydlidar_ros2_driver/ # YDLiDAR 驱动 (ament_cmake)
|
|
||||||
├── tools/ # 独立命令行工具(不依赖 ROS)
|
|
||||||
│ ├── udp_control.py # 机械臂控制(逆运动学 + 插值)
|
|
||||||
│ ├── camera_to_base.py # 相机→基座坐标系变换
|
|
||||||
│ ├── camera_capture.py # MJPEG 流帧采集
|
|
||||||
│ ├── udp_server.py # UDP 回显调试服务器
|
|
||||||
│ └── README.md # 工具详细文档
|
|
||||||
├── docs/ # 技术文档
|
|
||||||
│ ├── arm.md # 机械臂运动学推导
|
|
||||||
│ ├── localization.md # 定位系统完整文档
|
|
||||||
│ └── box_detection_grasp.md # 方框检测与自动抓取
|
|
||||||
└── dataset/ # 竞赛训练数据(黑方块图像 + 标定)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心功能
|
## 硬件
|
||||||
|
|
||||||
|
| 组件 | 型号 |
|
||||||
|
|------|------|
|
||||||
|
| 主控 | ESP32-S3-WROOM-1-N16R8 (16MB Flash, 8MB PSRAM Octal) |
|
||||||
|
| 摄像头 | OV2640 (XGA 1024×768, JPEG) |
|
||||||
|
| 舵机 | 飞特 SCS/STS 系列串行舵机 |
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
### 1. ESP32-S3 固件 (`jxbeye/`)
|
### 1. ESP32-S3 固件 (`jxbeye/`)
|
||||||
|
|
||||||
- **双核架构**:Core 0 采集 OV2640 JPEG,Core 1 WiFi 推流 + 异步 UDP 命令接收
|
- **双核并行架构**:Core 0 负责摄像头采集,Core 1 负责 WiFi 推流
|
||||||
- **MJPEG 推流**:`http://<IP>` 实时查看,`/stream` 端点供 OpenCV/YOLO 消费
|
- **MJPEG 实时推流**:Web 浏览器直接访问 `http://<IP>` 查看实时画面
|
||||||
- **UDP 控制**:端口 8888,非阻塞中断回调处理底盘 + 机械臂 + 激光指令
|
- **AsyncUDP 非阻塞通讯**:UDP 指令由中断回调处理,不干扰推流
|
||||||
- **WiFi 配置**:首次启动创建热点 `ESP32-S3-Camera`(密码 `12345678`),串口发送 `WIFI:SSID:PASSWORD` 切换 Station 模式
|
- **ESP-NOW**:低延迟点对点通信
|
||||||
|
- **在线 WiFi 配置**:通过 Web 页面或串口修改 SSID/密码,自动保存到 NVS
|
||||||
|
|
||||||
### 2. 机械臂控制与视觉抓取 (`ros2/src/udp_teleop/`)
|
### 2. ROS 2 键盘遥控 (`ros2/src/udp_teleop/`)
|
||||||
|
|
||||||
| 节点 | 功能 |
|
- **底盘遥控**:W/S 前进后退、A/D 左右平移、Q/E 旋转(差速驱动)
|
||||||
|------|------|
|
- **机械臂遥控**:6 轴关节角度独立控制,高度升降
|
||||||
| `arm_control` | 完整逆运动学/正运动学,关节空间 + 笛卡尔空间运动,4 个 ROS 服务,状态发布 (10Hz),自动归零,动态 J5/z4 适配 |
|
- **多平台键盘后端**:
|
||||||
| `vision_grasp` | 相机坐标→基坐标变换,自动抓取/释放流程(松开→移动→夹取→回收),多线程服务调用 |
|
- `stdin` — Linux/macOS 终端原生输入,零依赖
|
||||||
| `box_detection_grasp` | YOLO 实时方框检测(MJPEG 流),单目深度估计,自动/手动模式,检测到即触发抓取 |
|
- `pynput` — 跨平台按键监听
|
||||||
| `keyboard_control` | 键盘 UDP 遥控(底盘 WASD/QE + 机械臂 ↑↓←→ + 关节选择 2-6),3 种输入后端 |
|
- `win_poll` — Windows Win32 API 轮询
|
||||||
|
- **可配置参数**:速度、步长、目标 IP/端口均通过 YAML 配置
|
||||||
|
|
||||||
|
### 3. UDP 通讯协议
|
||||||
|
|
||||||
|
所有指令通过 UDP 端口 `8888` 发送,格式:
|
||||||
|
|
||||||
**机械臂服务接口**:
|
|
||||||
```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/get_pose arm_control_msgs/srv/GetPose
|
|
||||||
ros2 service call /arm_control/set_gripper arm_control_msgs/srv/SetGripper "{grip: true}"
|
|
||||||
```
|
```
|
||||||
|
# 底盘控制
|
||||||
|
XYW:<X速度>:<Y速度>:<W角速度>:XZHY\n
|
||||||
|
|
||||||
### 3. 激光 SLAM 定位与导航 (`ros2/src/craic_localization/`)
|
# 机械臂控制
|
||||||
|
JXB:<高度>:<J2>:<J3>:<J4>:<J5>:<J6>:0:0:EZHY\n
|
||||||
|
|
||||||
三个阶段式比赛流程:
|
# 激光控制
|
||||||
|
LASERON / LASEROFF
|
||||||
|
|
||||||
| 阶段 | Launch 文件 | 功能 | TF 链 |
|
# 串口透传
|
||||||
|------|------------|------|-------|
|
<any payload ending with ZHY or \n>
|
||||||
| P3 标定 | `bringup.launch.py` | 轮式里程计 + 激光驱动 + RViz | `odom → base_footprint → laser_frame` |
|
```
|
||||||
| P4 建图 | `mapping.launch.py` | 激光 + rf2o 里程计 + slam_gmapping | `map → odom → base_footprint → laser_frame` |
|
|
||||||
| P5 导航 | `localization.launch.py` | 激光 + rf2o + map_server + AMCL | `map → odom → base_footprint → laser_frame` |
|
|
||||||
|
|
||||||
**定位系统节点**:
|
|
||||||
|
|
||||||
| 节点 | 功能 |
|
|
||||||
|------|------|
|
|
||||||
| `chassis_odometry` | 串口读取 LZUCAR 底盘 MCU 轮式里程计(32 字节协议) |
|
|
||||||
| `teach_points` | 交互式示教工具:回车记录 map 帧位姿,支持预设点位序列 (B_1..F_1),自动存 YAML |
|
|
||||||
| `navigate_to_point` | 全向 P 控制器自主导航到示教点,UDP XYW 指令闭环驱动,干运行/单次模式可选 |
|
|
||||||
| `show_points` | RViz MarkerArray 可视化示教点(箭头 + 标签),实时更新 |
|
|
||||||
|
|
||||||
### 4. 命令行工具 (`tools/`)
|
|
||||||
|
|
||||||
| 工具 | 功能 |
|
|
||||||
|------|------|
|
|
||||||
| `udp_control.py` | 机械臂关节/笛卡尔空间控制,逆运动学,轨迹插值,状态缓存,干运行 |
|
|
||||||
| `camera_to_base.py` | 相机→TCP→基座完整变换链,支持相机安装偏移与旋转 |
|
|
||||||
| `camera_capture.py` | ESP32 MJPEG 流帧采集,自动子网扫描寻找相机 |
|
|
||||||
| `udp_server.py` | UDP 回显服务器,用于调试协议通信 |
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### ESP32 固件
|
### 编译 & 烧录 ESP32 固件
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd jxbeye
|
cd jxbeye
|
||||||
@@ -131,195 +92,55 @@ pio run -t upload
|
|||||||
pio device monitor
|
pio device monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
### ROS 2 控制系统
|
首次启动后 ESP32 创建热点 `ESP32-S3-Camera`(密码 `12345678`),浏览器访问 `http://192.168.4.1`。
|
||||||
|
|
||||||
|
### 运行 ROS 2 键盘遥控
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 环境准备(首次)
|
# 创建环境(首次)
|
||||||
conda create -n ros2_humble -c robostack-staging -c conda-forge ros-humble-desktop
|
conda create -n ros2_humble -c robostack-staging -c conda-forge ros-humble-desktop
|
||||||
conda activate ros2_humble
|
conda install -n ros2_humble -c robostack-staging -c conda-forge colcon-common-extensions
|
||||||
|
conda run -n ros2_humble pip install pynput
|
||||||
|
|
||||||
# 编译(一键)
|
# 构建 & 运行
|
||||||
|
conda activate ros2_humble
|
||||||
cd ros2
|
cd ros2
|
||||||
./build.sh
|
colcon build --symlink-install --packages-select udp_teleop
|
||||||
source install/setup.bash
|
source install/setup.bash
|
||||||
|
|
||||||
# 或按需编译指定包
|
# 启动键盘遥控(默认目标 192.168.233.67:8888)
|
||||||
./build.sh --packages-select craic_localization udp_teleop
|
|
||||||
```
|
|
||||||
|
|
||||||
**启动机械臂控制**:
|
|
||||||
```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
|
|
||||||
|
|
||||||
# 或一键启动全部(含 YOLO 检测)
|
|
||||||
ros2 launch udp_teleop vision_grasp.launch.py detection:=true auto_grasp:=true
|
|
||||||
```
|
|
||||||
|
|
||||||
**启动键盘遥控**:
|
|
||||||
```bash
|
|
||||||
ros2 run udp_teleop keyboard_control \
|
ros2 run udp_teleop keyboard_control \
|
||||||
--ros-args --params-file src/udp_teleop/config/params.yaml
|
--ros-args --params-file src/udp_teleop/config/params.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
**启动定位与导航**:
|
### 测试 UDP 通讯
|
||||||
```bash
|
|
||||||
# P4 建图
|
|
||||||
ros2 launch craic_localization mapping.launch.py
|
|
||||||
# 建图完成后保存
|
|
||||||
ros2 run nav2_map_server map_saver_cli -f src/craic_localization/maps/craic
|
|
||||||
|
|
||||||
# P5 定位 + 导航
|
|
||||||
ros2 launch craic_localization localization.launch.py
|
|
||||||
|
|
||||||
# 新终端:示教记录点位
|
|
||||||
ros2 run craic_localization teach_points \
|
|
||||||
--ros-args -p output_file:=$PWD/src/craic_localization/config/taught_points.yaml
|
|
||||||
|
|
||||||
# 新终端:自主导航到示教点
|
|
||||||
ros2 run craic_localization navigate_to_point
|
|
||||||
```
|
|
||||||
|
|
||||||
### 命令行工具
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 机械臂控制
|
# 启动回显服务器
|
||||||
python tools/udp_control.py pose --x 200 --y 100 --z -100 --phi 45 --duration 2.0
|
|
||||||
python tools/udp_control.py joints --height -100 --j2 10 --j3 20 --j4 30
|
|
||||||
|
|
||||||
# 相机采集
|
|
||||||
python tools/camera_capture.py --ip 192.168.4.1
|
|
||||||
python tools/camera_capture.py --scan # 自动扫描
|
|
||||||
|
|
||||||
# UDP 调试
|
|
||||||
python tools/udp_server.py
|
python tools/udp_server.py
|
||||||
|
|
||||||
|
# 发送测试指令
|
||||||
echo 'XYW:100:0:0:XZHY' | nc -u 127.0.0.1 8888
|
echo 'XYW:100:0:0:XZHY' | nc -u 127.0.0.1 8888
|
||||||
```
|
```
|
||||||
|
|
||||||
## UDP 协议
|
### 串口配置 WiFi
|
||||||
|
|
||||||
所有指令通过 UDP 端口 `8888` 发送,ASCII 文本协议:
|
在 ESP32 串口监视器中发送:
|
||||||
|
|
||||||
```
|
```
|
||||||
# 底盘控制(麦克纳姆轮全向速度)
|
WIFI:热点名称:密码
|
||||||
XYW:<X_speed>:<Y_speed>:<W_angular>:XZHY\n
|
|
||||||
|
|
||||||
# 机械臂控制(6 轴角度 + 高度)
|
|
||||||
JXB:<height>:<J2>:<J3>:<J4>:<J5>:<J6>:0:0:EZHY\n
|
|
||||||
|
|
||||||
# 激光控制
|
|
||||||
LASERON\n
|
|
||||||
LASEROFF\n
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- 角度单位:度(已包含零点偏移)
|
设备自动保存配置并重启。
|
||||||
- 高度单位:mm
|
|
||||||
- 速度单位:X/Y mm/s,W deg/s
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### ROS 服务调用
|
|
||||||
|
|
||||||
```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}"
|
|
||||||
|
|
||||||
# 查询位姿
|
|
||||||
ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
|
|
||||||
|
|
||||||
# 视觉抓取(发布相机坐标)
|
|
||||||
ros2 topic pub --once /vision_grasp/grasp_target geometry_msgs/Point \
|
|
||||||
"{x: 10.0, y: 5.0, z: 250.0}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 定位导航工作流
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 启动定位系统
|
|
||||||
ros2 launch craic_localization localization.launch.py
|
|
||||||
|
|
||||||
# 2. 键盘遥控到目标位置(另一终端)
|
|
||||||
ros2 run udp_teleop keyboard_control \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/params.yaml
|
|
||||||
|
|
||||||
# 3. 示教记录点位(第三终端)
|
|
||||||
ros2 run craic_localization teach_points \
|
|
||||||
--ros-args -p output_file:=$PWD/src/craic_localization/config/taught_points.yaml
|
|
||||||
# 交互命令:回车记录 → r 记录 → p 查看 → save 保存 → q 退出
|
|
||||||
|
|
||||||
# 4. 自主导航到示教点
|
|
||||||
ros2 run craic_localization navigate_to_point
|
|
||||||
# 输入目标点名(如 B_1),机器人自动导航到位
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python 集成
|
|
||||||
|
|
||||||
```python
|
|
||||||
import rclpy
|
|
||||||
from geometry_msgs.msg import Point
|
|
||||||
|
|
||||||
class VisionDetector(rclpy.node.Node):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__('vision_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)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 坐标系说明
|
|
||||||
|
|
||||||
**机械臂基坐标系**(Z 轴朝上):
|
|
||||||
- 原点:J1 线性滑轨底部
|
|
||||||
- X 轴:基座正前方,Y 轴:基座左侧,Z 轴:竖直向上
|
|
||||||
- 单位:mm
|
|
||||||
|
|
||||||
**相机坐标系**(水平安装):
|
|
||||||
- X 轴:右侧,Y 轴:向下,Z 轴:正前方(光轴)
|
|
||||||
- 单位:mm
|
|
||||||
|
|
||||||
**底盘坐标系**(ROS TF):
|
|
||||||
```
|
|
||||||
map ──(AMCL)──> odom ──(rf2o)──> base_footprint ──(static)──> laser_frame
|
|
||||||
──(static)──> base_link
|
|
||||||
```
|
|
||||||
- `map`:全局固定坐标系(AMCL 定位输出)
|
|
||||||
- `odom`:连续里程计坐标系(rf2o 激光里程计)
|
|
||||||
- `base_footprint`:底盘投影中心
|
|
||||||
- `laser_frame`:激光雷达安装位置(外参可调:`lidar_x`, `lidar_y`, `lidar_yaw`)
|
|
||||||
|
|
||||||
## 文档
|
|
||||||
|
|
||||||
| 文档 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| [docs/arm.md](docs/arm.md) | 机械臂逆运动学完整数学推导(2 连杆平面臂 + z4 偏移) |
|
|
||||||
| [docs/localization.md](docs/localization.md) | 定位系统文档:SLAM 建图、AMCL 定位、示教导航、故障排查 |
|
|
||||||
| [docs/box_detection_grasp.md](docs/box_detection_grasp.md) | YOLO 方框检测与自动抓取:配置、服务、深度估计原理 |
|
|
||||||
| [ros2/README.md](ros2/README.md) | ROS 2 节点详细文档 |
|
|
||||||
| [tools/README.md](tools/README.md) | 命令行工具完整参数与工作流示例 |
|
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
| 环境 | 依赖 |
|
| 环境 | 依赖 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| ESP32 | PlatformIO, Arduino framework, esp32-camera |
|
| ESP32 | PlatformIO, Arduino framework, esp32-camera |
|
||||||
| ROS 2 | ROS 2 Humble, `slam_gmapping`, `nav2_amcl`, `nav2_map_server`, `nav2_lifecycle_manager`, `serial` |
|
| ROS 2 | ROS 2 Humble (Conda robostack), pynput |
|
||||||
| Python | NumPy, OpenCV, Ultralytics (YOLO), pynput (可选) |
|
| 测试 | Python 3, Unix netcat |
|
||||||
| 系统 | YDLidar-SDK, colcon, conda (robostack) |
|
|
||||||
|
|
||||||
## 许可
|
## 许可
|
||||||
|
|
||||||
MIT License
|
待定。
|
||||||
|
|||||||
424
docs/arm.md
424
docs/arm.md
@@ -1,424 +0,0 @@
|
|||||||
# 机械臂运动学推导
|
|
||||||
|
|
||||||
6-DOF 机械臂逆运动学和正运动学完整推导。
|
|
||||||
|
|
||||||
## 机械臂结构
|
|
||||||
|
|
||||||
### 关节配置
|
|
||||||
|
|
||||||
```
|
|
||||||
J1: 线性滑轨(垂直运动,高度 d1)
|
|
||||||
J2: 基座旋转(绕 Z 轴,XY 平面)
|
|
||||||
J3: 肘关节(绕 Z 轴,XY 平面)
|
|
||||||
J4: 腕关节(绕 Z 轴,XY 平面)
|
|
||||||
J5: 末端俯仰(0° = 水平)
|
|
||||||
J6: 夹爪旋转
|
|
||||||
```
|
|
||||||
|
|
||||||
### 几何参数
|
|
||||||
|
|
||||||
| 参数 | 值 (mm) | 说明 |
|
|
||||||
|------|---------|------|
|
|
||||||
| L1 | 125 | J2-J3 连杆长度 |
|
|
||||||
| L2 | 125 | J3-J4 连杆长度 |
|
|
||||||
| x4 | 110 | J4-TCP 水平偏移 |
|
|
||||||
| z4 | 80 | J4-TCP 垂直偏移(夹爪状态相关:张开 55mm,闭合 -100mm) |
|
|
||||||
|
|
||||||
### 坐标系
|
|
||||||
|
|
||||||
**基坐标系**(右手系,Z 轴朝上):
|
|
||||||
```
|
|
||||||
Z↑ (高度)
|
|
||||||
|
|
|
||||||
|
|
|
||||||
o----→ X (前)
|
|
||||||
/
|
|
||||||
/
|
|
||||||
Y (左)
|
|
||||||
```
|
|
||||||
|
|
||||||
- 原点:J1 滑轨底部
|
|
||||||
- d1 范围:[-290, 0] mm(负值表示在基准面以下)
|
|
||||||
|
|
||||||
## 正运动学(FK)
|
|
||||||
|
|
||||||
已知关节状态 → 计算 TCP 位姿
|
|
||||||
|
|
||||||
### 输入
|
|
||||||
|
|
||||||
- `d1`: 高度(J1 线性位移)
|
|
||||||
- `θ2, θ3, θ4`: J2/J3/J4 角度(度)
|
|
||||||
|
|
||||||
### 输出
|
|
||||||
|
|
||||||
- `(x, y, z)`: TCP 位置(mm)
|
|
||||||
- `φ`: TCP 偏航角(度)
|
|
||||||
|
|
||||||
### 推导
|
|
||||||
|
|
||||||
**步骤 1**:计算 J4 中心位置(XY 平面)
|
|
||||||
|
|
||||||
J2 和 J3 形成二连杆机构:
|
|
||||||
|
|
||||||
$$
|
|
||||||
x_{j4} = L_1 \cos\theta_2 + L_2 \cos(\theta_2 + \theta_3)
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
y_{j4} = L_1 \sin\theta_2 + L_2 \sin(\theta_2 + \theta_3)
|
|
||||||
$$
|
|
||||||
|
|
||||||
**步骤 2**:计算 TCP 偏航角
|
|
||||||
|
|
||||||
$$
|
|
||||||
\phi = \theta_2 + \theta_3 + \theta_4
|
|
||||||
$$
|
|
||||||
|
|
||||||
**步骤 3**:计算 TCP 位置
|
|
||||||
|
|
||||||
从 J4 中心沿 φ 方向偏移 x4,垂直偏移 z4:
|
|
||||||
|
|
||||||
$$
|
|
||||||
x = x_{j4} + x_4 \cos\phi
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
y = y_{j4} + x_4 \sin\phi
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
z = d_1 - z_4
|
|
||||||
$$
|
|
||||||
|
|
||||||
### Python 实现
|
|
||||||
|
|
||||||
```python
|
|
||||||
def forward_kinematics(d1, theta2_deg, theta3_deg, theta4_deg, L1=125, L2=125, x4=110, z4=80):
|
|
||||||
"""正运动学"""
|
|
||||||
theta2 = math.radians(theta2_deg)
|
|
||||||
theta3 = math.radians(theta3_deg)
|
|
||||||
theta4 = math.radians(theta4_deg)
|
|
||||||
phi = theta2 + theta3 + theta4
|
|
||||||
|
|
||||||
# J4 中心
|
|
||||||
x_j4 = L1 * math.cos(theta2) + L2 * math.cos(theta2 + theta3)
|
|
||||||
y_j4 = L1 * math.sin(theta2) + L2 * math.sin(theta2 + theta3)
|
|
||||||
|
|
||||||
# TCP 位置
|
|
||||||
x = x_j4 + x4 * math.cos(phi)
|
|
||||||
y = y_j4 + x4 * math.sin(phi)
|
|
||||||
z = d1 - z4
|
|
||||||
|
|
||||||
return x, y, z, math.degrees(phi)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 逆运动学(IK)
|
|
||||||
|
|
||||||
已知 TCP 目标位姿 → 计算关节角度
|
|
||||||
|
|
||||||
### 输入
|
|
||||||
|
|
||||||
- `(x, y, z)`: 目标位置(mm)
|
|
||||||
- `φ`: 目标偏航角(度)
|
|
||||||
|
|
||||||
### 输出
|
|
||||||
|
|
||||||
- `d1`: 高度
|
|
||||||
- `θ2, θ3, θ4`: 关节角度(度)
|
|
||||||
|
|
||||||
### 推导
|
|
||||||
|
|
||||||
**步骤 1**:计算 J4 中心目标位置
|
|
||||||
|
|
||||||
从 TCP 位置反向计算 J4 位置:
|
|
||||||
|
|
||||||
$$
|
|
||||||
x_{j4} = x - x_4 \cos\phi
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
y_{j4} = y - x_4 \sin\phi
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
z_{j4} = z + z_4
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
d_1 = z_{j4}
|
|
||||||
$$
|
|
||||||
|
|
||||||
**步骤 2**:计算平面距离
|
|
||||||
|
|
||||||
$$
|
|
||||||
r = \sqrt{x_{j4}^2 + y_{j4}^2}
|
|
||||||
$$
|
|
||||||
|
|
||||||
**步骤 3**:求解 θ3(余弦定理)
|
|
||||||
|
|
||||||
二连杆机构的标准解法:
|
|
||||||
|
|
||||||
$$
|
|
||||||
\cos\theta_3 = \frac{r^2 - L_1^2 - L_2^2}{2L_1L_2}
|
|
||||||
$$
|
|
||||||
|
|
||||||
检查工作空间:
|
|
||||||
|
|
||||||
$$
|
|
||||||
-1 \leq \cos\theta_3 \leq 1 \quad \Rightarrow \quad |L_1 - L_2| \leq r \leq L_1 + L_2
|
|
||||||
$$
|
|
||||||
|
|
||||||
否则目标超出工作空间。
|
|
||||||
|
|
||||||
有两个解(肘部朝上 / 朝下):
|
|
||||||
|
|
||||||
$$
|
|
||||||
\theta_3 = \pm \arccos\left(\frac{r^2 - L_1^2 - L_2^2}{2L_1L_2}\right)
|
|
||||||
$$
|
|
||||||
|
|
||||||
**步骤 4**:求解 θ2
|
|
||||||
|
|
||||||
$$
|
|
||||||
\theta_2 = \arctan2(y_{j4}, x_{j4}) - \arctan2\left(L_2\sin\theta_3, L_1 + L_2\cos\theta_3\right)
|
|
||||||
$$
|
|
||||||
|
|
||||||
**步骤 5**:求解 θ4
|
|
||||||
|
|
||||||
$$
|
|
||||||
\theta_4 = \phi - \theta_2 - \theta_3
|
|
||||||
$$
|
|
||||||
|
|
||||||
### Python 实现
|
|
||||||
|
|
||||||
```python
|
|
||||||
def inverse_kinematics(x, y, z, phi_deg, elbow_up=False, L1=125, L2=125, x4=110, z4=80):
|
|
||||||
"""逆运动学"""
|
|
||||||
phi = math.radians(phi_deg)
|
|
||||||
|
|
||||||
# 步骤 1: 计算 J4 中心
|
|
||||||
x_j4 = x - x4 * math.cos(phi)
|
|
||||||
y_j4 = y - x4 * math.sin(phi)
|
|
||||||
z_j4 = z + z4
|
|
||||||
d1 = z_j4
|
|
||||||
|
|
||||||
# 步骤 2: 平面距离
|
|
||||||
r2 = x_j4**2 + y_j4**2
|
|
||||||
r = math.sqrt(r2)
|
|
||||||
|
|
||||||
# 步骤 3: 求解 theta3
|
|
||||||
cos_theta3 = (r2 - L1**2 - L2**2) / (2 * L1 * L2)
|
|
||||||
|
|
||||||
if cos_theta3 < -1 or cos_theta3 > 1:
|
|
||||||
raise ValueError(f"目标超出工作空间,r={r:.1f}mm")
|
|
||||||
|
|
||||||
# 肘部朝上:负角度,肘部朝下:正角度
|
|
||||||
sin_theta3 = -math.sqrt(1 - cos_theta3**2) if elbow_up else math.sqrt(1 - cos_theta3**2)
|
|
||||||
theta3 = math.atan2(sin_theta3, cos_theta3)
|
|
||||||
|
|
||||||
# 步骤 4: 求解 theta2
|
|
||||||
theta2 = math.atan2(y_j4, x_j4) - math.atan2(
|
|
||||||
L2 * sin_theta3,
|
|
||||||
L1 + L2 * cos_theta3
|
|
||||||
)
|
|
||||||
|
|
||||||
# 步骤 5: 求解 theta4
|
|
||||||
theta4 = phi - theta2 - theta3
|
|
||||||
|
|
||||||
# 角度归一化到 [-180, 180)
|
|
||||||
def normalize(angle):
|
|
||||||
a = (angle + math.pi) % (2 * math.pi) - math.pi
|
|
||||||
return a if a != -math.pi or angle <= 0 else math.pi
|
|
||||||
|
|
||||||
return (
|
|
||||||
d1,
|
|
||||||
math.degrees(normalize(theta2)),
|
|
||||||
math.degrees(normalize(theta3)),
|
|
||||||
math.degrees(normalize(theta4))
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 零点偏移
|
|
||||||
|
|
||||||
物理零点(机械对齐)与数学零点(直线构型)存在偏差:
|
|
||||||
|
|
||||||
| 关节 | 零点偏移 |
|
|
||||||
|------|----------|
|
|
||||||
| J2 | +3° |
|
|
||||||
| J3 | +7° |
|
|
||||||
| J4 | +25° |
|
|
||||||
|
|
||||||
### 转换关系
|
|
||||||
|
|
||||||
**命令角度 ↔ 数学角度**:
|
|
||||||
|
|
||||||
$$
|
|
||||||
\theta_{\text{command}} = \theta_{\text{math}} + \text{offset}
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
\theta_{\text{math}} = \theta_{\text{command}} - \text{offset}
|
|
||||||
$$
|
|
||||||
|
|
||||||
**使用**:
|
|
||||||
- 正运动学:先减去偏移(命令 → 数学),再计算
|
|
||||||
- 逆运动学:先计算(数学),再加上偏移(数学 → 命令)
|
|
||||||
|
|
||||||
## 工作空间
|
|
||||||
|
|
||||||
### 高度范围
|
|
||||||
|
|
||||||
$$
|
|
||||||
z \in [-290 - z_4, 0 - z_4] = [-370, -80] \text{ mm}
|
|
||||||
$$
|
|
||||||
|
|
||||||
### 水平范围
|
|
||||||
|
|
||||||
$$
|
|
||||||
r_{\min} = |L_1 - L_2| + x_4 = 0 + 110 = 110 \text{ mm}
|
|
||||||
$$
|
|
||||||
|
|
||||||
$$
|
|
||||||
r_{\max} = L_1 + L_2 + x_4 = 125 + 125 + 110 = 360 \text{ mm}
|
|
||||||
$$
|
|
||||||
|
|
||||||
**可达圆环**:半径 110mm 到 360mm
|
|
||||||
|
|
||||||
### 关节限位
|
|
||||||
|
|
||||||
| 关节 | 最小值 | 最大值 | 单位 |
|
|
||||||
|------|--------|--------|------|
|
|
||||||
| height | -290 | 0 | mm |
|
|
||||||
| J2 | -110 | 115 | ° |
|
|
||||||
| J3 | -120 | 145 | ° |
|
|
||||||
| J4 | -90 | 130 | ° |
|
|
||||||
| J5 | -180 | 180 | ° |
|
|
||||||
| J6 | -180 | 180 | ° |
|
|
||||||
|
|
||||||
## 奇异点
|
|
||||||
|
|
||||||
### 1. 肩部奇异点
|
|
||||||
|
|
||||||
当 J4 中心在原点正上方:
|
|
||||||
|
|
||||||
$$
|
|
||||||
x_{j4} = y_{j4} = 0 \quad \Rightarrow \quad r = 0
|
|
||||||
$$
|
|
||||||
|
|
||||||
此时 θ2 无定义(可取任意值)。
|
|
||||||
|
|
||||||
**避免**:保持 `r > 10mm`
|
|
||||||
|
|
||||||
### 2. 肘部奇异点
|
|
||||||
|
|
||||||
当机械臂完全伸直或完全折叠:
|
|
||||||
|
|
||||||
$$
|
|
||||||
\theta_3 = 0° \text{ 或 } \pm 180°
|
|
||||||
$$
|
|
||||||
|
|
||||||
此时运动控制不稳定。
|
|
||||||
|
|
||||||
**避免**:保持 `|θ3| > 5°`
|
|
||||||
|
|
||||||
## 运动插值
|
|
||||||
|
|
||||||
关节空间线性插值,避免笛卡尔空间的复杂路径规划。
|
|
||||||
|
|
||||||
### 算法
|
|
||||||
|
|
||||||
给定起点 `q_start` 和终点 `q_end`,生成 N 个中间点:
|
|
||||||
|
|
||||||
$$
|
|
||||||
q_i = q_{\text{start}} + \frac{i}{N}(q_{\text{end}} - q_{\text{start}}), \quad i = 1, 2, \ldots, N
|
|
||||||
$$
|
|
||||||
|
|
||||||
### 参数
|
|
||||||
|
|
||||||
- `duration`: 运动时长(秒)
|
|
||||||
- `rate`: 插值频率(Hz)
|
|
||||||
- `steps = ceil(duration × rate)`: 插值点数
|
|
||||||
|
|
||||||
**示例**:`duration=2.0s`, `rate=20Hz` → `steps=40`
|
|
||||||
|
|
||||||
### Python 实现
|
|
||||||
|
|
||||||
```python
|
|
||||||
def interpolate_joints(start, end, duration=2.0, rate=20.0):
|
|
||||||
"""关节空间插值"""
|
|
||||||
steps = max(1, int(math.ceil(duration * rate)))
|
|
||||||
trajectory = []
|
|
||||||
|
|
||||||
for i in range(1, steps + 1):
|
|
||||||
t = i / steps
|
|
||||||
state = {
|
|
||||||
key: int(round(start[key] + t * (end[key] - start[key])))
|
|
||||||
for key in start.keys()
|
|
||||||
}
|
|
||||||
trajectory.append(state)
|
|
||||||
|
|
||||||
return trajectory
|
|
||||||
```
|
|
||||||
|
|
||||||
## 完整示例
|
|
||||||
|
|
||||||
### 示例 1:前方抓取
|
|
||||||
|
|
||||||
**目标**:抓取前方 250mm,右侧 50mm,高度 -150mm 的物体
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 逆运动学
|
|
||||||
d1, theta2, theta3, theta4 = inverse_kinematics(
|
|
||||||
x=250, y=50, z=-150, phi_deg=0
|
|
||||||
)
|
|
||||||
# 输出: d1=-70, theta2=11.3°, theta3=-48.6°, theta4=37.3°
|
|
||||||
|
|
||||||
# 验证:正运动学
|
|
||||||
x, y, z, phi = forward_kinematics(d1, theta2, theta3, theta4)
|
|
||||||
# 输出: (250.0, 50.0, -150.0, 0.0°) ✓
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例 2:多段轨迹
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 初始位置
|
|
||||||
start = {'height': 0, 'j2': 0, 'j3': 0, 'j4': 0, 'j5': 81, 'j6': 30}
|
|
||||||
|
|
||||||
# 目标 1: 上方
|
|
||||||
_, theta2, theta3, theta4 = inverse_kinematics(200, 100, -50, 45)
|
|
||||||
waypoint1 = {'height': -50, 'j2': theta2, 'j3': theta3, 'j4': theta4, 'j5': 81, 'j6': 30}
|
|
||||||
|
|
||||||
# 目标 2: 抓取位置
|
|
||||||
_, theta2, theta3, theta4 = inverse_kinematics(200, 100, -150, 45)
|
|
||||||
waypoint2 = {'height': -150, 'j2': theta2, 'j3': theta3, 'j4': theta4, 'j5': -100, 'j6': -5}
|
|
||||||
|
|
||||||
# 生成轨迹
|
|
||||||
traj1 = interpolate_joints(start, waypoint1, duration=2.0) # 2秒到上方
|
|
||||||
traj2 = interpolate_joints(waypoint1, waypoint2, duration=1.0) # 1秒下降抓取
|
|
||||||
```
|
|
||||||
|
|
||||||
## 参考
|
|
||||||
|
|
||||||
### UDP 命令格式
|
|
||||||
|
|
||||||
```
|
|
||||||
JXB:<height>:<J2>:<J3>:<J4>:<J5>:<J6>:0:0:EZHY\n
|
|
||||||
```
|
|
||||||
|
|
||||||
- 所有角度值已包含零点偏移
|
|
||||||
- 直接发送到 ESP32 的 UDP 端口 8888
|
|
||||||
|
|
||||||
### 相关工具
|
|
||||||
|
|
||||||
- `tools/udp_control.py` - 命令行控制(支持 `joints` 和 `pose` 模式)
|
|
||||||
- `ros2/src/udp_teleop/udp_teleop/arm_control.py` - ROS 节点
|
|
||||||
- `docs/vision_calibration_horizontal.md` - 相机坐标变换
|
|
||||||
|
|
||||||
### 测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 测试逆运动学
|
|
||||||
python tools/udp_control.py pose --x 200 --y 100 --z -100 --phi 45 --dry-run
|
|
||||||
|
|
||||||
# 发送命令
|
|
||||||
python tools/udp_control.py pose --x 200 --y 100 --z -100 --phi 45 --duration 2.0
|
|
||||||
```
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
# 方框检测与自动抓取节点
|
|
||||||
|
|
||||||
基于 YOLO 模型检测方框,并自动调用视觉抓取节点完成抓取。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- ✅ 实时 YOLO 方框检测
|
|
||||||
- ✅ 自动/手动模式切换
|
|
||||||
- ✅ 检测成功后自动停止识别
|
|
||||||
- ✅ 自动发布抓取目标到 `/vision_grasp/grasp_target`
|
|
||||||
- ✅ 3D 坐标估计(基于方框尺寸)
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 1. 启动必要的节点
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 终端 1: 启动 arm_control 节点
|
|
||||||
ros2 run udp_teleop arm_control \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/arm_control.yaml
|
|
||||||
|
|
||||||
# 终端 2: 启动 vision_grasp 节点
|
|
||||||
ros2 run udp_teleop vision_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/vision_grasp.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 启动方框检测节点
|
|
||||||
|
|
||||||
#### 自动模式(检测到方框后自动抓取)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ros2 run udp_teleop box_detection_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/box_detection_grasp.yaml \
|
|
||||||
-p auto_grasp:=true
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 手动模式(仅检测,不自动抓取)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ros2 run udp_teleop box_detection_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/box_detection_grasp.yaml \
|
|
||||||
-p auto_grasp:=false
|
|
||||||
```
|
|
||||||
|
|
||||||
## 工作流程
|
|
||||||
|
|
||||||
### 自动模式 (auto_grasp=true)
|
|
||||||
|
|
||||||
1. 节点启动后立即开始实时检测
|
|
||||||
2. 检测到方框后:
|
|
||||||
- 停止检测(避免重复)
|
|
||||||
- 计算 3D 坐标(相机坐标系,单位:mm)
|
|
||||||
- 发布到 `/vision_grasp/grasp_target`
|
|
||||||
- vision_grasp 节点接收并执行抓取
|
|
||||||
3. 等待 10 秒后完成(可自定义)
|
|
||||||
4. 节点保持运行但不再检测
|
|
||||||
|
|
||||||
### 手动模式 (auto_grasp=false)
|
|
||||||
|
|
||||||
- 节点启动后不检测
|
|
||||||
- 通过服务触发检测:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动检测
|
|
||||||
ros2 service call /box_detection/start std_srvs/srv/Trigger
|
|
||||||
|
|
||||||
# 停止检测
|
|
||||||
ros2 service call /box_detection/stop std_srvs/srv/Trigger
|
|
||||||
```
|
|
||||||
|
|
||||||
## 服务接口
|
|
||||||
|
|
||||||
### `/box_detection/start`
|
|
||||||
|
|
||||||
启动方框检测。
|
|
||||||
|
|
||||||
**类型**:`std_srvs/srv/Trigger`
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
```bash
|
|
||||||
ros2 service call /box_detection/start std_srvs/srv/Trigger
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**:
|
|
||||||
- `success: true` - 检测已启动
|
|
||||||
- `success: false` - 检测已在运行或抓取进行中
|
|
||||||
|
|
||||||
### `/box_detection/stop`
|
|
||||||
|
|
||||||
停止方框检测。
|
|
||||||
|
|
||||||
**类型**:`std_srvs/srv/Trigger`
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
```bash
|
|
||||||
ros2 service call /box_detection/stop std_srvs/srv/Trigger
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应**:
|
|
||||||
- `success: true` - 检测已停止
|
|
||||||
- `success: false` - 检测未运行
|
|
||||||
|
|
||||||
## 使用场景
|
|
||||||
|
|
||||||
### 场景 1:自动抓取流水线
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动自动模式
|
|
||||||
ros2 run udp_teleop box_detection_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/box_detection_grasp.yaml \
|
|
||||||
-p auto_grasp:=true
|
|
||||||
|
|
||||||
# 节点自动检测并抓取,无需人工干预
|
|
||||||
```
|
|
||||||
|
|
||||||
### 场景 2:手动触发抓取
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 终端 1: 启动手动模式
|
|
||||||
ros2 run udp_teleop box_detection_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/box_detection_grasp.yaml \
|
|
||||||
-p auto_grasp:=false
|
|
||||||
|
|
||||||
# 终端 2: 需要抓取时手动触发
|
|
||||||
ros2 service call /box_detection/start std_srvs/srv/Trigger
|
|
||||||
|
|
||||||
# 检测到方框后自动抓取,完成后停止
|
|
||||||
# 需要再次抓取时,再次调用 start 服务
|
|
||||||
ros2 service call /box_detection/start std_srvs/srv/Trigger
|
|
||||||
```
|
|
||||||
|
|
||||||
### 场景 3:紧急停止
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 在检测过程中紧急停止
|
|
||||||
ros2 service call /box_detection/stop std_srvs/srv/Trigger
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置参数
|
|
||||||
|
|
||||||
编辑 `config/box_detection_grasp.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
box_detection_grasp:
|
|
||||||
ros__parameters:
|
|
||||||
# 相机流
|
|
||||||
stream_url: "http://192.168.4.1/stream"
|
|
||||||
|
|
||||||
# 模型路径
|
|
||||||
model_path: "/path/to/model.pt"
|
|
||||||
|
|
||||||
# 检测参数
|
|
||||||
confidence: 0.35 # 置信度阈值
|
|
||||||
imgsz: 768 # YOLO 输入尺寸
|
|
||||||
device: "" # "cpu", "cuda", 或 "" (自动)
|
|
||||||
detection_rate: 10.0 # 检测频率 (Hz)
|
|
||||||
|
|
||||||
# 方框尺寸(用于深度估计)
|
|
||||||
box_size_m: 0.03 # 方框边长 (米)
|
|
||||||
|
|
||||||
# 相机参数
|
|
||||||
horizontal_fov_deg: 66.0 # 水平视场角
|
|
||||||
|
|
||||||
# 调试
|
|
||||||
show_debug_window: false # 显示检测窗口
|
|
||||||
|
|
||||||
# 控制
|
|
||||||
auto_grasp: false # 自动抓取开关
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调试
|
|
||||||
|
|
||||||
### 显示检测窗口
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ros2 run udp_teleop box_detection_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/box_detection_grasp.yaml \
|
|
||||||
-p auto_grasp:=true \
|
|
||||||
-p show_debug_window:=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 监控话题
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看抓取目标
|
|
||||||
ros2 topic echo /vision_grasp/grasp_target
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
ros2 node list
|
|
||||||
ros2 node info /box_detection_grasp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 坐标系说明
|
|
||||||
|
|
||||||
- **输入**:YOLO 检测框(图像像素坐标)
|
|
||||||
- **输出**:相机坐标系 3D 坐标(单位:mm)
|
|
||||||
- X: 向右
|
|
||||||
- Y: 向下
|
|
||||||
- Z: 向前(深度)
|
|
||||||
- **传递**:vision_grasp 节点自动转换到基坐标系
|
|
||||||
|
|
||||||
## 深度估计原理
|
|
||||||
|
|
||||||
使用透视投影原理:
|
|
||||||
|
|
||||||
```
|
|
||||||
Z = (实际尺寸 × 焦距) / 像素尺寸
|
|
||||||
X = (u - cx) × Z / fx
|
|
||||||
Y = (v - cy) × Z / fy
|
|
||||||
```
|
|
||||||
|
|
||||||
其中:
|
|
||||||
- `box_size_m` = 方框实际边长(米)
|
|
||||||
- `horizontal_fov_deg` = 相机水平视场角
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 问题:无法连接到 ESP32 相机
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
1. 检查 ESP32 IP 地址:`ping 192.168.4.1`
|
|
||||||
2. 浏览器访问:`http://192.168.4.1`
|
|
||||||
3. 修改配置:`-p stream_url:=http://<IP>/stream`
|
|
||||||
|
|
||||||
### 问题:检测不到方框
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
1. 降低置信度:`-p confidence:=0.25`
|
|
||||||
2. 检查模型路径:`ls -lh src/udp_teleop/models/box_detection.pt`
|
|
||||||
3. 启用调试窗口:`-p show_debug_window:=true`
|
|
||||||
|
|
||||||
### 问题:深度估计不准确
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
1. 校准方框尺寸:`-p box_size_m:=0.03`
|
|
||||||
2. 校准相机 FOV:`-p horizontal_fov_deg:=66.0`
|
|
||||||
|
|
||||||
## 依赖
|
|
||||||
|
|
||||||
- ROS 2 Humble
|
|
||||||
- Python 3.8+
|
|
||||||
- OpenCV (`pip install opencv-python`)
|
|
||||||
- NumPy (`pip install numpy`)
|
|
||||||
- Ultralytics YOLO (`pip install ultralytics`)
|
|
||||||
|
|
||||||
## 示例:完整启动脚本
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# 启动完整的方框检测与抓取系统
|
|
||||||
|
|
||||||
# 确保在 ros2 目录
|
|
||||||
cd ~/Dev/craic/ros2
|
|
||||||
source install/setup.bash
|
|
||||||
|
|
||||||
# 启动 arm_control
|
|
||||||
gnome-terminal -- bash -c "
|
|
||||||
source install/setup.bash
|
|
||||||
ros2 run udp_teleop arm_control \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/arm_control.yaml
|
|
||||||
exec bash"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# 启动 vision_grasp
|
|
||||||
gnome-terminal -- bash -c "
|
|
||||||
source install/setup.bash
|
|
||||||
ros2 run udp_teleop vision_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/vision_grasp.yaml
|
|
||||||
exec bash"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# 启动方框检测(自动抓取模式)
|
|
||||||
gnome-terminal -- bash -c "
|
|
||||||
source install/setup.bash
|
|
||||||
ros2 run udp_teleop box_detection_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/box_detection_grasp.yaml \
|
|
||||||
-p auto_grasp:=true \
|
|
||||||
-p show_debug_window:=true
|
|
||||||
exec bash"
|
|
||||||
|
|
||||||
echo "所有节点已启动!"
|
|
||||||
```
|
|
||||||
|
|
||||||
保存为 `launch_box_grasp.sh` 并运行:
|
|
||||||
```bash
|
|
||||||
chmod +x launch_box_grasp.sh
|
|
||||||
./launch_box_grasp.sh
|
|
||||||
```
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
# CRAIC 定位系统文档
|
|
||||||
|
|
||||||
激光 SLAM 建图 + AMCL 精准定位 + 示教记录点位。移植自 `lzu_robot` / `move_try`
|
|
||||||
(同一赛事的参考实现),适配 CRAIC 麦轮底盘 + ESP32 + 机械臂平台。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 概述
|
|
||||||
|
|
||||||
- **目标**:在已知(预先测量)的竞赛场地上对底盘做绝对、无累积漂移的精准定位,并提供示教记录关键点位的工具。
|
|
||||||
- **方案**:YDLiDAR → `rf2o` 激光里程计提供连续 `odom`;`slam_gmapping` 建一次场地地图;`nav2 AMCL` 用激光匹配已知地图做全局校正。
|
|
||||||
- **运行环境**:ROS 2 Humble(conda/robostack `ros2_humble`)。机器人端工作区在 `~/Desktop/ros2`(下文命令默认在此目录、且已 `source install/setup.bash`)。
|
|
||||||
|
|
||||||
> 设计上原打算用**轮式里程计**做运动来源,但实测 CRAIC 底盘板上电后只短时上报里程计(见 §8),
|
|
||||||
> 故改用 `rf2o`(激光里程计)提供 `odom→base`,与 `lzu_robot`/`move_try` 一致。轮式里程计节点保留,
|
|
||||||
> 待底盘板问题解决后可用 EKF 融合提精度。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 系统架构
|
|
||||||
|
|
||||||
### TF 树
|
|
||||||
```
|
|
||||||
map ──(amcl,激光匹配已知地图)──> odom
|
|
||||||
odom ──(rf2o,来自连续 /scan)──────> base_footprint
|
|
||||||
base_footprint ──(静态外参)────────> laser_frame # 激光安装位,lidar_yaw 等参数
|
|
||||||
base_footprint ──(静态恒等)────────> base_link # 兼容 gmapping/Nav2 默认基准帧
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据流
|
|
||||||
```
|
|
||||||
YDLiDAR ──/dev/ttylzulaser──> ydlidar 驱动 ──> /scan(10Hz)
|
|
||||||
/scan ──> rf2o ──> /odom + TF(odom→base_footprint)
|
|
||||||
/scan + odom ──> slam_gmapping ──> /map + TF(map→odom) # 建图阶段
|
|
||||||
/scan + 已知地图 ──> amcl ──> TF(map→odom) = 绝对位姿 # 定位阶段
|
|
||||||
底盘速度命令: keyboard_control ──UDP──> ESP32 ──UART──> 底盘板 # 与定位互不干扰
|
|
||||||
轮式里程计(可选): 底盘板 ──/dev/ttylzucar──> chassis_odometry ──> /odom(独立测试用)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 软件清单
|
|
||||||
|
|
||||||
### 自建包 `ros2/src/craic_localization`
|
|
||||||
| 文件 | 作用 |
|
|
||||||
|---|---|
|
|
||||||
| `craic_localization/chassis_odometry.py` | 轮式里程计节点(读 `/dev/ttylzucar`,发 `/odom`+TF)。**默认不参与建图/定位**,独立测试用 |
|
|
||||||
| `craic_localization/teach_points.py` | 示教节点:记录 map 系下位姿存 YAML |
|
|
||||||
| `craic_localization/navigate_to_point.py` | 导航节点:自动驱动到示教点(读 taught_points.yaml + AMCL 闭环 + UDP 发 XYW) |
|
|
||||||
| `craic_localization/show_points.py` | 在 rviz 显示所有示教点位(MarkerArray 箭头+文字标签) |
|
|
||||||
| `launch/lidar.launch.py` | YDLiDAR 驱动 + 静态外参(base_footprint→laser_frame、→base_link) |
|
|
||||||
| `launch/bringup.launch.py` | 轮式里程计 + 激光 + rviz(传感器自检) |
|
|
||||||
| `launch/mapping.launch.py` | 激光 + rf2o + slam_gmapping(建图) |
|
|
||||||
| `launch/localization.launch.py` | 激光 + rf2o + map_server + amcl + lifecycle_manager(AMCL 定位) |
|
|
||||||
| `config/lidar.yaml` | YDLiDAR TminiPro 参数(端口 `/dev/ttylzulaser`) |
|
|
||||||
| `config/gmapping.yaml` | gmapping 建图参数(`/**` 通配键,base_footprint/odom/map) |
|
|
||||||
| `config/amcl.yaml` | map_server + amcl 参数(omni 运动模型) |
|
|
||||||
| `rviz/localization.rviz` | rviz 配置(TF/Map/LaserScan/Odometry) |
|
|
||||||
| `maps/` | 建图存盘输出(`craic.pgm` + `craic.yaml`) |
|
|
||||||
| `config/taught_points.yaml` | 示教输出(运行时生成) |
|
|
||||||
|
|
||||||
### 移植包
|
|
||||||
- `ros2/src/ydlidar_ros2_driver` —— 来自 `move_try`,依赖系统 `ydlidar_sdk`(已装在 conda env)。
|
|
||||||
- `ros2/src/rf2o_laser_odometry` —— 来自 `lzu_robot`,激光里程计(ICRA'16)。
|
|
||||||
|
|
||||||
### 外部依赖(Nav2,已装入 conda env)
|
|
||||||
`nav2_amcl`(amcl)、`nav2_map_server`(map_server, map_saver_cli)、`nav2_lifecycle_manager`(lifecycle_manager)、`slam_gmapping`/`openslam_gmapping`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 环境准备与编译
|
|
||||||
|
|
||||||
一次性安装 Nav2 定位组件(若环境无):
|
|
||||||
```bash
|
|
||||||
conda install -n ros2_humble -c robostack-staging -c conda-forge \
|
|
||||||
ros-humble-nav2-amcl ros-humble-nav2-map-server ros-humble-nav2-lifecycle-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
编译(在工作区根目录):
|
|
||||||
```bash
|
|
||||||
conda activate ros2_humble
|
|
||||||
cd ~/Desktop/ros2
|
|
||||||
colcon build --symlink-install --packages-select \
|
|
||||||
ydlidar_ros2_driver rf2o_laser_odometry craic_localization
|
|
||||||
source install/setup.bash
|
|
||||||
```
|
|
||||||
|
|
||||||
> **开发机 ↔ 机器人同步**:代码在开发机维护,需同步到机器人 `~/Desktop/ros2` 后**重新 colcon build**。
|
|
||||||
> 新增/改动包时务必确认源码已同步(尤其新包 `ydlidar_ros2_driver`、`rf2o_laser_odometry`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 硬件接口
|
|
||||||
|
|
||||||
| 设备 | 接口 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| 底盘 MCU | `/dev/ttylzucar` @115200 | 上报 32 字节里程计包;命令通过 ESP32 转发,非直连 |
|
|
||||||
| 激光雷达 | `/dev/ttylzulaser` @230400 | YDLiDAR TminiPro,发 `/scan` 10Hz |
|
|
||||||
| 底盘命令 | UDP `192.168.4.1:8888` → ESP32 → UART | `XYW:<x>:<y>:<w>:XZHY`(麦轮速度) |
|
|
||||||
|
|
||||||
**底盘里程计协议**(移植自 lzu seriallzucar):32 字节,帧尾 `'L''Z''U'`;`packet[0]`=校验和=`sum(packet[1:30])%256`;
|
|
||||||
`packet[1:29]`=`<fffffff>`=`x,y,yaw,4轮位置`(x/y 单位 mm);`packet[29]`=模式字节,`'D'`=有效。
|
|
||||||
打开串口后下发 `UP0LZU` 使能上报(参数 `enable_cmd`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 使用流程
|
|
||||||
|
|
||||||
> 前置:每个终端都先 `cd ~/Desktop/ros2 && source install/setup.bash`。
|
|
||||||
|
|
||||||
### 6.1 传感器自检(可选)
|
|
||||||
```bash
|
|
||||||
ros2 launch craic_localization bringup.launch.py
|
|
||||||
```
|
|
||||||
rviz 看 `/scan`、TF。用于核对激光朝向(调 `lidar_yaw` 等,见 §7)。
|
|
||||||
|
|
||||||
### 6.2 建图
|
|
||||||
```bash
|
|
||||||
# 终端1:建图栈(激光 + rf2o + gmapping + rviz)
|
|
||||||
ros2 launch craic_localization mapping.launch.py
|
|
||||||
# 终端2:遥控慢速走遍场地(见 6.6)
|
|
||||||
# 终端3:地图满意后存盘(gmapping 保持运行)
|
|
||||||
mkdir -p src/craic_localization/maps
|
|
||||||
ros2 run nav2_map_server map_saver_cli \
|
|
||||||
-f src/craic_localization/maps/craic \
|
|
||||||
--ros-args -p save_map_timeout:=10000.0
|
|
||||||
```
|
|
||||||
**走图要点**:慢、贴墙走遍每条边界、最后绕回起点;rviz Fixed Frame 设 `map`,墙应是单条清晰线、无重影。
|
|
||||||
**起点即原点**:建图启动时机器人所在位置=map(0,0)、正前方=map +X。建议从固定起点开始。
|
|
||||||
|
|
||||||
存盘得到 `craic.pgm` + `craic.yaml`。**确认 `craic.yaml` 含 `free_thresh`**(缺则 map_server 加载失败):
|
|
||||||
```bash
|
|
||||||
tail -1 src/craic_localization/maps/craic.yaml # 应为 free_thresh: 0.25
|
|
||||||
# 若缺: printf '\nfree_thresh: 0.25\n' >> src/craic_localization/maps/craic.yaml
|
|
||||||
```
|
|
||||||
存盘后重编一次让默认地图安装:`colcon build --symlink-install --packages-select craic_localization`。
|
|
||||||
> 地图四周大片灰色是 gmapping 预留画布(`xmin/xmax ±10` → 20m×20m),属正常,不影响 AMCL。
|
|
||||||
|
|
||||||
### 6.3 定位(AMCL)
|
|
||||||
```bash
|
|
||||||
ros2 launch craic_localization localization.launch.py \
|
|
||||||
map:=$HOME/Desktop/ros2/src/craic_localization/maps/craic.yaml
|
|
||||||
# 起点不在地图原点时给初值:加 init_x:= init_y:= init_yaw:=,或 rviz 工具栏 "2D Pose Estimate" 点
|
|
||||||
```
|
|
||||||
正常表现:rviz(Fixed Frame=`map`) 红色激光点贴合墙体,移动时始终咬住墙、位姿稳定不漂。
|
|
||||||
|
|
||||||
### 6.4 定位精度测试
|
|
||||||
```bash
|
|
||||||
ros2 run tf2_ros tf2_echo map base_footprint # 实时读 x/y/yaw
|
|
||||||
ros2 topic echo /amcl_pose # 位姿 + 协方差
|
|
||||||
```
|
|
||||||
- **重复定位**:标记一物理点记位姿 → 开一圈回同点再记 → 差值 < 3cm 为佳。
|
|
||||||
- **定长移动**:精确前进 1.00m,AMCL 报的位移误差应在几 cm 内。
|
|
||||||
- (可选) rviz Add → PoseArray → `/particlecloud`,粒子收紧=收敛。
|
|
||||||
|
|
||||||
### 6.5 示教记录点位
|
|
||||||
```bash
|
|
||||||
# 终端1:定位栈(6.3) 终端2:遥控(6.6) 终端3:示教
|
|
||||||
ros2 run craic_localization teach_points --ros-args -p output_file:=$PWD/src/craic_localization/config/taught_points.yaml
|
|
||||||
```
|
|
||||||
交互命令:**回车/`r`** 记录当前预设点并前进;`p` 查看当前位姿;`name <X>` 记自定义点;
|
|
||||||
`del <X>` 删除;`list` 列出;`skip`/`back` 跳过/回退;`save` 存盘;`q` 退出。
|
|
||||||
预设顺序 `B_1..B_6, C_1, D_1, E_1, F_1`(每记一点自动存盘)。记 A 区用 `name A_1`…,或改 `points` 参数。
|
|
||||||
输出(map 系,米/弧度):
|
|
||||||
```yaml
|
|
||||||
points:
|
|
||||||
B_1: {x: 1.234, y: 0.456, yaw: 1.571}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
**在 rviz 看示教点**:`localization.launch.py` 已自动启动 `show_points`(青色箭头=位置+朝向,白字=点名,话题 `/taught_points`,rviz 内置 MarkerArray 显示)。
|
|
||||||
想在示教**过程中实时**看到新点,给定位 launch 传 `points_file:=<你的 taught_points.yaml 绝对路径>`,或单独运行:
|
|
||||||
```bash
|
|
||||||
ros2 run craic_localization show_points --ros-args \
|
|
||||||
-p points_file:=$PWD/src/craic_localization/config/taught_points.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.6 自动导航到示教点
|
|
||||||
读取 `taught_points.yaml`,用 AMCL 闭环把底盘开到指定点(麦轮 holonomic P 控制,经 UDP 发 XYW)。
|
|
||||||
**首次务必先 dry_run 验证方向,再低速实测,手放 Ctrl+C(退出自动停车)。** 前置:定位栈(6.3) 在运行。
|
|
||||||
```bash
|
|
||||||
# ① 干跑:只打印命令不发,确认逻辑与方向符号
|
|
||||||
ros2 run craic_localization navigate_to_point --ros-args \
|
|
||||||
-p points_file:=$PWD/src/craic_localization/config/taught_points.yaml \
|
|
||||||
-p dry_run:=true -p goal:=B_1
|
|
||||||
# ② 交互选点(低速实测)
|
|
||||||
ros2 run craic_localization navigate_to_point --ros-args \
|
|
||||||
-p points_file:=$PWD/src/craic_localization/config/taught_points.yaml \
|
|
||||||
-p max_linear:=40.0 -p max_angular:=20.0
|
|
||||||
# ③ 一次性去某点 / 供上层任务程序用话题指定
|
|
||||||
ros2 run craic_localization navigate_to_point --ros-args -p points_file:=... -p goal:=C_1
|
|
||||||
ros2 topic pub -1 /goto std_msgs/String "{data: 'C_1'}"
|
|
||||||
```
|
|
||||||
交互命令:输入点名→导航;`s` 停车;`q` 退出。到达 `pos_tolerance`(5cm)/`yaw_tolerance`(~3°) 容差内停车。
|
|
||||||
**若实车方向相反**:翻转 `sign_x`/`sign_y`/`sign_w`(默认 -1,按 keyboard_control 推导)。如左右反了 `-p sign_x:=1`。
|
|
||||||
|
|
||||||
### 6.7 遥控
|
|
||||||
```bash
|
|
||||||
ros2 run udp_teleop keyboard_control --ros-args \
|
|
||||||
--params-file src/udp_teleop/config/params.yaml \
|
|
||||||
-p chassis_linear_speed:=40 -p chassis_angular_speed:=20 # 建图建议降速
|
|
||||||
```
|
|
||||||
按键:`W/S` 前后、`A/D` 左右平移、`Q/E` 左右转;`Ctrl+C` 退出。须 `ros2 run`、交互终端。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 配置 / 关键参数
|
|
||||||
|
|
||||||
**`lidar.launch.py`**:`lidar_x/y/z`、`lidar_yaw`(默认 -3.14159=-180°,使机体 X 轴对准底盘实际前进方向;若前后颠倒改 0.0)、
|
|
||||||
`intensity`(true;基础 Tmini 无强度时改 false)、`sample_rate`(4)、`baudrate`(230400)、`base_frame`、`laser_frame`。
|
|
||||||
|
|
||||||
**`localization.launch.py`**:`map`(地图 yaml)、`init_x/init_y/init_yaw`(初始位姿)、`use_rviz`。
|
|
||||||
|
|
||||||
**`mapping.launch.py`**:`use_rviz`。范围 `xmin/xmax/ymin/ymax`(±10) 在 `config/gmapping.yaml`,可收小到 ±3 得更紧凑地图(需重新建图)。
|
|
||||||
|
|
||||||
**`amcl.yaml`**:`odom_model_type: omni`(麦轮)、`base_frame_id: base_footprint`、`laser_max_range: 12.0`、`alpha1..5`(里程计噪声)。
|
|
||||||
|
|
||||||
**`chassis_odometry`**(独立测试):`port`、`enable_cmd`(UP0LZU)、`enable_cmd_period`(0=仅打开时发一次)、`require_mode_d`(true)。
|
|
||||||
|
|
||||||
**`navigate_to_point`**:`points_file`、`goal`(一次性目标)、`max_linear`(60)/`max_angular`(30)、`kp_linear`(150)/`kp_angular`(40)、`pos_tolerance`(0.05)/`yaw_tolerance`(0.05)、`sign_x/sign_y/sign_w`(默认 -1,方向修正)、`goal_timeout`(30s)、`dry_run`、`udp_ip/udp_port`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 故障排查
|
|
||||||
|
|
||||||
| 现象 | 原因 | 处理 |
|
|
||||||
|---|---|---|
|
|
||||||
| 激光狂刷 `Check Sum X != Y` | 有第二个进程在读 `/dev/ttylzulaser`,字节流被撕裂 | `pgrep -af ydlidar`;`pkill -f ydlidar_ros2_driver_node`;保证单实例。若仍报:试 `intensity:=false`(Tmini 无强度) |
|
|
||||||
| gmapping `"base_link" ... does not exist` | gmapping 硬编码基准帧 `base_link`,而 TF 树只有 base_footprint | 已修:`lidar.launch.py` 发 base_footprint→base_link 恒等 TF |
|
|
||||||
| gmapping `Message Filter ... queue is full`、地图/位置不动 | `odom→base` TF 冻结:轮式里程计板上电后只短时上报 | 已修:改用 `rf2o` 从 /scan 连续提供 odom→base(见 §1) |
|
|
||||||
| map_server 加载地图失败 | `craic.yaml` 缺 `free_thresh` | 补 `free_thresh: 0.25`(见 6.2) |
|
|
||||||
| rviz 地图上一串绿箭头 | Odometry 显示保留历史(Keep) | 已改默认 `Keep:1`;或 rviz 里取消勾选 Odometry(不影响地图) |
|
|
||||||
| 轮式里程计只在上电后几帧有数据 | 底盘板上电后停止上报(固有行为,UP0LZU 未能维持) | 建图/定位已不依赖它;如需启用试 `enable_cmd_period:=1.0` 保活,或确认主机串口 TX 已接、或经 ESP32 通道使能 |
|
|
||||||
| 时间戳显示 ~2000 年 | 机器人系统时钟未对时 | 单机不影响;多机协同前用 NTP/RTC 对时 |
|
|
||||||
| 导航时往错误方向开 / 原地打转 | XYW 速度符号与实车不一致 | 翻转 `sign_x/sign_y/sign_w`(默认 -1);先 `dry_run:=true` 核对,再低速实测 |
|
|
||||||
| rviz 里机体朝向/前进方向与遥控差 90° | 激光外参 `lidar_yaw` 未校准 | 设 `lidar_yaw=-180°`(前后颠倒则 0.0);改后 **yaw 变了需重新示教**,建议重建图 |
|
|
||||||
| rviz 里示教点/地图整体乱飘跳动(机器人/激光却平滑) | Fixed Frame 设成了 `odom`,map 帧内容随 AMCL 每次校正跳变 | rviz Fixed Frame 改 `map`(已设为默认) |
|
|
||||||
| 平移一段再返回,回位坐标误差大(静止时稳定) | 激光里程计 rf2o 运动跟踪偏差 + 麦轮物理漂移;**非地图大小问题** | 慢速驱动;根治用轮式里程计+EKF(见 §9);先做物理标记/对比 rf2o 与 AMCL 定位区分原因 |
|
|
||||||
| 激光点云形状对、但整体偏离墙线,AMCL 不往墙上贴 | `sigma_hit` 太小(0.05)→偏差超几 cm 就无梯度,AMCL 无法纠正 | 调大 `sigma_hit`(0.2)、`z_hit`(0.9)、`laser_max_range`(≥场地对角线)、`laser_likelihood_max_dist`(2.0);并给准初始位姿 |
|
|
||||||
|
|
||||||
通用排查:`ros2 run tf2_ros tf2_echo map base_footprint`、`ros2 topic hz /scan /odom /map`、
|
|
||||||
`ros2 run tf2_tools view_frames`(看 TF 树连通)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 已知限制与后续
|
|
||||||
|
|
||||||
- **激光外参标定**:`lidar_yaw` 已校准为 -180°,使机体 X 轴对准底盘实际前进方向(rviz 显示与遥控一致,导航默认符号 `sign_x/y/w=-1` 也随之正确)。平移 `lidar_x/y` 仍为 0 占位,需要更高位置精度可再标。
|
|
||||||
**改动 `lidar_yaw` 后**:机体朝向变了 —— 旧地图仍可定位(AMCL 自动补偿),但**示教点的 yaw 需重新记录**,建议重新建图让地图朝向也一致。
|
|
||||||
- **轮式里程计**:底盘板“持续上报”问题待解;解决后可用 `robot_localization` EKF 融合 轮速 + rf2o 再喂 AMCL 提精度。
|
|
||||||
- **地图坐标系**:`map(0,0)` = 建图起点。比赛复现时让机器人从同一物理起点开机,或用 “2D Pose Estimate” 给初值。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 参考来源
|
|
||||||
- `lzu_robot/src/cgbot/cgbot/seriallzucar.py`(里程计协议)、`maps/amcl_config.yaml`、`src/slam_gmapping`、`src/rf2o_laser_odometry`、`src/ydlidar_ros2_driver-humble`
|
|
||||||
- `move_try/src/ydlidar_ros2_driver/params/TminiPro.yaml`、`src/move_try`(同赛事场地/规划参考)
|
|
||||||
@@ -366,17 +366,7 @@ void setup() {
|
|||||||
Serial.print("🌐 IP: "); Serial.println(WiFi.localIP());
|
Serial.print("🌐 IP: "); Serial.println(WiFi.localIP());
|
||||||
Serial.printf("🌐 访问:http://%s\n", WiFi.localIP().toString().c_str());
|
Serial.printf("🌐 访问:http://%s\n", WiFi.localIP().toString().c_str());
|
||||||
} else {
|
} else {
|
||||||
Serial.println("\n❌ STA 连接失败,切换到 AP 模式...");
|
Serial.println("\n❌ WiFi 连接失败,继续运行 (无网络)");
|
||||||
WiFi.mode(WIFI_AP);
|
|
||||||
if (WiFi.softAP("ESP32-S3-Camera", "12345678")) {
|
|
||||||
Serial.println("✅ AP 模式已启动");
|
|
||||||
Serial.println("📶 SSID: ESP32-S3-Camera");
|
|
||||||
Serial.println("🔑 密码: 12345678");
|
|
||||||
Serial.print("🌐 IP: "); Serial.println(WiFi.localIP());
|
|
||||||
Serial.printf("🌐 访问:http://%s\n", WiFi.localIP().toString().c_str());
|
|
||||||
} else {
|
|
||||||
Serial.println("❌ AP 启动失败,继续运行 (无网络)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println("\n[3] 摄像头初始化 (SVGA)...");
|
Serial.println("\n[3] 摄像头初始化 (SVGA)...");
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# Repository Guidelines
|
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
|
||||||
This repository is a ROS 2 workspace. Keep hand-written code under `src/`; treat top-level `build/`, `install/`, and `log/` as generated output. Core packages include `src/arm_control_msgs` for ROS messages/services, `src/udp_teleop` for arm and vision Python nodes, `src/craic_localization` for localization/navigation Python nodes, and C++ sensor packages such as `src/rf2o_laser_odometry` and `src/ydlidar_ros2_driver`. Launch files live in package `launch/`, runtime parameters in `config/` or `params/`, and RViz assets in `rviz/`.
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
|
||||||
Use the workspace script for normal builds:
|
|
||||||
```bash
|
|
||||||
./build.sh
|
|
||||||
source install/setup.bash
|
|
||||||
```
|
|
||||||
`build.sh` sources ROS 2 Humble and runs `colcon build --symlink-install`.
|
|
||||||
|
|
||||||
For targeted builds, prefer:
|
|
||||||
```bash
|
|
||||||
colcon build --packages-select craic_localization udp_teleop
|
|
||||||
colcon build --packages-select arm_control_msgs --cmake-args -DPython_EXECUTABLE=$CONDA_PREFIX/bin/python
|
|
||||||
```
|
|
||||||
Run nodes with `ros2 run`, for example:
|
|
||||||
```bash
|
|
||||||
ros2 run udp_teleop arm_control --ros-args --params-file src/udp_teleop/config/arm_control.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
|
||||||
Python uses 4-space indentation, `snake_case` modules, and `snake_case` console script names such as `navigate_to_point`. ROS interfaces use `PascalCase` (`MovePose.srv`, `TCPPose.msg`). Launch files should end in `.launch.py`. Follow existing ROS 2 defaults: concise docstrings, parameter-driven behavior, and package-local config files. C++ packages build with C++14 and warnings enabled (`-Wall -Wextra -Wpedantic`); keep headers in `include/<package>/` and source in `src/`.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
Run package tests before opening a PR:
|
|
||||||
```bash
|
|
||||||
colcon test --packages-select udp_teleop craic_localization
|
|
||||||
colcon test-result --verbose
|
|
||||||
```
|
|
||||||
`udp_teleop/test/` currently contains ROS linter tests for `flake8` and `pep257`. Add new Python tests under `test/` with `test_*.py` names. For C++ packages, enable `BUILD_TESTING`-compatible tests when adding nontrivial logic.
|
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
|
||||||
Recent history follows Conventional Commits, often with scopes: `feat(craic_localization): ...`, `chore: ...`, `docs: ...`. Keep subjects imperative and under one line. PRs should describe affected packages, runtime impact, and exact validation commands. Include screenshots or RViz captures when changing visualization, launch behavior, or operator-facing workflows.
|
|
||||||
|
|
||||||
## Generated Files & Configuration
|
|
||||||
Do not commit edits inside generated `build/`, `install/`, or `log/` directories. Update source files instead, then rebuild. Keep machine-specific IPs, ports, and calibration values in package YAML files under `config/` or `params/`, and document any hardware assumptions in the PR.
|
|
||||||
329
ros2/README.md
329
ros2/README.md
@@ -1,268 +1,101 @@
|
|||||||
# ROS 2 机械臂控制系统
|
# ros2 — ROS 2 工作空间
|
||||||
|
|
||||||
CRAIC 项目的 ROS 2 机械臂控制和视觉抓取系统。
|
## 环境搭建
|
||||||
|
|
||||||
## 📦 包含组件
|
支持以下三种安装方式,任选其一。
|
||||||
|
|
||||||
### 1. arm_control_msgs
|
### 方式一:Conda (robostack,跨平台)
|
||||||
消息和服务定义包。
|
|
||||||
|
|
||||||
**消息类型**:
|
适用于 Linux / macOS / Windows,无需 root 权限。
|
||||||
- `JointState` - 关节状态
|
|
||||||
- `TCPPose` - TCP 位姿
|
|
||||||
|
|
||||||
**服务类型**:
|
|
||||||
- `MoveJoints` - 关节空间运动
|
|
||||||
- `MovePose` - 笛卡尔空间运动
|
|
||||||
- `GetPose` - 查询当前位姿
|
|
||||||
- `SetGripper` - 夹爪控制
|
|
||||||
|
|
||||||
### 2. arm_control 节点
|
|
||||||
独立的机械臂控制节点(不依赖 tools/udp_control.py)。
|
|
||||||
|
|
||||||
**功能**:
|
|
||||||
- 关节空间和笛卡尔空间运动控制
|
|
||||||
- 完整的逆运动学和正运动学
|
|
||||||
- **自动 z4 适配**:根据目标 z 坐标自动选择夹爪状态
|
|
||||||
- `z > -55mm`: UP(J5=-100,z4=-100),工作范围 z ∈ [-190, 110]mm
|
|
||||||
- `z ≤ -55mm`: DOWN(J5=81,z4=55),工作范围 z ∈ [-345, -55]mm
|
|
||||||
- UDP 通信(与 ESP32)
|
|
||||||
- 状态发布(10Hz)
|
|
||||||
|
|
||||||
### 3. vision_grasp 节点
|
|
||||||
自动化视觉抓取节点。
|
|
||||||
|
|
||||||
**功能**:
|
|
||||||
- 相机坐标到基坐标系的自动转换
|
|
||||||
- **自动处理相机旋转**:根据 J5 状态自动调整图像坐标转换
|
|
||||||
- 抓取流程:释放 → 移动 → 抓取 → 回收
|
|
||||||
- 释放流程:移动 → 释放 → 回收
|
|
||||||
|
|
||||||
**相机旋转说明**:
|
|
||||||
- J5 < 0°(闭合/UP):相机正向,`(xc, yc, zc) = (x_img, -y_img, z)`
|
|
||||||
- J5 > 0°(张开/DOWN):相机旋转 180°,`(xc, yc, zc) = (-x_img, y_img, z)`
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 编译
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 安装 Miniconda
|
||||||
|
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
|
||||||
|
bash Miniconda3-latest-Linux-x86_64.sh
|
||||||
|
|
||||||
|
# 创建 ROS 2 Humble 环境
|
||||||
|
conda create -n ros2_humble -c robostack-staging -c conda-forge ros-humble-desktop
|
||||||
|
conda activate ros2_humble
|
||||||
|
|
||||||
|
# 安装构建工具
|
||||||
|
conda install -c robostack-staging -c conda-forge \
|
||||||
|
colcon-common-extensions \
|
||||||
|
ros-humble-ament-cmake \
|
||||||
|
pip
|
||||||
|
|
||||||
|
# Python 依赖
|
||||||
|
pip install pynput
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:apt 原生安装 (Ubuntu 22.04)
|
||||||
|
|
||||||
|
官方推荐的 Ubuntu 安装方式,系统级集成。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 添加 ROS 2 源
|
||||||
|
sudo apt update && sudo apt install curl gnupg lsb-release
|
||||||
|
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
|
||||||
|
-o /usr/share/keyrings/ros-archive-keyring.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
|
||||||
|
|
||||||
|
# 安装 ROS 2 Humble
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install ros-humble-desktop python3-colcon-common-extensions
|
||||||
|
|
||||||
|
# Python 依赖
|
||||||
|
pip install pynput
|
||||||
|
|
||||||
|
# 环境配置(或写入 ~/.bashrc)
|
||||||
|
source /opt/ros/humble/setup.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式三:Docker
|
||||||
|
|
||||||
|
推荐用于 CI/CD 或快速体验,无需污染宿主机环境。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 拉取镜像
|
||||||
|
docker pull osrf/ros:humble-desktop
|
||||||
|
|
||||||
|
# 启动容器(挂载工作空间)
|
||||||
|
docker run -it --rm \
|
||||||
|
-v $(pwd)/ros2:/ws \
|
||||||
|
osrf/ros:humble-desktop \
|
||||||
|
bash
|
||||||
|
|
||||||
|
# 容器内安装依赖
|
||||||
|
apt update && apt install -y python3-colcon-common-extensions python3-pip
|
||||||
|
pip install pynput
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 每次新终端都需要先加载 ROS 2 环境
|
||||||
|
source /opt/ros/humble/setup.zsh
|
||||||
|
|
||||||
cd ros2
|
cd ros2
|
||||||
|
colcon build --symlink-install --packages-select udp_teleop
|
||||||
# 设置 Python 环境变量(robostack 需要)
|
|
||||||
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
|
|
||||||
|
|
||||||
# 编译
|
|
||||||
colcon build --packages-select arm_control_msgs \
|
|
||||||
--cmake-args \
|
|
||||||
-DPython_EXECUTABLE=$PYTHON_EXECUTABLE \
|
|
||||||
-DPython_INCLUDE_DIR=$PYTHON_INCLUDE_DIR \
|
|
||||||
-DPython_LIBRARY=$PYTHON_LIBRARY
|
|
||||||
|
|
||||||
colcon build --packages-select udp_teleop
|
|
||||||
|
|
||||||
# Source 环境
|
|
||||||
source install/setup.bash
|
source install/setup.bash
|
||||||
```
|
```
|
||||||
|
|
||||||
### 运行
|
> `--symlink-install`:修改 Python 源文件后无需重新构建,直接生效。
|
||||||
|
|
||||||
**机械臂控制**:
|
## 运行
|
||||||
```bash
|
|
||||||
ros2 run udp_teleop arm_control \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/arm_control.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
**视觉抓取**:
|
|
||||||
```bash
|
|
||||||
# 终端 1: 启动 arm_control
|
|
||||||
ros2 run udp_teleop arm_control \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/arm_control.yaml
|
|
||||||
|
|
||||||
# 终端 2: 启动 vision_grasp
|
|
||||||
ros2 run udp_teleop vision_grasp \
|
|
||||||
--ros-args --params-file src/udp_teleop/config/vision_grasp.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 使用示例
|
|
||||||
|
|
||||||
### 1. 控制机械臂
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查询位姿
|
ros2 run udp_teleop keyboard_control \
|
||||||
ros2 service call /arm_control/get_pose arm_control_msgs/srv/GetPose
|
--ros-args --params-file src/udp_teleop/config/params.yaml
|
||||||
|
|
||||||
# 关节运动
|
|
||||||
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}"
|
|
||||||
|
|
||||||
# 笛卡尔运动
|
|
||||||
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}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 视觉抓取
|
也可以通过命令行覆盖参数:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 发布抓取目标(相机坐标)
|
ros2 run udp_teleop keyboard_control \
|
||||||
ros2 topic pub --once /vision_grasp/grasp_target geometry_msgs/Point \
|
--ros-args -p udp_ip:=192.168.1.100 -p udp_port:=9999
|
||||||
"{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}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Python 集成
|
## 包文档
|
||||||
|
|
||||||
```python
|
详见 [src/udp_teleop/README.md](src/udp_teleop/README.md),包含按键映射、UDP 协议、参数配置等。
|
||||||
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, 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 = VisionDetector()
|
|
||||||
node.detect_and_grasp(10.0, 5.0, 250.0)
|
|
||||||
rclpy.spin(node)
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ 配置
|
|
||||||
|
|
||||||
### arm_control 参数
|
|
||||||
|
|
||||||
编辑 `src/udp_teleop/config/arm_control.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
arm_control:
|
|
||||||
ros__parameters:
|
|
||||||
udp_ip: '192.168.4.1' # ESP32 IP
|
|
||||||
udp_port: 8888
|
|
||||||
|
|
||||||
# 几何参数 (mm)
|
|
||||||
l1: 125.0
|
|
||||||
l2: 125.0
|
|
||||||
x4: 110.0
|
|
||||||
z4: 80.0
|
|
||||||
|
|
||||||
# 关节限位 (mm 或度)
|
|
||||||
height_min: -290
|
|
||||||
height_max: 0
|
|
||||||
j2_min: -110
|
|
||||||
j2_max: 115
|
|
||||||
|
|
||||||
# 运动参数
|
|
||||||
default_duration: 1.0 # 默认运动时长 (秒)
|
|
||||||
default_rate: 20.0 # 插值频率 (Hz)
|
|
||||||
```
|
|
||||||
|
|
||||||
### vision_grasp 参数
|
|
||||||
|
|
||||||
编辑 `src/udp_teleop/config/vision_grasp.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
vision_grasp:
|
|
||||||
ros__parameters:
|
|
||||||
# 相机到 TCP 的变换
|
|
||||||
cam_tx: 0.0
|
|
||||||
cam_ty: 0.0
|
|
||||||
cam_tz: 0.0
|
|
||||||
cam_pitch: 0.0 # 如果相机有俯仰角
|
|
||||||
|
|
||||||
# 回收位置
|
|
||||||
retract_position_x: 200.0
|
|
||||||
retract_position_y: 0.0
|
|
||||||
|
|
||||||
# 运动时长
|
|
||||||
grasp_duration: 3.0
|
|
||||||
release_duration: 2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📡 话题和服务
|
|
||||||
|
|
||||||
### arm_control
|
|
||||||
|
|
||||||
**服务**:
|
|
||||||
- `/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)
|
|
||||||
|
|
||||||
### vision_grasp
|
|
||||||
|
|
||||||
**话题**(订阅):
|
|
||||||
- `/vision_grasp/grasp_target` - 抓取目标(相机坐标)
|
|
||||||
- `/vision_grasp/release_target` - 释放目标(基坐标)
|
|
||||||
|
|
||||||
## 🐛 故障排查
|
|
||||||
|
|
||||||
### 编译失败
|
|
||||||
|
|
||||||
**问题**:找不到 Python 开发文件
|
|
||||||
|
|
||||||
**解决**:设置环境变量
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 服务调用超时
|
|
||||||
|
|
||||||
**问题**:vision_grasp 节点服务调用超时
|
|
||||||
|
|
||||||
**原因**:在回调中使用 `time.sleep()` 阻塞了执行器
|
|
||||||
|
|
||||||
**解决**:已使用多线程执行器和独立线程处理
|
|
||||||
|
|
||||||
### 移动失败
|
|
||||||
|
|
||||||
**检查**:
|
|
||||||
1. ESP32 是否在线:`ping 192.168.4.1`
|
|
||||||
2. UDP 是否可达:`echo 'XYW:0:0:0:XZHY' | nc -u 192.168.4.1 8888`
|
|
||||||
3. 目标是否在工作空间内
|
|
||||||
4. 关节限位是否合理
|
|
||||||
|
|
||||||
## 📖 相关文档
|
|
||||||
|
|
||||||
- **机械臂运动学**:`docs/arm.md` - 完整的运动学推导
|
|
||||||
- **视觉标定**:`docs/vision_calibration_horizontal.md` - 相机标定指南
|
|
||||||
- **原始工具**:`tools/README.md` - 命令行工具文档
|
|
||||||
|
|
||||||
## 🔗 依赖关系
|
|
||||||
|
|
||||||
```
|
|
||||||
vision_grasp
|
|
||||||
↓ (依赖)
|
|
||||||
arm_control
|
|
||||||
↓ (依赖)
|
|
||||||
arm_control_msgs
|
|
||||||
```
|
|
||||||
|
|
||||||
所有节点都独立运行,通过 ROS 服务通信。
|
|
||||||
|
|
||||||
## 📝 下一步
|
|
||||||
|
|
||||||
1. **集成物体检测**:订阅相机图像,检测物体,发布到 `/vision_grasp/grasp_target`
|
|
||||||
2. **添加轨迹规划**:避障和路径优化
|
|
||||||
3. **可视化**:RViz 显示机械臂状态和检测结果
|
|
||||||
4. **多物体处理**:队列管理和优先级排序
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
source /opt/ros/humble/setup.bash
|
|
||||||
colcon build --symlink-install "$@"
|
|
||||||
echo ""
|
|
||||||
echo "Done. Source with: source install/setup.bash"
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 关节状态消息
|
|
||||||
std_msgs/Header header
|
|
||||||
int32 height # 高度 (mm)
|
|
||||||
int32 j2 # 关节 2 角度 (度)
|
|
||||||
int32 j3 # 关节 3 角度 (度)
|
|
||||||
int32 j4 # 关节 4 角度 (度)
|
|
||||||
int32 j5 # 关节 5 角度 (度)
|
|
||||||
int32 j6 # 关节 6 角度 (度)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# TCP 位姿消息
|
|
||||||
std_msgs/Header header
|
|
||||||
float64 x # X 坐标 (mm)
|
|
||||||
float64 y # Y 坐标 (mm)
|
|
||||||
float64 z # Z 坐标 (mm)
|
|
||||||
float64 phi # 偏航角 (度)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
|
||||||
<package format="3">
|
|
||||||
<name>arm_control_msgs</name>
|
|
||||||
<version>0.0.1</version>
|
|
||||||
<description>Message and service definitions for arm control</description>
|
|
||||||
<maintainer email="fallensigh@gmail.com">fallensigh</maintainer>
|
|
||||||
<license>MIT</license>
|
|
||||||
|
|
||||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
|
||||||
<buildtool_depend>rosidl_default_generators</buildtool_depend>
|
|
||||||
|
|
||||||
<depend>std_msgs</depend>
|
|
||||||
|
|
||||||
<exec_depend>rosidl_default_runtime</exec_depend>
|
|
||||||
|
|
||||||
<member_of_group>rosidl_interface_packages</member_of_group>
|
|
||||||
|
|
||||||
<export>
|
|
||||||
<build_type>ament_cmake</build_type>
|
|
||||||
</export>
|
|
||||||
</package>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# 查询当前位姿服务
|
|
||||||
---
|
|
||||||
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 角度 (度)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 关节空间运动服务
|
|
||||||
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 # 返回信息
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 笛卡尔空间运动服务
|
|
||||||
float64 x # 目标 X 坐标 (mm)
|
|
||||||
float64 y # 目标 Y 坐标 (mm)
|
|
||||||
float64 z # 目标 Z 坐标 (mm)
|
|
||||||
float64 phi # 目标偏航角 (度)
|
|
||||||
bool elbow_up # 是否使用肘部向上解
|
|
||||||
bool up # 夹爪朝上(z4=-100)
|
|
||||||
bool down # 夹爪朝下(z4=55)
|
|
||||||
bool grip # 是否抓取(J6=-5)
|
|
||||||
bool release # 是否释放(J6=80)
|
|
||||||
float64 duration # 运动时长 (秒,0 表示使用默认值)
|
|
||||||
|
|
||||||
# 已废弃:使用 up/down 代替
|
|
||||||
uint8 gripper_state # 夹爪状态: 0=保持, 1=打开, 2=闭合
|
|
||||||
uint8 GRIPPER_KEEP = 0
|
|
||||||
uint8 GRIPPER_OPEN = 1
|
|
||||||
uint8 GRIPPER_CLOSED = 2
|
|
||||||
---
|
|
||||||
bool success # 是否成功
|
|
||||||
string message # 返回信息
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# 夹爪控制服务
|
|
||||||
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 # 返回信息
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# P5 AMCL 定位参数 —— 改自 lzu_robot/maps/amcl_config.yaml
|
|
||||||
# 麦轮底盘用 omni 运动模型;帧与 chassis_odometry / 激光一致。
|
|
||||||
# 地图文件由 localization.launch.py 的 map:= 参数注入 map_server 的 yaml_filename。
|
|
||||||
|
|
||||||
map_server:
|
|
||||||
ros__parameters:
|
|
||||||
use_sim_time: false
|
|
||||||
yaml_filename: "" # 由 launch 的 map:= 覆盖
|
|
||||||
topic_name: "map"
|
|
||||||
frame_id: "map"
|
|
||||||
|
|
||||||
amcl:
|
|
||||||
ros__parameters:
|
|
||||||
use_sim_time: false
|
|
||||||
# ===== 帧 / 话题 =====
|
|
||||||
odom_model_type: "omni" # 麦轮全向;差速底盘改 "diff"
|
|
||||||
global_frame_id: "map"
|
|
||||||
base_frame_id: "base_footprint"
|
|
||||||
odom_frame_id: "odom"
|
|
||||||
scan_topic: "scan"
|
|
||||||
|
|
||||||
# ===== 运动模型噪声 =====
|
|
||||||
alpha1: 0.05
|
|
||||||
alpha2: 0.05
|
|
||||||
alpha3: 0.1
|
|
||||||
alpha4: 0.1
|
|
||||||
alpha5: 0.05
|
|
||||||
|
|
||||||
# ===== 粒子滤波 =====
|
|
||||||
min_particles: 500
|
|
||||||
max_particles: 1000
|
|
||||||
pf_err: 0.05
|
|
||||||
resample_interval: 5
|
|
||||||
|
|
||||||
# ===== 激光模型(匹配 5×5m 边界)=====
|
|
||||||
laser_model_type: "likelihood_field"
|
|
||||||
laser_max_range: 8.0 # 4×4m 场地对角 ~5.7m,留余量,避免远墙光束被裁掉
|
|
||||||
laser_min_range: -1.0
|
|
||||||
max_beams: 500
|
|
||||||
sigma_hit: 0.2 # 关键:原 0.05 太尖锐,偏差>几cm 就无梯度、AMCL 拉不回来;0.2 加宽收敛域
|
|
||||||
z_hit: 0.9 # 加大“命中”权重,更用力把激光贴到墙上
|
|
||||||
z_rand: 0.1
|
|
||||||
laser_likelihood_max_dist: 2.0 # 似然场范围,加宽收敛域
|
|
||||||
|
|
||||||
# ===== 更新策略 =====
|
|
||||||
update_min_d: 0.02
|
|
||||||
update_min_a: 0.03
|
|
||||||
transform_tolerance: 0.5
|
|
||||||
|
|
||||||
# ===== 恢复:小场地关闭随机粒子注入 =====
|
|
||||||
recovery_alpha_slow: 0.0
|
|
||||||
recovery_alpha_fast: 0.0
|
|
||||||
|
|
||||||
# ===== 初始位姿(按机器人在场地中的起始点设置;可被 launch 覆盖)=====
|
|
||||||
set_initial_pose: true
|
|
||||||
initial_pose:
|
|
||||||
x: 0.0
|
|
||||||
y: 0.0
|
|
||||||
z: 0.0
|
|
||||||
yaw: 0.0
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# gmapping(slam_gmapping) 建图参数 —— 为 4×4m 场地优化
|
|
||||||
# 帧:base_frame=base_footprint, odom_frame=odom, map_frame=map
|
|
||||||
# 地图缩减到 5×5m(场地 4×4m + 各 0.5m 余量),栅格分辨率 1cm
|
|
||||||
/**:
|
|
||||||
ros__parameters:
|
|
||||||
base_frame: base_footprint
|
|
||||||
odom_frame: odom
|
|
||||||
map_frame: map
|
|
||||||
map_update_interval: 5.0
|
|
||||||
maxUrange: 4.0
|
|
||||||
maxRange: 5.0
|
|
||||||
delta: 0.01
|
|
||||||
particles: 50
|
|
||||||
linearUpdate: 0.1
|
|
||||||
angularUpdate: 0.2
|
|
||||||
temporalUpdate: 1.0
|
|
||||||
resampleThreshold: 0.5
|
|
||||||
minimum_score: 0.0
|
|
||||||
# 运动模型噪声
|
|
||||||
srr: 0.1
|
|
||||||
srt: 0.2
|
|
||||||
str: 0.1
|
|
||||||
stt: 0.2
|
|
||||||
# 扫描匹配
|
|
||||||
sigma: 0.05
|
|
||||||
kernelSize: 1
|
|
||||||
lstep: 0.05
|
|
||||||
astep: 0.05
|
|
||||||
iterations: 5
|
|
||||||
lsigma: 0.075
|
|
||||||
ogain: 3.0
|
|
||||||
lskip: 0
|
|
||||||
llsamplerange: 0.01
|
|
||||||
llsamplestep: 0.01
|
|
||||||
lasamplerange: 0.005
|
|
||||||
lasamplestep: 0.005
|
|
||||||
# 地图范围(米)—— 4×4m 场地 + 0.5m 余量
|
|
||||||
xmin: -0.5
|
|
||||||
xmax: 4.5
|
|
||||||
ymin: -0.5
|
|
||||||
ymax: 4.5
|
|
||||||
occ_thresh: 0.25
|
|
||||||
transform_publish_period: 0.05
|
|
||||||
use_sim_time: false
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttylzulaser
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 230400
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 4
|
|
||||||
intensity_bit: 8
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: true
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 12.0
|
|
||||||
range_min: 0.03
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""底盘轮式里程计节点 (chassis_odometry)
|
|
||||||
|
|
||||||
直连底盘 MCU (lzucar 板) 串口 `/dev/ttylzucar`,**只读**解析 32 字节里程计数据包,
|
|
||||||
发布 nav_msgs/Odometry (/odom) 与 TF (odom -> base_footprint)。
|
|
||||||
|
|
||||||
数据包协议(移植自 lzu_robot/src/cgbot/cgbot/seriallzucar.py,已上真机验证的同款底盘):
|
|
||||||
- 帧尾标志 3 字节: 'L', 'Z', 'U'
|
|
||||||
- 整包 32 字节,以 'LZU' 结尾对齐
|
|
||||||
- packet[0] = 校验和 = sum(packet[1:30]) % 256
|
|
||||||
- packet[1:29] = <fffffff> = x, y, yaw, posFR, posFL, posBL, posBR
|
|
||||||
(x/y 单位 mm,需 /1000 转米; yaw 单位弧度)
|
|
||||||
- packet[29] = 模式字节, 'D' 表示航位推算有效
|
|
||||||
- packet[29:32] = 帧尾 'L','Z','U'(对齐用)
|
|
||||||
|
|
||||||
本节点不发送任何命令;既有底盘速度控制 (UDP->ESP32->UART) 不受影响,互不抢占串口。
|
|
||||||
若后续需把底盘命令也迁到直连串口,应在此节点内合并发送逻辑(参考 seriallzucar 的 pack_floats_send)。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import struct
|
|
||||||
import time
|
|
||||||
from threading import Lock, Thread
|
|
||||||
|
|
||||||
import rclpy
|
|
||||||
import serial
|
|
||||||
from geometry_msgs.msg import Quaternion, TransformStamped
|
|
||||||
from nav_msgs.msg import Odometry
|
|
||||||
from rclpy.node import Node
|
|
||||||
from tf2_ros import TransformBroadcaster
|
|
||||||
|
|
||||||
PACKET_SIZE = 32
|
|
||||||
MODE_DEAD_RECKON = ord('D')
|
|
||||||
|
|
||||||
|
|
||||||
def yaw_to_quaternion(yaw: float) -> Quaternion:
|
|
||||||
"""绕 Z 轴偏航角 -> 四元数。"""
|
|
||||||
q = Quaternion()
|
|
||||||
q.x = 0.0
|
|
||||||
q.y = 0.0
|
|
||||||
q.z = math.sin(yaw / 2.0)
|
|
||||||
q.w = math.cos(yaw / 2.0)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
class ChassisOdometry(Node):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__('chassis_odometry')
|
|
||||||
|
|
||||||
# ---- 参数 ----
|
|
||||||
self.port = self.declare_parameter('port', '/dev/ttylzucar').value
|
|
||||||
self.baudrate = int(self.declare_parameter('baudrate', 115200).value)
|
|
||||||
self.odom_frame_id = self.declare_parameter('odom_frame_id', 'odom').value
|
|
||||||
self.base_frame_id = self.declare_parameter('base_frame_id', 'base_footprint').value
|
|
||||||
self.odom_topic = self.declare_parameter('odom_topic', 'odom').value
|
|
||||||
self.publish_tf = bool(self.declare_parameter('publish_tf', True).value)
|
|
||||||
# 仅当模式字节为 'D'(航位推算有效) 时才发布;置 False 可在调试期放行所有包
|
|
||||||
self.require_mode_d = bool(self.declare_parameter('require_mode_d', True).value)
|
|
||||||
# 串口打开后下发的使能命令,让底盘板持续上报里程计;空字符串=不发。
|
|
||||||
# lzu_robot/seriallzucar.open() 与 move_try/serial_bridge 都在打开后发 "UP0LZU",
|
|
||||||
# 缺它则底盘上电后只发几帧就停,odom TF 变陈旧、gmapping 丢弃所有 scan。
|
|
||||||
self.enable_cmd = str(self.declare_parameter('enable_cmd', 'UP0LZU').value)
|
|
||||||
# >0 时按该周期(秒)重发使能命令做保活;0=仅打开时发一次(与参考实现一致)
|
|
||||||
self.enable_cmd_period = float(self.declare_parameter('enable_cmd_period', 0.0).value)
|
|
||||||
|
|
||||||
# ---- 发布器 ----
|
|
||||||
self.odom_pub = self.create_publisher(Odometry, self.odom_topic, 10)
|
|
||||||
self.tf_broadcaster = TransformBroadcaster(self) if self.publish_tf else None
|
|
||||||
|
|
||||||
# ---- 串口接收环形缓冲 ----
|
|
||||||
self.BUFFER_SIZE = 1024
|
|
||||||
self.buffer = bytearray(self.BUFFER_SIZE)
|
|
||||||
self.write_index = 0
|
|
||||||
self.buffer_lock = Lock()
|
|
||||||
|
|
||||||
# ---- 上一帧状态(用于差分求速度)----
|
|
||||||
self.prev_x = None
|
|
||||||
self.prev_y = None
|
|
||||||
self.prev_yaw = None
|
|
||||||
self.prev_t = None
|
|
||||||
|
|
||||||
# ---- 串口 + 接收线程 ----
|
|
||||||
self.ser = None
|
|
||||||
self._running = True
|
|
||||||
self._reader = Thread(target=self._receiver_thread, daemon=True)
|
|
||||||
self._reader.start()
|
|
||||||
|
|
||||||
# 可选:周期性重发使能命令做保活
|
|
||||||
if self.enable_cmd and self.enable_cmd_period > 0.0:
|
|
||||||
self.create_timer(self.enable_cmd_period, self._send_enable_cmd)
|
|
||||||
|
|
||||||
self.get_logger().info(
|
|
||||||
f'chassis_odometry 启动: port={self.port}@{self.baudrate} '
|
|
||||||
f'odom_frame={self.odom_frame_id} base_frame={self.base_frame_id} '
|
|
||||||
f'publish_tf={self.publish_tf}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 串口接收 / 重连
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def _open_serial(self) -> bool:
|
|
||||||
try:
|
|
||||||
self.ser = serial.Serial(self.port, self.baudrate, timeout=1)
|
|
||||||
self.get_logger().info(f'串口已连接 {self.port}')
|
|
||||||
self._send_enable_cmd()
|
|
||||||
return True
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.get_logger().warning(f'串口打开失败 {self.port}: {exc}')
|
|
||||||
self.ser = None
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _send_enable_cmd(self) -> None:
|
|
||||||
"""下发里程计使能命令(默认 UP0LZU);空命令或串口未开则跳过。"""
|
|
||||||
if not self.enable_cmd or self.ser is None or not self.ser.is_open:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.ser.write(self.enable_cmd.encode('utf-8'))
|
|
||||||
self.ser.flush()
|
|
||||||
self.get_logger().info(f'已下发里程计使能命令: {self.enable_cmd}')
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.get_logger().warning(f'使能命令发送失败: {exc}')
|
|
||||||
|
|
||||||
def _receiver_thread(self) -> None:
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
if self.ser is None or not self.ser.is_open:
|
|
||||||
if not self._open_serial():
|
|
||||||
time.sleep(1.0)
|
|
||||||
continue
|
|
||||||
waiting = self.ser.in_waiting
|
|
||||||
if waiting > 0:
|
|
||||||
self._write_to_buffer(self.ser.read(waiting))
|
|
||||||
else:
|
|
||||||
time.sleep(0.005)
|
|
||||||
except serial.SerialException as exc:
|
|
||||||
self.get_logger().warning(f'串口中断,准备重连: {exc}')
|
|
||||||
self._close_serial()
|
|
||||||
time.sleep(1.0)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.get_logger().error(f'接收线程异常: {exc}')
|
|
||||||
self._close_serial()
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
def _close_serial(self) -> None:
|
|
||||||
if self.ser is not None:
|
|
||||||
try:
|
|
||||||
self.ser.close()
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
self.ser = None
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 帧解析(逐字节移植自 seriallzucar,保持已验证行为)
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def _read_byte(self, index: int) -> int:
|
|
||||||
return self.buffer[(index + self.BUFFER_SIZE) % self.BUFFER_SIZE]
|
|
||||||
|
|
||||||
def _read_from_buffer(self, start_index: int, size: int) -> bytearray:
|
|
||||||
data = bytearray()
|
|
||||||
for i in range(size):
|
|
||||||
data.append(self.buffer[(start_index + i) % self.BUFFER_SIZE])
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _write_to_buffer(self, data: bytes) -> None:
|
|
||||||
with self.buffer_lock:
|
|
||||||
for byte in data:
|
|
||||||
self.buffer[self.write_index] = byte
|
|
||||||
# 检测帧尾 'L','Z','U'
|
|
||||||
if (byte == ord('U')
|
|
||||||
and self._read_byte(self.write_index - 1) == ord('Z')
|
|
||||||
and self._read_byte(self.write_index - 2) == ord('L')):
|
|
||||||
start_index = (self.write_index - PACKET_SIZE + self.BUFFER_SIZE) % self.BUFFER_SIZE
|
|
||||||
packet = self._read_from_buffer(start_index, PACKET_SIZE)
|
|
||||||
self._process_packet(packet)
|
|
||||||
self.write_index = (self.write_index + 1) % self.BUFFER_SIZE
|
|
||||||
|
|
||||||
def _process_packet(self, packet: bytearray) -> None:
|
|
||||||
# 校验和
|
|
||||||
if (sum(packet[1:30]) % 256) != packet[0]:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
x, y, yaw, _fr, _fl, _bl, _br = struct.unpack('<fffffff', packet[1:29])
|
|
||||||
mode = packet[29]
|
|
||||||
except struct.error:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.require_mode_d and mode != MODE_DEAD_RECKON:
|
|
||||||
return
|
|
||||||
|
|
||||||
x /= 1000.0 # mm -> m
|
|
||||||
y /= 1000.0
|
|
||||||
self._publish(x, y, yaw)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# 发布 Odometry + TF
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def _publish(self, x: float, y: float, yaw: float) -> None:
|
|
||||||
now = self.get_clock().now()
|
|
||||||
stamp = now.to_msg()
|
|
||||||
quat = yaw_to_quaternion(yaw)
|
|
||||||
|
|
||||||
# 差分求速度(body 系);首帧或 dt 过小时置零
|
|
||||||
vx = vy = wz = 0.0
|
|
||||||
t = now.nanoseconds * 1e-9
|
|
||||||
if self.prev_t is not None:
|
|
||||||
dt = t - self.prev_t
|
|
||||||
if dt > 1e-3:
|
|
||||||
dx = x - self.prev_x
|
|
||||||
dy = y - self.prev_y
|
|
||||||
dyaw = math.atan2(math.sin(yaw - self.prev_yaw),
|
|
||||||
math.cos(yaw - self.prev_yaw))
|
|
||||||
# 世界系位移旋到 body 系
|
|
||||||
vx = (dx * math.cos(yaw) + dy * math.sin(yaw)) / dt
|
|
||||||
vy = (-dx * math.sin(yaw) + dy * math.cos(yaw)) / dt
|
|
||||||
wz = dyaw / dt
|
|
||||||
self.prev_x, self.prev_y, self.prev_yaw, self.prev_t = x, y, yaw, t
|
|
||||||
|
|
||||||
odom = Odometry()
|
|
||||||
odom.header.stamp = stamp
|
|
||||||
odom.header.frame_id = self.odom_frame_id
|
|
||||||
odom.child_frame_id = self.base_frame_id
|
|
||||||
odom.pose.pose.position.x = x
|
|
||||||
odom.pose.pose.position.y = y
|
|
||||||
odom.pose.pose.position.z = 0.0
|
|
||||||
odom.pose.pose.orientation = quat
|
|
||||||
odom.twist.twist.linear.x = vx
|
|
||||||
odom.twist.twist.linear.y = vy
|
|
||||||
odom.twist.twist.angular.z = wz
|
|
||||||
# 平面机器人协方差: x,y,yaw 给小值, z/roll/pitch 不可观给大值
|
|
||||||
odom.pose.covariance[0] = 0.01 # x
|
|
||||||
odom.pose.covariance[7] = 0.01 # y
|
|
||||||
odom.pose.covariance[14] = 1e6 # z
|
|
||||||
odom.pose.covariance[21] = 1e6 # roll
|
|
||||||
odom.pose.covariance[28] = 1e6 # pitch
|
|
||||||
odom.pose.covariance[35] = 0.02 # yaw
|
|
||||||
odom.twist.covariance[0] = 0.01
|
|
||||||
odom.twist.covariance[7] = 0.01
|
|
||||||
odom.twist.covariance[35] = 0.02
|
|
||||||
self.odom_pub.publish(odom)
|
|
||||||
|
|
||||||
if self.tf_broadcaster is not None:
|
|
||||||
tf = TransformStamped()
|
|
||||||
tf.header.stamp = stamp
|
|
||||||
tf.header.frame_id = self.odom_frame_id
|
|
||||||
tf.child_frame_id = self.base_frame_id
|
|
||||||
tf.transform.translation.x = x
|
|
||||||
tf.transform.translation.y = y
|
|
||||||
tf.transform.translation.z = 0.0
|
|
||||||
tf.transform.rotation = quat
|
|
||||||
self.tf_broadcaster.sendTransform(tf)
|
|
||||||
|
|
||||||
def destroy_node(self) -> bool:
|
|
||||||
self._running = False
|
|
||||||
self._close_serial()
|
|
||||||
return super().destroy_node()
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None) -> None:
|
|
||||||
rclpy.init(args=args)
|
|
||||||
node = ChassisOdometry()
|
|
||||||
try:
|
|
||||||
rclpy.spin(node)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
node.destroy_node()
|
|
||||||
if rclpy.ok():
|
|
||||||
rclpy.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""导航程序 (navigate_to_point):自动驱动麦轮底盘到示教记录的点位。
|
|
||||||
|
|
||||||
- 目标点:读取 teach_points 生成的 taught_points.yaml(map 系 x,y,yaw)。
|
|
||||||
- 反馈:TF map->base_footprint(= AMCL 实时定位),需 localization.launch.py 在运行。
|
|
||||||
- 控制:holonomic P 控制器,把 map 系位姿误差投影到机体系,生成 (前进, 侧移, 转向)
|
|
||||||
速度,按 udp_teleop 的 XYW 约定经 UDP→ESP32 下发,到达容差内停车。
|
|
||||||
|
|
||||||
速度符号默认按 udp_teleop/keyboard_control 推导:
|
|
||||||
前进 W→chassis_y 取负, 左移 A→chassis_x 取负, 左转(CCW) Q→chassis_w 取负
|
|
||||||
=> sign_x = sign_y = sign_w = -1。若实车方向相反,翻转对应 sign 参数即可。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
# 交互选点
|
|
||||||
ros2 run craic_localization navigate_to_point --ros-args \
|
|
||||||
-p points_file:=$PWD/src/craic_localization/config/taught_points.yaml
|
|
||||||
# 一次性去某点
|
|
||||||
ros2 run craic_localization navigate_to_point --ros-args -p points_file:=... -p goal:=B_1
|
|
||||||
# 运行中用话题指定(供上层任务程序调用)
|
|
||||||
ros2 topic pub -1 /goto std_msgs/String "{data: 'C_1'}"
|
|
||||||
# 干跑(只打印命令不发,安全验证逻辑/符号)
|
|
||||||
ros2 run craic_localization navigate_to_point --ros-args -p points_file:=... -p dry_run:=true
|
|
||||||
|
|
||||||
安全:首次务必先 dry_run,再低速单点测试,手放 Ctrl+C(退出会自动发停车)。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import rclpy
|
|
||||||
import yaml
|
|
||||||
from rclpy.duration import Duration
|
|
||||||
from rclpy.node import Node
|
|
||||||
from rclpy.time import Time
|
|
||||||
from std_msgs.msg import String
|
|
||||||
from tf2_ros import (
|
|
||||||
Buffer,
|
|
||||||
ConnectivityException,
|
|
||||||
ExtrapolationException,
|
|
||||||
LookupException,
|
|
||||||
TransformListener,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def quat_to_yaw(qx, qy, qz, qw):
|
|
||||||
return math.atan2(2.0 * (qw * qz + qx * qy), 1.0 - 2.0 * (qy * qy + qz * qz))
|
|
||||||
|
|
||||||
|
|
||||||
def norm_angle(a):
|
|
||||||
return math.atan2(math.sin(a), math.cos(a))
|
|
||||||
|
|
||||||
|
|
||||||
def clamp(v, lo, hi):
|
|
||||||
return max(lo, min(hi, v))
|
|
||||||
|
|
||||||
|
|
||||||
class NavigateToPoint(Node):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__('navigate_to_point')
|
|
||||||
p = self.declare_parameter
|
|
||||||
self.points_file = p('points_file', 'taught_points.yaml').value
|
|
||||||
self.udp_ip = p('udp_ip', '192.168.4.1').value
|
|
||||||
self.udp_port = int(p('udp_port', 8888).value)
|
|
||||||
self.map_frame = p('map_frame', 'map').value
|
|
||||||
self.base_frame = p('base_frame', 'base_footprint').value
|
|
||||||
self.control_rate = float(p('control_rate', 20.0).value)
|
|
||||||
self.max_linear = float(p('max_linear', 60.0).value) # 麦轮线速度命令上限(同 keyboard ~100)
|
|
||||||
self.max_angular = float(p('max_angular', 30.0).value) # 角速度命令上限(同 keyboard ~45)
|
|
||||||
self.kp_linear = float(p('kp_linear', 150.0).value) # m -> 命令单位
|
|
||||||
self.kp_angular = float(p('kp_angular', 40.0).value) # rad -> 命令单位
|
|
||||||
self.pos_tol = float(p('pos_tolerance', 0.05).value) # 到位容差(米)
|
|
||||||
self.yaw_tol = float(p('yaw_tolerance', 0.05).value) # 到位容差(弧度)
|
|
||||||
self.sign_x = float(p('sign_x', -1.0).value) # 侧移符号(机体左为正)
|
|
||||||
self.sign_y = float(p('sign_y', -1.0).value) # 前进符号(机体前为正)
|
|
||||||
self.sign_w = float(p('sign_w', -1.0).value) # 转向符号(CCW 为正)
|
|
||||||
self.goal_timeout = float(p('goal_timeout', 30.0).value)
|
|
||||||
self.dry_run = bool(p('dry_run', False).value)
|
|
||||||
goal_arg = str(p('goal', '').value)
|
|
||||||
|
|
||||||
self.points = self._load_points()
|
|
||||||
self.tf_buffer = Buffer()
|
|
||||||
self.tf_listener = TransformListener(self.tf_buffer, self)
|
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
self.goal_name = None
|
|
||||||
self.goal = None # (x, y, yaw)
|
|
||||||
self.goal_deadline = None
|
|
||||||
self.one_shot = bool(goal_arg)
|
|
||||||
self.done = False # one_shot 完成标志
|
|
||||||
|
|
||||||
self.create_subscription(String, 'goto', self._goto_cb, 10)
|
|
||||||
self.timer = self.create_timer(1.0 / self.control_rate, self._control_tick)
|
|
||||||
|
|
||||||
self.get_logger().info(
|
|
||||||
f'navigate_to_point 就绪 (UDP {self.udp_ip}:{self.udp_port}, dry_run={self.dry_run})')
|
|
||||||
if goal_arg:
|
|
||||||
self.set_goal(goal_arg)
|
|
||||||
|
|
||||||
# -------- 点位 / 目标 -------- #
|
|
||||||
def _load_points(self):
|
|
||||||
try:
|
|
||||||
with open(self.points_file, 'r', encoding='utf-8') as f:
|
|
||||||
data = yaml.safe_load(f) or {}
|
|
||||||
pts = dict(data.get('points', {}))
|
|
||||||
self.get_logger().info(f'载入 {len(pts)} 个点: {", ".join(pts) or "(空)"}')
|
|
||||||
return pts
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.get_logger().error(f'读取点位文件失败 {self.points_file}: {exc}')
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def set_goal(self, name):
|
|
||||||
pt = self.points.get(name)
|
|
||||||
if pt is None:
|
|
||||||
self.get_logger().warn(f'未知点位: {name}(可用: {", ".join(self.points)})')
|
|
||||||
return False
|
|
||||||
with self._lock:
|
|
||||||
self.goal_name = name
|
|
||||||
self.goal = (float(pt['x']), float(pt['y']), float(pt.get('yaw', 0.0)))
|
|
||||||
self.goal_deadline = self.get_clock().now() + Duration(seconds=self.goal_timeout)
|
|
||||||
self.get_logger().info(
|
|
||||||
f'前往 {name}: x={self.goal[0]:.3f} y={self.goal[1]:.3f} yaw={self.goal[2]:.3f}')
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _goto_cb(self, msg):
|
|
||||||
self.set_goal(msg.data.strip())
|
|
||||||
|
|
||||||
# -------- 反馈 / 下发 -------- #
|
|
||||||
def _lookup(self):
|
|
||||||
try:
|
|
||||||
t = self.tf_buffer.lookup_transform(self.map_frame, self.base_frame, Time())
|
|
||||||
except (LookupException, ConnectivityException, ExtrapolationException):
|
|
||||||
return None
|
|
||||||
tr = t.transform.translation
|
|
||||||
q = t.transform.rotation
|
|
||||||
return (tr.x, tr.y, quat_to_yaw(q.x, q.y, q.z, q.w))
|
|
||||||
|
|
||||||
def _send(self, cx, cy, cw):
|
|
||||||
cmd = f'XYW:{int(round(cx))}:{int(round(cy))}:{int(round(cw))}:XZHY\n'.encode()
|
|
||||||
if self.dry_run:
|
|
||||||
self.get_logger().info(f'[dry] {cmd.decode().strip()}')
|
|
||||||
else:
|
|
||||||
self.sock.sendto(cmd, (self.udp_ip, self.udp_port))
|
|
||||||
|
|
||||||
def _stop(self):
|
|
||||||
self._send(0, 0, 0)
|
|
||||||
|
|
||||||
# -------- 控制环 -------- #
|
|
||||||
def _control_tick(self):
|
|
||||||
with self._lock:
|
|
||||||
goal, deadline, name = self.goal, self.goal_deadline, self.goal_name
|
|
||||||
if goal is None:
|
|
||||||
return
|
|
||||||
if self.get_clock().now() > deadline:
|
|
||||||
self.get_logger().warn(f'{name} 超时未到达,停车')
|
|
||||||
self._finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
pose = self._lookup()
|
|
||||||
if pose is None:
|
|
||||||
self._stop()
|
|
||||||
self.get_logger().warn('无定位 (map->base_footprint),停车等待…',
|
|
||||||
throttle_duration_sec=2.0)
|
|
||||||
return
|
|
||||||
|
|
||||||
px, py, pyaw = pose
|
|
||||||
tx, ty, tyaw = goal
|
|
||||||
dx, dy = tx - px, ty - py
|
|
||||||
dist = math.hypot(dx, dy)
|
|
||||||
dyaw = norm_angle(tyaw - pyaw)
|
|
||||||
|
|
||||||
if dist < self.pos_tol and abs(dyaw) < self.yaw_tol:
|
|
||||||
self.get_logger().info(
|
|
||||||
f'到达 {name}(位置误差 {dist * 100:.1f}cm,朝向误差 {math.degrees(dyaw):.1f}°)')
|
|
||||||
self._finish()
|
|
||||||
return
|
|
||||||
|
|
||||||
# map 系误差投影到机体系(x 前, y 左, REP-103)
|
|
||||||
e_fwd = dx * math.cos(pyaw) + dy * math.sin(pyaw)
|
|
||||||
e_left = -dx * math.sin(pyaw) + dy * math.cos(pyaw)
|
|
||||||
|
|
||||||
v_fwd = self.kp_linear * e_fwd
|
|
||||||
v_left = self.kp_linear * e_left
|
|
||||||
# 限制平移合速度(防止 x+y 叠加超限)
|
|
||||||
speed = math.hypot(v_fwd, v_left)
|
|
||||||
if speed > self.max_linear:
|
|
||||||
s = self.max_linear / speed
|
|
||||||
v_fwd *= s
|
|
||||||
v_left *= s
|
|
||||||
# 已到位置容差内则只修朝向
|
|
||||||
if dist < self.pos_tol:
|
|
||||||
v_fwd = v_left = 0.0
|
|
||||||
w = clamp(self.kp_angular * dyaw, -self.max_angular, self.max_angular)
|
|
||||||
|
|
||||||
self.get_logger().info(
|
|
||||||
f'{name}: 距离 {dist * 100:.1f}cm, 朝向差 {math.degrees(dyaw):.1f}°',
|
|
||||||
throttle_duration_sec=0.5)
|
|
||||||
|
|
||||||
# 机体速度 -> XYW(侧移=chassis_x, 前进=chassis_y, 转向=chassis_w)
|
|
||||||
self._send(self.sign_x * v_left, self.sign_y * v_fwd, self.sign_w * w)
|
|
||||||
|
|
||||||
def _finish(self):
|
|
||||||
self._stop()
|
|
||||||
with self._lock:
|
|
||||||
self.goal = self.goal_name = self.goal_deadline = None
|
|
||||||
if self.one_shot:
|
|
||||||
self.done = True
|
|
||||||
|
|
||||||
def destroy_node(self):
|
|
||||||
try:
|
|
||||||
self._stop()
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
return super().destroy_node()
|
|
||||||
|
|
||||||
|
|
||||||
def interactive(node: NavigateToPoint, stop_flag: threading.Event):
|
|
||||||
while not stop_flag.is_set() and rclpy.ok():
|
|
||||||
try:
|
|
||||||
line = input(f'去哪个点? ({", ".join(node.points) or "无"} | q 退出) > ').strip()
|
|
||||||
except EOFError:
|
|
||||||
break
|
|
||||||
low = line.lower()
|
|
||||||
if low in ('q', 'quit', 'exit'):
|
|
||||||
stop_flag.set()
|
|
||||||
break
|
|
||||||
elif low in ('s', 'stop'):
|
|
||||||
node._finish()
|
|
||||||
elif line:
|
|
||||||
node.set_goal(line)
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
rclpy.init(args=args)
|
|
||||||
node = NavigateToPoint()
|
|
||||||
stop_flag = threading.Event()
|
|
||||||
if not node.one_shot:
|
|
||||||
threading.Thread(target=interactive, args=(node, stop_flag), daemon=True).start()
|
|
||||||
try:
|
|
||||||
while rclpy.ok() and not stop_flag.is_set() and not (node.one_shot and node.done):
|
|
||||||
rclpy.spin_once(node, timeout_sec=0.1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
node._stop()
|
|
||||||
node.destroy_node()
|
|
||||||
if rclpy.ok():
|
|
||||||
rclpy.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""在 rviz 显示所有示教点位:读 taught_points.yaml,发布 MarkerArray。
|
|
||||||
|
|
||||||
每个点画:① 箭头 = 位置 + 朝向(yaw);② 文字 = 点名。frame_id = map。
|
|
||||||
周期性重读文件并发布,因此示教(teach_points)时新增/删除的点会实时反映到 rviz。
|
|
||||||
|
|
||||||
rviz 里加 MarkerArray 显示、话题 /taught_points(localization.rviz 已内置该显示)。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
ros2 run craic_localization show_points --ros-args \
|
|
||||||
-p points_file:=$PWD/src/craic_localization/config/taught_points.yaml
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
|
|
||||||
import rclpy
|
|
||||||
import yaml
|
|
||||||
from geometry_msgs.msg import Quaternion
|
|
||||||
from rclpy.node import Node
|
|
||||||
from visualization_msgs.msg import Marker, MarkerArray
|
|
||||||
|
|
||||||
|
|
||||||
def yaw_to_quat(yaw):
|
|
||||||
q = Quaternion()
|
|
||||||
q.z = math.sin(yaw / 2.0)
|
|
||||||
q.w = math.cos(yaw / 2.0)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
class ShowPoints(Node):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__('show_points')
|
|
||||||
self.points_file = os.path.abspath(
|
|
||||||
self.declare_parameter('points_file', 'taught_points.yaml').value)
|
|
||||||
self.frame_id = self.declare_parameter('frame_id', 'map').value
|
|
||||||
self.rate = float(self.declare_parameter('rate', 1.0).value)
|
|
||||||
|
|
||||||
self.pub = self.create_publisher(MarkerArray, 'taught_points', 1)
|
|
||||||
self.timer = self.create_timer(1.0 / self.rate, self._tick)
|
|
||||||
self.get_logger().info(
|
|
||||||
f'show_points: {self.points_file} -> /taught_points (frame={self.frame_id})')
|
|
||||||
|
|
||||||
def _load(self):
|
|
||||||
try:
|
|
||||||
with open(self.points_file, 'r', encoding='utf-8') as f:
|
|
||||||
data = yaml.safe_load(f) or {}
|
|
||||||
return dict(data.get('points', {}))
|
|
||||||
except FileNotFoundError:
|
|
||||||
return {}
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
self.get_logger().warn(f'读取点位失败: {exc}', throttle_duration_sec=5.0)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _tick(self):
|
|
||||||
pts = self._load()
|
|
||||||
now = self.get_clock().now().to_msg()
|
|
||||||
arr = MarkerArray()
|
|
||||||
|
|
||||||
# 先清除上一帧(与新增放同一条消息,rviz 一次性处理,无闪烁),自动反映删除
|
|
||||||
clear = Marker()
|
|
||||||
clear.header.frame_id = self.frame_id
|
|
||||||
clear.action = Marker.DELETEALL
|
|
||||||
arr.markers.append(clear)
|
|
||||||
|
|
||||||
for i, (name, p) in enumerate(pts.items()):
|
|
||||||
try:
|
|
||||||
x, y = float(p['x']), float(p['y'])
|
|
||||||
yaw = float(p.get('yaw', 0.0))
|
|
||||||
except (KeyError, TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 箭头:位置 + 朝向
|
|
||||||
a = Marker()
|
|
||||||
a.header.frame_id = self.frame_id
|
|
||||||
a.header.stamp = now
|
|
||||||
a.ns = 'arrow'
|
|
||||||
a.id = i
|
|
||||||
a.type = Marker.ARROW
|
|
||||||
a.action = Marker.ADD
|
|
||||||
a.pose.position.x = x
|
|
||||||
a.pose.position.y = y
|
|
||||||
a.pose.position.z = 0.05
|
|
||||||
a.pose.orientation = yaw_to_quat(yaw)
|
|
||||||
a.scale.x = 0.25 # 杆长
|
|
||||||
a.scale.y = 0.04 # 杆径
|
|
||||||
a.scale.z = 0.04 # 头径
|
|
||||||
a.color.r, a.color.g, a.color.b, a.color.a = 0.1, 0.8, 1.0, 1.0 # 青色
|
|
||||||
arr.markers.append(a)
|
|
||||||
|
|
||||||
# 文字:点名
|
|
||||||
t = Marker()
|
|
||||||
t.header.frame_id = self.frame_id
|
|
||||||
t.header.stamp = now
|
|
||||||
t.ns = 'label'
|
|
||||||
t.id = i
|
|
||||||
t.type = Marker.TEXT_VIEW_FACING
|
|
||||||
t.action = Marker.ADD
|
|
||||||
t.pose.position.x = x
|
|
||||||
t.pose.position.y = y
|
|
||||||
t.pose.position.z = 0.22
|
|
||||||
t.pose.orientation.w = 1.0
|
|
||||||
t.scale.z = 0.12 # 字高
|
|
||||||
t.color.r, t.color.g, t.color.b, t.color.a = 1.0, 1.0, 1.0, 1.0
|
|
||||||
t.text = name
|
|
||||||
arr.markers.append(t)
|
|
||||||
|
|
||||||
self.pub.publish(arr)
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
rclpy.init(args=args)
|
|
||||||
node = ShowPoints()
|
|
||||||
try:
|
|
||||||
rclpy.spin(node)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
node.destroy_node()
|
|
||||||
if rclpy.ok():
|
|
||||||
rclpy.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""示教程序 (teach_points):记录机器人在 map 坐标系下的位姿,存为 YAML 供导航复用。
|
|
||||||
|
|
||||||
依赖定位栈在运行(localization.launch.py 提供 AMCL),位姿来源为 TF: map -> base_footprint
|
|
||||||
(= AMCL 实时定位结果)。用法是另开终端用 keyboard_control 把车开到目标点,回到本程序按回车记录。
|
|
||||||
|
|
||||||
预设要记录的点(可用参数 points 覆盖):B_1..B_6, C_1, D_1, E_1, F_1。
|
|
||||||
也可随时用 `name <名字>` 记录任意自定义点。
|
|
||||||
|
|
||||||
运行:
|
|
||||||
# 终端1:定位栈
|
|
||||||
ros2 launch craic_localization localization.launch.py map:=.../craic.yaml
|
|
||||||
# 终端2:遥控(开车到点)
|
|
||||||
ros2 run udp_teleop keyboard_control --ros-args --params-file src/udp_teleop/config/params.yaml
|
|
||||||
# 终端3:示教(记录),建议把结果存到包的 config 下供导航读取
|
|
||||||
ros2 run craic_localization teach_points --ros-args \
|
|
||||||
-p output_file:=$PWD/src/craic_localization/config/taught_points.yaml
|
|
||||||
|
|
||||||
交互命令:
|
|
||||||
回车 / r 记录"当前待记录点"的位姿,并前进到下一个
|
|
||||||
p 只打印当前位姿,不记录
|
|
||||||
name <X> 把当前位姿记录为自定义名字 X(已存在则覆盖)
|
|
||||||
del <X> 删除已记录的点 X
|
|
||||||
list 列出已记录的点
|
|
||||||
skip / back 跳过 / 回退当前待记录点
|
|
||||||
save 立即保存到文件
|
|
||||||
h 帮助
|
|
||||||
q 保存并退出
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import rclpy
|
|
||||||
import yaml
|
|
||||||
from rclpy.duration import Duration
|
|
||||||
from rclpy.node import Node
|
|
||||||
from rclpy.time import Time
|
|
||||||
from tf2_ros import (
|
|
||||||
Buffer,
|
|
||||||
ConnectivityException,
|
|
||||||
ExtrapolationException,
|
|
||||||
LookupException,
|
|
||||||
TransformListener,
|
|
||||||
)
|
|
||||||
|
|
||||||
DEFAULT_POINTS = ['B_1', 'B_2', 'B_3', 'B_4', 'B_5', 'B_6', 'C_1', 'D_1', 'E_1', 'F_1']
|
|
||||||
|
|
||||||
|
|
||||||
def quat_to_yaw(qx, qy, qz, qw):
|
|
||||||
"""四元数 -> 绕 Z 偏航角(弧度)。"""
|
|
||||||
return math.atan2(2.0 * (qw * qz + qx * qy), 1.0 - 2.0 * (qy * qy + qz * qz))
|
|
||||||
|
|
||||||
|
|
||||||
class TeachPoints(Node):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__('teach_points')
|
|
||||||
self.map_frame = self.declare_parameter('map_frame', 'map').value
|
|
||||||
self.base_frame = self.declare_parameter('base_frame', 'base_footprint').value
|
|
||||||
self.output_file = os.path.abspath(
|
|
||||||
self.declare_parameter('output_file', 'taught_points.yaml').value)
|
|
||||||
self.points = list(self.declare_parameter('points', DEFAULT_POINTS).value)
|
|
||||||
|
|
||||||
self.tf_buffer = Buffer()
|
|
||||||
self.tf_listener = TransformListener(self.tf_buffer, self)
|
|
||||||
|
|
||||||
self.recorded = {} # name -> {x, y, yaw}
|
|
||||||
self._load_existing()
|
|
||||||
|
|
||||||
# -------- 位姿读取 -------- #
|
|
||||||
def lookup_pose(self):
|
|
||||||
"""读取 map->base_frame 的最新变换,返回 ({x,y,yaw}, None) 或 (None, 错误信息)。"""
|
|
||||||
try:
|
|
||||||
t = self.tf_buffer.lookup_transform(
|
|
||||||
self.map_frame, self.base_frame, Time(),
|
|
||||||
timeout=Duration(seconds=1.0))
|
|
||||||
except (LookupException, ConnectivityException, ExtrapolationException) as exc:
|
|
||||||
return None, str(exc)
|
|
||||||
tr = t.transform.translation
|
|
||||||
q = t.transform.rotation
|
|
||||||
yaw = quat_to_yaw(q.x, q.y, q.z, q.w)
|
|
||||||
return {'x': round(tr.x, 4), 'y': round(tr.y, 4), 'yaw': round(yaw, 4)}, None
|
|
||||||
|
|
||||||
# -------- 文件读写 -------- #
|
|
||||||
def _load_existing(self):
|
|
||||||
if os.path.isfile(self.output_file):
|
|
||||||
try:
|
|
||||||
with open(self.output_file, 'r', encoding='utf-8') as f:
|
|
||||||
data = yaml.safe_load(f) or {}
|
|
||||||
self.recorded = dict(data.get('points', {}))
|
|
||||||
if self.recorded:
|
|
||||||
print(f'已载入已有记录 {len(self.recorded)} 个点:{self.output_file}')
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
print(f'载入已有文件失败(忽略):{exc}')
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
os.makedirs(os.path.dirname(self.output_file) or '.', exist_ok=True)
|
|
||||||
header = (
|
|
||||||
'# 示教记录的点位(map 坐标系;x,y 单位米,yaw 单位弧度)\n'
|
|
||||||
'# 由 craic_localization/teach_points 生成\n'
|
|
||||||
)
|
|
||||||
body = yaml.safe_dump({'points': self.recorded},
|
|
||||||
default_flow_style=False, sort_keys=False,
|
|
||||||
allow_unicode=True)
|
|
||||||
with open(self.output_file, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(header + body)
|
|
||||||
|
|
||||||
|
|
||||||
HELP = __doc__.split('交互命令:', 1)[1]
|
|
||||||
|
|
||||||
|
|
||||||
def interactive(node: TeachPoints):
|
|
||||||
idx = 0
|
|
||||||
print('=' * 60)
|
|
||||||
print(' 示教程序 teach_points —— 记录 map 系下位姿')
|
|
||||||
print(f' 输出文件: {node.output_file}')
|
|
||||||
print(f' 预设点: {", ".join(node.points)}')
|
|
||||||
print(' (输入 h 查看命令)')
|
|
||||||
print('=' * 60)
|
|
||||||
|
|
||||||
while rclpy.ok():
|
|
||||||
target = node.points[idx] if idx < len(node.points) else None
|
|
||||||
tag = target if target else '(预设已完,用 name <X> 记录自定义点)'
|
|
||||||
try:
|
|
||||||
line = input(f'[已记录 {len(node.recorded)}] 下一个: {tag} > ').strip()
|
|
||||||
except EOFError:
|
|
||||||
break
|
|
||||||
cmd = line.lower()
|
|
||||||
|
|
||||||
if cmd in ('q', 'quit', 'exit'):
|
|
||||||
node.save()
|
|
||||||
print(f'已保存到 {node.output_file},退出。')
|
|
||||||
break
|
|
||||||
|
|
||||||
elif cmd in ('', 'r'):
|
|
||||||
if target is None:
|
|
||||||
print(' 预设点已记录完。用 "name <X>" 记录自定义点,或 q 退出。')
|
|
||||||
continue
|
|
||||||
pose, err = node.lookup_pose()
|
|
||||||
if err:
|
|
||||||
print(f' ✗ 读取位姿失败(定位栈在运行吗?): {err}')
|
|
||||||
continue
|
|
||||||
node.recorded[target] = pose
|
|
||||||
node.save()
|
|
||||||
print(f' ✓ {target} = x:{pose["x"]} y:{pose["y"]} '
|
|
||||||
f'yaw:{pose["yaw"]}rad ({math.degrees(pose["yaw"]):.1f}°) [已存盘]')
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
elif cmd == 'p':
|
|
||||||
pose, err = node.lookup_pose()
|
|
||||||
if err:
|
|
||||||
print(f' ✗ 读取位姿失败: {err}')
|
|
||||||
else:
|
|
||||||
print(f' 当前位姿 x:{pose["x"]} y:{pose["y"]} '
|
|
||||||
f'yaw:{pose["yaw"]}rad ({math.degrees(pose["yaw"]):.1f}°)')
|
|
||||||
|
|
||||||
elif cmd.startswith('name '):
|
|
||||||
name = line[5:].strip()
|
|
||||||
if not name:
|
|
||||||
print(' 用法: name <名字>')
|
|
||||||
continue
|
|
||||||
pose, err = node.lookup_pose()
|
|
||||||
if err:
|
|
||||||
print(f' ✗ 读取位姿失败: {err}')
|
|
||||||
continue
|
|
||||||
node.recorded[name] = pose
|
|
||||||
node.save()
|
|
||||||
print(f' ✓ {name} = x:{pose["x"]} y:{pose["y"]} '
|
|
||||||
f'yaw:{pose["yaw"]}rad [已存盘]')
|
|
||||||
|
|
||||||
elif cmd.startswith('del '):
|
|
||||||
name = line[4:].strip()
|
|
||||||
if node.recorded.pop(name, None) is not None:
|
|
||||||
node.save()
|
|
||||||
print(f' 已删除 {name} [已存盘]')
|
|
||||||
else:
|
|
||||||
print(f' 没有名为 {name} 的记录')
|
|
||||||
|
|
||||||
elif cmd == 'list':
|
|
||||||
if not node.recorded:
|
|
||||||
print(' (空)')
|
|
||||||
for k, v in node.recorded.items():
|
|
||||||
print(f' {k}: x:{v["x"]} y:{v["y"]} yaw:{v["yaw"]}')
|
|
||||||
|
|
||||||
elif cmd == 'skip':
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
elif cmd == 'back':
|
|
||||||
idx = max(0, idx - 1)
|
|
||||||
|
|
||||||
elif cmd == 'save':
|
|
||||||
node.save()
|
|
||||||
print(f' 已保存到 {node.output_file}')
|
|
||||||
|
|
||||||
elif cmd in ('h', 'help', '?'):
|
|
||||||
print(HELP)
|
|
||||||
|
|
||||||
else:
|
|
||||||
print(' 未知命令,输入 h 查看帮助。')
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
rclpy.init(args=args)
|
|
||||||
node = TeachPoints()
|
|
||||||
# 后台 spin 以填充 TF 缓冲
|
|
||||||
spin_thread = threading.Thread(target=rclpy.spin, args=(node,), daemon=True)
|
|
||||||
spin_thread.start()
|
|
||||||
try:
|
|
||||||
interactive(node)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
node.save()
|
|
||||||
print(f'\n中断,已保存到 {node.output_file}')
|
|
||||||
finally:
|
|
||||||
node.destroy_node()
|
|
||||||
if rclpy.ok():
|
|
||||||
rclpy.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""定位栈总 bringup:轮式里程计 + 激光雷达 + (可选) rviz。
|
|
||||||
|
|
||||||
组成完整 TF 树:odom ──(chassis_odometry)──> base_footprint ──(静态外参)──> laser_frame
|
|
||||||
用于 P3 激光外参标定与传感器联调:在 rviz 里同时看 /odom 与 /scan,
|
|
||||||
调 lidar_yaw/lidar_x/lidar_y 直到激光轮廓与实际场地吻合。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
ros2 launch craic_localization bringup.launch.py
|
|
||||||
ros2 launch craic_localization bringup.launch.py use_rviz:=false
|
|
||||||
ros2 launch craic_localization bringup.launch.py lidar_yaw:=-1.5707963 intensity:=true
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
|
|
||||||
from launch.conditions import IfCondition
|
|
||||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
pkg_share = get_package_share_directory('craic_localization')
|
|
||||||
lidar_launch = os.path.join(pkg_share, 'launch', 'lidar.launch.py')
|
|
||||||
default_rviz = os.path.join(pkg_share, 'rviz', 'localization.rviz')
|
|
||||||
|
|
||||||
use_rviz = LaunchConfiguration('use_rviz')
|
|
||||||
chassis_port = LaunchConfiguration('chassis_port')
|
|
||||||
rviz_config = LaunchConfiguration('rviz_config')
|
|
||||||
lidar_x = LaunchConfiguration('lidar_x')
|
|
||||||
lidar_y = LaunchConfiguration('lidar_y')
|
|
||||||
lidar_z = LaunchConfiguration('lidar_z')
|
|
||||||
lidar_yaw = LaunchConfiguration('lidar_yaw')
|
|
||||||
intensity = LaunchConfiguration('intensity')
|
|
||||||
sample_rate = LaunchConfiguration('sample_rate')
|
|
||||||
baudrate = LaunchConfiguration('baudrate')
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
DeclareLaunchArgument('use_rviz', default_value='true'),
|
|
||||||
DeclareLaunchArgument('chassis_port', default_value='/dev/ttylzucar'),
|
|
||||||
DeclareLaunchArgument('rviz_config', default_value=default_rviz),
|
|
||||||
DeclareLaunchArgument('lidar_x', default_value='0.0'),
|
|
||||||
DeclareLaunchArgument('lidar_y', default_value='0.0'),
|
|
||||||
DeclareLaunchArgument('lidar_z', default_value='0.02'),
|
|
||||||
DeclareLaunchArgument('lidar_yaw', default_value='-3.14159'),
|
|
||||||
DeclareLaunchArgument('intensity', default_value='true'),
|
|
||||||
DeclareLaunchArgument('sample_rate', default_value='4'),
|
|
||||||
DeclareLaunchArgument('baudrate', default_value='230400'),
|
|
||||||
|
|
||||||
# 轮式里程计:odom -> base_footprint
|
|
||||||
Node(
|
|
||||||
package='craic_localization',
|
|
||||||
executable='chassis_odometry',
|
|
||||||
name='chassis_odometry',
|
|
||||||
output='screen',
|
|
||||||
parameters=[{'port': chassis_port}],
|
|
||||||
),
|
|
||||||
|
|
||||||
# 激光雷达 + base_footprint -> laser_frame 静态外参(参数见 lidar.launch.py)
|
|
||||||
IncludeLaunchDescription(
|
|
||||||
PythonLaunchDescriptionSource(lidar_launch),
|
|
||||||
launch_arguments={
|
|
||||||
'lidar_x': lidar_x,
|
|
||||||
'lidar_y': lidar_y,
|
|
||||||
'lidar_z': lidar_z,
|
|
||||||
'lidar_yaw': lidar_yaw,
|
|
||||||
'intensity': intensity,
|
|
||||||
'sample_rate': sample_rate,
|
|
||||||
'baudrate': baudrate,
|
|
||||||
}.items(),
|
|
||||||
),
|
|
||||||
|
|
||||||
# 可视化(标定 / 联调)
|
|
||||||
Node(
|
|
||||||
package='rviz2',
|
|
||||||
executable='rviz2',
|
|
||||||
name='rviz2',
|
|
||||||
arguments=['-d', rviz_config],
|
|
||||||
condition=IfCondition(use_rviz),
|
|
||||||
output='screen',
|
|
||||||
),
|
|
||||||
])
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""激光雷达 bringup:启动 YDLiDAR TminiPro 驱动 + 发布 base_footprint→laser_frame 静态外参。
|
|
||||||
|
|
||||||
YDLiDAR 驱动节点(ydlidar_ros2_driver_node)内部是普通 rclcpp::Node,启动即在 `scan`
|
|
||||||
话题持续发布 sensor_msgs/LaserScan,无需 lifecycle configure/activate 转换。
|
|
||||||
|
|
||||||
静态外参默认值仅作 CRAIC 当前安装的初始猜测,需按实车标定。若 scan 形状对、但在地图里整体
|
|
||||||
平移或轻微转角对不齐,优先调 `lidar_x/lidar_y/lidar_yaw`。可通过 launch 参数覆盖。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
ros2 launch craic_localization lidar.launch.py
|
|
||||||
ros2 launch craic_localization lidar.launch.py lidar_x:=0.03 lidar_y:=-0.01 lidar_yaw:=-3.14159
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch.actions import DeclareLaunchArgument
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
from launch_ros.descriptions import ParameterValue
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
pkg_share = get_package_share_directory('craic_localization')
|
|
||||||
default_params = os.path.join(pkg_share, 'config', 'lidar.yaml')
|
|
||||||
|
|
||||||
params_file = LaunchConfiguration('params_file')
|
|
||||||
lidar_x = LaunchConfiguration('lidar_x')
|
|
||||||
lidar_y = LaunchConfiguration('lidar_y')
|
|
||||||
lidar_z = LaunchConfiguration('lidar_z')
|
|
||||||
lidar_yaw = LaunchConfiguration('lidar_yaw')
|
|
||||||
base_frame = LaunchConfiguration('base_frame')
|
|
||||||
laser_frame = LaunchConfiguration('laser_frame')
|
|
||||||
# 决定数据包解析格式的关键参数,作为 launch 覆盖项暴露,便于排查 Check Sum 报错:
|
|
||||||
# 基础 Tmini 无强度 -> intensity:=false;TminiPro 有强度 -> intensity:=true
|
|
||||||
intensity = LaunchConfiguration('intensity')
|
|
||||||
sample_rate = LaunchConfiguration('sample_rate')
|
|
||||||
baudrate = LaunchConfiguration('baudrate')
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
DeclareLaunchArgument('params_file', default_value=default_params,
|
|
||||||
description='YDLiDAR 参数文件路径'),
|
|
||||||
DeclareLaunchArgument('base_frame', default_value='base_footprint'),
|
|
||||||
DeclareLaunchArgument('laser_frame', default_value='laser_frame'),
|
|
||||||
# 外参默认值;按实车标定后可在 launch 命令中覆盖。
|
|
||||||
DeclareLaunchArgument('lidar_x', default_value='0.0'),
|
|
||||||
DeclareLaunchArgument('lidar_y', default_value='0.0'),
|
|
||||||
DeclareLaunchArgument('lidar_z', default_value='0.02'),
|
|
||||||
DeclareLaunchArgument('lidar_yaw', default_value='-3.14159',
|
|
||||||
description='激光相对底盘偏航角(rad)。-180°=让机体X轴对准底盘实际前进(W键)方向;'
|
|
||||||
'若实测前后颠倒,改 0.0'),
|
|
||||||
# 数据包格式覆盖项(排查 Check Sum 报错用)
|
|
||||||
DeclareLaunchArgument('intensity', default_value='true',
|
|
||||||
description='设备是否输出强度:基础 Tmini=false,TminiPro=true。不匹配会刷 Check Sum 报错'),
|
|
||||||
DeclareLaunchArgument('sample_rate', default_value='4',
|
|
||||||
description='采样率(K),Tmini 系列为 4'),
|
|
||||||
DeclareLaunchArgument('baudrate', default_value='230400'),
|
|
||||||
|
|
||||||
Node(
|
|
||||||
package='ydlidar_ros2_driver',
|
|
||||||
executable='ydlidar_ros2_driver_node',
|
|
||||||
name='ydlidar_ros2_driver_node',
|
|
||||||
output='screen',
|
|
||||||
emulate_tty=True,
|
|
||||||
parameters=[
|
|
||||||
params_file,
|
|
||||||
{
|
|
||||||
'intensity': ParameterValue(intensity, value_type=bool),
|
|
||||||
'sample_rate': ParameterValue(sample_rate, value_type=int),
|
|
||||||
'baudrate': ParameterValue(baudrate, value_type=int),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
namespace='/',
|
|
||||||
),
|
|
||||||
Node(
|
|
||||||
package='tf2_ros',
|
|
||||||
executable='static_transform_publisher',
|
|
||||||
name='static_tf_base_to_laser',
|
|
||||||
arguments=[
|
|
||||||
'--x', lidar_x, '--y', lidar_y, '--z', lidar_z,
|
|
||||||
'--yaw', lidar_yaw, '--pitch', '0.0', '--roll', '0.0',
|
|
||||||
'--frame-id', base_frame, '--child-frame-id', laser_frame,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
# base_footprint -> base_link 恒等变换。
|
|
||||||
# slam_gmapping(本 build)不从参数读 base_frame,硬编码默认 base_link;
|
|
||||||
# 提供该帧让其默认值可解析。本工程 base_footprint 即底盘基准,两者重合。
|
|
||||||
Node(
|
|
||||||
package='tf2_ros',
|
|
||||||
executable='static_transform_publisher',
|
|
||||||
name='static_tf_base_link',
|
|
||||||
arguments=[
|
|
||||||
'--x', '0.0', '--y', '0.0', '--z', '0.0',
|
|
||||||
'--yaw', '0.0', '--pitch', '0.0', '--roll', '0.0',
|
|
||||||
'--frame-id', base_frame, '--child-frame-id', 'base_link',
|
|
||||||
],
|
|
||||||
),
|
|
||||||
])
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""P5 精准定位:激光 + rf2o 激光里程计 + 已知地图 + AMCL。
|
|
||||||
|
|
||||||
完整 TF 树:map ─(amcl)→ odom ─(rf2o)→ base_footprint ─(静态)→ laser_frame
|
|
||||||
odom→base_footprint 由 rf2o 从 /scan 连续提供(不依赖底盘里程计板),AMCL 用它做运动模型、
|
|
||||||
激光匹配已知地图做校正,输出绝对、无漂移位姿。需要 P4 建好并存盘的地图。
|
|
||||||
|
|
||||||
说明:底盘轮式里程计板上电后仅短时上报,暂不参与 odom→base;待其连续上报问题解决后,
|
|
||||||
可用 robot_localization EKF 融合轮速 + rf2o 再喂 AMCL 提升鲁棒性。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
ros2 launch craic_localization localization.launch.py map:=/abs/path/maps/craic.yaml
|
|
||||||
# 起点不在地图原点时,rviz 用 "2D Pose Estimate" 给初值,或:
|
|
||||||
ros2 launch craic_localization localization.launch.py map:=... init_x:=0.3 init_y:=0.2 init_yaw:=1.57
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
|
|
||||||
from launch.conditions import IfCondition
|
|
||||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
from launch_ros.descriptions import ParameterValue
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
pkg_share = get_package_share_directory('craic_localization')
|
|
||||||
lidar_launch = os.path.join(pkg_share, 'launch', 'lidar.launch.py')
|
|
||||||
amcl_params = os.path.join(pkg_share, 'config', 'amcl.yaml')
|
|
||||||
default_rviz = os.path.join(pkg_share, 'rviz', 'localization.rviz')
|
|
||||||
default_map = os.path.join(pkg_share, 'maps', 'craic.yaml')
|
|
||||||
|
|
||||||
use_rviz = LaunchConfiguration('use_rviz')
|
|
||||||
map_yaml = LaunchConfiguration('map')
|
|
||||||
init_x = LaunchConfiguration('init_x')
|
|
||||||
init_y = LaunchConfiguration('init_y')
|
|
||||||
init_yaw = LaunchConfiguration('init_yaw')
|
|
||||||
show_points = LaunchConfiguration('show_points')
|
|
||||||
points_file = LaunchConfiguration('points_file')
|
|
||||||
lidar_x = LaunchConfiguration('lidar_x')
|
|
||||||
lidar_y = LaunchConfiguration('lidar_y')
|
|
||||||
lidar_z = LaunchConfiguration('lidar_z')
|
|
||||||
lidar_yaw = LaunchConfiguration('lidar_yaw')
|
|
||||||
intensity = LaunchConfiguration('intensity')
|
|
||||||
sample_rate = LaunchConfiguration('sample_rate')
|
|
||||||
baudrate = LaunchConfiguration('baudrate')
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
DeclareLaunchArgument('use_rviz', default_value='true'),
|
|
||||||
DeclareLaunchArgument('map', default_value=default_map,
|
|
||||||
description='地图 yaml 路径(P4 由 map_saver 存盘得到)'),
|
|
||||||
DeclareLaunchArgument('init_x', default_value='0.0'),
|
|
||||||
DeclareLaunchArgument('init_y', default_value='0.0'),
|
|
||||||
DeclareLaunchArgument('init_yaw', default_value='0.0'),
|
|
||||||
DeclareLaunchArgument('show_points', default_value='true'),
|
|
||||||
DeclareLaunchArgument('points_file',
|
|
||||||
default_value=os.path.join(pkg_share, 'config', 'taught_points.yaml'),
|
|
||||||
description='示教点位文件(rviz 显示);想实时看示教就指向你的 taught_points.yaml'),
|
|
||||||
DeclareLaunchArgument('lidar_x', default_value='0.0',
|
|
||||||
description='base_footprint -> laser_frame 平移 X (m)'),
|
|
||||||
DeclareLaunchArgument('lidar_y', default_value='0.0',
|
|
||||||
description='base_footprint -> laser_frame 平移 Y (m)'),
|
|
||||||
DeclareLaunchArgument('lidar_z', default_value='0.02',
|
|
||||||
description='base_footprint -> laser_frame 平移 Z (m)'),
|
|
||||||
DeclareLaunchArgument('lidar_yaw', default_value='-3.14159',
|
|
||||||
description='base_footprint -> laser_frame 偏航角 (rad)'),
|
|
||||||
DeclareLaunchArgument('intensity', default_value='true'),
|
|
||||||
DeclareLaunchArgument('sample_rate', default_value='4'),
|
|
||||||
DeclareLaunchArgument('baudrate', default_value='230400'),
|
|
||||||
|
|
||||||
# ===== 传感器 + 激光里程计(odom -> base_footprint) =====
|
|
||||||
IncludeLaunchDescription(
|
|
||||||
PythonLaunchDescriptionSource(lidar_launch),
|
|
||||||
launch_arguments={
|
|
||||||
'lidar_x': lidar_x,
|
|
||||||
'lidar_y': lidar_y,
|
|
||||||
'lidar_z': lidar_z,
|
|
||||||
'lidar_yaw': lidar_yaw,
|
|
||||||
'intensity': intensity,
|
|
||||||
'sample_rate': sample_rate,
|
|
||||||
'baudrate': baudrate,
|
|
||||||
}.items(),
|
|
||||||
),
|
|
||||||
Node(
|
|
||||||
package='rf2o_laser_odometry',
|
|
||||||
executable='rf2o_laser_odometry_node',
|
|
||||||
name='rf2o_laser_odometry',
|
|
||||||
output='screen',
|
|
||||||
parameters=[{
|
|
||||||
'laser_scan_topic': '/scan',
|
|
||||||
'odom_topic': '/odom',
|
|
||||||
'publish_tf': True,
|
|
||||||
'base_frame_id': 'base_footprint',
|
|
||||||
'odom_frame_id': 'odom',
|
|
||||||
'init_pose_from_topic': '',
|
|
||||||
'freq': 10.0,
|
|
||||||
}],
|
|
||||||
),
|
|
||||||
|
|
||||||
# ===== 地图服务 =====
|
|
||||||
Node(
|
|
||||||
package='nav2_map_server',
|
|
||||||
executable='map_server',
|
|
||||||
name='map_server',
|
|
||||||
output='screen',
|
|
||||||
parameters=[amcl_params, {'yaml_filename': map_yaml}],
|
|
||||||
),
|
|
||||||
|
|
||||||
# ===== AMCL 定位 =====
|
|
||||||
Node(
|
|
||||||
package='nav2_amcl',
|
|
||||||
executable='amcl',
|
|
||||||
name='amcl',
|
|
||||||
output='screen',
|
|
||||||
parameters=[amcl_params, {
|
|
||||||
'set_initial_pose': True,
|
|
||||||
'initial_pose.x': ParameterValue(init_x, value_type=float),
|
|
||||||
'initial_pose.y': ParameterValue(init_y, value_type=float),
|
|
||||||
'initial_pose.yaw': ParameterValue(init_yaw, value_type=float),
|
|
||||||
}],
|
|
||||||
),
|
|
||||||
|
|
||||||
# ===== 生命周期管理:自动 configure+activate map_server 与 amcl =====
|
|
||||||
Node(
|
|
||||||
package='nav2_lifecycle_manager',
|
|
||||||
executable='lifecycle_manager',
|
|
||||||
name='lifecycle_manager_localization',
|
|
||||||
output='screen',
|
|
||||||
parameters=[{
|
|
||||||
'use_sim_time': False,
|
|
||||||
'autostart': True,
|
|
||||||
'node_names': ['map_server', 'amcl'],
|
|
||||||
}],
|
|
||||||
),
|
|
||||||
|
|
||||||
# ===== 示教点位可视化 =====
|
|
||||||
Node(
|
|
||||||
package='craic_localization',
|
|
||||||
executable='show_points',
|
|
||||||
name='show_points',
|
|
||||||
output='screen',
|
|
||||||
parameters=[{'points_file': points_file}],
|
|
||||||
condition=IfCondition(show_points),
|
|
||||||
),
|
|
||||||
|
|
||||||
Node(
|
|
||||||
package='rviz2',
|
|
||||||
executable='rviz2',
|
|
||||||
name='rviz2',
|
|
||||||
arguments=['-d', default_rviz],
|
|
||||||
condition=IfCondition(use_rviz),
|
|
||||||
output='screen',
|
|
||||||
),
|
|
||||||
])
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""P4 建图:激光 + rf2o 激光里程计 + slam_gmapping(SLAM)。
|
|
||||||
|
|
||||||
odom→base_footprint 由 rf2o(从连续 /scan 推算)提供,**不依赖底盘里程计板** ——
|
|
||||||
该板上电后仅短时上报,会让 odom TF 冻结、gmapping 报 "queue is full" 丢弃所有 scan。
|
|
||||||
本管线对齐 lzu_robot 可正常建图的做法(rf2o 提供 odom→base,gmapping 出图)。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
ros2 launch craic_localization mapping.launch.py
|
|
||||||
# 另开终端遥控走图;满意后存盘:
|
|
||||||
ros2 run nav2_map_server map_saver_cli -f <pkg>/maps/craic --ros-args -p save_map_timeout:=10000.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
|
|
||||||
from launch.conditions import IfCondition
|
|
||||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
pkg_share = get_package_share_directory('craic_localization')
|
|
||||||
lidar_launch = os.path.join(pkg_share, 'launch', 'lidar.launch.py')
|
|
||||||
gmapping_params = os.path.join(pkg_share, 'config', 'gmapping.yaml')
|
|
||||||
default_rviz = os.path.join(pkg_share, 'rviz', 'localization.rviz')
|
|
||||||
|
|
||||||
use_rviz = LaunchConfiguration('use_rviz')
|
|
||||||
lidar_x = LaunchConfiguration('lidar_x')
|
|
||||||
lidar_y = LaunchConfiguration('lidar_y')
|
|
||||||
lidar_z = LaunchConfiguration('lidar_z')
|
|
||||||
lidar_yaw = LaunchConfiguration('lidar_yaw')
|
|
||||||
intensity = LaunchConfiguration('intensity')
|
|
||||||
sample_rate = LaunchConfiguration('sample_rate')
|
|
||||||
baudrate = LaunchConfiguration('baudrate')
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
DeclareLaunchArgument('use_rviz', default_value='true'),
|
|
||||||
DeclareLaunchArgument('lidar_x', default_value='0.0',
|
|
||||||
description='base_footprint -> laser_frame 平移 X (m)'),
|
|
||||||
DeclareLaunchArgument('lidar_y', default_value='0.0',
|
|
||||||
description='base_footprint -> laser_frame 平移 Y (m)'),
|
|
||||||
DeclareLaunchArgument('lidar_z', default_value='0.02',
|
|
||||||
description='base_footprint -> laser_frame 平移 Z (m)'),
|
|
||||||
DeclareLaunchArgument('lidar_yaw', default_value='-3.14159',
|
|
||||||
description='base_footprint -> laser_frame 偏航角 (rad)'),
|
|
||||||
DeclareLaunchArgument('intensity', default_value='true'),
|
|
||||||
DeclareLaunchArgument('sample_rate', default_value='4'),
|
|
||||||
DeclareLaunchArgument('baudrate', default_value='230400'),
|
|
||||||
|
|
||||||
# 激光 + 静态外参(base_footprint->laser_frame, base_footprint->base_link)
|
|
||||||
IncludeLaunchDescription(
|
|
||||||
PythonLaunchDescriptionSource(lidar_launch),
|
|
||||||
launch_arguments={
|
|
||||||
'lidar_x': lidar_x,
|
|
||||||
'lidar_y': lidar_y,
|
|
||||||
'lidar_z': lidar_z,
|
|
||||||
'lidar_yaw': lidar_yaw,
|
|
||||||
'intensity': intensity,
|
|
||||||
'sample_rate': sample_rate,
|
|
||||||
'baudrate': baudrate,
|
|
||||||
}.items(),
|
|
||||||
),
|
|
||||||
|
|
||||||
# 激光里程计:从 /scan 连续输出 odom -> base_footprint(建图运动来源)
|
|
||||||
Node(
|
|
||||||
package='rf2o_laser_odometry',
|
|
||||||
executable='rf2o_laser_odometry_node',
|
|
||||||
name='rf2o_laser_odometry',
|
|
||||||
output='screen',
|
|
||||||
parameters=[{
|
|
||||||
'laser_scan_topic': '/scan',
|
|
||||||
'odom_topic': '/odom',
|
|
||||||
'publish_tf': True,
|
|
||||||
'base_frame_id': 'base_footprint',
|
|
||||||
'odom_frame_id': 'odom',
|
|
||||||
'init_pose_from_topic': '',
|
|
||||||
'freq': 10.0,
|
|
||||||
}],
|
|
||||||
),
|
|
||||||
|
|
||||||
# SLAM:发布 /map 与 TF map -> odom
|
|
||||||
Node(
|
|
||||||
package='slam_gmapping',
|
|
||||||
executable='slam_gmapping',
|
|
||||||
name='slam_gmapping',
|
|
||||||
output='screen',
|
|
||||||
parameters=[gmapping_params],
|
|
||||||
),
|
|
||||||
|
|
||||||
Node(
|
|
||||||
package='rviz2',
|
|
||||||
executable='rviz2',
|
|
||||||
name='rviz2',
|
|
||||||
arguments=['-d', default_rviz],
|
|
||||||
condition=IfCondition(use_rviz),
|
|
||||||
output='screen',
|
|
||||||
),
|
|
||||||
])
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
|
||||||
<package format="3">
|
|
||||||
<name>craic_localization</name>
|
|
||||||
<version>0.0.0</version>
|
|
||||||
<description>Wheel odometry + LiDAR localization for the CRAIC mobile base</description>
|
|
||||||
<maintainer email="fallensigh@gmail.com">fallensigh</maintainer>
|
|
||||||
<license>MIT</license>
|
|
||||||
|
|
||||||
<depend>rclpy</depend>
|
|
||||||
<depend>nav_msgs</depend>
|
|
||||||
<depend>geometry_msgs</depend>
|
|
||||||
<depend>std_msgs</depend>
|
|
||||||
<depend>tf2_ros</depend>
|
|
||||||
<depend>visualization_msgs</depend>
|
|
||||||
<exec_depend>python3-yaml</exec_depend>
|
|
||||||
|
|
||||||
<test_depend>ament_copyright</test_depend>
|
|
||||||
<test_depend>ament_flake8</test_depend>
|
|
||||||
<test_depend>ament_pep257</test_depend>
|
|
||||||
<test_depend>python3-pytest</test_depend>
|
|
||||||
|
|
||||||
<export>
|
|
||||||
<build_type>ament_python</build_type>
|
|
||||||
</export>
|
|
||||||
</package>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
Panels:
|
|
||||||
- Class: rviz_common/Displays
|
|
||||||
Name: Displays
|
|
||||||
Property Tree Widget:
|
|
||||||
Expanded:
|
|
||||||
- /TF1
|
|
||||||
- /LaserScan1
|
|
||||||
- /LaserScan1/Topic1
|
|
||||||
- /Odometry1
|
|
||||||
Splitter Ratio: 0.5
|
|
||||||
- Class: rviz_common/Views
|
|
||||||
Name: Views
|
|
||||||
Visualization Manager:
|
|
||||||
Class: ""
|
|
||||||
Displays:
|
|
||||||
- Class: rviz_default_plugins/Grid
|
|
||||||
Name: Grid
|
|
||||||
Enabled: true
|
|
||||||
Reference Frame: <Fixed Frame>
|
|
||||||
Plane Cell Count: 20
|
|
||||||
Cell Size: 0.5
|
|
||||||
Color: 160; 160; 164
|
|
||||||
Line Style:
|
|
||||||
Line Width: 0.03
|
|
||||||
Value: Lines
|
|
||||||
Value: true
|
|
||||||
- Class: rviz_default_plugins/TF
|
|
||||||
Name: TF
|
|
||||||
Enabled: true
|
|
||||||
Show Names: true
|
|
||||||
Show Axes: true
|
|
||||||
Show Arrows: true
|
|
||||||
Marker Scale: 0.5
|
|
||||||
Update Interval: 0
|
|
||||||
Frame Timeout: 15
|
|
||||||
Value: true
|
|
||||||
- Class: rviz_default_plugins/Map
|
|
||||||
Name: Map
|
|
||||||
Enabled: true
|
|
||||||
Topic:
|
|
||||||
Value: /map
|
|
||||||
Depth: 1
|
|
||||||
Durability Policy: Transient Local
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: Reliable
|
|
||||||
Color Scheme: map
|
|
||||||
Draw Behind: true
|
|
||||||
Alpha: 0.7
|
|
||||||
Value: true
|
|
||||||
- Class: rviz_default_plugins/LaserScan
|
|
||||||
Name: LaserScan
|
|
||||||
Enabled: true
|
|
||||||
Topic:
|
|
||||||
Value: /scan
|
|
||||||
Depth: 5
|
|
||||||
Durability Policy: Volatile
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: Best Effort
|
|
||||||
Filter size: 10
|
|
||||||
Style: Points
|
|
||||||
Size (m): 0.02
|
|
||||||
Size (Pixels): 3
|
|
||||||
Color Transformer: FlatColor
|
|
||||||
Color: 255; 30; 30
|
|
||||||
Decay Time: 0
|
|
||||||
Position Transformer: XYZ
|
|
||||||
Value: true
|
|
||||||
- Class: rviz_default_plugins/Odometry
|
|
||||||
Name: Odometry
|
|
||||||
Enabled: true
|
|
||||||
Topic:
|
|
||||||
Value: /odom
|
|
||||||
Depth: 10
|
|
||||||
Durability Policy: Volatile
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: Reliable
|
|
||||||
Keep: 1
|
|
||||||
Position Tolerance: 0.05
|
|
||||||
Angle Tolerance: 0.05
|
|
||||||
Shape:
|
|
||||||
Value: Arrow
|
|
||||||
Color: 30; 255; 30
|
|
||||||
Shaft Length: 0.3
|
|
||||||
Shaft Radius: 0.03
|
|
||||||
Head Length: 0.1
|
|
||||||
Head Radius: 0.06
|
|
||||||
Alpha: 1
|
|
||||||
Covariance:
|
|
||||||
Value: false
|
|
||||||
Value: true
|
|
||||||
- Class: rviz_default_plugins/MarkerArray
|
|
||||||
Name: TaughtPoints
|
|
||||||
Enabled: true
|
|
||||||
Topic:
|
|
||||||
Value: /taught_points
|
|
||||||
Depth: 5
|
|
||||||
Durability Policy: Volatile
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: Reliable
|
|
||||||
Namespaces: {}
|
|
||||||
Value: true
|
|
||||||
Global Options:
|
|
||||||
Fixed Frame: map
|
|
||||||
Background Color: 48; 48; 48
|
|
||||||
Frame Rate: 30
|
|
||||||
Tools:
|
|
||||||
- Class: rviz_default_plugins/MoveCamera
|
|
||||||
- Class: rviz_default_plugins/Select
|
|
||||||
- Class: rviz_default_plugins/FocusCamera
|
|
||||||
- Class: rviz_default_plugins/Measure
|
|
||||||
Views:
|
|
||||||
Current:
|
|
||||||
Class: rviz_default_plugins/Orbit
|
|
||||||
Name: Current View
|
|
||||||
Distance: 8
|
|
||||||
Focal Point:
|
|
||||||
X: 0
|
|
||||||
Y: 0
|
|
||||||
Z: 0
|
|
||||||
Pitch: 1.2
|
|
||||||
Yaw: 3.14
|
|
||||||
Target Frame: <Fixed Frame>
|
|
||||||
Saved: ~
|
|
||||||
Window Geometry:
|
|
||||||
Height: 800
|
|
||||||
Width: 1200
|
|
||||||
Displays:
|
|
||||||
collapsed: false
|
|
||||||
Views:
|
|
||||||
collapsed: false
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[develop]
|
|
||||||
script_dir=$base/lib/craic_localization
|
|
||||||
[install]
|
|
||||||
install_scripts=$base/lib/craic_localization
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import os
|
|
||||||
from glob import glob
|
|
||||||
|
|
||||||
from setuptools import find_packages, setup
|
|
||||||
|
|
||||||
package_name = 'craic_localization'
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name=package_name,
|
|
||||||
version='0.0.0',
|
|
||||||
packages=find_packages(exclude=['test']),
|
|
||||||
data_files=[
|
|
||||||
('share/ament_index/resource_index/packages',
|
|
||||||
['resource/' + package_name]),
|
|
||||||
('share/' + package_name, ['package.xml']),
|
|
||||||
(os.path.join('share', package_name, 'config'),
|
|
||||||
glob('config/*.yaml')),
|
|
||||||
(os.path.join('share', package_name, 'launch'),
|
|
||||||
glob('launch/*.launch.py')),
|
|
||||||
(os.path.join('share', package_name, 'rviz'),
|
|
||||||
glob('rviz/*.rviz')),
|
|
||||||
(os.path.join('share', package_name, 'urdf'),
|
|
||||||
glob('urdf/*')),
|
|
||||||
(os.path.join('share', package_name, 'maps'),
|
|
||||||
glob('maps/*')),
|
|
||||||
],
|
|
||||||
install_requires=['setuptools'],
|
|
||||||
zip_safe=True,
|
|
||||||
maintainer='fallensigh',
|
|
||||||
maintainer_email='fallensigh@gmail.com',
|
|
||||||
description='Wheel odometry + LiDAR localization for the CRAIC mobile base',
|
|
||||||
license='MIT',
|
|
||||||
extras_require={
|
|
||||||
'test': [
|
|
||||||
'pytest',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
entry_points={
|
|
||||||
'console_scripts': [
|
|
||||||
'chassis_odometry = craic_localization.chassis_odometry:main',
|
|
||||||
'teach_points = craic_localization.teach_points:main',
|
|
||||||
'navigate_to_point = craic_localization.navigate_to_point:main',
|
|
||||||
'show_points = craic_localization.show_points:main',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.5)
|
|
||||||
project(rf2o_laser_odometry)
|
|
||||||
|
|
||||||
|
|
||||||
if(NOT CMAKE_CXX_STANDARD)
|
|
||||||
set(CMAKE_CXX_STANDARD 14)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
||||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_package(ament_cmake REQUIRED)
|
|
||||||
find_package(rclcpp REQUIRED)
|
|
||||||
find_package(std_msgs REQUIRED)
|
|
||||||
find_package(geometry_msgs REQUIRED)
|
|
||||||
find_package(tf2_geometry_msgs REQUIRED)
|
|
||||||
find_package(tf2 REQUIRED)
|
|
||||||
find_package(tf2_ros REQUIRED)
|
|
||||||
find_package(sensor_msgs REQUIRED)
|
|
||||||
find_package(nav_msgs REQUIRED)
|
|
||||||
|
|
||||||
## System dependencies are found with CMake's conventions
|
|
||||||
find_package(Boost REQUIRED)
|
|
||||||
find_package(eigen3_cmake_module REQUIRED)
|
|
||||||
find_package(Eigen3 REQUIRED)
|
|
||||||
|
|
||||||
|
|
||||||
set(EXECUTABLE_NAME "rf2o_laser_odometry_node")
|
|
||||||
|
|
||||||
## Specify additional locations of header files
|
|
||||||
## Your package locations should be listed before other locations
|
|
||||||
add_executable(${EXECUTABLE_NAME}
|
|
||||||
src/CLaserOdometry2DNode.cpp
|
|
||||||
src/CLaserOdometry2D.cpp)
|
|
||||||
|
|
||||||
include_directories(
|
|
||||||
include include/
|
|
||||||
)
|
|
||||||
|
|
||||||
include_directories(
|
|
||||||
${Boost_INCLUDE_DIRS}
|
|
||||||
##${EIGEN3_INCLUDE_DIRS}
|
|
||||||
)
|
|
||||||
|
|
||||||
set(DEPENDENCIES
|
|
||||||
"rclcpp"
|
|
||||||
"std_msgs"
|
|
||||||
"geometry_msgs"
|
|
||||||
"tf2_geometry_msgs"
|
|
||||||
"tf2"
|
|
||||||
"tf2_ros"
|
|
||||||
"sensor_msgs"
|
|
||||||
"nav_msgs"
|
|
||||||
"eigen3_cmake_module"
|
|
||||||
)
|
|
||||||
|
|
||||||
##target_include_directories(${PROJECT_NAME} PUBLIC ${Eigen3_INCLUDE_DIRS})
|
|
||||||
ament_target_dependencies(${EXECUTABLE_NAME} ${DEPENDENCIES})
|
|
||||||
|
|
||||||
## Declare a cpp library
|
|
||||||
add_library(${PROJECT_NAME} src/CLaserOdometry2D.cpp)
|
|
||||||
|
|
||||||
## Declare a cpp executable
|
|
||||||
ament_target_dependencies(${PROJECT_NAME} ${DEPENDENCIES})
|
|
||||||
|
|
||||||
install(TARGETS ${EXECUTABLE_NAME}
|
|
||||||
DESTINATION lib/${PROJECT_NAME})
|
|
||||||
|
|
||||||
install(
|
|
||||||
DIRECTORY launch
|
|
||||||
DESTINATION share/${PROJECT_NAME})
|
|
||||||
|
|
||||||
ament_package()
|
|
||||||
|
|
||||||
@@ -1,674 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
{one line to give the program's name and a brief idea of what it does.}
|
|
||||||
Copyright (C) {year} {name of author}
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
{project} Copyright (C) {year} {fullname}
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# rf2o_laser_odometry
|
|
||||||
|
|
||||||
Estimation of 2D odometry based on planar laser scans. rf2o is a fast and precise method to estimate the planar motion of a lidar from consecutive range scans. Useful for mobile robots with inaccurate wheel odometry.
|
|
||||||
|
|
||||||
For every scanned point we formulate the range flow constraint equation in terms of the sensor velocity, and minimize a robust function of the resulting geometric constraints to obtain the motion estimate. Conversely to traditional approaches, this method does not search for correspondences but performs dense scan alignment based on the scan gradients, in the fashion of dense 3D visual odometry.
|
|
||||||
|
|
||||||
The very low computational cost (0.9 milliseconds on a single CPU core) together whit its high precision, makes RF2O a suitable method for those robotic applications that require planar odometry.
|
|
||||||
|
|
||||||
For a full description of the algorithm, please refer to: **Planar Odometry from a Radial Laser Scanner. A Range Flow-based Approach. ICRA 2016** Available at: http://mapir.uma.es/papersrepo/2016/2016_Jaimez_ICRA_RF2O.pdf
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/** ****************************************************************************************
|
|
||||||
* Planar Odometry from a Radial Laser Scanner. A Range Flow-based Approach. ICRA'16.
|
|
||||||
* Refactored: Pure C++ algorithm class, NO rclcpp dependencies.
|
|
||||||
******************************************************************************************** */
|
|
||||||
#ifndef CLaserOdometry2D_H
|
|
||||||
#define CLaserOdometry2D_H
|
|
||||||
|
|
||||||
// ✅ [CLEANED] Removed all rclcpp headers. Only msg types and math libs remain.
|
|
||||||
#include <nav_msgs/msg/odometry.hpp>
|
|
||||||
#include <geometry_msgs/msg/quaternion.hpp>
|
|
||||||
#include <sensor_msgs/msg/laser_scan.hpp>
|
|
||||||
#include <geometry_msgs/msg/pose.hpp>
|
|
||||||
|
|
||||||
#include <eigen3/Eigen/Dense>
|
|
||||||
#include <eigen3/Eigen/Geometry>
|
|
||||||
#include <vector>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace rf2o {
|
|
||||||
|
|
||||||
template <typename T> inline T sign(const T x) { return x < T(0) ? -1 : 1; }
|
|
||||||
|
|
||||||
template <typename Derived>
|
|
||||||
inline typename Eigen::MatrixBase<Derived>::Scalar getYaw(const Eigen::MatrixBase<Derived>& r) {
|
|
||||||
return std::atan2(r(1, 0), r(0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
inline Eigen::Matrix<T, 3, 3> matrixRollPitchYaw(const T roll, const T pitch, const T yaw) {
|
|
||||||
const Eigen::AngleAxis<T> ax = Eigen::AngleAxis<T>(roll, Eigen::Matrix<T, 3, 1>::UnitX());
|
|
||||||
const Eigen::AngleAxis<T> ay = Eigen::AngleAxis<T>(pitch, Eigen::Matrix<T, 3, 1>::UnitY());
|
|
||||||
const Eigen::AngleAxis<T> az = Eigen::AngleAxis<T>(yaw, Eigen::Matrix<T, 3, 1>::UnitZ());
|
|
||||||
return (az * ay * ax).toRotationMatrix().matrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
inline Eigen::Matrix<T, 3, 3> matrixYaw(const T yaw) {
|
|
||||||
return matrixRollPitchYaw<T>(0, 0, yaw);
|
|
||||||
}
|
|
||||||
|
|
||||||
using Scalar = float;
|
|
||||||
using Pose3d = Eigen::Isometry3d;
|
|
||||||
using MatrixS31 = Eigen::Matrix<Scalar, 3, 1>;
|
|
||||||
using IncrementCov = Eigen::Matrix<Scalar, 3, 3>;
|
|
||||||
|
|
||||||
// ✅ [FIXED] No longer inherits rclcpp::Node. Pure C++ class.
|
|
||||||
class CLaserOdometry2D
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
CLaserOdometry2D();
|
|
||||||
|
|
||||||
void init(const sensor_msgs::msg::LaserScan& scan,
|
|
||||||
const geometry_msgs::msg::Pose& initial_robot_pose);
|
|
||||||
|
|
||||||
bool is_initialized();
|
|
||||||
|
|
||||||
// ✅ [MODIFIED] Accepts external timestamp instead of using internal clock
|
|
||||||
bool odometryCalculation(const sensor_msgs::msg::LaserScan& scan, double current_time_sec);
|
|
||||||
|
|
||||||
void setLaserPose(const Pose3d& laser_pose);
|
|
||||||
|
|
||||||
const Pose3d& getIncrement() const;
|
|
||||||
const IncrementCov& getIncrementCovariance() const;
|
|
||||||
Pose3d& getPose();
|
|
||||||
const Pose3d& getPose() const;
|
|
||||||
|
|
||||||
bool verbose, module_initialized, first_laser_scan;
|
|
||||||
|
|
||||||
// ✅ [MODIFIED] Replaced rclcpp::Time with plain double (seconds)
|
|
||||||
double last_odom_time_sec;
|
|
||||||
double current_scan_time_sec;
|
|
||||||
|
|
||||||
// Internal Data Structures (Unchanged)
|
|
||||||
std::vector<Eigen::MatrixXf> range, range_old, range_inter, range_warped;
|
|
||||||
std::vector<Eigen::MatrixXf> xx, xx_inter, xx_old, xx_warped;
|
|
||||||
std::vector<Eigen::MatrixXf> yy, yy_inter, yy_old, yy_warped;
|
|
||||||
std::vector<Eigen::MatrixXf> transformations;
|
|
||||||
Eigen::MatrixXf range_wf, dtita, dt, rtita, normx, normy, norm_ang, weights;
|
|
||||||
Eigen::MatrixXi null;
|
|
||||||
Eigen::MatrixXf A, Aw, B, Bw;
|
|
||||||
MatrixS31 Var;
|
|
||||||
IncrementCov cov_odo;
|
|
||||||
float fps, fovh;
|
|
||||||
unsigned int cols, cols_i, width, ctf_levels, image_level, level, num_valid_range, iter_irls;
|
|
||||||
float g_mask[5];
|
|
||||||
double lin_speed, ang_speed;
|
|
||||||
MatrixS31 kai_abs_, kai_loc_, kai_loc_old_, kai_loc_level_;
|
|
||||||
Pose3d last_increment_, laser_pose_on_robot_, laser_pose_on_robot_inv_;
|
|
||||||
Pose3d laser_pose_, laser_oldpose_, robot_pose_, robot_oldpose_;
|
|
||||||
bool test;
|
|
||||||
std::vector<double> last_m_lin_speeds, last_m_ang_speeds;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void createImagePyramid();
|
|
||||||
void calculateCoord();
|
|
||||||
void performWarping();
|
|
||||||
void calculaterangeDerivativesSurface();
|
|
||||||
void computeNormals();
|
|
||||||
void computeWeights();
|
|
||||||
void findNullPoints();
|
|
||||||
void solveSystemOneLevel();
|
|
||||||
void solveSystemNonLinear();
|
|
||||||
bool filterLevelSolution();
|
|
||||||
void PoseUpdate();
|
|
||||||
void Reset(const Pose3d& ini_pose);
|
|
||||||
};
|
|
||||||
|
|
||||||
} /* namespace rf2o */
|
|
||||||
#endif
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#include "rf2o_laser_odometry/CLaserOdometry2D.hpp"
|
|
||||||
|
|
||||||
#include <tf2/convert.h>
|
|
||||||
#include <tf2/exceptions.h>
|
|
||||||
#include <tf2_ros/transform_broadcaster.h>
|
|
||||||
#include <tf2_ros/transform_listener.h>
|
|
||||||
#include <tf2_ros/buffer.h>
|
|
||||||
#include <tf2/impl/utils.h>
|
|
||||||
#include <tf2_geometry_msgs/tf2_geometry_msgs.h>
|
|
||||||
#include <tf2/utils.h>
|
|
||||||
|
|
||||||
namespace rf2o {
|
|
||||||
|
|
||||||
class CLaserOdometry2DNode : public rclcpp::Node
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
CLaserOdometry2DNode();
|
|
||||||
void process();
|
|
||||||
void publish();
|
|
||||||
bool setLaserPoseFromTf();
|
|
||||||
bool scan_available();
|
|
||||||
|
|
||||||
// Params & vars
|
|
||||||
CLaserOdometry2D rf2o_ref;
|
|
||||||
bool publish_tf, new_scan_available;
|
|
||||||
double freq;
|
|
||||||
std::string laser_scan_topic;
|
|
||||||
std::string odom_topic;
|
|
||||||
std::string base_frame_id;
|
|
||||||
std::string odom_frame_id;
|
|
||||||
std::string init_pose_from_topic;
|
|
||||||
|
|
||||||
sensor_msgs::msg::LaserScan last_scan;
|
|
||||||
bool GT_pose_initialized;
|
|
||||||
std::shared_ptr<tf2_ros::Buffer> buffer_;
|
|
||||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
|
||||||
std::unique_ptr<tf2_ros::TransformBroadcaster> odom_broadcaster;
|
|
||||||
nav_msgs::msg::Odometry initial_robot_pose;
|
|
||||||
|
|
||||||
// Subscriptions & Publishers
|
|
||||||
rclcpp::Subscription<sensor_msgs::msg::LaserScan>::SharedPtr laser_sub;
|
|
||||||
rclcpp::Subscription<nav_msgs::msg::Odometry>::SharedPtr initPose_sub;
|
|
||||||
rclcpp::Publisher<nav_msgs::msg::Odometry>::SharedPtr odom_pub;
|
|
||||||
|
|
||||||
// CallBacks
|
|
||||||
void LaserCallBack(const sensor_msgs::msg::LaserScan::SharedPtr new_scan);
|
|
||||||
void initPoseCallBack(const nav_msgs::msg::Odometry::SharedPtr new_initPose);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
from launch import LaunchDescription
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
|
|
||||||
Node(
|
|
||||||
package='rf2o_laser_odometry',
|
|
||||||
executable='rf2o_laser_odometry_node',
|
|
||||||
name='rf2o_laser_odometry',
|
|
||||||
parameters=[{
|
|
||||||
'laser_scan_topic' : '/scan',
|
|
||||||
'odom_topic' : '/odom',
|
|
||||||
'publish_tf' : True,
|
|
||||||
'base_frame_id' : 'base_footprint',#base_footprint
|
|
||||||
'odom_frame_id' : 'odom',
|
|
||||||
'init_pose_from_topic' : '',
|
|
||||||
'freq' : 10.0}],
|
|
||||||
),
|
|
||||||
])
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<package>
|
|
||||||
<name>rf2o_laser_odometry</name>
|
|
||||||
<version>0.1.0</version>
|
|
||||||
<description>
|
|
||||||
Estimation of 2D odometry based on planar laser scans. Useful for mobile robots with innacurate base odometry.
|
|
||||||
For full description of the algorithm, please refer to:
|
|
||||||
Planar Odometry from a Radial Laser Scanner. A Range Flow-based Approach. ICRA 2016
|
|
||||||
Available at: http://mapir.isa.uma.es/mapirwebsite/index.php/mapir-downloads/papers/217
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<maintainer email="jgmonroy@uma.es">Javier G. Monroy</maintainer>
|
|
||||||
<author email="marianojt@uma.es"> Mariano Jaimez</author>
|
|
||||||
<author email="jgmonroy@uma.es"> Javier G. Monroy</author>
|
|
||||||
<license>GPL v3</license>
|
|
||||||
|
|
||||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
|
||||||
<buildtool_depend>eigen3_cmake_module</buildtool_depend>
|
|
||||||
<build_depend>geometry_msgs</build_depend>
|
|
||||||
<build_depend>rclcpp</build_depend>
|
|
||||||
<build_depend>std_msgs</build_depend>
|
|
||||||
<build_depend>tf2</build_depend>
|
|
||||||
<build_depend>tf2_ros</build_depend>
|
|
||||||
<build_depend>eigen</build_depend>
|
|
||||||
<build_depend>cmake_modules</build_depend>
|
|
||||||
<build_depend>sensor_msgs</build_depend>
|
|
||||||
<build_depend>tf2_geometry_msgs</build_depend>
|
|
||||||
<run_depend>geometry_msgs</run_depend>
|
|
||||||
<run_depend>rclcpp</run_depend>
|
|
||||||
<run_depend>std_msgs</run_depend>
|
|
||||||
<run_depend>tf2</run_depend>
|
|
||||||
<run_depend>tf2_ros</run_depend>
|
|
||||||
<run_depend>eigen</run_depend>
|
|
||||||
<run_depend>cmake_modules</run_depend>
|
|
||||||
<run_depend>sensor_msgs</run_depend>
|
|
||||||
<run_depend>tf2_geometry_msgs</run_depend>
|
|
||||||
<export>
|
|
||||||
<build_type>ament_cmake</build_type>
|
|
||||||
</export>
|
|
||||||
</package>
|
|
||||||
@@ -1,840 +0,0 @@
|
|||||||
/** ****************************************************************************************
|
|
||||||
* This node presents a fast and precise method to estimate the planar motion of a lidar
|
|
||||||
* from consecutive range scans. It is very useful for the estimation of the robot odometry from
|
|
||||||
* 2D laser range measurements.
|
|
||||||
* This module is developed for mobile robots with innacurate or inexistent built-in odometry.
|
|
||||||
* It allows the estimation of a precise odometry with low computational cost.
|
|
||||||
* For more information, please refer to:
|
|
||||||
*
|
|
||||||
* Planar Odometry from a Radial Laser Scanner. A Range Flow-based Approach. ICRA'16.
|
|
||||||
* Available at: http://mapir.uma.es/papersrepo/2016/2016_Jaimez_ICRA_RF2O.pdf
|
|
||||||
*
|
|
||||||
* Maintainer: Javier G. Monroy
|
|
||||||
* MAPIR group: https://mapir.isa.uma.es
|
|
||||||
*
|
|
||||||
* Modifications: Jeremie Deray & (see contributons on github)
|
|
||||||
******************************************************************************************** */
|
|
||||||
|
|
||||||
#include "rf2o_laser_odometry/CLaserOdometry2D.hpp"
|
|
||||||
|
|
||||||
namespace rf2o {
|
|
||||||
|
|
||||||
|
|
||||||
// ✅ [FIXED] Constructor no longer calls Node("..."). Pure member initialization only.
|
|
||||||
CLaserOdometry2D::CLaserOdometry2D() :
|
|
||||||
verbose(false),
|
|
||||||
module_initialized(false),
|
|
||||||
first_laser_scan(true),
|
|
||||||
last_increment_(Pose3d::Identity()),
|
|
||||||
laser_pose_on_robot_(Pose3d::Identity()),
|
|
||||||
laser_pose_on_robot_inv_(Pose3d::Identity()),
|
|
||||||
laser_pose_(Pose3d::Identity()),
|
|
||||||
laser_oldpose_(Pose3d::Identity()),
|
|
||||||
robot_pose_(Pose3d::Identity()),
|
|
||||||
robot_oldpose_(Pose3d::Identity())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2D::setLaserPose(const Pose3d& laser_pose) {
|
|
||||||
laser_pose_on_robot_ = laser_pose;
|
|
||||||
laser_pose_on_robot_inv_ = laser_pose_on_robot_.inverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CLaserOdometry2D::is_initialized() {
|
|
||||||
return module_initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2D::init(const sensor_msgs::msg::LaserScan& scan,
|
|
||||||
const geometry_msgs::msg::Pose& initial_robot_pose) {
|
|
||||||
// ✅ [CLEANED] All RCLCPP_INFO logs removed completely.
|
|
||||||
|
|
||||||
width = scan.ranges.size();
|
|
||||||
cols = width;
|
|
||||||
fovh = std::abs(scan.angle_max - scan.angle_min);
|
|
||||||
ctf_levels = 5;
|
|
||||||
iter_irls = 5;
|
|
||||||
|
|
||||||
Pose3d robot_initial_pose = Pose3d::Identity();
|
|
||||||
robot_initial_pose = Eigen::Quaterniond(initial_robot_pose.orientation.w,
|
|
||||||
initial_robot_pose.orientation.x,
|
|
||||||
initial_robot_pose.orientation.y,
|
|
||||||
initial_robot_pose.orientation.z);
|
|
||||||
robot_initial_pose.translation()(0) = initial_robot_pose.position.x;
|
|
||||||
robot_initial_pose.translation()(1) = initial_robot_pose.position.y;
|
|
||||||
|
|
||||||
laser_pose_ = robot_initial_pose * laser_pose_on_robot_;
|
|
||||||
laser_oldpose_ = laser_oldpose_;
|
|
||||||
|
|
||||||
range_wf = Eigen::MatrixXf::Constant(1, width, 1);
|
|
||||||
|
|
||||||
transformations.resize(ctf_levels);
|
|
||||||
for (unsigned int i = 0; i < ctf_levels; i++)
|
|
||||||
transformations[i].resize(3, 3);
|
|
||||||
|
|
||||||
unsigned int s, cols_i;
|
|
||||||
const unsigned int pyr_levels = std::round(std::log2(round(float(width) / float(cols)))) + ctf_levels;
|
|
||||||
range.resize(pyr_levels); range_old.resize(pyr_levels); range_inter.resize(pyr_levels);
|
|
||||||
xx.resize(pyr_levels); xx_inter.resize(pyr_levels); xx_old.resize(pyr_levels); xx_warped.resize(pyr_levels);
|
|
||||||
yy.resize(pyr_levels); yy_inter.resize(pyr_levels); yy_old.resize(pyr_levels); yy_warped.resize(pyr_levels);
|
|
||||||
range_warped.resize(pyr_levels);
|
|
||||||
|
|
||||||
for (unsigned int i = 0; i < pyr_levels; i++) {
|
|
||||||
s = std::pow(2.f, int(i));
|
|
||||||
cols_i = std::ceil(float(width) / float(s));
|
|
||||||
range[i] = Eigen::MatrixXf::Constant(1, cols_i, 0.f);
|
|
||||||
range_old[i] = Eigen::MatrixXf::Constant(1, cols_i, 0.f);
|
|
||||||
range_inter[i].resize(1, cols_i);
|
|
||||||
xx[i] = Eigen::MatrixXf::Constant(1, cols_i, 0.f);
|
|
||||||
xx_old[i] = Eigen::MatrixXf::Constant(1, cols_i, 0.f);
|
|
||||||
yy[i] = Eigen::MatrixXf::Constant(1, cols_i, 0.f);
|
|
||||||
yy_old[i] = Eigen::MatrixXf::Constant(1, cols_i, 0.f);
|
|
||||||
xx_inter[i].resize(1, cols_i); yy_inter[i].resize(1, cols_i);
|
|
||||||
if (cols_i <= cols) {
|
|
||||||
range_warped[i].resize(1, cols_i);
|
|
||||||
xx_warped[i].resize(1, cols_i);
|
|
||||||
yy_warped[i].resize(1, cols_i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dt.resize(1, cols); dtita.resize(1, cols);
|
|
||||||
normx.resize(1, cols); normy.resize(1, cols); norm_ang.resize(1, cols); weights.resize(1, cols);
|
|
||||||
null = Eigen::MatrixXi::Constant(1, cols, 0);
|
|
||||||
cov_odo = IncrementCov::Zero();
|
|
||||||
fps = 1.f;
|
|
||||||
num_valid_range = 0;
|
|
||||||
|
|
||||||
g_mask[0] = 1.f / 16.f; g_mask[1] = 0.25f; g_mask[2] = 6.f / 16.f;
|
|
||||||
g_mask[3] = g_mask[1]; g_mask[4] = g_mask[0];
|
|
||||||
|
|
||||||
kai_abs_ = MatrixS31::Zero();
|
|
||||||
kai_loc_old_ = MatrixS31::Zero();
|
|
||||||
|
|
||||||
module_initialized = true;
|
|
||||||
// ✅ [MODIFIED] Convert ROS timestamp to plain double seconds
|
|
||||||
last_odom_time_sec = scan.header.stamp.sec + scan.header.stamp.nanosec / 1e9;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Pose3d& CLaserOdometry2D::getIncrement() const { return last_increment_; }
|
|
||||||
const IncrementCov& CLaserOdometry2D::getIncrementCovariance() const { return cov_odo; }
|
|
||||||
Pose3d& CLaserOdometry2D::getPose() { return robot_pose_; }
|
|
||||||
const Pose3d& CLaserOdometry2D::getPose() const { return robot_pose_; }
|
|
||||||
|
|
||||||
// ✅ [MODIFIED] Signature changed to accept external time
|
|
||||||
bool CLaserOdometry2D::odometryCalculation(const sensor_msgs::msg::LaserScan& scan, double current_time_sec) {
|
|
||||||
range_wf = Eigen::Map<const Eigen::MatrixXf>(scan.ranges.data(), width, 1);
|
|
||||||
|
|
||||||
double start_sec = current_time_sec; // ✅ [MODIFIED] No more get_clock()->now()
|
|
||||||
|
|
||||||
createImagePyramid();
|
|
||||||
|
|
||||||
for (unsigned int i=0; i<ctf_levels; i++) {
|
|
||||||
transformations[i].setIdentity();
|
|
||||||
level = i;
|
|
||||||
unsigned int s = std::pow(2.f,int(ctf_levels-(i+1)));
|
|
||||||
cols_i = std::ceil(float(cols)/float(s));
|
|
||||||
image_level = ctf_levels - i + std::round(std::log2(std::round(float(width)/float(cols)))) - 1;
|
|
||||||
|
|
||||||
if (i == 0) {
|
|
||||||
range_warped[image_level] = range[image_level];
|
|
||||||
xx_warped[image_level] = xx[image_level];
|
|
||||||
yy_warped[image_level] = yy[image_level];
|
|
||||||
} else {
|
|
||||||
performWarping();
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateCoord();
|
|
||||||
findNullPoints();
|
|
||||||
calculaterangeDerivativesSurface();
|
|
||||||
computeWeights();
|
|
||||||
|
|
||||||
if (num_valid_range > 3)
|
|
||||||
solveSystemNonLinear();
|
|
||||||
else
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!filterLevelSolution()) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ [CLEANED] Execution time log removed completely.
|
|
||||||
// If you need it later, use: printf("[RF2O] exec ms: %f\n", (current_time_sec - start_sec)*1000);
|
|
||||||
|
|
||||||
PoseUpdate();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the Pyramid (multi-resolution) from the current laser range data
|
|
||||||
*/
|
|
||||||
void CLaserOdometry2D::createImagePyramid()
|
|
||||||
{
|
|
||||||
// Push the scans back
|
|
||||||
range_old.swap(range);
|
|
||||||
xx_old.swap(xx);
|
|
||||||
yy_old.swap(yy);
|
|
||||||
|
|
||||||
// The number of levels of the pyramid does not always match the number of levels used
|
|
||||||
// in the odometry computation (because we sometimes want to finish with lower resolutions)
|
|
||||||
unsigned int pyr_levels = std::round(std::log2(std::round(float(width)/float(cols)))) + ctf_levels;
|
|
||||||
|
|
||||||
// Generate Pyramid levels
|
|
||||||
const float max_range_dif = 0.3f;
|
|
||||||
for (unsigned int i = 0; i<pyr_levels; i++)
|
|
||||||
{
|
|
||||||
unsigned int s = std::pow(2.f,int(i));
|
|
||||||
cols_i = std::ceil(float(width)/float(s));
|
|
||||||
|
|
||||||
const unsigned int i_1 = i-1;
|
|
||||||
|
|
||||||
// First level -> Filter (not downsampling);
|
|
||||||
if (i == 0)
|
|
||||||
{
|
|
||||||
for (unsigned int u = 0; u < cols_i; u++)
|
|
||||||
{
|
|
||||||
const float dcenter = range_wf(u);
|
|
||||||
|
|
||||||
// Inner pixels (avoid first and last points in the scan)
|
|
||||||
if ((u>1)&&(u<cols_i-2))
|
|
||||||
{
|
|
||||||
if (std::isfinite(dcenter) && dcenter > 0.f)
|
|
||||||
{
|
|
||||||
float sum = 0.f;
|
|
||||||
float weight = 0.f;
|
|
||||||
|
|
||||||
for (int l=-2; l<3; l++)
|
|
||||||
{
|
|
||||||
const float abs_dif = std::abs(range_wf(u+l)-dcenter);
|
|
||||||
if (abs_dif < max_range_dif)
|
|
||||||
{
|
|
||||||
const float aux_w = g_mask[2+l]*(max_range_dif - abs_dif);
|
|
||||||
weight += aux_w;
|
|
||||||
sum += aux_w*range_wf(u+l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
range[i](u) = sum/weight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
range[i](u) = 0.f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boundary points
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (std::isfinite(dcenter) && dcenter > 0.f)
|
|
||||||
{
|
|
||||||
float sum = 0.f;
|
|
||||||
float weight = 0.f;
|
|
||||||
|
|
||||||
for (int l=-2; l<3; l++)
|
|
||||||
{
|
|
||||||
const int indu = u+l;
|
|
||||||
if ((indu>=0)&&(indu<int(cols_i)))
|
|
||||||
{
|
|
||||||
const float abs_dif = std::abs(range_wf(indu)-dcenter);
|
|
||||||
if (abs_dif < max_range_dif)
|
|
||||||
{
|
|
||||||
const float aux_w = g_mask[2+l]*(max_range_dif - abs_dif);
|
|
||||||
weight += aux_w;
|
|
||||||
sum += aux_w*range_wf(indu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
range[i](u) = sum/weight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
range[i](u) = 0.f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second level and forth -> Downsampling
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (unsigned int u = 0; u < cols_i; u++)
|
|
||||||
{
|
|
||||||
const int u2 = 2*u;
|
|
||||||
const float dcenter = range[i_1](u2);
|
|
||||||
|
|
||||||
// Inner pixels (avoid first/last point in the scan)
|
|
||||||
if ((u>0)&&(u<cols_i-1))
|
|
||||||
{
|
|
||||||
if (dcenter > 0.f)
|
|
||||||
{
|
|
||||||
float sum = 0.f;
|
|
||||||
float weight = 0.f;
|
|
||||||
|
|
||||||
for (int l=-2; l<3; l++)
|
|
||||||
{
|
|
||||||
const float abs_dif = std::abs(range[i_1](u2+l)-dcenter);
|
|
||||||
if (abs_dif < max_range_dif)
|
|
||||||
{
|
|
||||||
const float aux_w = g_mask[2+l]*(max_range_dif - abs_dif);
|
|
||||||
weight += aux_w;
|
|
||||||
sum += aux_w*range[i_1](u2+l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
range[i](u) = sum/weight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
range[i](u) = 0.f;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//Boundary
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (dcenter > 0.f)
|
|
||||||
{
|
|
||||||
float sum = 0.f;
|
|
||||||
float weight = 0.f;
|
|
||||||
const unsigned int cols_i2 = range[i_1].cols();
|
|
||||||
|
|
||||||
|
|
||||||
for (int l=-2; l<3; l++)
|
|
||||||
{
|
|
||||||
const int indu = u2+l;
|
|
||||||
if ((indu>=0)&&(indu<int(cols_i2)))
|
|
||||||
{
|
|
||||||
const float abs_dif = std::abs(range[i_1](indu)-dcenter);
|
|
||||||
if (abs_dif < max_range_dif)
|
|
||||||
{
|
|
||||||
const float aux_w = g_mask[2+l]*(max_range_dif - abs_dif);
|
|
||||||
weight += aux_w;
|
|
||||||
sum += aux_w*range[i_1](indu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
range[i](u) = sum/weight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
range[i](u) = 0.f;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate coordinates "xy" of the points
|
|
||||||
for (unsigned int u = 0; u < cols_i; u++)
|
|
||||||
{
|
|
||||||
if (range[i](u) > 0.f)
|
|
||||||
{
|
|
||||||
const float tita = -0.5*fovh + float(u)*fovh/float(cols_i-1);
|
|
||||||
xx[i](u) = range[i](u)*std::cos(tita);
|
|
||||||
yy[i](u) = range[i](u)*std::sin(tita);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
xx[i](u) = 0.f;
|
|
||||||
yy[i](u) = 0.f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void CLaserOdometry2D::calculateCoord()
|
|
||||||
{
|
|
||||||
for (unsigned int u = 0; u < cols_i; u++)
|
|
||||||
{
|
|
||||||
if ((range_old[image_level](u) == 0.f) || (range_warped[image_level](u) == 0.f))
|
|
||||||
{
|
|
||||||
range_inter[image_level](u) = 0.f;
|
|
||||||
xx_inter[image_level](u) = 0.f;
|
|
||||||
yy_inter[image_level](u) = 0.f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
range_inter[image_level](u) = 0.5f*(range_old[image_level](u) + range_warped[image_level](u));
|
|
||||||
xx_inter[image_level](u) = 0.5f*(xx_old[image_level](u) + xx_warped[image_level](u));
|
|
||||||
yy_inter[image_level](u) = 0.5f*(yy_old[image_level](u) + yy_warped[image_level](u));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void CLaserOdometry2D::calculaterangeDerivativesSurface()
|
|
||||||
{
|
|
||||||
//The gradient size ir reserved at the maximum size (at the constructor)
|
|
||||||
|
|
||||||
//Compute connectivity
|
|
||||||
|
|
||||||
//Defined in a different way now, without inversion
|
|
||||||
rtita = Eigen::MatrixXf::Constant(1, cols_i, 1.f);
|
|
||||||
|
|
||||||
for (unsigned int u = 0; u < cols_i-1; u++)
|
|
||||||
{
|
|
||||||
float dista = xx_inter[image_level](u+1) - xx_inter[image_level](u);
|
|
||||||
dista *= dista;
|
|
||||||
float distb = yy_inter[image_level](u+1) - yy_inter[image_level](u);
|
|
||||||
distb *= distb;
|
|
||||||
const float dist = dista + distb;
|
|
||||||
|
|
||||||
if (dist > 0.f)
|
|
||||||
rtita(u) = std::sqrt(dist);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Spatial derivatives
|
|
||||||
for (unsigned int u = 1; u < cols_i-1; u++)
|
|
||||||
dtita(u) = (rtita(u-1)*(range_inter[image_level](u+1)-
|
|
||||||
range_inter[image_level](u)) + rtita(u)*(range_inter[image_level](u) -
|
|
||||||
range_inter[image_level](u-1)))/(rtita(u)+rtita(u-1));
|
|
||||||
|
|
||||||
dtita(0) = dtita(1);
|
|
||||||
dtita(cols_i-1) = dtita(cols_i-2);
|
|
||||||
|
|
||||||
//Temporal derivative
|
|
||||||
for (unsigned int u = 0; u < cols_i; u++)
|
|
||||||
dt(u) = fps*(range_warped[image_level](u) - range_old[image_level](u));
|
|
||||||
|
|
||||||
|
|
||||||
//Apply median filter to the range derivatives
|
|
||||||
//MatrixXf dtitamed = dtita, dtmed = dt;
|
|
||||||
//vector<float> svector(3);
|
|
||||||
//for (unsigned int u=1; u<cols_i-1; u++)
|
|
||||||
//{
|
|
||||||
// svector.at(0) = dtita(u-1); svector.at(1) = dtita(u); svector.at(2) = dtita(u+1);
|
|
||||||
// std::sort(svector.begin(), svector.end());
|
|
||||||
// dtitamed(u) = svector.at(1);
|
|
||||||
|
|
||||||
// svector.at(0) = dt(u-1); svector.at(1) = dt(u); svector.at(2) = dt(u+1);
|
|
||||||
// std::sort(svector.begin(), svector.end());
|
|
||||||
// dtmed(u) = svector.at(1);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//dtitamed(0) = dtitamed(1);
|
|
||||||
//dtitamed(cols_i-1) = dtitamed(cols_i-2);
|
|
||||||
//dtmed(0) = dtmed(1);
|
|
||||||
//dtmed(cols_i-1) = dtmed(cols_i-2);
|
|
||||||
|
|
||||||
//dtitamed.swap(dtita);
|
|
||||||
//dtmed.swap(dt);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2D::computeNormals()
|
|
||||||
{
|
|
||||||
normx.setConstant(1, cols, 0.f);
|
|
||||||
normy.setConstant(1, cols, 0.f);
|
|
||||||
norm_ang.setConstant(1, cols, 0.f);
|
|
||||||
|
|
||||||
const float incr_tita = fovh/float(cols_i-1);
|
|
||||||
for (unsigned int u=0; u<cols_i; u++)
|
|
||||||
{
|
|
||||||
if (null(u) == 0.f)
|
|
||||||
{
|
|
||||||
const float tita = -0.5f*fovh + float(u)*incr_tita;
|
|
||||||
const float alfa = -std::atan2(2.f*dtita(u), 2.f*range[image_level](u)*incr_tita);
|
|
||||||
norm_ang(u) = tita + alfa;
|
|
||||||
if (norm_ang(u) < -M_PI)
|
|
||||||
norm_ang(u) += 2.f*M_PI;
|
|
||||||
else if (norm_ang(u) < 0.f)
|
|
||||||
norm_ang(u) += M_PI;
|
|
||||||
else if (norm_ang(u) > M_PI)
|
|
||||||
norm_ang(u) -= M_PI;
|
|
||||||
|
|
||||||
normx(u) = std::cos(tita + alfa);
|
|
||||||
normy(u) = std::sin(tita + alfa);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2D::computeWeights()
|
|
||||||
{
|
|
||||||
//The maximum weight size is reserved at the constructor
|
|
||||||
weights.setConstant(1, cols, 0.f);
|
|
||||||
|
|
||||||
//Parameters for error_linearization
|
|
||||||
const float kdtita = 1.f;
|
|
||||||
const float kdt = kdtita / (fps*fps);
|
|
||||||
const float k2d = 0.2f;
|
|
||||||
|
|
||||||
for (unsigned int u = 1; u < cols_i-1; u++)
|
|
||||||
if (null(u) == 0)
|
|
||||||
{
|
|
||||||
// Compute derivatives
|
|
||||||
//-----------------------------------------------------------------------
|
|
||||||
const float ini_dtita = range_old[image_level](u+1) - range_old[image_level](u-1);
|
|
||||||
const float final_dtita = range_warped[image_level](u+1) - range_warped[image_level](u-1);
|
|
||||||
|
|
||||||
const float dtitat = ini_dtita - final_dtita;
|
|
||||||
const float dtita2 = dtita(u+1) - dtita(u-1);
|
|
||||||
|
|
||||||
const float w_der = kdt*(dt(u)*dt(u)) +
|
|
||||||
kdtita*(dtita(u)*dtita(u)) +
|
|
||||||
k2d*(std::abs(dtitat) + std::abs(dtita2));
|
|
||||||
|
|
||||||
weights(u) = std::sqrt(1.f/w_der);
|
|
||||||
}
|
|
||||||
|
|
||||||
const float inv_max = 1.f / weights.maxCoeff();
|
|
||||||
weights = inv_max*weights;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2D::findNullPoints()
|
|
||||||
{
|
|
||||||
//Size of null matrix is set to its maximum size (constructor)
|
|
||||||
num_valid_range = 0;
|
|
||||||
|
|
||||||
for (unsigned int u = 1; u < cols_i-1; u++)
|
|
||||||
{
|
|
||||||
if (range_inter[image_level](u) == 0.f)
|
|
||||||
null(u) = 1;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
num_valid_range++;
|
|
||||||
null(u) = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solves the system without considering any robust-function
|
|
||||||
void CLaserOdometry2D::solveSystemOneLevel()
|
|
||||||
{
|
|
||||||
A.resize(num_valid_range, 3);
|
|
||||||
B.resize(num_valid_range, 1);
|
|
||||||
|
|
||||||
unsigned int cont = 0;
|
|
||||||
const float kdtita = (cols_i-1)/fovh;
|
|
||||||
|
|
||||||
//Fill the matrix A and the vector B
|
|
||||||
//The order of the variables will be (vx, vy, wz)
|
|
||||||
|
|
||||||
for (unsigned int u = 1; u < cols_i-1; u++)
|
|
||||||
if (null(u) == 0)
|
|
||||||
{
|
|
||||||
// Precomputed expressions
|
|
||||||
const float tw = weights(u);
|
|
||||||
const float tita = -0.5*fovh + u/kdtita;
|
|
||||||
|
|
||||||
//Fill the matrix A
|
|
||||||
A(cont, 0) = tw*(std::cos(tita) + dtita(u)*kdtita*std::sin(tita)/range_inter[image_level](u));
|
|
||||||
A(cont, 1) = tw*(std::sin(tita) - dtita(u)*kdtita*std::cos(tita)/range_inter[image_level](u));
|
|
||||||
A(cont, 2) = tw*(-yy[image_level](u)*std::cos(tita) + xx[image_level](u)*std::sin(tita) - dtita(u)*kdtita);
|
|
||||||
B(cont, 0) = tw*(-dt(u));
|
|
||||||
|
|
||||||
cont++;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Solve the linear system of equations using a minimum least squares method
|
|
||||||
Eigen::MatrixXf AtA, AtB;
|
|
||||||
AtA = A.transpose()*A;
|
|
||||||
AtB = A.transpose()*B;
|
|
||||||
Var = AtA.ldlt().solve(AtB);
|
|
||||||
|
|
||||||
//Covariance matrix calculation Cov Order -> vx,vy,wz
|
|
||||||
Eigen::MatrixXf res(num_valid_range,1);
|
|
||||||
res = A*Var - B;
|
|
||||||
cov_odo = (1.f/float(num_valid_range-3))*AtA.inverse()*res.squaredNorm();
|
|
||||||
|
|
||||||
kai_loc_level_ = Var;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solves the system by considering the Cauchy M-estimator robust-function
|
|
||||||
void CLaserOdometry2D::solveSystemNonLinear()
|
|
||||||
{
|
|
||||||
A.resize(num_valid_range, 3); Aw.resize(num_valid_range, 3);
|
|
||||||
B.resize(num_valid_range, 1); Bw.resize(num_valid_range, 1);
|
|
||||||
unsigned int cont = 0;
|
|
||||||
const float kdtita = float(cols_i-1)/fovh;
|
|
||||||
|
|
||||||
//Fill the matrix A and the vector B
|
|
||||||
//The order of the variables will be (vx, vy, wz)
|
|
||||||
|
|
||||||
for (unsigned int u = 1; u < cols_i-1; u++)
|
|
||||||
if (null(u) == 0)
|
|
||||||
{
|
|
||||||
// Precomputed expressions
|
|
||||||
const float tw = weights(u);
|
|
||||||
const float tita = -0.5*fovh + u/kdtita;
|
|
||||||
|
|
||||||
//Fill the matrix A
|
|
||||||
A(cont, 0) = tw*(std::cos(tita) + dtita(u)*kdtita*std::sin(tita)/range_inter[image_level](u));
|
|
||||||
A(cont, 1) = tw*(std::sin(tita) - dtita(u)*kdtita*std::cos(tita)/range_inter[image_level](u));
|
|
||||||
A(cont, 2) = tw*(-yy[image_level](u)*std::cos(tita) + xx[image_level](u)*std::sin(tita) - dtita(u)*kdtita);
|
|
||||||
B(cont, 0) = tw*(-dt(u));
|
|
||||||
|
|
||||||
cont++;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Solve the linear system of equations using a minimum least squares method
|
|
||||||
Eigen::MatrixXf AtA, AtB;
|
|
||||||
AtA = A.transpose()*A;
|
|
||||||
AtB = A.transpose()*B;
|
|
||||||
Var = AtA.ldlt().solve(AtB);
|
|
||||||
|
|
||||||
//Covariance matrix calculation Cov Order -> vx,vy,wz
|
|
||||||
Eigen::MatrixXf res(num_valid_range,1);
|
|
||||||
res = A*Var - B;
|
|
||||||
//cout << endl << "max res: " << res.maxCoeff();
|
|
||||||
//cout << endl << "min res: " << res.minCoeff();
|
|
||||||
|
|
||||||
////Compute the energy
|
|
||||||
//Compute the average dt
|
|
||||||
float aver_dt = 0.f, aver_res = 0.f; unsigned int ind = 0;
|
|
||||||
for (unsigned int u = 1; u < cols_i-1; u++)
|
|
||||||
if (null(u) == 0)
|
|
||||||
{
|
|
||||||
aver_dt += std::abs(dt(u));
|
|
||||||
aver_res += std::abs(res(ind++));
|
|
||||||
}
|
|
||||||
aver_dt /= cont; aver_res /= cont;
|
|
||||||
// printf("\n Aver dt = %f, aver res = %f", aver_dt, aver_res);
|
|
||||||
|
|
||||||
|
|
||||||
const float k = 10.f/aver_dt; //200
|
|
||||||
//float energy = 0.f;
|
|
||||||
//for (unsigned int i=0; i<res.rows(); i++)
|
|
||||||
// energy += log(1.f + mrpt::math::square(k*res(i)));
|
|
||||||
//printf("\n\nEnergy(0) = %f", energy);
|
|
||||||
|
|
||||||
//Solve iterative reweighted least squares
|
|
||||||
//===================================================================
|
|
||||||
for (unsigned int i=1; i<=iter_irls; i++)
|
|
||||||
{
|
|
||||||
cont = 0;
|
|
||||||
|
|
||||||
for (unsigned int u = 1; u < cols_i-1; u++)
|
|
||||||
if (null(u) == 0)
|
|
||||||
{
|
|
||||||
const float res_weight = std::sqrt(1.f/(1.f + ((k*res(cont))*(k*res(cont)))));
|
|
||||||
|
|
||||||
//Fill the matrix Aw
|
|
||||||
Aw(cont,0) = res_weight*A(cont,0);
|
|
||||||
Aw(cont,1) = res_weight*A(cont,1);
|
|
||||||
Aw(cont,2) = res_weight*A(cont,2);
|
|
||||||
Bw(cont) = res_weight*B(cont);
|
|
||||||
cont++;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Solve the linear system of equations using a minimum least squares method
|
|
||||||
AtA = Aw.transpose()*Aw;
|
|
||||||
AtB = Aw.transpose()*Bw;
|
|
||||||
Var = AtA.ldlt().solve(AtB);
|
|
||||||
res = A*Var - B;
|
|
||||||
|
|
||||||
////Compute the energy
|
|
||||||
//energy = 0.f;
|
|
||||||
//for (unsigned int j=0; j<res.rows(); j++)
|
|
||||||
// energy += log(1.f + mrpt::math::square(k*res(j)));
|
|
||||||
//printf("\nEnergy(%d) = %f", i, energy);
|
|
||||||
}
|
|
||||||
|
|
||||||
cov_odo = (1.f/float(num_valid_range-3))*AtA.inverse()*res.squaredNorm();
|
|
||||||
kai_loc_level_ = Var;
|
|
||||||
|
|
||||||
//RCLCPP_INFO_STREAM(get_logger(), "[rf2o] COV_ODO:\n" << cov_odo);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2D::Reset(const Pose3d& ini_pose/*, CObservation2DRangeScan scan*/)
|
|
||||||
{
|
|
||||||
//Set the initial pose
|
|
||||||
laser_pose_ = ini_pose;
|
|
||||||
laser_oldpose_ = ini_pose;
|
|
||||||
|
|
||||||
//readLaser(scan);
|
|
||||||
createImagePyramid();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs warping on the scan points of the current pyramid lvl(coarse2fine)
|
|
||||||
*/
|
|
||||||
void CLaserOdometry2D::performWarping()
|
|
||||||
{
|
|
||||||
Eigen::Matrix3f acu_trans;
|
|
||||||
|
|
||||||
acu_trans.setIdentity();
|
|
||||||
for (unsigned int i=1; i<=level; i++)
|
|
||||||
acu_trans = transformations[i-1]*acu_trans;
|
|
||||||
|
|
||||||
Eigen::MatrixXf wacu = Eigen::MatrixXf::Constant(1, cols_i, 0.f);
|
|
||||||
|
|
||||||
range_warped[image_level].setConstant(1, cols_i, 0.f);
|
|
||||||
|
|
||||||
const float cols_lim = float(cols_i-1);
|
|
||||||
const float kdtita = cols_lim/fovh;
|
|
||||||
|
|
||||||
for (unsigned int j = 0; j<cols_i; j++)
|
|
||||||
{
|
|
||||||
if (range[image_level](j) > 0.f)
|
|
||||||
{
|
|
||||||
//Transform point to the warped reference frame
|
|
||||||
const float x_w = acu_trans(0,0)*xx[image_level](j) + acu_trans(0,1)*yy[image_level](j) + acu_trans(0,2);
|
|
||||||
const float y_w = acu_trans(1,0)*xx[image_level](j) + acu_trans(1,1)*yy[image_level](j) + acu_trans(1,2);
|
|
||||||
const float tita_w = std::atan2(y_w, x_w);
|
|
||||||
const float range_w = std::sqrt(x_w*x_w + y_w*y_w);
|
|
||||||
|
|
||||||
//Calculate warping
|
|
||||||
const float uwarp = kdtita*(tita_w + 0.5*fovh);
|
|
||||||
|
|
||||||
//The warped pixel (which is not integer in general) contributes to all the surrounding ones
|
|
||||||
if (( uwarp >= 0.f)&&( uwarp < cols_lim))
|
|
||||||
{
|
|
||||||
const int uwarp_l = uwarp;
|
|
||||||
const int uwarp_r = uwarp_l + 1;
|
|
||||||
const float delta_r = float(uwarp_r) - uwarp;
|
|
||||||
const float delta_l = uwarp - float(uwarp_l);
|
|
||||||
|
|
||||||
//Very close pixel
|
|
||||||
if (std::abs(std::round(uwarp) - uwarp) < 0.05f)
|
|
||||||
{
|
|
||||||
range_warped[image_level]((int)round(uwarp)) += range_w;
|
|
||||||
wacu((int)std::round(uwarp)) += 1.f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const float w_r = delta_l*delta_l;
|
|
||||||
range_warped[image_level](uwarp_r) += w_r*range_w;
|
|
||||||
wacu(uwarp_r) += w_r;
|
|
||||||
|
|
||||||
const float w_l = delta_r*delta_r;
|
|
||||||
range_warped[image_level](uwarp_l) += w_l*range_w;
|
|
||||||
wacu(uwarp_l) += w_l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Scale the averaged range and compute coordinates
|
|
||||||
for (unsigned int u = 0; u<cols_i; u++)
|
|
||||||
{
|
|
||||||
if (wacu(u) > 0.f)
|
|
||||||
{
|
|
||||||
const float tita = -0.5f*fovh + float(u)/kdtita;
|
|
||||||
range_warped[image_level](u) /= wacu(u);
|
|
||||||
xx_warped[image_level](u) = range_warped[image_level](u)*std::cos(tita);
|
|
||||||
yy_warped[image_level](u) = range_warped[image_level](u)*std::sin(tita);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
range_warped[image_level](u) = 0.f;
|
|
||||||
xx_warped[image_level](u) = 0.f;
|
|
||||||
yy_warped[image_level](u) = 0.f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CLaserOdometry2D::filterLevelSolution()
|
|
||||||
{
|
|
||||||
// Calculate Eigenvalues and Eigenvectors
|
|
||||||
//----------------------------------------------------------
|
|
||||||
Eigen::SelfAdjointEigenSolver<Eigen::MatrixXf> eigensolver(cov_odo);
|
|
||||||
if (eigensolver.info() != Eigen::Success)
|
|
||||||
{
|
|
||||||
// RCLCPP_WARN(get_logger(), "WARNING: Eigensolver couldn't find a solution. Pose is not updated");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//First, we have to describe both the new linear and angular speeds in the "eigenvector" basis
|
|
||||||
//-------------------------------------------------------------------------------------------------
|
|
||||||
Eigen::Matrix<float,3,3> Bii;
|
|
||||||
Eigen::Matrix<float,3,1> kai_b;
|
|
||||||
Bii = eigensolver.eigenvectors();
|
|
||||||
|
|
||||||
kai_b = Bii.colPivHouseholderQr().solve(kai_loc_level_);
|
|
||||||
|
|
||||||
assert((kai_loc_level_).isApprox(Bii*kai_b, 1e-5) && "Ax=b has no solution." && __LINE__);
|
|
||||||
|
|
||||||
//Second, we have to describe both the old linear and angular speeds in the "eigenvector" basis too
|
|
||||||
//-------------------------------------------------------------------------------------------------
|
|
||||||
MatrixS31 kai_loc_sub;
|
|
||||||
|
|
||||||
//Important: we have to substract the solutions from previous levels
|
|
||||||
Eigen::Matrix3f acu_trans;
|
|
||||||
acu_trans.setIdentity();
|
|
||||||
for (unsigned int i=0; i<level; i++)
|
|
||||||
acu_trans = transformations[i]*acu_trans;
|
|
||||||
|
|
||||||
kai_loc_sub(0) = -fps*acu_trans(0,2);
|
|
||||||
kai_loc_sub(1) = -fps*acu_trans(1,2);
|
|
||||||
if (acu_trans(0,0) > 1.f)
|
|
||||||
kai_loc_sub(2) = 0.f;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
kai_loc_sub(2) = -fps*std::acos(acu_trans(0,0))*rf2o::sign(acu_trans(1,0));
|
|
||||||
}
|
|
||||||
kai_loc_sub += kai_loc_old_;
|
|
||||||
|
|
||||||
Eigen::Matrix<float,3,1> kai_b_old;
|
|
||||||
kai_b_old = Bii.colPivHouseholderQr().solve(kai_loc_sub);
|
|
||||||
|
|
||||||
assert((kai_loc_sub).isApprox(Bii*kai_b_old, 1e-5) && "Ax=b has no solution." && __LINE__);
|
|
||||||
|
|
||||||
//Filter speed
|
|
||||||
const float cf = 15e3f*std::exp(-float(int(level))),
|
|
||||||
df = 0.05f*std::exp(-float(int(level)));
|
|
||||||
|
|
||||||
Eigen::Matrix<float,3,1> kai_b_fil;
|
|
||||||
for (unsigned int i=0; i<3; i++)
|
|
||||||
{
|
|
||||||
kai_b_fil(i) = (kai_b(i) + (cf*eigensolver.eigenvalues()(i,0) + df)*kai_b_old(i))/(1.f + cf*eigensolver.eigenvalues()(i,0) + df);
|
|
||||||
//kai_b_fil_f(i,0) = (1.f*kai_b(i,0) + 0.f*kai_b_old_f(i,0))/(1.0f + 0.f);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Transform filtered speed to local reference frame and compute transformation
|
|
||||||
Eigen::Matrix<float, 3, 1> kai_loc_fil = Bii.inverse().colPivHouseholderQr().solve(kai_b_fil);
|
|
||||||
|
|
||||||
assert((kai_b_fil).isApprox(Bii.inverse()*kai_loc_fil, 1e-5) && "Ax=b has no solution." && __LINE__);
|
|
||||||
|
|
||||||
//transformation
|
|
||||||
const float incrx = kai_loc_fil(0)/fps;
|
|
||||||
const float incry = kai_loc_fil(1)/fps;
|
|
||||||
const float rot = kai_loc_fil(2)/fps;
|
|
||||||
|
|
||||||
transformations[level](0,0) = std::cos(rot);
|
|
||||||
transformations[level](0,1) = -std::sin(rot);
|
|
||||||
transformations[level](1,0) = std::sin(rot);
|
|
||||||
transformations[level](1,1) = std::cos(rot);
|
|
||||||
transformations[level](0,2) = incrx;
|
|
||||||
transformations[level](1,2) = incry;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void CLaserOdometry2D::PoseUpdate() {
|
|
||||||
Eigen::Matrix3f acu_trans;
|
|
||||||
acu_trans.setIdentity();
|
|
||||||
for (unsigned int i=1; i<=ctf_levels; i++)
|
|
||||||
acu_trans = transformations[i-1]*acu_trans;
|
|
||||||
|
|
||||||
kai_loc_(0) = fps*acu_trans(0,2);
|
|
||||||
kai_loc_(1) = fps*acu_trans(1,2);
|
|
||||||
if (acu_trans(0,0) > 1.f) kai_loc_(2) = 0.f;
|
|
||||||
else kai_loc_(2) = fps*std::acos(acu_trans(0,0))*rf2o::sign(acu_trans(1,0));
|
|
||||||
|
|
||||||
float phi = rf2o::getYaw(laser_pose_.rotation());
|
|
||||||
kai_abs_(0) = kai_loc_(0)*std::cos(phi) - kai_loc_(1)*std::sin(phi);
|
|
||||||
kai_abs_(1) = kai_loc_(0)*std::sin(phi) + kai_loc_(1)*std::cos(phi);
|
|
||||||
kai_abs_(2) = kai_loc_(2);
|
|
||||||
|
|
||||||
laser_oldpose_ = laser_pose_;
|
|
||||||
Pose3d pose_aux_2D = Pose3d::Identity();
|
|
||||||
pose_aux_2D = rf2o::matrixYaw(double(kai_loc_(2)/fps));
|
|
||||||
pose_aux_2D.translation()(0) = acu_trans(0,2);
|
|
||||||
pose_aux_2D.translation()(1) = acu_trans(1,2);
|
|
||||||
laser_pose_ = laser_pose_ * pose_aux_2D;
|
|
||||||
last_increment_ = pose_aux_2D;
|
|
||||||
|
|
||||||
phi = rf2o::getYaw(laser_pose_.rotation());
|
|
||||||
kai_loc_old_(0) = kai_abs_(0)*std::cos(phi) + kai_abs_(1)*std::sin(phi);
|
|
||||||
kai_loc_old_(1) = -kai_abs_(0)*std::sin(phi) + kai_abs_(1)*std::cos(phi);
|
|
||||||
kai_loc_old_(2) = kai_abs_(2);
|
|
||||||
|
|
||||||
// ✅ [CLEANED] All odom position/yaw logs removed completely.
|
|
||||||
|
|
||||||
robot_pose_ = laser_pose_ * laser_pose_on_robot_inv_;
|
|
||||||
|
|
||||||
// ✅ [MODIFIED] Plain double arithmetic instead of rclcpp::Time operations
|
|
||||||
double time_inc_sec = current_scan_time_sec - last_odom_time_sec;
|
|
||||||
last_odom_time_sec = current_scan_time_sec;
|
|
||||||
|
|
||||||
if (time_inc_sec > 1e-9) {
|
|
||||||
lin_speed = acu_trans(0,2) / time_inc_sec;
|
|
||||||
double ang_inc = rf2o::getYaw(robot_pose_.rotation()) - rf2o::getYaw(robot_oldpose_.rotation());
|
|
||||||
if (ang_inc > 3.14159) ang_inc -= 2*3.14159;
|
|
||||||
if (ang_inc < -3.14159) ang_inc += 2*3.14159;
|
|
||||||
ang_speed = ang_inc/time_inc_sec;
|
|
||||||
}
|
|
||||||
|
|
||||||
robot_oldpose_ = robot_pose_;
|
|
||||||
}
|
|
||||||
} /* namespace rf2o */
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
/** ****************************************************************************************
|
|
||||||
* Planar Odometry from a Radial Laser Scanner. A Range Flow-based Approach. ICRA'16.
|
|
||||||
* Adapted: Updated to work with refactored pure-C++ CLaserOdometry2D algorithm class.
|
|
||||||
******************************************************************************************** */
|
|
||||||
|
|
||||||
#include "rf2o_laser_odometry/CLaserOdometry2DNode.hpp"
|
|
||||||
#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp> // ✅ 确保包含此头文件以支持 tf2::convert
|
|
||||||
|
|
||||||
using namespace rf2o;
|
|
||||||
|
|
||||||
CLaserOdometry2DNode::CLaserOdometry2DNode(): Node("rf2o_laser_odometry_node")
|
|
||||||
{
|
|
||||||
RCLCPP_INFO(get_logger(), "Initializing RF2O node...");
|
|
||||||
|
|
||||||
this->declare_parameter<std::string>("laser_scan_topic", "/scan");
|
|
||||||
this->get_parameter("laser_scan_topic", laser_scan_topic);
|
|
||||||
this->declare_parameter<std::string>("odom_topic", "/odom_rf2o");
|
|
||||||
this->get_parameter("odom_topic", odom_topic);
|
|
||||||
this->declare_parameter<std::string>("base_frame_id", "base_link");
|
|
||||||
this->get_parameter("base_frame_id", base_frame_id);
|
|
||||||
this->declare_parameter<std::string>("odom_frame_id", "odom");
|
|
||||||
this->get_parameter("odom_frame_id", odom_frame_id);
|
|
||||||
this->declare_parameter<bool>("publish_tf", true);
|
|
||||||
this->get_parameter("publish_tf", publish_tf);
|
|
||||||
this->declare_parameter<std::string>("init_pose_from_topic", "/base_pose_ground_truth");
|
|
||||||
this->get_parameter("init_pose_from_topic", init_pose_from_topic);
|
|
||||||
this->declare_parameter<double>("freq", 10.0);
|
|
||||||
this->get_parameter("freq", freq);
|
|
||||||
|
|
||||||
buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock());
|
|
||||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*buffer_);
|
|
||||||
odom_broadcaster = std::make_unique<tf2_ros::TransformBroadcaster>(this);
|
|
||||||
odom_pub = this->create_publisher<nav_msgs::msg::Odometry>(odom_topic, 5);
|
|
||||||
laser_sub = this->create_subscription<sensor_msgs::msg::LaserScan>(
|
|
||||||
laser_scan_topic,
|
|
||||||
rclcpp::QoS(rclcpp::KeepLast(1)).best_effort().durability_volatile(),
|
|
||||||
std::bind(&CLaserOdometry2DNode::LaserCallBack, this, std::placeholders::_1));
|
|
||||||
|
|
||||||
if (init_pose_from_topic != "") {
|
|
||||||
initPose_sub = this->create_subscription<nav_msgs::msg::Odometry>(
|
|
||||||
init_pose_from_topic,
|
|
||||||
rclcpp::QoS(rclcpp::KeepLast(1)).best_effort().durability_volatile(),
|
|
||||||
std::bind(&CLaserOdometry2DNode::initPoseCallBack, this, std::placeholders::_1));
|
|
||||||
GT_pose_initialized = false;
|
|
||||||
} else {
|
|
||||||
GT_pose_initialized = true;
|
|
||||||
initial_robot_pose.pose.pose.position.x = 0;
|
|
||||||
initial_robot_pose.pose.pose.position.y = 0;
|
|
||||||
initial_robot_pose.pose.pose.position.z = 0;
|
|
||||||
initial_robot_pose.pose.pose.orientation.w = 1; // ✅ 修正:初始四元数 w 应为 1
|
|
||||||
}
|
|
||||||
|
|
||||||
rf2o_ref.module_initialized = false;
|
|
||||||
rf2o_ref.first_laser_scan = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2DNode::LaserCallBack(const sensor_msgs::msg::LaserScan::SharedPtr new_scan)
|
|
||||||
{
|
|
||||||
if (GT_pose_initialized) {
|
|
||||||
last_scan = *new_scan;
|
|
||||||
|
|
||||||
// ✅ 关键修改:将 ROS 时间戳转换为 double 秒
|
|
||||||
rf2o_ref.current_scan_time_sec =
|
|
||||||
static_cast<double>(last_scan.header.stamp.sec) +
|
|
||||||
static_cast<double>(last_scan.header.stamp.nanosec) / 1e9;
|
|
||||||
|
|
||||||
if (!rf2o_ref.first_laser_scan) {
|
|
||||||
for (unsigned int i = 0; i < rf2o_ref.width; i++)
|
|
||||||
rf2o_ref.range_wf(i) = new_scan->ranges[i];
|
|
||||||
new_scan_available = true;
|
|
||||||
} else {
|
|
||||||
setLaserPoseFromTf();
|
|
||||||
rf2o_ref.init(last_scan, initial_robot_pose.pose.pose);
|
|
||||||
rf2o_ref.first_laser_scan = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CLaserOdometry2DNode::setLaserPoseFromTf()
|
|
||||||
{
|
|
||||||
bool retrieved = false;
|
|
||||||
geometry_msgs::msg::TransformStamped tf_laser;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tf_laser = buffer_->lookupTransform(base_frame_id, last_scan.header.frame_id, tf2::TimePointZero);
|
|
||||||
retrieved = true;
|
|
||||||
} catch (const tf2::TransformException &ex) {
|
|
||||||
RCLCPP_ERROR(get_logger(), "%s", ex.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retrieved) {
|
|
||||||
tf2::Transform transform;
|
|
||||||
tf2::convert(tf_laser.transform, transform);
|
|
||||||
const tf2::Matrix3x3 &basis = transform.getBasis();
|
|
||||||
Eigen::Matrix3d R;
|
|
||||||
for(int r = 0; r < 3; r++)
|
|
||||||
for(int c = 0; c < 3; c++)
|
|
||||||
R(r,c) = basis[r][c];
|
|
||||||
|
|
||||||
Pose3d laser_tf(R);
|
|
||||||
const tf2::Vector3 &t = transform.getOrigin();
|
|
||||||
laser_tf.translation()(0) = t[0];
|
|
||||||
laser_tf.translation()(1) = t[1];
|
|
||||||
laser_tf.translation()(2) = t[2];
|
|
||||||
rf2o_ref.setLaserPose(laser_tf);
|
|
||||||
}
|
|
||||||
return retrieved;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CLaserOdometry2DNode::scan_available() {
|
|
||||||
return new_scan_available;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2DNode::process()
|
|
||||||
{
|
|
||||||
if (rf2o_ref.is_initialized() && scan_available()) {
|
|
||||||
// ✅ 关键修改:传入当前时间戳
|
|
||||||
double now_sec = this->now().seconds();
|
|
||||||
rf2o_ref.odometryCalculation(last_scan, now_sec);
|
|
||||||
publish();
|
|
||||||
new_scan_available = false;
|
|
||||||
} else {
|
|
||||||
RCLCPP_WARN(get_logger(), "Waiting for laser_scans....");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2DNode::initPoseCallBack(const nav_msgs::msg::Odometry::SharedPtr new_initPose)
|
|
||||||
{
|
|
||||||
if (!GT_pose_initialized) {
|
|
||||||
initial_robot_pose = *new_initPose;
|
|
||||||
GT_pose_initialized = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CLaserOdometry2DNode::publish()
|
|
||||||
{
|
|
||||||
tf2::Quaternion tf_quaternion;
|
|
||||||
tf_quaternion.setRPY(0.0, 0.0, rf2o::getYaw(rf2o_ref.robot_pose_.rotation()));
|
|
||||||
geometry_msgs::msg::Quaternion quaternion = tf2::toMsg(tf_quaternion);
|
|
||||||
|
|
||||||
nav_msgs::msg::Odometry odom;
|
|
||||||
|
|
||||||
// ✅ 关键修改:将 double 秒转换回 ROS 时间戳
|
|
||||||
odom.header.stamp.sec = static_cast<int32_t>(rf2o_ref.last_odom_time_sec);
|
|
||||||
odom.header.stamp.nanosec = static_cast<uint32_t>(
|
|
||||||
(rf2o_ref.last_odom_time_sec - odom.header.stamp.sec) * 1e9);
|
|
||||||
|
|
||||||
odom.header.frame_id = odom_frame_id;
|
|
||||||
odom.pose.pose.position.x = rf2o_ref.robot_pose_.translation()(0);
|
|
||||||
odom.pose.pose.position.y = rf2o_ref.robot_pose_.translation()(1);
|
|
||||||
odom.pose.pose.position.z = 0.0;
|
|
||||||
odom.pose.pose.orientation = quaternion;
|
|
||||||
odom.child_frame_id = base_frame_id;
|
|
||||||
odom.twist.twist.linear.x = rf2o_ref.lin_speed;
|
|
||||||
odom.twist.twist.linear.y = 0.0;
|
|
||||||
odom.twist.twist.angular.z = rf2o_ref.ang_speed;
|
|
||||||
|
|
||||||
odom_pub->publish(odom);
|
|
||||||
|
|
||||||
if (publish_tf) {
|
|
||||||
geometry_msgs::msg::TransformStamped odom_trans;
|
|
||||||
odom_trans.header.stamp = odom.header.stamp; // ✅ 复用已转换的时间戳
|
|
||||||
odom_trans.header.frame_id = odom_frame_id;
|
|
||||||
odom_trans.child_frame_id = base_frame_id;
|
|
||||||
odom_trans.transform.translation.x = rf2o_ref.robot_pose_.translation()(0);
|
|
||||||
odom_trans.transform.translation.y = rf2o_ref.robot_pose_.translation()(1);
|
|
||||||
odom_trans.transform.translation.z = 0.0;
|
|
||||||
odom_trans.transform.rotation = quaternion;
|
|
||||||
odom_broadcaster->sendTransform(odom_trans);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
rclcpp::init(argc, argv);
|
|
||||||
auto myLaserOdomNode = std::make_shared<rf2o::CLaserOdometry2DNode>();
|
|
||||||
rclcpp::Rate rate(myLaserOdomNode->freq);
|
|
||||||
|
|
||||||
while (rclcpp::ok()) {
|
|
||||||
rclcpp::spin_some(myLaserOdomNode);
|
|
||||||
myLaserOdomNode->process();
|
|
||||||
rate.sleep();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
165
ros2/src/udp_teleop/README.md
Normal file
165
ros2/src/udp_teleop/README.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# udp_teleop — ROS 2 底盘 + 机械臂键盘 UDP 遥控
|
||||||
|
|
||||||
|
通过键盘实时控制底盘(差速驱动)和机械臂(6 电机),指令通过 **单一 UDP socket** 发送到设备端。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ros2/
|
||||||
|
├── build/ # colcon 构建产物(自动生成)
|
||||||
|
├── install/ # colcon 安装产物(自动生成)
|
||||||
|
├── log/ # 构建日志(自动生成)
|
||||||
|
└── src/
|
||||||
|
└── udp_teleop/ # ROS 2 包
|
||||||
|
├── config/
|
||||||
|
│ └── params.yaml # 可配置参数
|
||||||
|
├── launch/ # launch 文件(预留)
|
||||||
|
├── udp_teleop/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── keyboard_control.py # 键盘遥控节点
|
||||||
|
├── resource/
|
||||||
|
├── test/
|
||||||
|
├── package.xml
|
||||||
|
├── setup.cfg
|
||||||
|
└── setup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境搭建
|
||||||
|
|
||||||
|
### 1. 安装 Conda
|
||||||
|
|
||||||
|
使用 Miniconda 或 Anaconda。推荐 Miniconda:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 下载并安装 Miniconda
|
||||||
|
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
|
||||||
|
bash Miniconda3-latest-Linux-x86_64.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 创建 ROS 2 Humble 环境
|
||||||
|
|
||||||
|
使用 robostack 频道安装 ROS 2 Humble Desktop 完整版:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda create -n ros2_humble -c robostack-staging -c conda-forge ros-humble-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 安装构建工具
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda activate ros2_humble
|
||||||
|
conda install -c robostack-staging -c conda-forge \
|
||||||
|
colcon-common-extensions \
|
||||||
|
ros-humble-ament-cmake \
|
||||||
|
python3-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 安装 Python 依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pynput
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 激活环境
|
||||||
|
|
||||||
|
每次使用前:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda activate ros2_humble
|
||||||
|
source /path/to/ros2/install/setup.bash # 首次构建后执行
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ros2
|
||||||
|
colcon build --symlink-install --packages-select udp_teleop
|
||||||
|
source install/setup.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
> `--symlink-install`:修改 Python 源文件后无需重新构建,直接生效。
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
### 使用参数文件(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ros2 run udp_teleop keyboard_control \
|
||||||
|
--ros-args --params-file src/udp_teleop/config/params.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命令行覆盖参数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ros2 run udp_teleop keyboard_control \
|
||||||
|
--ros-args -p udp_ip:=192.168.1.100 -p udp_port:=9999
|
||||||
|
```
|
||||||
|
|
||||||
|
## 按键映射
|
||||||
|
|
||||||
|
### 底盘控制
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `W` / `S` | 前进 / 后退 |
|
||||||
|
| `A` / `D` | 左移 / 右移 |
|
||||||
|
| `Q` / `E` | 左转 / 右转 |
|
||||||
|
|
||||||
|
### 机械臂控制
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `↑` / `↓` | 升降高度(↑ 升高,↓ 降低) |
|
||||||
|
| `2` ~ `6` | 选择关节 J2 ~ J6 |
|
||||||
|
| `←` / `→` | 减小 / 增大当前关节角度 |
|
||||||
|
|
||||||
|
> 底盘和机械臂可以**同时操控**。机械臂指令仅在按下机械臂相关按键后发送。
|
||||||
|
|
||||||
|
### 其他
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `Ctrl+C` | 退出并发送底盘停止指令 |
|
||||||
|
|
||||||
|
## UDP 协议
|
||||||
|
|
||||||
|
### 底盘指令
|
||||||
|
|
||||||
|
```
|
||||||
|
XYW:<X速度>:<Y速度>:<W角速度>:XZHY\n
|
||||||
|
```
|
||||||
|
|
||||||
|
### 机械臂指令
|
||||||
|
|
||||||
|
```
|
||||||
|
JXB:<高度>:<J2>:<J3>:<J4>:<J5>:<J6>:0:0:EZHY\n
|
||||||
|
```
|
||||||
|
|
||||||
|
- 6 个电机:电机 1 控制高度,电机 2~6 对应关节 J2~J6
|
||||||
|
- 末尾补零至 8 个值
|
||||||
|
|
||||||
|
## 参数配置
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `udp_ip` | `127.0.0.1` | UDP 目标 IP 地址 |
|
||||||
|
| `udp_port` | `8888` | UDP 目标端口 |
|
||||||
|
| `chassis_linear_speed` | `100` | 底盘线速度 |
|
||||||
|
| `chassis_angular_speed` | `45` | 底盘角速度 |
|
||||||
|
| `arm_height_step` | `5` | 高度每步变化量 |
|
||||||
|
| `arm_joint_step` | `5` | 关节角度每步变化量 |
|
||||||
|
| `update_rate` | `0.05` | 控制循环周期(秒) |
|
||||||
|
| `stdin_hold_time` | `0.04` | 按键持续时间(秒),修复箭头键时序问题 |
|
||||||
|
| `debug_keys` | `false` | 是否在状态行显示当前按键 |
|
||||||
|
| `keyboard_backend` | `auto` | 键盘后端:`auto` / `stdin` / `pynput` / `win_poll` |
|
||||||
|
|
||||||
|
## 键盘后端
|
||||||
|
|
||||||
|
| 后端 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `auto` | 自动选择:Linux/macOS 用 `stdin`,Windows 用 `win_poll` |
|
||||||
|
| `stdin` | 基于终端原始输入,无需额外依赖,**需要交互终端** |
|
||||||
|
| `pynput` | 基于 pynput 库,跨平台,需要 `pip install pynput` |
|
||||||
|
| `win_poll` | Windows 专用,通过 Win32 API 轮询按键状态 |
|
||||||
|
|
||||||
|
> `ros2 launch` 启动的子进程**没有交互终端**,使用 `stdin` 后端会报错。必须通过 `ros2 run` 在终端直接运行。
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 机械臂控制节点参数配置
|
|
||||||
|
|
||||||
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 # 是否使用状态缓存
|
|
||||||
|
|
||||||
# 启动复位
|
|
||||||
auto_reset_on_startup: true # 启动时是否自动复位
|
|
||||||
reset_x: 200.0 # 复位位置 X (mm)
|
|
||||||
reset_y: 0.0 # 复位位置 Y (mm)
|
|
||||||
reset_z: -285.0 # 复位位置 Z (mm)
|
|
||||||
reset_phi: 0.0 # 复位朝向 (度)
|
|
||||||
reset_duration: 3.0 # 复位运动时长 (秒)
|
|
||||||
|
|
||||||
# 状态发布频率
|
|
||||||
publish_rate: 10.0 # Hz
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
box_detection_grasp:
|
|
||||||
ros__parameters:
|
|
||||||
# ESP32 相机流(由 camera_ip 拼接 http://<ip>/stream)
|
|
||||||
camera_ip: "192.168.4.1"
|
|
||||||
|
|
||||||
# YOLO 模型路径
|
|
||||||
model_path: "/home/fallensigh/Dev/craic/ros2/src/udp_teleop/models/box_detection.pt"
|
|
||||||
|
|
||||||
# 检测参数
|
|
||||||
confidence: 0.35 # 置信度阈值
|
|
||||||
imgsz: 768 # YOLO 输入尺寸
|
|
||||||
device: "" # 空字符串=自动选择,可设为 "cpu" 或 "cuda"
|
|
||||||
detection_rate: 10.0 # 检测频率 (Hz)
|
|
||||||
|
|
||||||
# 方框尺寸(用于深度估计)
|
|
||||||
box_size_m: 0.03 # 方框边长 (米)
|
|
||||||
|
|
||||||
# 相机参数
|
|
||||||
horizontal_fov_deg: 66.0 # 水平视场角 (度)
|
|
||||||
max_frame_age: 0.5 # 最大帧延迟 (秒)
|
|
||||||
|
|
||||||
# 调试
|
|
||||||
show_debug_window: false # 是否显示调试窗口
|
|
||||||
|
|
||||||
# 控制参数
|
|
||||||
auto_grasp: false # 是否自动抓取(true=启动后自动检测并抓取)
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
keyboard_udp_control:
|
keyboard_udp_control:
|
||||||
ros__parameters:
|
ros__parameters:
|
||||||
udp_ip: "192.168.4.1"
|
udp_ip: "127.0.0.1"
|
||||||
udp_port: 8888
|
udp_port: 8888
|
||||||
chassis_linear_speed: 250
|
chassis_linear_speed: 100
|
||||||
chassis_angular_speed: 100
|
chassis_angular_speed: 45
|
||||||
arm_height_step: 5
|
arm_height_step: 5
|
||||||
arm_joint_step: 5
|
arm_joint_step: 5
|
||||||
update_rate: 0.03
|
update_rate: 0.05
|
||||||
debug_keys: false
|
debug_keys: false
|
||||||
keyboard_backend: "auto"
|
keyboard_backend: "auto"
|
||||||
stdin_hold_time: 0.08
|
stdin_hold_time: 0.04
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# 视觉抓取节点参数配置
|
|
||||||
|
|
||||||
vision_grasp:
|
|
||||||
ros__parameters:
|
|
||||||
# 相机到 TCP 的变换参数
|
|
||||||
cam_tx: 0.0 # X 平移 (mm)
|
|
||||||
cam_ty: 0.0 # Y 平移 (mm)
|
|
||||||
cam_tz: 30.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 # 夹爪动作时长 (秒)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""视觉抓取 bringup:机械臂控制 + 视觉抓取 + (可选) YOLO 方框检测。
|
|
||||||
|
|
||||||
组件依赖:arm_control → vision_grasp → box_detection_grasp
|
|
||||||
三个节点通过 ROS 服务通信,启动顺序由 lifecycle 自发处理。
|
|
||||||
|
|
||||||
用法:
|
|
||||||
ros2 launch udp_teleop vision_grasp.launch.py
|
|
||||||
ros2 launch udp_teleop vision_grasp.launch.py udp_ip:=192.168.4.1 detection:=true
|
|
||||||
ros2 launch udp_teleop vision_grasp.launch.py detection:=true auto_grasp:=true
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch.actions import DeclareLaunchArgument
|
|
||||||
from launch.conditions import IfCondition
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
pkg_share = get_package_share_directory('udp_teleop')
|
|
||||||
|
|
||||||
arm_params = os.path.join(pkg_share, 'config', 'arm_control.yaml')
|
|
||||||
vision_params = os.path.join(pkg_share, 'config', 'vision_grasp.yaml')
|
|
||||||
detection_params = os.path.join(pkg_share, 'config', 'box_detection_grasp.yaml')
|
|
||||||
|
|
||||||
detection = LaunchConfiguration('detection')
|
|
||||||
auto_grasp = LaunchConfiguration('auto_grasp')
|
|
||||||
udp_ip = LaunchConfiguration('udp_ip')
|
|
||||||
show_debug = LaunchConfiguration('show_debug')
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
DeclareLaunchArgument('detection', default_value='false',
|
|
||||||
description='是否启动 YOLO 方框检测节点'),
|
|
||||||
DeclareLaunchArgument('auto_grasp', default_value='false',
|
|
||||||
description='检测到方框后自动抓取'),
|
|
||||||
DeclareLaunchArgument('udp_ip', default_value='192.168.4.1',
|
|
||||||
description='ESP32 IP(UDP 控制 + 相机流共用)'),
|
|
||||||
DeclareLaunchArgument('show_debug', default_value='false',
|
|
||||||
description='显示 YOLO 检测调试窗口'),
|
|
||||||
|
|
||||||
# ===== arm_control =====
|
|
||||||
Node(
|
|
||||||
package='udp_teleop',
|
|
||||||
executable='arm_control',
|
|
||||||
name='arm_control',
|
|
||||||
output='screen',
|
|
||||||
parameters=[arm_params, {'udp_ip': udp_ip}],
|
|
||||||
),
|
|
||||||
|
|
||||||
# ===== vision_grasp =====
|
|
||||||
Node(
|
|
||||||
package='udp_teleop',
|
|
||||||
executable='vision_grasp',
|
|
||||||
name='vision_grasp',
|
|
||||||
output='screen',
|
|
||||||
parameters=[vision_params],
|
|
||||||
),
|
|
||||||
|
|
||||||
# ===== box_detection_grasp (可选) =====
|
|
||||||
Node(
|
|
||||||
package='udp_teleop',
|
|
||||||
executable='box_detection_grasp',
|
|
||||||
name='box_detection_grasp',
|
|
||||||
output='screen',
|
|
||||||
parameters=[detection_params, {
|
|
||||||
'camera_ip': udp_ip,
|
|
||||||
'auto_grasp': auto_grasp,
|
|
||||||
'show_debug_window': show_debug,
|
|
||||||
}],
|
|
||||||
condition=IfCondition(detection),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
Binary file not shown.
@@ -3,13 +3,9 @@
|
|||||||
<package format="3">
|
<package format="3">
|
||||||
<name>udp_teleop</name>
|
<name>udp_teleop</name>
|
||||||
<version>0.0.0</version>
|
<version>0.0.0</version>
|
||||||
<description>UDP teleoperation and arm control for CRAIC robot</description>
|
<description>TODO: Package description</description>
|
||||||
<maintainer email="fallensigh@gmail.com">fallensigh</maintainer>
|
<maintainer email="fallensigh@gmail.com">fallensigh</maintainer>
|
||||||
<license>MIT</license>
|
<license>TODO: License declaration</license>
|
||||||
|
|
||||||
<depend>rclpy</depend>
|
|
||||||
<depend>std_msgs</depend>
|
|
||||||
<depend>arm_control_msgs</depend>
|
|
||||||
|
|
||||||
<test_depend>ament_copyright</test_depend>
|
<test_depend>ament_copyright</test_depend>
|
||||||
<test_depend>ament_flake8</test_depend>
|
<test_depend>ament_flake8</test_depend>
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ setup(
|
|||||||
('share/' + package_name, ['package.xml']),
|
('share/' + package_name, ['package.xml']),
|
||||||
(os.path.join('share', package_name, 'config'),
|
(os.path.join('share', package_name, 'config'),
|
||||||
glob('config/*.yaml')),
|
glob('config/*.yaml')),
|
||||||
(os.path.join('share', package_name, 'launch'),
|
|
||||||
glob('launch/*.launch.py')),
|
|
||||||
(os.path.join('share', package_name, 'models'),
|
|
||||||
glob('models/*.pt')),
|
|
||||||
],
|
],
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
zip_safe=True,
|
zip_safe=True,
|
||||||
@@ -33,10 +29,7 @@ setup(
|
|||||||
},
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'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',
|
|
||||||
'box_detection_grasp = udp_teleop.box_detection_grasp:main',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,868 +0,0 @@
|
|||||||
#!/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
|
|
||||||
|
|
||||||
# Z4 值根据夹爪状态变化
|
|
||||||
# J5 = -100 (闭合) → 夹爪朝上 (UP) → z4 = -100
|
|
||||||
# J5 = 81 (张开) → 夹爪朝下 (DOWN) → z4 = 55
|
|
||||||
Z4_UP = -100 # 夹爪朝上(J5=-100,闭合)
|
|
||||||
Z4_DOWN = 55 # 夹爪朝下(J5=81,张开)
|
|
||||||
|
|
||||||
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 # 仅用于配置默认值,实际使用动态 z4
|
|
||||||
|
|
||||||
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 resolve_z4_from_j5(j5: int) -> float:
|
|
||||||
"""根据 J5 状态确定 z4 值
|
|
||||||
|
|
||||||
- J5 = -100 (闭合): 夹爪朝上,z4 = -100mm
|
|
||||||
- J5 = 81 (张开): 夹爪朝下,z4 = 55mm
|
|
||||||
"""
|
|
||||||
return Z4_UP if j5 < 0 else Z4_DOWN
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_j5_from_z(z: float) -> int:
|
|
||||||
"""根据目标 z 坐标自动选择夹爪状态
|
|
||||||
|
|
||||||
- z > -55: 使用朝上状态 (J5=-100, z4=-100)
|
|
||||||
- z <= -55: 使用朝下状态 (J5=81, z4=55)
|
|
||||||
"""
|
|
||||||
return J5_CLOSED if z > -55 else J5_OPEN
|
|
||||||
|
|
||||||
|
|
||||||
def forward_kinematics(geometry: ArmGeometry, state: ArmMathState, z4: float) -> ArmPose:
|
|
||||||
"""正运动学:关节角度 → TCP 位姿
|
|
||||||
|
|
||||||
Args:
|
|
||||||
geometry: 机械臂几何参数
|
|
||||||
state: 数学坐标系的关节状态
|
|
||||||
z4: J4 到 TCP 的 Z 偏移(根据 J5 状态确定)
|
|
||||||
"""
|
|
||||||
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 - z4 # 使用动态 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,
|
|
||||||
z4: float,
|
|
||||||
) -> ArmMathState:
|
|
||||||
"""逆运动学:TCP 位姿 → 关节角度
|
|
||||||
|
|
||||||
Args:
|
|
||||||
geometry: 机械臂几何参数
|
|
||||||
pose: 目标 TCP 位姿
|
|
||||||
limits: 关节限位
|
|
||||||
elbow_up: 肘部朝上/朝下
|
|
||||||
j5: J5 角度
|
|
||||||
j6: J6 角度
|
|
||||||
z4: J4 到 TCP 的 Z 偏移(根据 J5 状态确定)
|
|
||||||
"""
|
|
||||||
# 计算 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 + z4 # 使用动态 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),
|
|
||||||
('auto_reset_on_startup', True),
|
|
||||||
('reset_x', 200.0),
|
|
||||||
('reset_y', 0.0),
|
|
||||||
('reset_z', -285.0),
|
|
||||||
('reset_phi', 0.0),
|
|
||||||
('reset_duration', 3.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.auto_reset_on_startup = self.get_parameter('auto_reset_on_startup').value
|
|
||||||
self.reset_x = self.get_parameter('reset_x').value
|
|
||||||
self.reset_y = self.get_parameter('reset_y').value
|
|
||||||
self.reset_z = self.get_parameter('reset_z').value
|
|
||||||
self.reset_phi = self.get_parameter('reset_phi').value
|
|
||||||
self.reset_duration = self.get_parameter('reset_duration').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}')
|
|
||||||
|
|
||||||
# 自动复位
|
|
||||||
if self.auto_reset_on_startup:
|
|
||||||
self.get_logger().info('执行启动复位...')
|
|
||||||
import threading
|
|
||||||
threading.Thread(target=self._auto_reset, daemon=True).start()
|
|
||||||
|
|
||||||
def _auto_reset(self):
|
|
||||||
"""自动复位线程"""
|
|
||||||
import time
|
|
||||||
time.sleep(1.0) # 等待节点完全启动
|
|
||||||
|
|
||||||
try:
|
|
||||||
target_pose = ArmPose(
|
|
||||||
x=self.reset_x,
|
|
||||||
y=self.reset_y,
|
|
||||||
z=self.reset_z,
|
|
||||||
phi_deg=self.reset_phi
|
|
||||||
)
|
|
||||||
|
|
||||||
# 根据目标 z 坐标自动选择 J5 和 z4
|
|
||||||
j5 = resolve_j5_from_z(target_pose.z)
|
|
||||||
j6 = DEFAULT_FIXED_J6
|
|
||||||
z4 = resolve_z4_from_j5(j5)
|
|
||||||
|
|
||||||
# 逆运动学
|
|
||||||
math_state = inverse_kinematics(
|
|
||||||
geometry=self.geometry,
|
|
||||||
pose=target_pose,
|
|
||||||
limits=self.limits,
|
|
||||||
elbow_up=False,
|
|
||||||
j5=j5,
|
|
||||||
j6=j6,
|
|
||||||
z4=z4,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 转换为命令状态
|
|
||||||
target_state = math_to_command_state(
|
|
||||||
math_state, self.zero_offsets, self.limits, j5, j6
|
|
||||||
)
|
|
||||||
|
|
||||||
# 生成插值轨迹
|
|
||||||
steps = max(1, int(self.reset_duration * self.default_rate))
|
|
||||||
trajectory = interpolate_command_states(
|
|
||||||
self.current_state, target_state, steps
|
|
||||||
)
|
|
||||||
|
|
||||||
# 执行运动
|
|
||||||
self.get_logger().info(f'复位到: ({self.reset_x:.1f}, {self.reset_y:.1f}, {self.reset_z:.1f}), phi={self.reset_phi:.1f}°')
|
|
||||||
for state in trajectory:
|
|
||||||
self.send_udp_commands([state], 0.0)
|
|
||||||
self.current_state = state
|
|
||||||
self.save_state()
|
|
||||||
time.sleep(1.0 / self.default_rate)
|
|
||||||
|
|
||||||
self.get_logger().info('✓ 复位完成')
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().error(f'复位失败: {e}')
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# 解析 z4:优先级 up/down > 自动选择
|
|
||||||
if request.up:
|
|
||||||
z4 = Z4_UP # 夹爪朝上,z4=-100
|
|
||||||
elif request.down:
|
|
||||||
z4 = Z4_DOWN # 夹爪朝下,z4=55
|
|
||||||
else:
|
|
||||||
# 自动选择:根据目标 z 坐标
|
|
||||||
j5_auto = resolve_j5_from_z(target_pose.z)
|
|
||||||
z4 = resolve_z4_from_j5(j5_auto)
|
|
||||||
|
|
||||||
# 解析夹爪开合(J6:grip/release)
|
|
||||||
if request.grip:
|
|
||||||
j6 = GRIP_ANGLE
|
|
||||||
elif request.release:
|
|
||||||
j6 = RELEASE_ANGLE
|
|
||||||
else:
|
|
||||||
j6 = self.current_state.j6
|
|
||||||
|
|
||||||
# 根据 z4 反推 J5(用于 UDP 命令)
|
|
||||||
j5 = J5_CLOSED if z4 == Z4_UP else J5_OPEN
|
|
||||||
|
|
||||||
# 逆运动学
|
|
||||||
math_state = inverse_kinematics(
|
|
||||||
geometry=self.geometry,
|
|
||||||
pose=target_pose,
|
|
||||||
limits=self.limits,
|
|
||||||
elbow_up=request.elbow_up,
|
|
||||||
j5=j5,
|
|
||||||
j6=j6,
|
|
||||||
z4=z4,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 转换为命令状态
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 根据当前 J5 状态确定 z4
|
|
||||||
z4 = resolve_z4_from_j5(self.current_state.j5)
|
|
||||||
pose = forward_kinematics(self.geometry, math_state, z4)
|
|
||||||
|
|
||||||
response.success = True
|
|
||||||
response.x = float(pose.x)
|
|
||||||
response.y = float(pose.y)
|
|
||||||
response.z = float(pose.z)
|
|
||||||
response.phi = float(pose.phi_deg)
|
|
||||||
response.height = int(self.current_state.height)
|
|
||||||
response.j2 = int(self.current_state.j2)
|
|
||||||
response.j3 = int(self.current_state.j3)
|
|
||||||
response.j4 = int(self.current_state.j4)
|
|
||||||
response.j5 = int(self.current_state.j5)
|
|
||||||
response.j6 = int(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)
|
|
||||||
z4 = resolve_z4_from_j5(self.current_state.j5)
|
|
||||||
pose = forward_kinematics(self.geometry, math_state, z4)
|
|
||||||
|
|
||||||
pose_msg = TCPPose()
|
|
||||||
pose_msg.header.stamp = self.get_clock().now().to_msg()
|
|
||||||
pose_msg.x = float(pose.x)
|
|
||||||
pose_msg.y = float(pose.y)
|
|
||||||
pose_msg.z = float(pose.z)
|
|
||||||
pose_msg.phi = float(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()
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""方框检测与自动抓取节点
|
|
||||||
|
|
||||||
基于 YOLO 模型检测方框,并自动调用视觉抓取节点完成抓取。
|
|
||||||
|
|
||||||
功能:
|
|
||||||
1. 启动参数控制:auto_grasp=true 时自动抓取
|
|
||||||
2. 实时检测方框
|
|
||||||
3. 检测成功后停止识别
|
|
||||||
4. 发布方框中心坐标到 /vision_grasp/grasp_target
|
|
||||||
5. 等待抓取完成
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
import rclpy
|
|
||||||
from rclpy.node import Node
|
|
||||||
from geometry_msgs.msg import Point
|
|
||||||
from std_srvs.srv import Trigger
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cv2
|
|
||||||
except ImportError:
|
|
||||||
cv2 = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
import numpy as np
|
|
||||||
except ImportError:
|
|
||||||
np = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
from ultralytics import YOLO
|
|
||||||
except ImportError:
|
|
||||||
YOLO = None
|
|
||||||
|
|
||||||
|
|
||||||
class MjpegFrameReader:
|
|
||||||
"""持续读取 ESP32 MJPEG 流并保持最新帧"""
|
|
||||||
|
|
||||||
def __init__(self, url, logger, reconnect_delay=1.0):
|
|
||||||
self.url = url
|
|
||||||
self.logger = logger
|
|
||||||
self.reconnect_delay = reconnect_delay
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.latest_frame = None
|
|
||||||
self.latest_stamp = None
|
|
||||||
self.running = False
|
|
||||||
self.thread = None
|
|
||||||
self.capture = None
|
|
||||||
self.last_log_time = 0.0
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.running = True
|
|
||||||
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
||||||
self.thread.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.running = False
|
|
||||||
if self.thread is not None:
|
|
||||||
self.thread.join(timeout=2.0)
|
|
||||||
if self.capture is not None:
|
|
||||||
self.capture.release()
|
|
||||||
self.capture = None
|
|
||||||
|
|
||||||
def get_latest(self):
|
|
||||||
with self.lock:
|
|
||||||
if self.latest_frame is None:
|
|
||||||
return None, None
|
|
||||||
return self.latest_frame.copy(), self.latest_stamp
|
|
||||||
|
|
||||||
def _log_periodic(self, message, period=5.0):
|
|
||||||
now = time.monotonic()
|
|
||||||
if now - self.last_log_time >= period:
|
|
||||||
self.logger.warning(message)
|
|
||||||
self.last_log_time = now
|
|
||||||
|
|
||||||
def _open_capture(self):
|
|
||||||
cap = cv2.VideoCapture(self.url)
|
|
||||||
if not cap.isOpened():
|
|
||||||
cap.release()
|
|
||||||
return None
|
|
||||||
|
|
||||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
||||||
self.logger.info(f"已连接到 ESP32 流: {self.url}")
|
|
||||||
return cap
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
while self.running:
|
|
||||||
if self.capture is None:
|
|
||||||
self.capture = self._open_capture()
|
|
||||||
if self.capture is None:
|
|
||||||
self._log_periodic(f"无法打开 ESP32 流 {self.url},重试中...")
|
|
||||||
time.sleep(self.reconnect_delay)
|
|
||||||
continue
|
|
||||||
|
|
||||||
ok, frame = self.capture.read()
|
|
||||||
if not ok or frame is None:
|
|
||||||
self._log_periodic("ESP32 流读取失败,重新连接中...")
|
|
||||||
self.capture.release()
|
|
||||||
self.capture = None
|
|
||||||
time.sleep(self.reconnect_delay)
|
|
||||||
continue
|
|
||||||
|
|
||||||
with self.lock:
|
|
||||||
self.latest_frame = frame
|
|
||||||
self.latest_stamp = time.monotonic()
|
|
||||||
|
|
||||||
|
|
||||||
class BoxDetectionGraspNode(Node):
|
|
||||||
"""方框检测与自动抓取节点"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__('box_detection_grasp')
|
|
||||||
|
|
||||||
# 检查依赖
|
|
||||||
if cv2 is None:
|
|
||||||
raise RuntimeError("需要 OpenCV。安装: pip install opencv-python")
|
|
||||||
if np is None:
|
|
||||||
raise RuntimeError("需要 NumPy。安装: pip install numpy")
|
|
||||||
if YOLO is None:
|
|
||||||
raise RuntimeError("需要 ultralytics。安装: pip install ultralytics")
|
|
||||||
|
|
||||||
# 声明参数
|
|
||||||
self.declare_parameter('camera_ip', '192.168.4.1')
|
|
||||||
self.declare_parameter('model_path', '/home/fallensigh/Dev/craic/tmp/best.pt')
|
|
||||||
self.declare_parameter('confidence', 0.35)
|
|
||||||
self.declare_parameter('imgsz', 768)
|
|
||||||
self.declare_parameter('device', '')
|
|
||||||
self.declare_parameter('detection_rate', 10.0)
|
|
||||||
self.declare_parameter('box_size_m', 0.03)
|
|
||||||
self.declare_parameter('horizontal_fov_deg', 66.0)
|
|
||||||
self.declare_parameter('max_frame_age', 0.5)
|
|
||||||
self.declare_parameter('show_debug_window', False)
|
|
||||||
self.declare_parameter('auto_grasp', False) # 是否自动抓取
|
|
||||||
|
|
||||||
# 获取参数
|
|
||||||
self.stream_url = f"http://{self.get_parameter('camera_ip').value}/stream"
|
|
||||||
self.model_path = self.get_parameter('model_path').value
|
|
||||||
self.confidence = float(self.get_parameter('confidence').value)
|
|
||||||
self.imgsz = int(self.get_parameter('imgsz').value)
|
|
||||||
self.device = str(self.get_parameter('device').value)
|
|
||||||
self.detection_rate = float(self.get_parameter('detection_rate').value)
|
|
||||||
self.box_size_m = float(self.get_parameter('box_size_m').value)
|
|
||||||
self.horizontal_fov_deg = float(self.get_parameter('horizontal_fov_deg').value)
|
|
||||||
self.max_frame_age = float(self.get_parameter('max_frame_age').value)
|
|
||||||
self.show_debug_window = bool(self.get_parameter('show_debug_window').value)
|
|
||||||
self.auto_grasp = bool(self.get_parameter('auto_grasp').value)
|
|
||||||
|
|
||||||
# 状态标志
|
|
||||||
self.detection_active = self.auto_grasp # 如果 auto_grasp=true,自动开始检测
|
|
||||||
self.grasp_in_progress = False
|
|
||||||
|
|
||||||
# 发布器:发布到视觉抓取节点
|
|
||||||
self.grasp_target_pub = self.create_publisher(Point, '/vision_grasp/grasp_target', 10)
|
|
||||||
|
|
||||||
# 服务:外部触发检测
|
|
||||||
self.start_detection_srv = self.create_service(
|
|
||||||
Trigger,
|
|
||||||
'box_detection/start',
|
|
||||||
self.handle_start_detection
|
|
||||||
)
|
|
||||||
self.stop_detection_srv = self.create_service(
|
|
||||||
Trigger,
|
|
||||||
'box_detection/stop',
|
|
||||||
self.handle_stop_detection
|
|
||||||
)
|
|
||||||
|
|
||||||
# 启动相机流读取器
|
|
||||||
self.reader = MjpegFrameReader(self.stream_url, self.get_logger())
|
|
||||||
self.reader.start()
|
|
||||||
|
|
||||||
# 加载 YOLO 模型
|
|
||||||
self.get_logger().info(f'加载 YOLO 模型: {self.model_path}')
|
|
||||||
self.model = YOLO(self.model_path)
|
|
||||||
self.last_no_detection_log = 0.0
|
|
||||||
|
|
||||||
# 创建定时器
|
|
||||||
period = 1.0 / max(self.detection_rate, 0.1)
|
|
||||||
self.timer = self.create_timer(period, self.process_frame)
|
|
||||||
|
|
||||||
if self.auto_grasp:
|
|
||||||
self.get_logger().info('自动抓取模式已启动,开始实时检测方框...')
|
|
||||||
else:
|
|
||||||
self.get_logger().info('手动模式,等待外部触发检测')
|
|
||||||
|
|
||||||
def process_frame(self):
|
|
||||||
"""处理最新帧"""
|
|
||||||
if not self.detection_active or self.grasp_in_progress:
|
|
||||||
return
|
|
||||||
|
|
||||||
frame, frame_stamp = self.reader.get_latest()
|
|
||||||
if frame is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 检查帧是否过期
|
|
||||||
if frame_stamp is not None and time.monotonic() - frame_stamp > self.max_frame_age:
|
|
||||||
return
|
|
||||||
|
|
||||||
# YOLO 检测
|
|
||||||
predict_kwargs = {
|
|
||||||
'source': frame,
|
|
||||||
'imgsz': self.imgsz,
|
|
||||||
'conf': self.confidence,
|
|
||||||
'verbose': False,
|
|
||||||
}
|
|
||||||
if self.device:
|
|
||||||
predict_kwargs['device'] = self.device
|
|
||||||
|
|
||||||
results = self.model.predict(**predict_kwargs)
|
|
||||||
detection = self.select_best_box(results[0], frame)
|
|
||||||
|
|
||||||
if detection is None:
|
|
||||||
self.log_no_detection()
|
|
||||||
if self.show_debug_window:
|
|
||||||
cv2.imshow('box_detection', frame)
|
|
||||||
cv2.waitKey(1)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 估计 3D 坐标
|
|
||||||
point = self.estimate_camera_point(detection, frame.shape)
|
|
||||||
if point is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 检测成功!停止检测并触发抓取
|
|
||||||
self.get_logger().info(f'✓ 检测到方框: 相机坐标 ({point[0]*1000:.1f}, {point[1]*1000:.1f}, {point[2]*1000:.1f}) mm')
|
|
||||||
self.detection_active = False # 停止检测
|
|
||||||
self.grasp_in_progress = True
|
|
||||||
|
|
||||||
if self.show_debug_window:
|
|
||||||
self.show_debug_frame(frame, detection, point)
|
|
||||||
|
|
||||||
# 发布抓取目标(转换为 mm)
|
|
||||||
msg = Point()
|
|
||||||
msg.x = point[0] * 1000.0 # m -> mm
|
|
||||||
msg.y = point[1] * 1000.0
|
|
||||||
msg.z = point[2] * 1000.0
|
|
||||||
self.grasp_target_pub.publish(msg)
|
|
||||||
self.get_logger().info('已发布抓取目标到 /vision_grasp/grasp_target')
|
|
||||||
|
|
||||||
# 在独立线程中等待抓取完成
|
|
||||||
threading.Thread(target=self._wait_for_grasp_completion, daemon=True).start()
|
|
||||||
|
|
||||||
def select_best_box(self, result, frame):
|
|
||||||
"""选择最佳检测框"""
|
|
||||||
if result.boxes is None or len(result.boxes) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
best = None
|
|
||||||
best_conf = -1.0
|
|
||||||
|
|
||||||
for box in result.boxes:
|
|
||||||
conf = float(box.conf[0].item()) if box.conf is not None else 0.0
|
|
||||||
xyxy = box.xyxy[0].detach().cpu().numpy().astype(float)
|
|
||||||
x1, y1, x2, y2 = xyxy
|
|
||||||
w = max(0.0, x2 - x1)
|
|
||||||
h = max(0.0, y2 - y1)
|
|
||||||
|
|
||||||
if w < 2.0 or h < 2.0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if conf > best_conf:
|
|
||||||
best_conf = conf
|
|
||||||
best = {
|
|
||||||
'xyxy': xyxy,
|
|
||||||
'confidence': conf,
|
|
||||||
}
|
|
||||||
|
|
||||||
return best
|
|
||||||
|
|
||||||
def estimate_camera_point(self, detection, frame_shape):
|
|
||||||
"""估计相机坐标系 3D 坐标(单位:米)"""
|
|
||||||
frame_h, frame_w = frame_shape[:2]
|
|
||||||
x1, y1, x2, y2 = detection['xyxy']
|
|
||||||
bbox_w = max(1.0, x2 - x1)
|
|
||||||
bbox_h = max(1.0, y2 - y1)
|
|
||||||
pixel_side = (bbox_w + bbox_h) * 0.5
|
|
||||||
|
|
||||||
# 相机内参
|
|
||||||
cx = frame_w * 0.5
|
|
||||||
cy = frame_h * 0.5
|
|
||||||
fov_rad = math.radians(max(1.0, min(self.horizontal_fov_deg, 179.0)))
|
|
||||||
fx = frame_w / (2.0 * math.tan(fov_rad * 0.5))
|
|
||||||
fy = fx
|
|
||||||
|
|
||||||
if fx <= 0.0 or fy <= 0.0:
|
|
||||||
self.get_logger().error('无效的相机内参')
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 估计深度
|
|
||||||
focal = (fx + fy) * 0.5
|
|
||||||
z = self.box_size_m * focal / pixel_side
|
|
||||||
|
|
||||||
# 计算 3D 坐标
|
|
||||||
u = (x1 + x2) * 0.5
|
|
||||||
v = (y1 + y2) * 0.5
|
|
||||||
x = (u - cx) * z / fx
|
|
||||||
y = (v - cy) * z / fy
|
|
||||||
|
|
||||||
return x, y, z
|
|
||||||
|
|
||||||
def show_debug_frame(self, frame, detection, point):
|
|
||||||
"""显示调试窗口"""
|
|
||||||
x1, y1, x2, y2 = detection['xyxy'].astype(int)
|
|
||||||
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
||||||
text = f"x={point[0]*1000:.1f} y={point[1]*1000:.1f} z={point[2]*1000:.1f} mm"
|
|
||||||
cv2.putText(
|
|
||||||
frame, text, (x1, max(20, y1 - 8)),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA
|
|
||||||
)
|
|
||||||
cv2.imshow('box_detection', frame)
|
|
||||||
cv2.waitKey(1)
|
|
||||||
|
|
||||||
def log_no_detection(self):
|
|
||||||
"""周期性记录未检测到目标"""
|
|
||||||
now = time.monotonic()
|
|
||||||
if now - self.last_no_detection_log >= 5.0:
|
|
||||||
self.get_logger().info('当前帧未检测到方框')
|
|
||||||
self.last_no_detection_log = now
|
|
||||||
|
|
||||||
def _wait_for_grasp_completion(self):
|
|
||||||
"""等待抓取完成(独立线程)"""
|
|
||||||
self.get_logger().info('等待抓取完成...')
|
|
||||||
time.sleep(10.0) # 等待抓取流程完成
|
|
||||||
self.grasp_in_progress = False
|
|
||||||
self.get_logger().info('抓取流程完成,节点已停止检测')
|
|
||||||
|
|
||||||
def handle_start_detection(self, request, response):
|
|
||||||
"""处理启动检测服务"""
|
|
||||||
if self.detection_active:
|
|
||||||
response.success = False
|
|
||||||
response.message = '检测已在运行中'
|
|
||||||
return response
|
|
||||||
|
|
||||||
if self.grasp_in_progress:
|
|
||||||
response.success = False
|
|
||||||
response.message = '抓取流程正在进行中,请等待完成'
|
|
||||||
return response
|
|
||||||
|
|
||||||
self.detection_active = True
|
|
||||||
self.get_logger().info('✓ 启动方框检测')
|
|
||||||
response.success = True
|
|
||||||
response.message = '已启动检测'
|
|
||||||
return response
|
|
||||||
|
|
||||||
def handle_stop_detection(self, request, response):
|
|
||||||
"""处理停止检测服务"""
|
|
||||||
if not self.detection_active:
|
|
||||||
response.success = False
|
|
||||||
response.message = '检测未运行'
|
|
||||||
return response
|
|
||||||
|
|
||||||
self.detection_active = False
|
|
||||||
self.get_logger().info('✓ 停止方框检测')
|
|
||||||
response.success = True
|
|
||||||
response.message = '已停止检测'
|
|
||||||
return response
|
|
||||||
|
|
||||||
def destroy_node(self):
|
|
||||||
"""节点销毁时的清理"""
|
|
||||||
self.reader.stop()
|
|
||||||
if self.show_debug_window:
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
super().destroy_node()
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
rclpy.init(args=args)
|
|
||||||
node = BoxDetectionGraspNode()
|
|
||||||
try:
|
|
||||||
rclpy.spin(node)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
node.destroy_node()
|
|
||||||
rclpy.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -61,7 +61,7 @@ class KeyboardUdpControlNode(Node):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("keyboard_udp_control")
|
super().__init__("keyboard_udp_control")
|
||||||
|
|
||||||
self.declare_parameter("udp_ip", "192.168.4.1")
|
self.declare_parameter("udp_ip", "192.168.233.67")
|
||||||
self.declare_parameter("udp_port", 8888)
|
self.declare_parameter("udp_port", 8888)
|
||||||
self.declare_parameter("chassis_linear_speed", 100)
|
self.declare_parameter("chassis_linear_speed", 100)
|
||||||
self.declare_parameter("chassis_angular_speed", 45)
|
self.declare_parameter("chassis_angular_speed", 45)
|
||||||
@@ -104,7 +104,6 @@ class KeyboardUdpControlNode(Node):
|
|||||||
self.old_terminal_settings = None
|
self.old_terminal_settings = None
|
||||||
|
|
||||||
self._stdin_buf = ""
|
self._stdin_buf = ""
|
||||||
self._tick_count = 0
|
|
||||||
|
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
@@ -284,7 +283,7 @@ class KeyboardUdpControlNode(Node):
|
|||||||
self.arm_height = min(self.arm_height + self.arm_height_step, -10)
|
self.arm_height = min(self.arm_height + self.arm_height_step, -10)
|
||||||
arm_changed = True
|
arm_changed = True
|
||||||
if KEY_DOWN in keys:
|
if KEY_DOWN in keys:
|
||||||
self.arm_height = max(self.arm_height - self.arm_height_step, -285)
|
self.arm_height = max(self.arm_height - self.arm_height_step, -280)
|
||||||
arm_changed = True
|
arm_changed = True
|
||||||
|
|
||||||
joint_index = self.arm_selected_joint
|
joint_index = self.arm_selected_joint
|
||||||
@@ -327,9 +326,7 @@ class KeyboardUdpControlNode(Node):
|
|||||||
if self.arm_active or self.is_arm_key_pressed(keys):
|
if self.arm_active or self.is_arm_key_pressed(keys):
|
||||||
self.sock.sendto(self.build_arm_cmd(), (self.udp_ip, self.udp_port))
|
self.sock.sendto(self.build_arm_cmd(), (self.udp_ip, self.udp_port))
|
||||||
|
|
||||||
self._tick_count += 1
|
self.print_status(keys)
|
||||||
if self._tick_count % 5 == 0:
|
|
||||||
self.print_status(keys)
|
|
||||||
|
|
||||||
def print_status(self, keys):
|
def print_status(self, keys):
|
||||||
selected_joint = self.arm_selected_joint + 2
|
selected_joint = self.arm_selected_joint + 2
|
||||||
|
|||||||
@@ -1,445 +0,0 @@
|
|||||||
#!/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)
|
|
||||||
|
|
||||||
# 旋转矩阵:TCP → 基坐标系
|
|
||||||
# TCP X → 基坐标 -Y
|
|
||||||
# TCP Y → 基坐标 -Z
|
|
||||||
# TCP Z → 基坐标 X
|
|
||||||
R_tcp_to_base = np.array([
|
|
||||||
[-math.sin(phi), 0, math.cos(phi)], # X_base
|
|
||||||
[-math.cos(phi), 0, -math.sin(phi)], # Y_base (修正:添加负号)
|
|
||||||
[0, -1, 0] # Z_base
|
|
||||||
])
|
|
||||||
|
|
||||||
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, int]:
|
|
||||||
"""查询当前 TCP 位姿(包括 J5 状态)"""
|
|
||||||
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, result.j5
|
|
||||||
|
|
||||||
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:
|
|
||||||
# 获取当前 TCP 位姿(包括 J5 状态)
|
|
||||||
tcp_x, tcp_y, tcp_z, tcp_phi, j5 = self.get_current_tcp_pose()
|
|
||||||
self.get_logger().info(f'当前 TCP: ({tcp_x:.1f}, {tcp_y:.1f}, {tcp_z:.1f}), phi={tcp_phi:.1f}°, j5={j5}°')
|
|
||||||
|
|
||||||
# 图像坐标到相机坐标系转换
|
|
||||||
# 相机水平安装,但会随 J5 状态旋转 180°
|
|
||||||
# J5 = -100 (闭合) → 夹爪朝上 (UP) → 相机正向
|
|
||||||
# J5 = 81 (张开) → 夹爪朝下 (DOWN) → 相机旋转 180°
|
|
||||||
|
|
||||||
if j5 < 0:
|
|
||||||
# J5 闭合(UP):相机正向
|
|
||||||
# 图像 Y 向下 → 相机 -Y
|
|
||||||
xc = x
|
|
||||||
yc = -y
|
|
||||||
zc = z
|
|
||||||
self.get_logger().info(f'J5={j5}° (UP),相机正向,相机坐标: ({xc:.1f}, {yc:.1f}, {zc:.1f})')
|
|
||||||
else:
|
|
||||||
# J5 张开(DOWN):相机旋转 180°
|
|
||||||
# 图像 X,Y 都翻转(相对于 UP 状态)
|
|
||||||
xc = -x
|
|
||||||
yc = y
|
|
||||||
zc = z
|
|
||||||
self.get_logger().info(f'J5={j5}° (DOWN),相机旋转 180°,相机坐标: ({xc:.1f}, {yc:.1f}, {zc:.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()
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# Copyright(c) 2020 eaibot limited.
|
|
||||||
cmake_minimum_required(VERSION 3.5)
|
|
||||||
project(ydlidar_ros2_driver C CXX)
|
|
||||||
|
|
||||||
##################ros2#############################################
|
|
||||||
# Default to C++14
|
|
||||||
if(NOT CMAKE_CXX_STANDARD)
|
|
||||||
set(CMAKE_CXX_STANDARD 14)
|
|
||||||
endif()
|
|
||||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
||||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
####################find package#####################################
|
|
||||||
find_package(ament_cmake REQUIRED)
|
|
||||||
find_package(rclcpp REQUIRED)
|
|
||||||
find_package(rmw REQUIRED)
|
|
||||||
find_package(sensor_msgs REQUIRED)
|
|
||||||
find_package(visualization_msgs REQUIRED)
|
|
||||||
find_package(geometry_msgs REQUIRED)
|
|
||||||
find_package(std_srvs REQUIRED)
|
|
||||||
|
|
||||||
############## YDLIDAR SDK START#####################################
|
|
||||||
#find ydlidar_sdk package
|
|
||||||
find_package(ydlidar_sdk REQUIRED)
|
|
||||||
############## YDLIDAR SDK END#####################################
|
|
||||||
|
|
||||||
#Include directories
|
|
||||||
include_directories(
|
|
||||||
${PROJECT_SOURCE_DIR}
|
|
||||||
${PROJECT_SOURCE_DIR}/src
|
|
||||||
${YDLIDAR_SDK_INCLUDE_DIRS})
|
|
||||||
|
|
||||||
#link library directories
|
|
||||||
link_directories(${YDLIDAR_SDK_LIBRARY_DIRS})
|
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
# generate excutable and add libraries
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
add_executable(${PROJECT_NAME}_node
|
|
||||||
src/${PROJECT_NAME}_node.cpp)
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
# link libraries
|
|
||||||
#--------------------------------------------------------------------------------------
|
|
||||||
ament_target_dependencies(${PROJECT_NAME}_node
|
|
||||||
"rclcpp"
|
|
||||||
"sensor_msgs"
|
|
||||||
"visualization_msgs"
|
|
||||||
"geometry_msgs"
|
|
||||||
"std_srvs"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(${PROJECT_NAME}_node
|
|
||||||
${YDLIDAR_SDK_LIBRARIES})
|
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
# generate excutable and add libraries
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
add_executable(${PROJECT_NAME}_client
|
|
||||||
src/${PROJECT_NAME}_client.cpp)
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
# link libraries
|
|
||||||
#--------------------------------------------------------------------------------------
|
|
||||||
ament_target_dependencies(${PROJECT_NAME}_client
|
|
||||||
"rclcpp"
|
|
||||||
"sensor_msgs"
|
|
||||||
"visualization_msgs"
|
|
||||||
"geometry_msgs"
|
|
||||||
"std_srvs"
|
|
||||||
)
|
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
# Install
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
install(TARGETS
|
|
||||||
${PROJECT_NAME}_node ${PROJECT_NAME}_client
|
|
||||||
DESTINATION lib/${PROJECT_NAME})
|
|
||||||
|
|
||||||
install(DIRECTORY launch params startup config
|
|
||||||
DESTINATION share/${PROJECT_NAME})
|
|
||||||
|
|
||||||
if(BUILD_TESTING)
|
|
||||||
find_package(ament_lint_auto REQUIRED)
|
|
||||||
ament_lint_auto_find_test_dependencies()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
ament_package()
|
|
||||||
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||

|
|
||||||
# YDLIDAR ROS2 Driver
|
|
||||||
|
|
||||||
ydlidar_ros2_driver is a new ros package, which is designed to gradually become the standard driver package for ydlidar devices in the ros2 environment.
|
|
||||||
|
|
||||||
## How to [install ROS2](https://index.ros.org/doc/ros2/Installation)
|
|
||||||
[ubuntu](https://index.ros.org/doc/ros2/Installation/Dashing/Linux-Install-Debians/)
|
|
||||||
|
|
||||||
[windows](https://index.ros.org/doc/ros2/Installation/Dashing/Windows-Install-Binary/)
|
|
||||||
|
|
||||||
## How to Create a ROS2 workspace
|
|
||||||
[Create a workspace](https://index.ros.org/doc/ros2/Tutorials/Colcon-Tutorial/#create-a-workspace)
|
|
||||||
|
|
||||||
|
|
||||||
## Compile & Install YDLidar SDK
|
|
||||||
|
|
||||||
ydlidar_ros2_driver depends on YDLidar-SDK library. If you have never installed YDLidar-SDK library or it is out of date, you must first install YDLidar-SDK library. If you have installed the latest version of YDLidar-SDK, skip this step and go to the next step.
|
|
||||||
|
|
||||||
1. Download or clone the [YDLIDAR/YDLidar-SDK](https://github.com/YDLIDAR/YDLidar-SDK) repository on GitHub.
|
|
||||||
2. Compile and install the YDLidar-SDK under the ***build*** directory following `README.md` of YDLIDAR/YDLidar-SDK.
|
|
||||||
|
|
||||||
## Clone ydlidar_ros2_driver
|
|
||||||
|
|
||||||
1. Clone ydlidar_ros2_driver package for github :
|
|
||||||
|
|
||||||
`git clone https://github.com/YDLIDAR/ydlidar_ros2_driver.git ydlidar_ros2_ws/src/ydlidar_ros2_driver`
|
|
||||||
|
|
||||||
2. Build ydlidar_ros2_driver package :
|
|
||||||
|
|
||||||
```
|
|
||||||
cd ydlidar_ros2_ws
|
|
||||||
colcon build --symlink-install
|
|
||||||
```
|
|
||||||
Note: install colcon [see](https://index.ros.org/doc/ros2/Tutorials/Colcon-Tutorial/#install-colcon)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<font color=Red size=4>>Note: If the following error occurs, Please install [YDLIDAR/YDLidar-SDK](https://github.com/YDLIDAR/YDLidar-SDK) first.</font>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
3. Package environment setup :
|
|
||||||
|
|
||||||
`source ./install/setup.bash`
|
|
||||||
|
|
||||||
Note: Add permanent workspace environment variables.
|
|
||||||
It's convenientif the ROS2 environment variables are automatically added to your bash session every time a new shell is launched:
|
|
||||||
```
|
|
||||||
$echo "source ~/ydlidar_ros2_ws/install/setup.bash" >> ~/.bashrc
|
|
||||||
$source ~/.bashrc
|
|
||||||
```
|
|
||||||
4. Confirmation
|
|
||||||
To confirm that your package path has been set, printenv the `grep -i ROS` variable.
|
|
||||||
```
|
|
||||||
$ printenv | grep -i ROS
|
|
||||||
```
|
|
||||||
You should see something similar to:
|
|
||||||
`OLDPWD=/home/tony/ydlidar_ros2_ws/install`
|
|
||||||
|
|
||||||
5. Create serial port Alias [optional]
|
|
||||||
```
|
|
||||||
$chmod 0777 src/ydlidar_ros2_driver/startup/*
|
|
||||||
$sudo sh src/ydlidar_ros2_driver/startup/initenv.sh
|
|
||||||
```
|
|
||||||
Note: After completing the previous operation, replug the LiDAR again.
|
|
||||||
|
|
||||||
## Configure LiDAR [paramters](params/ydlidar.yaml)
|
|
||||||
```
|
|
||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 230400
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 9
|
|
||||||
abnormal_check_count: 4
|
|
||||||
resolution_fixed: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 64.0
|
|
||||||
range_min: 0.01
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run ydlidar_ros2_driver
|
|
||||||
|
|
||||||
##### Run ydlidar_ros2_driver using launch file
|
|
||||||
|
|
||||||
The command format is :
|
|
||||||
|
|
||||||
`ros2 launch ydlidar_ros2_driver [launch file].py`
|
|
||||||
|
|
||||||
1. Connect LiDAR uint(s).
|
|
||||||
```
|
|
||||||
ros2 launch ydlidar_ros2_driver ydlidar_launch.py
|
|
||||||
```
|
|
||||||
or
|
|
||||||
|
|
||||||
```
|
|
||||||
launch $(ros2 pkg prefix ydlidar_ros2_driver)/share/ydlidar_ros2_driver/launch/ydlidar.py
|
|
||||||
```
|
|
||||||
2. RVIZ
|
|
||||||
```
|
|
||||||
ros2 launch ydlidar_ros2_driver ydlidar_launch_view.py
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
3. echo scan topic
|
|
||||||
```
|
|
||||||
ros2 run ydlidar_ros2_driver ydlidar_ros2_driver_client or ros2 topic echo /scan
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Launch file introduction
|
|
||||||
|
|
||||||
The driver offers users a wealth of options when using different launch file. The launch file directory
|
|
||||||
|
|
||||||
is `"ydlidar_ros2_ws/src/ydlidar_ros2_driver/launch"`. All launch files are listed as below :
|
|
||||||
|
|
||||||
| launch file | features |
|
|
||||||
| ------------------------- | ------------------------------------------------------------ |
|
|
||||||
| ydlidar.py | Connect to defualt paramters<br/>Publish LaserScan message on `scan` topic |
|
|
||||||
| ydlidar_launch.py | Connect ydlidar.yaml Lidar specified by configuration parameters<br/>Publish LaserScan message on `scan` topic |
|
|
||||||
| ydlidar_launch_view.py | Connect ydlidar.yaml Lidar specified by configuration parameters and setup RVIZ<br/>Publish LaserScan message on `scan` topic |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Publish Topic
|
|
||||||
| Topic | Type | Description |
|
|
||||||
|----------------------|-------------------------|--------------------------------------------------|
|
|
||||||
| `scan` | sensor_msgs/LaserScan | 2D laser scan of the 0-angle ring |
|
|
||||||
|
|
||||||
## Subscribe Service
|
|
||||||
| Service | Type | Description |
|
|
||||||
|----------------------|-------------------------|--------------------------------------------------|
|
|
||||||
| `stop_scan` | std_srvs::Empty | turn off lidar |
|
|
||||||
| `start_scan` | std_srvs::Empty | turn on lidar |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Configure ydlidar_ros_driver internal parameter
|
|
||||||
|
|
||||||
The ydlidar_ros2_driver internal parameters are in the launch file, they are listed as below :
|
|
||||||
|
|
||||||
| Parameter name | Data Type | detail |
|
|
||||||
| -------------- | ------- | ------------------------------------------------------------ |
|
|
||||||
| port | string | Set Lidar the serial port or IP address <br/>it can be set to `/dev/ttyUSB0`, `192.168.1.11`, etc. <br/>default: `/dev/ydlidar` |
|
|
||||||
| frame_id | string | Lidar TF coordinate system name. <br/>default: `laser_frame` |
|
|
||||||
| ignore_array | string | LiDAR filtering angle area<br/>eg: `-90, -80, 30, 40` |
|
|
||||||
| baudrate | int | Lidar baudrate or network port. <br/>default: `230400` |
|
|
||||||
| lidar_type | int | Set lidar type <br/>0 -- TYPE_TOF<br/>1 -- TYPE_TRIANGLE<br/>2 -- TYPE_TOF_NET <br/>default: `1` |
|
|
||||||
| device_type | int | Set device type <br/>0 -- YDLIDAR_TYPE_SERIAL<br/>1 -- YDLIDAR_TYPE_TCP<br/>2 -- YDLIDAR_TYPE_UDP <br/>default: `0` |
|
|
||||||
| sample_rate | int | Set Lidar Sample Rate. <br/>default: `9` |
|
|
||||||
| abnormal_check_count | int | Set the number of abnormal startup data attempts. <br/>default: `4` |
|
|
||||||
| fixed_resolution | bool | Fixed angluar resolution. <br/>default: `true` |
|
|
||||||
| reversion | bool | Reversion LiDAR. <br/>default: `true` |
|
|
||||||
| inverted | bool | Inverted LiDAR.<br/>false -- ClockWise.<br/>true -- CounterClockWise <br/>default: `true` |
|
|
||||||
| auto_reconnect | bool | Automatically reconnect the LiDAR.<br/>true -- hot plug. <br/>default: `true` |
|
|
||||||
| isSingleChannel | bool | Whether LiDAR is a single-channel.<br/>default: `false` |
|
|
||||||
| intensity | bool | Whether LiDAR has intensity.<br/>true -- G2 LiDAR.<br/>default: `false` |
|
|
||||||
| support_motor_dtr | bool | Whether the Lidar can be started and stopped by Serial DTR.<br/>default: `false` |
|
|
||||||
| angle_min | float | Minimum Valid Angle.<br/>default: `-180` |
|
|
||||||
| angle_max | float | Maximum Valid Angle.<br/>default: `180` |
|
|
||||||
| range_min | float | Minimum Valid range.<br/>default: `0.1` |
|
|
||||||
| range_max | float | Maximum Valid range.<br/>default: `16.0` |
|
|
||||||
| frequency | float | Set Scanning Frequency.<br/>default: `10.0` |
|
|
||||||
| invalid_range_is_inf | bool | Invalid Range is inf.<br/>true -- inf.<br/>false -- 0.0.<br/>default: `false` |
|
|
||||||
More paramters details, see [here](details.md)
|
|
||||||
|
|
||||||
## Contact EAI
|
|
||||||

|
|
||||||
|
|
||||||
If you have any extra questions, please feel free to [contact us](http://www.ydlidar.cn/cn/contact)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
Panels:
|
|
||||||
- Class: rviz_common/Displays
|
|
||||||
Help Height: 78
|
|
||||||
Name: Displays
|
|
||||||
Property Tree Widget:
|
|
||||||
Expanded:
|
|
||||||
- /Global Options1
|
|
||||||
- /Status1
|
|
||||||
- /LaserScan1/Topic1
|
|
||||||
Splitter Ratio: 0.5
|
|
||||||
Tree Height: 617
|
|
||||||
- Class: rviz_common/Selection
|
|
||||||
Name: Selection
|
|
||||||
- Class: rviz_common/Tool Properties
|
|
||||||
Expanded:
|
|
||||||
- /2D Goal Pose1
|
|
||||||
- /Publish Point1
|
|
||||||
Name: Tool Properties
|
|
||||||
Splitter Ratio: 0.5886790156364441
|
|
||||||
- Class: rviz_common/Views
|
|
||||||
Expanded:
|
|
||||||
- /Current View1
|
|
||||||
Name: Views
|
|
||||||
Splitter Ratio: 0.5
|
|
||||||
Visualization Manager:
|
|
||||||
Class: ""
|
|
||||||
Displays:
|
|
||||||
- Alpha: 0.5
|
|
||||||
Cell Size: 1
|
|
||||||
Class: rviz_default_plugins/Grid
|
|
||||||
Color: 160; 160; 164
|
|
||||||
Enabled: true
|
|
||||||
Line Style:
|
|
||||||
Line Width: 0.029999999329447746
|
|
||||||
Value: Lines
|
|
||||||
Name: Grid
|
|
||||||
Normal Cell Count: 0
|
|
||||||
Offset:
|
|
||||||
X: 0
|
|
||||||
Y: 0
|
|
||||||
Z: 0
|
|
||||||
Plane: XY
|
|
||||||
Plane Cell Count: 10
|
|
||||||
Reference Frame: <Fixed Frame>
|
|
||||||
Value: true
|
|
||||||
- Alpha: 1
|
|
||||||
Autocompute Intensity Bounds: true
|
|
||||||
Autocompute Value Bounds:
|
|
||||||
Max Value: 10
|
|
||||||
Min Value: -10
|
|
||||||
Value: true
|
|
||||||
Axis: Z
|
|
||||||
Channel Name: intensity
|
|
||||||
Class: rviz_default_plugins/LaserScan
|
|
||||||
Color: 239; 41; 41
|
|
||||||
Color Transformer: FlatColor
|
|
||||||
Decay Time: 0
|
|
||||||
Enabled: true
|
|
||||||
Invert Rainbow: false
|
|
||||||
Max Color: 255; 255; 255
|
|
||||||
Max Intensity: 1012
|
|
||||||
Min Color: 0; 0; 0
|
|
||||||
Min Intensity: 1008
|
|
||||||
Name: LaserScan
|
|
||||||
Position Transformer: XYZ
|
|
||||||
Selectable: true
|
|
||||||
Size (Pixels): 3
|
|
||||||
Size (m): 0.05999999865889549
|
|
||||||
Style: Flat Squares
|
|
||||||
Topic:
|
|
||||||
Depth: 5
|
|
||||||
Durability Policy: Volatile
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: System Default
|
|
||||||
Value: /scan
|
|
||||||
Use Fixed Frame: true
|
|
||||||
Use rainbow: true
|
|
||||||
Value: true
|
|
||||||
- Class: rviz_default_plugins/TF
|
|
||||||
Enabled: true
|
|
||||||
Frame Timeout: 15
|
|
||||||
Frames:
|
|
||||||
All Enabled: true
|
|
||||||
base_link:
|
|
||||||
Value: true
|
|
||||||
laser_frame:
|
|
||||||
Value: true
|
|
||||||
Marker Scale: 1
|
|
||||||
Name: TF
|
|
||||||
Show Arrows: true
|
|
||||||
Show Axes: true
|
|
||||||
Show Names: false
|
|
||||||
Tree:
|
|
||||||
base_link:
|
|
||||||
laser_frame:
|
|
||||||
{}
|
|
||||||
Update Interval: 0
|
|
||||||
Value: true
|
|
||||||
Enabled: true
|
|
||||||
Global Options:
|
|
||||||
Background Color: 48; 48; 48
|
|
||||||
Fixed Frame: laser_frame
|
|
||||||
Frame Rate: 30
|
|
||||||
Name: root
|
|
||||||
Tools:
|
|
||||||
- Class: rviz_default_plugins/Interact
|
|
||||||
Hide Inactive Objects: true
|
|
||||||
- Class: rviz_default_plugins/MoveCamera
|
|
||||||
- Class: rviz_default_plugins/Select
|
|
||||||
- Class: rviz_default_plugins/FocusCamera
|
|
||||||
- Class: rviz_default_plugins/Measure
|
|
||||||
Line color: 128; 128; 0
|
|
||||||
- Class: rviz_default_plugins/SetInitialPose
|
|
||||||
Topic:
|
|
||||||
Depth: 5
|
|
||||||
Durability Policy: Volatile
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: Reliable
|
|
||||||
Value: /initialpose
|
|
||||||
- Class: rviz_default_plugins/SetGoal
|
|
||||||
Topic:
|
|
||||||
Depth: 5
|
|
||||||
Durability Policy: Volatile
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: Reliable
|
|
||||||
Value: /goal_pose
|
|
||||||
- Class: rviz_default_plugins/PublishPoint
|
|
||||||
Single click: true
|
|
||||||
Topic:
|
|
||||||
Depth: 5
|
|
||||||
Durability Policy: Volatile
|
|
||||||
History Policy: Keep Last
|
|
||||||
Reliability Policy: Reliable
|
|
||||||
Value: /clicked_point
|
|
||||||
Transformation:
|
|
||||||
Current:
|
|
||||||
Class: rviz_default_plugins/TF
|
|
||||||
Value: true
|
|
||||||
Views:
|
|
||||||
Current:
|
|
||||||
Class: rviz_default_plugins/Orbit
|
|
||||||
Distance: 10
|
|
||||||
Enable Stereo Rendering:
|
|
||||||
Stereo Eye Separation: 0.05999999865889549
|
|
||||||
Stereo Focal Distance: 1
|
|
||||||
Swap Stereo Eyes: false
|
|
||||||
Value: false
|
|
||||||
Focal Point:
|
|
||||||
X: 0
|
|
||||||
Y: 0
|
|
||||||
Z: 0
|
|
||||||
Focal Shape Fixed Size: true
|
|
||||||
Focal Shape Size: 0.05000000074505806
|
|
||||||
Invert Z Axis: false
|
|
||||||
Name: Current View
|
|
||||||
Near Clip Distance: 0.009999999776482582
|
|
||||||
Pitch: 1.5103975534439087
|
|
||||||
Target Frame: <Fixed Frame>
|
|
||||||
Value: Orbit (rviz)
|
|
||||||
Yaw: 6.163581848144531
|
|
||||||
Saved: ~
|
|
||||||
Window Geometry:
|
|
||||||
Displays:
|
|
||||||
collapsed: false
|
|
||||||
Height: 846
|
|
||||||
Hide Left Dock: false
|
|
||||||
Hide Right Dock: false
|
|
||||||
QMainWindow State: 000000ff00000000fd000000040000000000000156000002f4fc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003d000002f4000000c900fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000010f000002f4fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003d000002f4000000a400fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d006501000000000000045000000000000000000000023f000002f400000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
|
|
||||||
Selection:
|
|
||||||
collapsed: false
|
|
||||||
Tool Properties:
|
|
||||||
collapsed: false
|
|
||||||
Views:
|
|
||||||
collapsed: false
|
|
||||||
Width: 1200
|
|
||||||
X: 67
|
|
||||||
Y: 60
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# ROS Paramters Table
|
|
||||||
|
|
||||||
## Dataset
|
|
||||||
<table>
|
|
||||||
<tr><th>LIDAR <th> Model <th> Baudrate <th> SampleRate(K) <th> Range(m) <th> Frequency(HZ) <th> Intenstiy(bit) <th> SingleChannel<th> voltage(V)
|
|
||||||
<tr><th> F4 <td> 1 <td> 115200 <td> 4 <td> 0.12~12 <td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> S4 <td> 4 <td> 115200 <td> 4 <td> 0.10~8.0 <td> 5~12 (PWM) <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> S4B <td> 4/11 <td> 153600 <td> 4 <td> 0.10~8.0 <td> 5~12(PWM) <td> true(8) <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> S2 <td> 4/12 <td> 115200 <td> 3 <td> 0.10~8.0 <td> 4~8(PWM) <td> false <td> true <td> 4.8~5.2
|
|
||||||
<tr><th> G4 <td> 5 <td> 230400 <td> 9/8/4 <td> 0.28/0.26/0.1~16<td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> X4 <td> 6 <td> 128000 <td> 5 <td> 0.12~10 <td> 5~12(PWM) <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> X2/X2L <td> 6 <td> 115200 <td> 3 <td> 0.10~8.0 <td> 4~8(PWM) <td> false <td> true <td> 4.8~5.2
|
|
||||||
<tr><th> G4PRO <td> 7 <td> 230400 <td> 9/8/4 <td> 0.28/0.26/0.1~16<td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> F4PRO <td> 8 <td> 230400 <td> 4/6 <td> 0.12~12 <td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> R2 <td> 9 <td> 230400 <td> 5 <td> 0.12~16 <td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G6 <td> 13 <td> 512000 <td> 18/16/8 <td> 0.28/0.26/0.1~25<td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G2A <td> 14 <td> 230400 <td> 5 <td> 0.12~12 <td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G2 <td> 15 <td> 230400 <td> 5 <td> 0.28~16 <td> 5~12 <td> true(8) <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G2C <td> 16 <td> 115200 <td> 4 <td> 0.1~12 <td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G4B <td> 17 <td> 512000 <td> 10 <td> 0.12~16 <td> 5~12 <td> true(10) <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G4C <td> 18 <td> 115200 <td> 4 <td> 0.1~12 <td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G1 <td> 19 <td> 230400 <td> 9 <td> 0.28~16 <td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G5 <td> 20 <td> 230400 <td> 9/8/4 <td> 0.28/0.26/0.1~16<td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> G7 <td> 21 <td> 512000 <td> 18/16/8 <td> 0.28/0.26/0.1~25<td> 5~12 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> TX8 <td> 100 <td> 115200 <td> 4 <td> 0.05~8 <td> 4~8(PWM) <td> false <td> true <td> 4.8~5.2
|
|
||||||
<tr><th> TX20 <td> 100 <td> 115200 <td> 4 <td> 0.05~20 <td> 4~8(PWM) <td> false <td> true <td> 4.8~5.2
|
|
||||||
<tr><th> TG15 <td> 100 <td> 512000 <td> 20/18/10 <td> 0.05~30 <td> 3~16 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> TG30 <td> 101 <td> 512000 <td> 20/18/10 <td> 0.05~30 <td> 3~16 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> TG50 <td> 102 <td> 512000 <td> 20/18/10 <td> 0.05~50 <td> 3~16 <td> false <td> false <td> 4.8~5.2
|
|
||||||
<tr><th> T15 <td> 200 <td> 8000 <td> 20 <td> 0.05~30 <td> 5~35 <td> true <td> false <td> 4.8~5.2
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Baudrate Table
|
|
||||||
|
|
||||||
| LiDAR | baudrate |
|
|
||||||
|-----------------------------------------------|-----------------------|
|
|
||||||
|F4/S2/X2/X2L/S4/TX8/TX20/G4C | 115200 |
|
|
||||||
|X4 | 128000 |
|
|
||||||
|S4B | 153600 |
|
|
||||||
|G1/G2/R2/G4/G5/G4PRO/F4PRO | 230400 |
|
|
||||||
|G6/G7/TG15/TG30/TG50 | 512000 |
|
|
||||||
|T5/T15 | 8000 (network port) |
|
|
||||||
|
|
||||||
|
|
||||||
## SingleChannel Table
|
|
||||||
|
|
||||||
| LiDAR | isSingleChannel |
|
|
||||||
|-----------------------------------------------------------|-----------------------|
|
|
||||||
|G1/G2/G4/G5/G6/G7/F4/F4PRO/S4/S4B/X4/R2/G4C | false |
|
|
||||||
|S2/X2/X2L | true |
|
|
||||||
|TG15/TG30/TG50 | false |
|
|
||||||
|TX8/TX20 | true |
|
|
||||||
|T5/T15 | false (optional) |
|
|
||||||
|
|
||||||
|
|
||||||
## LidarType Table
|
|
||||||
|
|
||||||
| LiDAR | lidar_type |
|
|
||||||
|-----------------------------------------------------------------------|-----------------------|
|
|
||||||
|G1/G2/G4/G5/G6/G7/F4/F4PRO/S4/S4B/X4/R2/G4C/S2/X2/X2L | TYPE_TRIANGLE |
|
|
||||||
|TG15/TG30/TG50/TX8/TX20 | TYPE_TOF |
|
|
||||||
|T5/T15 | TYPE_TOF_NET |
|
|
||||||
|
|
||||||
## DeviceType Table
|
|
||||||
|
|
||||||
| LiDAR | lidar_type |
|
|
||||||
|-----------------------------------------------------------------------|-----------------------|
|
|
||||||
|G1/G2/G4/G5/G6/G7/F4/F4PRO/S4/S4B/X4/R2/G4C/S2/X2/X2L | YDLIDAR_TYPE_SERIAL |
|
|
||||||
|TG15/TG30/TG50/TX8/TX20 | YDLIDAR_TYPE_SERIAL |
|
|
||||||
|T5/T15 | YDLIDAR_TYPE_TCP |
|
|
||||||
|
|
||||||
|
|
||||||
## Sampling Rate Table
|
|
||||||
|
|
||||||
| LiDAR | sample_rate |
|
|
||||||
|-----------------------------|------------------------|
|
|
||||||
|G4/G5/F4 | 4,8,9 |
|
|
||||||
|F4PRO | 4,6 |
|
|
||||||
|G6/G7 | 8,16,18 |
|
|
||||||
|G2/R2/X4 | 5 |
|
|
||||||
|G1 | 9 |
|
|
||||||
|S4/S4B/G4C/TX8/TX20 | 4 |
|
|
||||||
|S2 | 3 |
|
|
||||||
|TG15/TG30/TG50 | 10,18,20 |
|
|
||||||
|T5/T15 | 20 |
|
|
||||||
|
|
||||||
|
|
||||||
## Frequency Table
|
|
||||||
|
|
||||||
| LiDAR | frequency |
|
|
||||||
|-----------------------------------------------|------------------------|
|
|
||||||
|G1/G2/R2/G6/G7/G4/G5/G4PRO/F4/F4PRO | 5-12Hz |
|
|
||||||
|S4/S4B/S2/TX8/TX20/X4 | Not Support |
|
|
||||||
|TG15/TG30/TG50 | 3-16Hz |
|
|
||||||
|T5/T15 | 5-35Hz |
|
|
||||||
|
|
||||||
Note: For unsupported LiDARs, adjusting the scanning frequency requires external access to PWM speed control.
|
|
||||||
|
|
||||||
## Reversion Table
|
|
||||||
<table>
|
|
||||||
<tr><th>LiDAR <th>reversion
|
|
||||||
<tr><th>G1/G2/G2A/G2C/F4/F4PRO/R2 <td>true
|
|
||||||
<tr><th>G4/G5/G4PRO/G4B/G4C/G6/G7 <td>true
|
|
||||||
<tr><th>TG15/TG30/TG50 <td>true
|
|
||||||
<tr><th>T5/T15 <td>true
|
|
||||||
<tr><th>S2/X2/X2L/X4/S4/S4B <td>false
|
|
||||||
<tr><th>TX8/TX20 <td>false
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Intensity Table
|
|
||||||
<table>
|
|
||||||
<tr><th>LiDAR <th>intensity
|
|
||||||
<tr><th>S4B/G2/G4B <td>true
|
|
||||||
<tr><th>G4/G5/G4C/G4PRO/F4/F4PRO/G6/G7 <td>false
|
|
||||||
<tr><th>G1/G2A/G2C/R2 <td>false
|
|
||||||
<tr><th>S2/X2/X2L/X4 <td>false
|
|
||||||
<tr><th>TG15/TG30/TG50 <td>false
|
|
||||||
<tr><th>TX8/TX20 <td>false
|
|
||||||
<tr><th>T5/T15 <td>true
|
|
||||||
<tr><th> <td>false
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## DTR Support Table
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><th>LiDAR <th>support_motor_dtr
|
|
||||||
<tr><th>S4/S4B/S2/X2/X2L/X4 <td>true
|
|
||||||
<tr><th>TX8/TX20 <td>true
|
|
||||||
<tr><th>G4/G5/G4C/G4PRO/F4/F4PRO/G6/G7 <td>false
|
|
||||||
<tr><th>G1/G2A/G2C/R2/G2/G4B <td>false
|
|
||||||
<tr><th>TG15/TG30/TG50 <td>false
|
|
||||||
<tr><th>T5/T15 <td>false
|
|
||||||
</table>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 515 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 128 KiB |
@@ -1,29 +0,0 @@
|
|||||||
from launch.exit_handler import ignore_exit_handler, restart_exit_handler
|
|
||||||
from ros2run.api import get_executable_path
|
|
||||||
|
|
||||||
|
|
||||||
def launch(launch_descriptor, argv):
|
|
||||||
ld = launch_descriptor
|
|
||||||
package = 'ydlidar_ros2_driver'
|
|
||||||
ld.add_process(
|
|
||||||
cmd=[get_executable_path(package_name=package, executable_name='ydlidar_ros2_driver_node')],
|
|
||||||
name='ydlidar_ros2_driver_node',
|
|
||||||
exit_handler=restart_exit_handler,
|
|
||||||
)
|
|
||||||
package = 'tf2_ros'
|
|
||||||
ld.add_process(
|
|
||||||
# The XYZ/Quat numbers for base_link -> laser_frame are taken from the
|
|
||||||
# turtlebot URDF in
|
|
||||||
# https://github.com/turtlebot/turtlebot/blob/931d045/turtlebot_description/urdf/sensors/astra.urdf.xacro
|
|
||||||
cmd=[
|
|
||||||
get_executable_path(
|
|
||||||
package_name=package, executable_name='static_transform_publisher'),
|
|
||||||
'0', '0', '0.02',
|
|
||||||
'0', '0', '0', '1',
|
|
||||||
'base_link',
|
|
||||||
'laser_frame'
|
|
||||||
],
|
|
||||||
name='static_tf_pub_laser',
|
|
||||||
exit_handler=restart_exit_handler,
|
|
||||||
)
|
|
||||||
return ld
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# Copyright 2020, EAIBOT
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch_ros.actions import LifecycleNode
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
from launch.actions import DeclareLaunchArgument
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch.actions import LogInfo
|
|
||||||
|
|
||||||
import lifecycle_msgs.msg
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
share_dir = get_package_share_directory('ydlidar_ros2_driver')
|
|
||||||
parameter_file = LaunchConfiguration('params_file')
|
|
||||||
node_name = 'ydlidar_ros2_driver_node'
|
|
||||||
|
|
||||||
params_declare = DeclareLaunchArgument('params_file',
|
|
||||||
default_value=os.path.join(
|
|
||||||
share_dir, 'params', 'TminiPro.yaml'),
|
|
||||||
description='FPath to the ROS2 parameters file to use.')
|
|
||||||
|
|
||||||
driver_node = LifecycleNode(package='ydlidar_ros2_driver',
|
|
||||||
executable='ydlidar_ros2_driver_node',
|
|
||||||
name='ydlidar_ros2_driver_node',
|
|
||||||
output='screen',
|
|
||||||
emulate_tty=True,
|
|
||||||
parameters=[parameter_file],
|
|
||||||
namespace='/',
|
|
||||||
)
|
|
||||||
# tf2_node = Node(package='tf2_ros',
|
|
||||||
# executable='static_transform_publisher',
|
|
||||||
# name='static_tf_pub_laser',
|
|
||||||
# arguments=['0', '0', '0.00','0', '0', '0', '1','base_link','laser_frame'],
|
|
||||||
# )
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
params_declare,
|
|
||||||
driver_node,
|
|
||||||
# tf2_node,
|
|
||||||
])
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# Copyright 2020, EAIBOTd
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch_ros.actions import LifecycleNode
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
from launch.actions import DeclareLaunchArgument
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch.actions import LogInfo
|
|
||||||
|
|
||||||
import lifecycle_msgs.msg
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
share_dir = get_package_share_directory('ydlidar_ros2_driver')
|
|
||||||
rviz_config_file = os.path.join(share_dir, 'config','ydlidar.rviz')
|
|
||||||
parameter_file = LaunchConfiguration('params_file')
|
|
||||||
node_name = 'ydlidar_ros2_driver_node'
|
|
||||||
|
|
||||||
params_declare = DeclareLaunchArgument('params_file',
|
|
||||||
default_value=os.path.join(
|
|
||||||
share_dir, 'params', 'TminiPro.yaml'),
|
|
||||||
description='FPath to the ROS2 parameters file to use.')
|
|
||||||
|
|
||||||
driver_node = LifecycleNode(package='ydlidar_ros2_driver',
|
|
||||||
executable='ydlidar_ros2_driver_node',
|
|
||||||
name='ydlidar_ros2_driver_node',
|
|
||||||
output='screen',
|
|
||||||
emulate_tty=True,
|
|
||||||
parameters=[parameter_file],
|
|
||||||
namespace='/',
|
|
||||||
)
|
|
||||||
tf2_node = Node(package='tf2_ros',
|
|
||||||
executable='static_transform_publisher',
|
|
||||||
name='static_tf_pub_laser',
|
|
||||||
arguments=['0', '0', '0.02','0', '0', '0', '1','base_link','laser_frame'],
|
|
||||||
)
|
|
||||||
# rviz2_node = Node(package='rviz2',
|
|
||||||
# executable='rviz2',
|
|
||||||
# name='rviz2',
|
|
||||||
# arguments=['-d', rviz_config_file],
|
|
||||||
# )
|
|
||||||
|
|
||||||
return LaunchDescription([
|
|
||||||
params_declare,
|
|
||||||
driver_node,
|
|
||||||
tf2_node,
|
|
||||||
#rviz2_node,
|
|
||||||
])
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
|
||||||
<package format="2">
|
|
||||||
<name>ydlidar_ros2_driver</name>
|
|
||||||
<version>1.0.1</version>
|
|
||||||
<description>
|
|
||||||
The ROS2 device driver for YDLIDAR LIDARS
|
|
||||||
</description>
|
|
||||||
<maintainer email="support@ydlidar.com">Tony</maintainer>
|
|
||||||
<license>MIT</license>
|
|
||||||
|
|
||||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
|
||||||
|
|
||||||
<build_depend>rclcpp</build_depend>
|
|
||||||
<build_depend>sensor_msgs</build_depend>
|
|
||||||
<build_depend>visualization_msgs</build_depend>
|
|
||||||
<build_depend>geometry_msgs</build_depend>
|
|
||||||
<build_depend>std_srvs</build_depend>
|
|
||||||
|
|
||||||
<exec_depend>rclcpp</exec_depend>
|
|
||||||
<exec_depend>sensor_msgs</exec_depend>
|
|
||||||
<exec_depend>visualization_msgs</exec_depend>
|
|
||||||
<exec_depend>geometry_msgs</exec_depend>
|
|
||||||
<exec_depend>std_srvs</exec_depend>
|
|
||||||
|
|
||||||
<test_depend>ament_cmake_gtest</test_depend>
|
|
||||||
<test_depend>ament_lint_auto</test_depend>
|
|
||||||
<test_depend>ament_lint_common</test_depend>
|
|
||||||
|
|
||||||
<export>
|
|
||||||
<build_type>ament_cmake</build_type>
|
|
||||||
</export>
|
|
||||||
</package>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 230400
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 9
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 16.0
|
|
||||||
range_min: 0.1
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 230400
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 4
|
|
||||||
intensity_bit: 10
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: true
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 16.0
|
|
||||||
range_min: 0.1
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 512000
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 18
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 25.0
|
|
||||||
range_min: 0.1
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 921600
|
|
||||||
lidar_type: 3
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 9
|
|
||||||
intensity_bit: 0
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 1.0
|
|
||||||
range_min: 0.025
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: 192.168.0.11
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 8090
|
|
||||||
lidar_type: 0
|
|
||||||
device_type: 1
|
|
||||||
sample_rate: 20
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: true
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 50.0
|
|
||||||
range_min: 0.01
|
|
||||||
frequency: 20.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 512000
|
|
||||||
lidar_type: 0
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 20
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 50.0
|
|
||||||
range_min: 0.01
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttylzulaser
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 230400
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 4
|
|
||||||
intensity_bit: 8
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: true
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 12.0
|
|
||||||
range_min: 0.10
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: true
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 115200
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 3
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: true
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: true
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 12.0
|
|
||||||
range_min: 0.1
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 128000
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 5
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: true
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 12.0
|
|
||||||
range_min: 0.1
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 128000
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 5
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: true
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 12.0
|
|
||||||
range_min: 0.1
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
ydlidar_ros2_driver_node:
|
|
||||||
ros__parameters:
|
|
||||||
port: /dev/ttyUSB0
|
|
||||||
frame_id: laser_frame
|
|
||||||
ignore_array: ""
|
|
||||||
baudrate: 230400
|
|
||||||
lidar_type: 1
|
|
||||||
device_type: 0
|
|
||||||
sample_rate: 9
|
|
||||||
intensity_bit: 0
|
|
||||||
abnormal_check_count: 4
|
|
||||||
fixed_resolution: true
|
|
||||||
reversion: true
|
|
||||||
inverted: true
|
|
||||||
auto_reconnect: true
|
|
||||||
isSingleChannel: false
|
|
||||||
intensity: false
|
|
||||||
support_motor_dtr: false
|
|
||||||
angle_max: 180.0
|
|
||||||
angle_min: -180.0
|
|
||||||
range_max: 64.0
|
|
||||||
range_min: 0.01
|
|
||||||
frequency: 10.0
|
|
||||||
invalid_range_is_inf: false
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* YDLIDAR SYSTEM
|
|
||||||
* YDLIDAR ROS 2 Node Client
|
|
||||||
*
|
|
||||||
* Copyright 2017 - 2020 EAI TEAM
|
|
||||||
* http://www.eaibot.com
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "rclcpp/rclcpp.hpp"
|
|
||||||
#include "sensor_msgs/msg/laser_scan.hpp"
|
|
||||||
#include <math.h>
|
|
||||||
|
|
||||||
#define RAD2DEG(x) ((x)*180./M_PI)
|
|
||||||
|
|
||||||
static void scanCb(sensor_msgs::msg::LaserScan::SharedPtr scan) {
|
|
||||||
int count = scan->scan_time / scan->time_increment;
|
|
||||||
printf("[YDLIDAR INFO]: I heard a laser scan %s[%d]:\n", scan->header.frame_id.c_str(), count);
|
|
||||||
printf("[YDLIDAR INFO]: angle_range : [%f, %f]\n", RAD2DEG(scan->angle_min),
|
|
||||||
RAD2DEG(scan->angle_max));
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
float degree = RAD2DEG(scan->angle_min + scan->angle_increment * i);
|
|
||||||
printf("[YDLIDAR INFO]: angle-distance : [%f, %f]\n", degree, scan->ranges[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
|
||||||
rclcpp::init(argc, argv);
|
|
||||||
|
|
||||||
auto node = rclcpp::Node::make_shared("ydlidar_ros2_driver_client");
|
|
||||||
|
|
||||||
auto lidar_info_sub = node->create_subscription<sensor_msgs::msg::LaserScan>(
|
|
||||||
"scan", rclcpp::SensorDataQoS(), scanCb);
|
|
||||||
|
|
||||||
rclcpp::spin(node);
|
|
||||||
|
|
||||||
rclcpp::shutdown();
|
|
||||||
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
/*
|
|
||||||
* YDLIDAR SYSTEM
|
|
||||||
* YDLIDAR ROS 2 Node
|
|
||||||
*
|
|
||||||
* Copyright 2017 - 2020 EAI TEAM
|
|
||||||
* http://www.eaibot.com
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
|
||||||
#ifndef _USE_MATH_DEFINES
|
|
||||||
#define _USE_MATH_DEFINES
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "src/CYdLidar.h"
|
|
||||||
#include <math.h>
|
|
||||||
#include <chrono>
|
|
||||||
#include <iostream>
|
|
||||||
#include <memory>
|
|
||||||
#include "sensor_msgs/msg/point_cloud.hpp"
|
|
||||||
#include "rclcpp/clock.hpp"
|
|
||||||
#include "rclcpp/rclcpp.hpp"
|
|
||||||
#include "rclcpp/time_source.hpp"
|
|
||||||
#include "sensor_msgs/msg/laser_scan.hpp"
|
|
||||||
#include "std_srvs/srv/empty.hpp"
|
|
||||||
#include <vector>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
#include <signal.h>
|
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
#define ROS2Verision "1.0.1"
|
|
||||||
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
rclcpp::init(argc, argv);
|
|
||||||
|
|
||||||
auto node = rclcpp::Node::make_shared("ydlidar_ros2_driver_node");
|
|
||||||
|
|
||||||
RCLCPP_INFO(node->get_logger(), "[YDLIDAR INFO] Current ROS Driver Version: %s\n", ((std::string)ROS2Verision).c_str());
|
|
||||||
|
|
||||||
CYdLidar laser;
|
|
||||||
std::string str_optvalue = "/dev/ydlidar";
|
|
||||||
node->declare_parameter("port", str_optvalue);
|
|
||||||
node->get_parameter("port", str_optvalue);
|
|
||||||
///lidar port
|
|
||||||
laser.setlidaropt(LidarPropSerialPort, str_optvalue.c_str(), str_optvalue.size());
|
|
||||||
|
|
||||||
///ignore array
|
|
||||||
str_optvalue = "";
|
|
||||||
node->declare_parameter("ignore_array", str_optvalue);
|
|
||||||
node->get_parameter("ignore_array", str_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropIgnoreArray, str_optvalue.c_str(), str_optvalue.size());
|
|
||||||
|
|
||||||
std::string frame_id = "laser_frame";
|
|
||||||
node->declare_parameter("frame_id", frame_id);
|
|
||||||
node->get_parameter("frame_id", frame_id);
|
|
||||||
|
|
||||||
//////////////////////int property/////////////////
|
|
||||||
/// lidar baudrate
|
|
||||||
int optval = 230400;
|
|
||||||
node->declare_parameter("baudrate", optval);
|
|
||||||
node->get_parameter("baudrate", optval);
|
|
||||||
laser.setlidaropt(LidarPropSerialBaudrate, &optval, sizeof(int));
|
|
||||||
/// tof lidar
|
|
||||||
optval = TYPE_TRIANGLE;
|
|
||||||
node->declare_parameter("lidar_type", optval);
|
|
||||||
node->get_parameter("lidar_type", optval);
|
|
||||||
laser.setlidaropt(LidarPropLidarType, &optval, sizeof(int));
|
|
||||||
/// device type
|
|
||||||
optval = YDLIDAR_TYPE_SERIAL;
|
|
||||||
node->declare_parameter("device_type", optval);
|
|
||||||
node->get_parameter("device_type", optval);
|
|
||||||
laser.setlidaropt(LidarPropDeviceType, &optval, sizeof(int));
|
|
||||||
/// sample rate
|
|
||||||
optval = 9;
|
|
||||||
node->declare_parameter("sample_rate", optval);
|
|
||||||
node->get_parameter("sample_rate", optval);
|
|
||||||
laser.setlidaropt(LidarPropSampleRate, &optval, sizeof(int));
|
|
||||||
/// abnormal count
|
|
||||||
optval = 4;
|
|
||||||
node->declare_parameter("abnormal_check_count", optval);
|
|
||||||
node->get_parameter("abnormal_check_count", optval);
|
|
||||||
laser.setlidaropt(LidarPropAbnormalCheckCount, &optval, sizeof(int));
|
|
||||||
|
|
||||||
/// Intenstiy bit count
|
|
||||||
optval = 8;
|
|
||||||
node->declare_parameter("intensity_bit", optval);
|
|
||||||
node->get_parameter("intensity_bit", optval);
|
|
||||||
laser.setlidaropt(LidarPropIntenstiyBit, &optval, sizeof(int));
|
|
||||||
|
|
||||||
//////////////////////bool property/////////////////
|
|
||||||
/// fixed angle resolution
|
|
||||||
bool b_optvalue = false;
|
|
||||||
node->declare_parameter("fixed_resolution", b_optvalue);
|
|
||||||
node->get_parameter("fixed_resolution", b_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropFixedResolution, &b_optvalue, sizeof(bool));
|
|
||||||
/// rotate 180
|
|
||||||
b_optvalue = true;
|
|
||||||
node->declare_parameter("reversion", b_optvalue);
|
|
||||||
node->get_parameter("reversion", b_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropReversion, &b_optvalue, sizeof(bool));
|
|
||||||
/// Counterclockwise
|
|
||||||
b_optvalue = true;
|
|
||||||
node->declare_parameter("inverted", b_optvalue);
|
|
||||||
node->get_parameter("inverted", b_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropInverted, &b_optvalue, sizeof(bool));
|
|
||||||
b_optvalue = true;
|
|
||||||
node->declare_parameter("auto_reconnect", b_optvalue);
|
|
||||||
node->get_parameter("auto_reconnect", b_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropAutoReconnect, &b_optvalue, sizeof(bool));
|
|
||||||
/// one-way communication
|
|
||||||
b_optvalue = false;
|
|
||||||
node->declare_parameter("isSingleChannel", b_optvalue);
|
|
||||||
node->get_parameter("isSingleChannel", b_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropSingleChannel, &b_optvalue, sizeof(bool));
|
|
||||||
/// intensity
|
|
||||||
b_optvalue = false;
|
|
||||||
node->declare_parameter("intensity", b_optvalue);
|
|
||||||
node->get_parameter("intensity", b_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropIntenstiy, &b_optvalue, sizeof(bool));
|
|
||||||
/// Motor DTR
|
|
||||||
b_optvalue = false;
|
|
||||||
node->declare_parameter("support_motor_dtr", b_optvalue);
|
|
||||||
node->get_parameter("support_motor_dtr", b_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropSupportMotorDtrCtrl, &b_optvalue, sizeof(bool));
|
|
||||||
|
|
||||||
//////////////////////float property/////////////////
|
|
||||||
/// unit: °
|
|
||||||
float f_optvalue = 180.0f;
|
|
||||||
node->declare_parameter("angle_max", f_optvalue);
|
|
||||||
node->get_parameter("angle_max", f_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropMaxAngle, &f_optvalue, sizeof(float));
|
|
||||||
f_optvalue = -180.0f;
|
|
||||||
node->declare_parameter("angle_min", f_optvalue);
|
|
||||||
node->get_parameter("angle_min", f_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropMinAngle, &f_optvalue, sizeof(float));
|
|
||||||
/// unit: m
|
|
||||||
f_optvalue = 64.f;
|
|
||||||
node->declare_parameter("range_max", f_optvalue);
|
|
||||||
node->get_parameter("range_max", f_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropMaxRange, &f_optvalue, sizeof(float));
|
|
||||||
f_optvalue = 0.1f;
|
|
||||||
node->declare_parameter("range_min", f_optvalue);
|
|
||||||
node->get_parameter("range_min", f_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropMinRange, &f_optvalue, sizeof(float));
|
|
||||||
/// unit: Hz
|
|
||||||
f_optvalue = 10.f;
|
|
||||||
node->declare_parameter("frequency", f_optvalue);
|
|
||||||
node->get_parameter("frequency", f_optvalue);
|
|
||||||
laser.setlidaropt(LidarPropScanFrequency, &f_optvalue, sizeof(float));
|
|
||||||
|
|
||||||
bool invalid_range_is_inf = false;
|
|
||||||
node->declare_parameter("invalid_range_is_inf", invalid_range_is_inf);
|
|
||||||
node->get_parameter("invalid_range_is_inf", invalid_range_is_inf);
|
|
||||||
|
|
||||||
|
|
||||||
bool ret = laser.initialize();
|
|
||||||
if (ret) {
|
|
||||||
ret = laser.turnOn();
|
|
||||||
} else {
|
|
||||||
RCLCPP_ERROR(node->get_logger(), "%s\n", laser.DescribeError());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto laser_pub = node->create_publisher<sensor_msgs::msg::LaserScan>("scan", rclcpp::SensorDataQoS());
|
|
||||||
auto pc_pub = node->create_publisher<sensor_msgs::msg::PointCloud>("point_cloud", rclcpp::SensorDataQoS());
|
|
||||||
|
|
||||||
auto stop_scan_service =
|
|
||||||
[&laser](const std::shared_ptr<rmw_request_id_t> request_header,
|
|
||||||
const std::shared_ptr<std_srvs::srv::Empty::Request> req,
|
|
||||||
std::shared_ptr<std_srvs::srv::Empty::Response> response) -> bool
|
|
||||||
{
|
|
||||||
return laser.turnOff();
|
|
||||||
};
|
|
||||||
|
|
||||||
auto stop_service = node->create_service<std_srvs::srv::Empty>("stop_scan",stop_scan_service);
|
|
||||||
|
|
||||||
auto start_scan_service =
|
|
||||||
[&laser](const std::shared_ptr<rmw_request_id_t> request_header,
|
|
||||||
const std::shared_ptr<std_srvs::srv::Empty::Request> req,
|
|
||||||
std::shared_ptr<std_srvs::srv::Empty::Response> response) -> bool
|
|
||||||
{
|
|
||||||
return laser.turnOn();
|
|
||||||
};
|
|
||||||
|
|
||||||
auto start_service = node->create_service<std_srvs::srv::Empty>("start_scan",start_scan_service);
|
|
||||||
|
|
||||||
rclcpp::WallRate loop_rate(20);
|
|
||||||
|
|
||||||
while (ret && rclcpp::ok()) {
|
|
||||||
|
|
||||||
LaserScan scan;//
|
|
||||||
|
|
||||||
if (laser.doProcessSimple(scan)) {
|
|
||||||
|
|
||||||
auto scan_msg = std::make_shared<sensor_msgs::msg::LaserScan>();
|
|
||||||
auto pc_msg = std::make_shared<sensor_msgs::msg::PointCloud>();
|
|
||||||
scan_msg->header.stamp = node->now();//this->now();
|
|
||||||
//scan_msg->header.stamp.sec = RCL_NS_TO_S(scan.stamp);
|
|
||||||
//scan_msg->header.stamp.nanosec = scan.stamp - RCL_S_TO_NS(scan_msg->header.stamp.sec);
|
|
||||||
scan_msg->header.frame_id = frame_id;
|
|
||||||
pc_msg->header = scan_msg->header;
|
|
||||||
scan_msg->angle_min = scan.config.min_angle;
|
|
||||||
scan_msg->angle_max = scan.config.max_angle;
|
|
||||||
scan_msg->angle_increment = scan.config.angle_increment;
|
|
||||||
scan_msg->scan_time = scan.config.scan_time;
|
|
||||||
scan_msg->time_increment = scan.config.time_increment;
|
|
||||||
scan_msg->range_min = scan.config.min_range;
|
|
||||||
scan_msg->range_max = scan.config.max_range;
|
|
||||||
|
|
||||||
int size = (scan.config.max_angle - scan.config.min_angle)/ scan.config.angle_increment + 1;
|
|
||||||
const float invalid_range_value =
|
|
||||||
invalid_range_is_inf ? std::numeric_limits<float>::infinity() : 0.0f;
|
|
||||||
scan_msg->ranges.assign(size, invalid_range_value);
|
|
||||||
scan_msg->intensities.assign(size, 0.0f);
|
|
||||||
|
|
||||||
pc_msg->channels.resize(2);
|
|
||||||
int idx_intensity = 0;
|
|
||||||
pc_msg->channels[idx_intensity].name = "intensities";
|
|
||||||
int idx_timestamp = 1;
|
|
||||||
pc_msg->channels[idx_timestamp].name = "stamps";
|
|
||||||
|
|
||||||
for(size_t i=0; i < scan.points.size(); i++) {
|
|
||||||
int index = std::ceil((scan.points[i].angle - scan.config.min_angle)/scan.config.angle_increment);
|
|
||||||
if(index >=0 && index < size) {
|
|
||||||
if (scan.points[i].range >= scan.config.min_range) {
|
|
||||||
scan_msg->ranges[index] = scan.points[i].range;
|
|
||||||
scan_msg->intensities[index] = scan.points[i].intensity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scan.points[i].range >= scan.config.min_range &&
|
|
||||||
scan.points[i].range <= scan.config.max_range) {
|
|
||||||
geometry_msgs::msg::Point32 point;
|
|
||||||
point.x = scan.points[i].range * cos(scan.points[i].angle);
|
|
||||||
point.y = scan.points[i].range * sin(scan.points[i].angle);
|
|
||||||
point.z = 0.0;
|
|
||||||
pc_msg->points.push_back(point);
|
|
||||||
pc_msg->channels[idx_intensity].values.push_back(scan.points[i].intensity);
|
|
||||||
pc_msg->channels[idx_timestamp].values.push_back(i * scan.config.time_increment);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
laser_pub->publish(*scan_msg);
|
|
||||||
pc_pub->publish(*pc_msg);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
RCLCPP_ERROR(node->get_logger(), "Failed to get scan");
|
|
||||||
}
|
|
||||||
if(!rclcpp::ok()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
rclcpp::spin_some(node);
|
|
||||||
loop_rate.sleep();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
RCLCPP_INFO(node->get_logger(), "[YDLIDAR INFO] Now YDLIDAR is stopping .......");
|
|
||||||
laser.turnOff();
|
|
||||||
laser.disconnecting();
|
|
||||||
rclcpp::shutdown();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
echo 'KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE:="0666", GROUP:="dialout", SYMLINK+="ydlidar"' >/etc/udev/rules.d/ydlidar.rules
|
|
||||||
|
|
||||||
echo 'KERNEL=="ttyACM*", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE:="0666", GROUP:="dialout", SYMLINK+="ydlidar"' >/etc/udev/rules.d/ydlidar-V2.rules
|
|
||||||
|
|
||||||
echo 'KERNEL=="ttyUSB*", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", MODE:="0666", GROUP:="dialout", SYMLINK+="ydlidar"' >/etc/udev/rules.d/ydlidar-2303.rules
|
|
||||||
|
|
||||||
service udev reload
|
|
||||||
sleep 2
|
|
||||||
service udev restart
|
|
||||||
|
|
||||||
336
tools/README.md
336
tools/README.md
@@ -1,327 +1,43 @@
|
|||||||
# CRAIC 工具脚本
|
# tools — 调试 & 测试工具
|
||||||
|
|
||||||
独立的命令行工具,用于机械臂控制、相机采集和坐标变换。
|
## 文件
|
||||||
|
|
||||||
## 工具列表
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `udp_server.py` | UDP 回显服务器,监听 `0.0.0.0:8888`,用于测试 UDP 通讯协议 |
|
||||||
|
|
||||||
### 1. udp_control.py
|
## 使用
|
||||||
机械臂命令行控制工具(带完整逆运动学)。
|
|
||||||
|
|
||||||
**功能**:
|
### UDP 回显服务器
|
||||||
- 关节空间运动控制
|
|
||||||
- 笛卡尔空间运动控制(自动逆运动学)
|
启动服务器,接收来自 ESP32 或 ROS 2 键盘遥控节点的 UDP 指令并打印:
|
||||||
- 轨迹插值(平滑运动)
|
|
||||||
- 状态缓存(连续运动)
|
|
||||||
|
|
||||||
**使用**:
|
|
||||||
```bash
|
```bash
|
||||||
# 关节空间控制
|
python tools/udp_server.py
|
||||||
python udp_control.py joints \
|
|
||||||
--height -100 --j2 10 --j3 20 --j4 30 \
|
|
||||||
--duration 2.0 --rate 20
|
|
||||||
|
|
||||||
# 笛卡尔空间控制
|
|
||||||
python udp_control.py pose \
|
|
||||||
--x 200 --y 100 --z -100 --phi 45 \
|
|
||||||
--duration 2.0
|
|
||||||
|
|
||||||
# 抓取动作
|
|
||||||
python udp_control.py pose \
|
|
||||||
--x 200 --y 100 --z -150 --phi 45 \
|
|
||||||
--grip --duration 1.0
|
|
||||||
|
|
||||||
# 释放动作
|
|
||||||
python udp_control.py pose \
|
|
||||||
--x 200 --y 100 --z -50 --phi 45 \
|
|
||||||
--release --duration 1.0
|
|
||||||
|
|
||||||
# 干运行(查看命令但不发送)
|
|
||||||
python udp_control.py pose --x 200 --y 0 --z -100 --phi 0 --dry-run
|
|
||||||
|
|
||||||
# 显示正运动学验证
|
|
||||||
python udp_control.py pose --x 200 --y 0 --z -100 --phi 0 --show-fk
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**参数**:
|
发送测试指令验证协议格式:
|
||||||
- `--ip` - ESP32 IP 地址(默认 192.168.4.1)
|
|
||||||
- `--port` - UDP 端口(默认 8888)
|
|
||||||
- `--duration` - 运动时长(秒,默认 1.0)
|
|
||||||
- `--rate` - 插值频率(Hz,默认 20.0)
|
|
||||||
- `--elbow-up` - 肘部朝上解(默认朝下)
|
|
||||||
- `--no-state-cache` - 禁用状态缓存
|
|
||||||
|
|
||||||
### 2. camera_to_base.py
|
|
||||||
相机坐标系到机械臂基坐标系的变换工具。
|
|
||||||
|
|
||||||
**功能**:
|
|
||||||
- 完整的坐标变换链:相机 → TCP → 基坐标系
|
|
||||||
- 支持相机安装偏移和旋转
|
|
||||||
- 水平安装相机的特化实现
|
|
||||||
|
|
||||||
**使用**:
|
|
||||||
```bash
|
```bash
|
||||||
# 基本变换(相机在 TCP 中心)
|
# 底盘指令
|
||||||
python camera_to_base.py \
|
|
||||||
--camera 10 -5 250 \
|
|
||||||
--tcp 200 0 -120 --phi 0
|
|
||||||
|
|
||||||
# 考虑相机偏移(相机不在 TCP 中心)
|
|
||||||
python camera_to_base.py \
|
|
||||||
--camera 10 -5 250 \
|
|
||||||
--tcp 200 0 -120 --phi 0 \
|
|
||||||
--cam-offset 0 10 50 \
|
|
||||||
--cam-rotation 0 15 0
|
|
||||||
```
|
|
||||||
|
|
||||||
**参数**:
|
|
||||||
- `--camera X Y Z` - 相机坐标系坐标
|
|
||||||
- `--tcp X Y Z` - 当前 TCP 位置
|
|
||||||
- `--phi` - 当前 TCP 偏航角(度)
|
|
||||||
- `--cam-offset TX TY TZ` - 相机到 TCP 的平移
|
|
||||||
- `--cam-rotation ROLL PITCH YAW` - 相机到 TCP 的旋转(度)
|
|
||||||
|
|
||||||
### 3. camera_capture.py
|
|
||||||
ESP32 MJPEG 流采集工具。
|
|
||||||
|
|
||||||
**功能**:
|
|
||||||
- 自动扫描局域网中的 ESP32 相机
|
|
||||||
- 解析 MJPEG 流并提取单帧
|
|
||||||
- 保存 JPEG 图像
|
|
||||||
|
|
||||||
**使用**:
|
|
||||||
```bash
|
|
||||||
# 指定 IP
|
|
||||||
python camera_capture.py --ip 192.168.4.1
|
|
||||||
|
|
||||||
# 自动扫描
|
|
||||||
python camera_capture.py --scan
|
|
||||||
|
|
||||||
# 指定输出文件
|
|
||||||
python camera_capture.py --ip 192.168.4.1 --output frame.jpg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. udp_server.py
|
|
||||||
UDP 回显测试服务器。
|
|
||||||
|
|
||||||
**功能**:
|
|
||||||
- 监听 UDP 端口
|
|
||||||
- 打印接收到的所有消息
|
|
||||||
- 用于调试 UDP 通信
|
|
||||||
|
|
||||||
**使用**:
|
|
||||||
```bash
|
|
||||||
python udp_server.py
|
|
||||||
|
|
||||||
# 指定端口
|
|
||||||
python udp_server.py --port 9999
|
|
||||||
```
|
|
||||||
|
|
||||||
**测试**:
|
|
||||||
```bash
|
|
||||||
# 终端 1: 启动服务器
|
|
||||||
python udp_server.py
|
|
||||||
|
|
||||||
# 终端 2: 发送测试
|
|
||||||
echo 'XYW:100:0:0:XZHY' | nc -u 127.0.0.1 8888
|
echo 'XYW:100:0:0:XZHY' | nc -u 127.0.0.1 8888
|
||||||
|
|
||||||
|
# 机械臂指令
|
||||||
|
echo 'JXB:-10:90:0:0:45:0:0:0:EZHY' | nc -u 127.0.0.1 8888
|
||||||
|
|
||||||
|
# 激光指令
|
||||||
|
echo 'LASERON' | nc -u 127.0.0.1 8888
|
||||||
```
|
```
|
||||||
|
|
||||||
## 坐标系说明
|
### 协议测试流程
|
||||||
|
|
||||||
### 机械臂基坐标系
|
1. 启动 `python tools/udp_server.py`
|
||||||
|
2. 修改 ROS 2 节点参数指向本机:`udp_ip:=127.0.0.1`
|
||||||
```
|
3. 运行 ROS 2 键盘遥控节点,观察服务器收到的指令
|
||||||
Z (上)
|
4. 确认协议格式正确后,将目标 IP 改为实际设备地址
|
||||||
|
|
|
||||||
|
|
|
||||||
o----→ X (前)
|
|
||||||
/
|
|
||||||
/
|
|
||||||
Y (左)
|
|
||||||
```
|
|
||||||
|
|
||||||
- 原点:J1 滑轨底部
|
|
||||||
- 单位:mm
|
|
||||||
|
|
||||||
### 相机坐标系(水平安装)
|
|
||||||
|
|
||||||
```
|
|
||||||
Y (下)
|
|
||||||
|
|
|
||||||
|
|
|
||||||
o----→ Z (前,光轴)
|
|
||||||
/
|
|
||||||
/
|
|
||||||
X (右)
|
|
||||||
```
|
|
||||||
|
|
||||||
- 原点:相机光心
|
|
||||||
- 单位:mm
|
|
||||||
|
|
||||||
### 图像坐标系
|
|
||||||
|
|
||||||
```
|
|
||||||
o----→ u (列,右)
|
|
||||||
|
|
|
||||||
|
|
|
||||||
↓
|
|
||||||
v (行,下)
|
|
||||||
```
|
|
||||||
|
|
||||||
- 原点:图像左上角
|
|
||||||
- 单位:像素
|
|
||||||
|
|
||||||
## 完整工作流示例
|
|
||||||
|
|
||||||
### 1. 测试机械臂连接
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 启动回显服务器
|
# 示例:ROS 2 键盘遥控 → 本地回显
|
||||||
python udp_server.py
|
ros2 run udp_teleop keyboard_control \
|
||||||
|
--ros-args -p udp_ip:=127.0.0.1 -p udp_port:=8888
|
||||||
# 2. 测试 UDP(新终端)
|
|
||||||
echo 'JXB:0:0:0:0:81:30:0:0:EZHY' | nc -u 127.0.0.1 8888
|
|
||||||
|
|
||||||
# 3. 验证服务器收到消息
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 机械臂运动测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 测试关节运动
|
|
||||||
python udp_control.py joints --height -50 --j2 10 --j3 20 --j4 30 --duration 2.0
|
|
||||||
|
|
||||||
# 测试笛卡尔运动
|
|
||||||
python udp_control.py pose --x 200 --y 100 --z -100 --phi 45 --duration 2.0
|
|
||||||
|
|
||||||
# 测试抓取
|
|
||||||
python udp_control.py pose --x 200 --y 100 --z -150 --phi 45 --grip --duration 1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 视觉抓取流程
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 步骤 1: 采集图像
|
|
||||||
python camera_capture.py --ip 192.168.4.1 --output target.jpg
|
|
||||||
|
|
||||||
# 步骤 2: 检测物体(假设得到像素坐标和宽度)
|
|
||||||
# u=320, v=240, pixel_width=50
|
|
||||||
# 使用相似三角形计算深度:depth = (real_width * focal_length) / pixel_width
|
|
||||||
# 转换到相机坐标:x_cam, y_cam, z_cam
|
|
||||||
|
|
||||||
# 步骤 3: 获取当前 TCP 位姿(从 ROS 或状态文件)
|
|
||||||
# tcp_x=200, tcp_y=0, tcp_z=-120, phi=0
|
|
||||||
|
|
||||||
# 步骤 4: 坐标变换
|
|
||||||
python camera_to_base.py \
|
|
||||||
--camera 10 -5 250 \
|
|
||||||
--tcp 200 0 -120 --phi 0
|
|
||||||
|
|
||||||
# 步骤 5: 移动到目标
|
|
||||||
python udp_control.py pose --x 323.5 --y 229.6 --z -108.6 --phi 0 --duration 3.0
|
|
||||||
|
|
||||||
# 步骤 6: 抓取
|
|
||||||
python udp_control.py pose --x 323.5 --y 229.6 --z -158.6 --phi 0 --grip --duration 1.0
|
|
||||||
|
|
||||||
# 步骤 7: 回收
|
|
||||||
python udp_control.py pose --x 200 --y 0 --z -158.6 --phi 0 --duration 2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## 状态缓存
|
|
||||||
|
|
||||||
`udp_control.py` 会将最后发送的关节状态保存到:
|
|
||||||
```
|
|
||||||
~/.ros/udp_control_state.json
|
|
||||||
```
|
|
||||||
|
|
||||||
这允许连续运动时从上一个位置平滑插值,而不是从原点跳跃。
|
|
||||||
|
|
||||||
**禁用缓存**:
|
|
||||||
```bash
|
|
||||||
python udp_control.py pose --x 200 --y 0 --z -100 --phi 0 --no-state-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
## UDP 协议
|
|
||||||
|
|
||||||
所有工具通过 UDP 端口 8888 与 ESP32 通信。
|
|
||||||
|
|
||||||
**机械臂命令格式**:
|
|
||||||
```
|
|
||||||
JXB:<height>:<J2>:<J3>:<J4>:<J5>:<J6>:0:0:EZHY\n
|
|
||||||
```
|
|
||||||
|
|
||||||
- 所有角度已包含零点偏移
|
|
||||||
- 高度单位:mm
|
|
||||||
- 角度单位:度
|
|
||||||
|
|
||||||
**底盘命令格式**:
|
|
||||||
```
|
|
||||||
XYW:<X>:<Y>:<W>:XZHY\n
|
|
||||||
```
|
|
||||||
|
|
||||||
## 依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install numpy
|
|
||||||
```
|
|
||||||
|
|
||||||
可选(用于相机采集):
|
|
||||||
```bash
|
|
||||||
pip install opencv-python requests
|
|
||||||
```
|
|
||||||
|
|
||||||
## 故障排查
|
|
||||||
|
|
||||||
### 1. 无法连接 ESP32
|
|
||||||
|
|
||||||
**检查**:
|
|
||||||
```bash
|
|
||||||
# 测试网络连接
|
|
||||||
ping 192.168.4.1
|
|
||||||
|
|
||||||
# 测试 UDP 端口
|
|
||||||
echo 'test' | nc -u 192.168.4.1 8888
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 逆运动学失败
|
|
||||||
|
|
||||||
**错误**:`目标超出工作空间`
|
|
||||||
|
|
||||||
**原因**:目标位置超出机械臂可达范围
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
- 检查目标是否在圆环内(半径 110-360mm)
|
|
||||||
- 检查高度是否在范围内(-370 到 -80mm)
|
|
||||||
|
|
||||||
### 3. 运动不平滑
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
- 增加 `--duration`(运动时间)
|
|
||||||
- 增加 `--rate`(插值频率)
|
|
||||||
- 确保状态缓存已启用
|
|
||||||
|
|
||||||
### 4. 关节角度超限
|
|
||||||
|
|
||||||
**错误**:`J2=-150 超出范围 [-110, 115]`
|
|
||||||
|
|
||||||
**解决**:
|
|
||||||
- 检查目标位姿是否合理
|
|
||||||
- 尝试 `--elbow-up` 切换肘部解
|
|
||||||
|
|
||||||
## 相关文档
|
|
||||||
|
|
||||||
- **运动学推导**:`docs/arm.md` - 完整数学推导
|
|
||||||
- **ROS 节点**:`ros2/README.md` - ROS 2 集成
|
|
||||||
- **视觉标定**:`docs/vision_calibration_horizontal.md` - 相机标定
|
|
||||||
|
|
||||||
## 开发
|
|
||||||
|
|
||||||
这些工具是独立的 Python 脚本,不依赖 ROS。可以直接在任何 Python 3 环境中运行。
|
|
||||||
|
|
||||||
**测试**:
|
|
||||||
```bash
|
|
||||||
# 测试逆运动学
|
|
||||||
python udp_control.py pose --x 200 --y 0 --z -100 --phi 0 --dry-run --show-fk
|
|
||||||
|
|
||||||
# 测试坐标变换
|
|
||||||
python camera_to_base.py --camera 0 0 300 --tcp 200 0 -120 --phi 0
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Capture a JPEG frame from the CRAIC ESP32-S3 camera via HTTP MJPEG stream.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python tools/camera_capture.py # auto-detect or default IP
|
|
||||||
python tools/camera_capture.py --ip 192.168.x.x # specify IP
|
|
||||||
python tools/camera_capture.py -o frame.jpg # custom output
|
|
||||||
python tools/camera_capture.py --scan # scan local subnet for ESP32
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def find_esp32_on_subnet(subnet_prefix: str) -> list[str]:
|
|
||||||
"""Ping sweep a /24 subnet and return IPs with port 80 open."""
|
|
||||||
candidates: list[str] = []
|
|
||||||
print(f"Scanning {subnet_prefix}.0/24 ...")
|
|
||||||
for i in range(1, 255):
|
|
||||||
ip = f"{subnet_prefix}.{i}"
|
|
||||||
r = os.system(f"ping -c 1 -W 1 {ip} >/dev/null 2>&1")
|
|
||||||
if r == 0:
|
|
||||||
try:
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.settimeout(0.5)
|
|
||||||
result = sock.connect_ex((ip, 80))
|
|
||||||
sock.close()
|
|
||||||
if result == 0:
|
|
||||||
candidates.append(ip)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if i % 50 == 0:
|
|
||||||
print(f" ... scanned {i}/254")
|
|
||||||
return candidates
|
|
||||||
|
|
||||||
|
|
||||||
def probe_esp32_status(ip: str, timeout: float = 3) -> bool:
|
|
||||||
"""Check if an IP serves the ESP32 camera JSON status endpoint."""
|
|
||||||
try:
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.settimeout(timeout)
|
|
||||||
sock.connect((ip, 80))
|
|
||||||
sock.sendall(
|
|
||||||
b"GET /status HTTP/1.1\r\n"
|
|
||||||
b"Host: " + ip.encode() + b"\r\n"
|
|
||||||
b"User-Agent: Mozilla/5.0\r\n"
|
|
||||||
b"Accept: */*\r\n"
|
|
||||||
b"Connection: close\r\n\r\n"
|
|
||||||
)
|
|
||||||
data = b""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
chunk = sock.recv(4096)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
data += chunk
|
|
||||||
except socket.timeout:
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
sock.close()
|
|
||||||
# ESP32 /status returns JSON with these keys
|
|
||||||
return b"capture_fps" in data or b"has_client" in data
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def grab_jpeg_frame(ip: str, port: int = 80, timeout: float = 10) -> bytes | None:
|
|
||||||
"""Connect to MJPEG stream, read raw bytes, extract first valid JPEG frame."""
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.settimeout(timeout)
|
|
||||||
try:
|
|
||||||
sock.connect((ip, port))
|
|
||||||
sock.sendall(
|
|
||||||
f"GET /stream HTTP/1.1\r\n"
|
|
||||||
f"Host: {ip}\r\n"
|
|
||||||
f"User-Agent: Mozilla/5.0\r\n"
|
|
||||||
f"Accept: */*\r\n"
|
|
||||||
f"Connection: close\r\n\r\n".encode()
|
|
||||||
)
|
|
||||||
|
|
||||||
buf = b""
|
|
||||||
start = time.time()
|
|
||||||
boundary = b"--frame"
|
|
||||||
|
|
||||||
while time.time() - start < timeout:
|
|
||||||
try:
|
|
||||||
chunk = sock.recv(4096)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
buf += chunk
|
|
||||||
except socket.timeout:
|
|
||||||
continue
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Scan buffer for a complete JPEG frame
|
|
||||||
# Format: --frame\r\n...headers...\r\n\r\n<JPEG>\r\n--frame
|
|
||||||
while True:
|
|
||||||
# Find JPEG start-of-image marker
|
|
||||||
soi = buf.find(b"\xff\xd8")
|
|
||||||
if soi == -1:
|
|
||||||
break
|
|
||||||
# Find JPEG end-of-image marker
|
|
||||||
eoi = buf.find(b"\xff\xd9", soi + 2)
|
|
||||||
if eoi == -1:
|
|
||||||
break
|
|
||||||
|
|
||||||
jpeg = buf[soi : eoi + 2]
|
|
||||||
|
|
||||||
# Verify it's realistically sized (>= 1KB) and preceded by boundary
|
|
||||||
if len(jpeg) >= 1024:
|
|
||||||
return jpeg
|
|
||||||
|
|
||||||
# False positive — keep scanning past this SOI
|
|
||||||
buf = buf[soi + 2:]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
except socket.timeout:
|
|
||||||
print(f" Timeout connecting to {ip}:{port}", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Socket error: {e}", file=sys.stderr)
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
sock.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_camera() -> str | None:
|
|
||||||
"""Try to find the ESP32 camera on local networks."""
|
|
||||||
# Get local IP
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
try:
|
|
||||||
s.connect(("8.8.8.8", 80))
|
|
||||||
local_ip = s.getsockname()[0]
|
|
||||||
s.close()
|
|
||||||
except:
|
|
||||||
local_ip = "127.0.0.1"
|
|
||||||
|
|
||||||
prefix = ".".join(local_ip.split(".")[:3])
|
|
||||||
|
|
||||||
# Collect candidate IPs to try
|
|
||||||
candidates: list[str] = []
|
|
||||||
|
|
||||||
# 1. Gateway (common for ESP32 AP mode)
|
|
||||||
candidates.append(f"{prefix}.1")
|
|
||||||
# 2. Common DHCP range
|
|
||||||
for i in range(100, 111):
|
|
||||||
candidates.append(f"{prefix}.{i}")
|
|
||||||
# 3. Some other common patterns
|
|
||||||
for i in [50, 51, 20, 30, 200, 201]:
|
|
||||||
candidates.append(f"{prefix}.{i}")
|
|
||||||
|
|
||||||
# Deduplicate
|
|
||||||
seen: set[str] = set()
|
|
||||||
unique: list[str] = []
|
|
||||||
for ip in candidates:
|
|
||||||
if ip not in seen:
|
|
||||||
seen.add(ip)
|
|
||||||
unique.append(ip)
|
|
||||||
|
|
||||||
print(f"Probing {len(unique)} likely IPs on {prefix}.0/24 ...")
|
|
||||||
for ip in unique:
|
|
||||||
print(f" {ip} ... ", end="", flush=True)
|
|
||||||
if probe_esp32_status(ip):
|
|
||||||
print("ESP32 camera found!")
|
|
||||||
return ip
|
|
||||||
print("no")
|
|
||||||
|
|
||||||
# Broader subnet scan
|
|
||||||
print(f"\nNo camera at common IPs. Scanning full subnet {prefix}.0/24 ...")
|
|
||||||
port80_hosts = find_esp32_on_subnet(prefix)
|
|
||||||
for ip in port80_hosts:
|
|
||||||
if probe_esp32_status(ip):
|
|
||||||
print(f"ESP32 camera found at {ip}")
|
|
||||||
return ip
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="从 CRAIC ESP32-S3 摄像头抓取 JPEG 帧"
|
|
||||||
)
|
|
||||||
parser.add_argument("--ip", help="ESP32 IP 地址 (默认自动检测)")
|
|
||||||
parser.add_argument("--port", type=int, default=80)
|
|
||||||
parser.add_argument("-o", "--output", help="输出文件路径")
|
|
||||||
parser.add_argument("--scan", action="store_true", help="扫描子网查找 ESP32")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# ---- resolve IP ----
|
|
||||||
ip = args.ip
|
|
||||||
if args.scan:
|
|
||||||
ip = resolve_camera()
|
|
||||||
elif ip is None:
|
|
||||||
ip = resolve_camera()
|
|
||||||
|
|
||||||
if ip is None:
|
|
||||||
print(
|
|
||||||
"ERROR: 找不到 ESP32 摄像头,请确保:\n"
|
|
||||||
" 1. ESP32 已上电\n"
|
|
||||||
" 2. ESP32 已连接 WiFi (STA 模式, SSID: FS)\n"
|
|
||||||
" 3. 本机与 ESP32 在同一子网\n"
|
|
||||||
" 或使用 --ip 直接指定地址",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# ---- capture ----
|
|
||||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
out = args.output or f"capture_{ts}.jpg"
|
|
||||||
|
|
||||||
print(f"Connecting to http://{ip}:{args.port}/stream ...")
|
|
||||||
jpeg = grab_jpeg_frame(ip, args.port)
|
|
||||||
|
|
||||||
if jpeg is None:
|
|
||||||
print("ERROR: 无法获取 JPEG 帧。", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
with open(out, "wb") as f:
|
|
||||||
f.write(jpeg)
|
|
||||||
print(f"Saved {out} ({len(jpeg)} bytes)")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""相机坐标系到机械臂基坐标系的直接变换工具。
|
|
||||||
|
|
||||||
使用场景:
|
|
||||||
1. 已经通过其他方式获得了相机坐标系坐标
|
|
||||||
2. 测试和验证坐标变换算法
|
|
||||||
3. 调试机械臂控制逻辑
|
|
||||||
|
|
||||||
输入:
|
|
||||||
- 相机坐标系坐标 (xc, yc, zc),单位 mm
|
|
||||||
- 当前 TCP 位姿 (x, y, z, phi)
|
|
||||||
|
|
||||||
输出:
|
|
||||||
- 基坐标系坐标 (xb, yb, zb),单位 mm
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import math
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
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 坐标系
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xc, yc, zc: 相机坐标系坐标 (mm)
|
|
||||||
tx, ty, tz: 相机到 TCP 的平移 (mm)
|
|
||||||
roll, pitch, yaw: 相机到 TCP 的旋转 (度)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(xt, yt, zt): TCP 坐标系坐标 (mm)
|
|
||||||
"""
|
|
||||||
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 坐标系 → 机械臂基坐标系(水平相机版本)
|
|
||||||
|
|
||||||
坐标映射:
|
|
||||||
- TCP X (相机右) → 基坐标系 垂直于 phi 方向
|
|
||||||
- TCP Y (相机下) → 基坐标系 -Z 方向(向下)
|
|
||||||
- TCP Z (相机前) → 基坐标系 phi 方向
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xt, yt, zt: TCP 坐标系坐标 (mm)
|
|
||||||
tcp_x, tcp_y, tcp_z: TCP 当前位置 (mm)
|
|
||||||
tcp_phi_deg: TCP 当前朝向角 (度)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(xb, yb, zb): 基坐标系坐标 (mm)
|
|
||||||
"""
|
|
||||||
phi = math.radians(tcp_phi_deg)
|
|
||||||
|
|
||||||
# 旋转矩阵:TCP → 基坐标系
|
|
||||||
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]:
|
|
||||||
"""完整变换:相机坐标系 → 基坐标系
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xc, yc, zc: 相机坐标系坐标 (mm)
|
|
||||||
tcp_x, tcp_y, tcp_z, tcp_phi_deg: TCP 当前位姿
|
|
||||||
cam_tx, cam_ty, cam_tz: 相机到 TCP 的平移
|
|
||||||
cam_roll, cam_pitch, cam_yaw: 相机到 TCP 的旋转
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(xb, yb, zb): 基坐标系坐标 (mm)
|
|
||||||
"""
|
|
||||||
# 步骤 1: 相机 → TCP
|
|
||||||
xt, yt, zt = camera_to_tcp(xc, yc, zc, cam_tx, cam_ty, cam_tz,
|
|
||||||
cam_roll, cam_pitch, cam_yaw)
|
|
||||||
|
|
||||||
# 步骤 2: TCP → 基坐标系
|
|
||||||
xb, yb, zb = tcp_to_base(xt, yt, zt, tcp_x, tcp_y, tcp_z, tcp_phi_deg)
|
|
||||||
|
|
||||||
return xb, yb, zb
|
|
||||||
|
|
||||||
|
|
||||||
def load_tcp_pose_from_state() -> Tuple[float, float, float, float]:
|
|
||||||
"""从状态缓存文件读取当前 TCP 位姿"""
|
|
||||||
state_file = Path("tools/.udp_control_state.json")
|
|
||||||
|
|
||||||
if not state_file.exists():
|
|
||||||
raise FileNotFoundError(
|
|
||||||
f"状态文件不存在: {state_file}\n"
|
|
||||||
"请先运行一次 udp_control.py 以初始化状态"
|
|
||||||
)
|
|
||||||
|
|
||||||
state = json.loads(state_file.read_text())
|
|
||||||
|
|
||||||
# 导入必要的模块计算正运动学
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
|
||||||
from udp_control import (
|
|
||||||
forward_kinematics,
|
|
||||||
ArmGeometry,
|
|
||||||
ArmMathState,
|
|
||||||
DEFAULT_L1, DEFAULT_L2, DEFAULT_X4, DEFAULT_Z4,
|
|
||||||
DEFAULT_ZERO_J2, DEFAULT_ZERO_J3, DEFAULT_ZERO_J4
|
|
||||||
)
|
|
||||||
|
|
||||||
geometry = ArmGeometry(l1=DEFAULT_L1, l2=DEFAULT_L2, x4=DEFAULT_X4, z4=DEFAULT_Z4)
|
|
||||||
math_state = ArmMathState(
|
|
||||||
d1=state["height"],
|
|
||||||
theta2_deg=state["j2"] - DEFAULT_ZERO_J2,
|
|
||||||
theta3_deg=state["j3"] - DEFAULT_ZERO_J3,
|
|
||||||
theta4_deg=state["j4"] - DEFAULT_ZERO_J4,
|
|
||||||
)
|
|
||||||
|
|
||||||
pose = forward_kinematics(geometry, math_state)
|
|
||||||
return pose.x, pose.y, pose.z, pose.phi_deg
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="相机坐标系到机械臂基坐标系的直接变换",
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
epilog="""
|
|
||||||
示例用法:
|
|
||||||
|
|
||||||
1. 基本用法(手动指定 TCP 位姿):
|
|
||||||
python %(prog)s --xc 10 --yc 5 --zc 250 \\
|
|
||||||
--tcp-x 150 --tcp-y 50 --tcp-z -100 --tcp-phi 45
|
|
||||||
|
|
||||||
2. 自动读取 TCP 位姿(从状态缓存):
|
|
||||||
python %(prog)s --xc 10 --yc 5 --zc 250 --auto-tcp
|
|
||||||
|
|
||||||
3. 包含相机到 TCP 的变换:
|
|
||||||
python %(prog)s --xc 10 --yc 5 --zc 250 --auto-tcp \\
|
|
||||||
--cam-tx 20 --cam-ty 10 --cam-tz 5
|
|
||||||
|
|
||||||
相机坐标系定义(水平安装):
|
|
||||||
Xc: 向右
|
|
||||||
Yc: 向下
|
|
||||||
Zc: 向前(水平,光轴方向)
|
|
||||||
|
|
||||||
机械臂基坐标系定义:
|
|
||||||
X, Y: 水平面
|
|
||||||
Z: 向上
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
# 相机坐标系输入
|
|
||||||
parser.add_argument("--xc", type=float, required=True,
|
|
||||||
help="相机坐标系 X 坐标(向右,mm)")
|
|
||||||
parser.add_argument("--yc", type=float, required=True,
|
|
||||||
help="相机坐标系 Y 坐标(向下,mm)")
|
|
||||||
parser.add_argument("--zc", type=float, required=True,
|
|
||||||
help="相机坐标系 Z 坐标(向前,mm)")
|
|
||||||
|
|
||||||
# TCP 位姿
|
|
||||||
tcp_group = parser.add_mutually_exclusive_group(required=True)
|
|
||||||
tcp_group.add_argument("--auto-tcp", action="store_true",
|
|
||||||
help="自动从状态缓存读取当前 TCP 位姿")
|
|
||||||
tcp_group.add_argument("--tcp-manual", action="store_true",
|
|
||||||
help="手动指定 TCP 位姿(需要 --tcp-x/y/z/phi)")
|
|
||||||
|
|
||||||
parser.add_argument("--tcp-x", type=float,
|
|
||||||
help="TCP X 坐标(mm)")
|
|
||||||
parser.add_argument("--tcp-y", type=float,
|
|
||||||
help="TCP Y 坐标(mm)")
|
|
||||||
parser.add_argument("--tcp-z", type=float,
|
|
||||||
help="TCP Z 坐标(mm)")
|
|
||||||
parser.add_argument("--tcp-phi", type=float,
|
|
||||||
help="TCP 朝向角(度)")
|
|
||||||
|
|
||||||
# 相机到 TCP 的变换(可选)
|
|
||||||
parser.add_argument("--cam-tx", type=float, default=0.0,
|
|
||||||
help="相机到 TCP 平移 X(mm,默认 0)")
|
|
||||||
parser.add_argument("--cam-ty", type=float, default=0.0,
|
|
||||||
help="相机到 TCP 平移 Y(mm,默认 0)")
|
|
||||||
parser.add_argument("--cam-tz", type=float, default=0.0,
|
|
||||||
help="相机到 TCP 平移 Z(mm,默认 0)")
|
|
||||||
parser.add_argument("--cam-roll", type=float, default=0.0,
|
|
||||||
help="相机到 TCP 旋转 roll(度,默认 0)")
|
|
||||||
parser.add_argument("--cam-pitch", type=float, default=0.0,
|
|
||||||
help="相机到 TCP 旋转 pitch(度,默认 0)")
|
|
||||||
parser.add_argument("--cam-yaw", type=float, default=0.0,
|
|
||||||
help="相机到 TCP 旋转 yaw(度,默认 0)")
|
|
||||||
|
|
||||||
# 输出格式
|
|
||||||
parser.add_argument("--json", action="store_true",
|
|
||||||
help="以 JSON 格式输出")
|
|
||||||
parser.add_argument("--quiet", action="store_true",
|
|
||||||
help="只输出坐标,不输出说明信息")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# 获取 TCP 位姿
|
|
||||||
if args.auto_tcp:
|
|
||||||
try:
|
|
||||||
tcp_x, tcp_y, tcp_z, tcp_phi = load_tcp_pose_from_state()
|
|
||||||
if not args.quiet:
|
|
||||||
print(f"从状态缓存读取 TCP 位姿: "
|
|
||||||
f"({tcp_x:.3f}, {tcp_y:.3f}, {tcp_z:.3f}), phi={tcp_phi:.3f}°")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"错误: 无法读取 TCP 位姿: {e}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
if not all([args.tcp_x is not None, args.tcp_y is not None,
|
|
||||||
args.tcp_z is not None, args.tcp_phi is not None]):
|
|
||||||
print("错误: 使用 --tcp-manual 时必须指定 --tcp-x, --tcp-y, --tcp-z, --tcp-phi",
|
|
||||||
file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
tcp_x, tcp_y, tcp_z, tcp_phi = args.tcp_x, args.tcp_y, args.tcp_z, args.tcp_phi
|
|
||||||
|
|
||||||
# 执行变换
|
|
||||||
try:
|
|
||||||
xb, yb, zb = camera_to_base(
|
|
||||||
args.xc, args.yc, args.zc,
|
|
||||||
tcp_x, tcp_y, tcp_z, tcp_phi,
|
|
||||||
args.cam_tx, args.cam_ty, args.cam_tz,
|
|
||||||
args.cam_roll, args.cam_pitch, args.cam_yaw
|
|
||||||
)
|
|
||||||
|
|
||||||
# 输出结果
|
|
||||||
if args.json:
|
|
||||||
result = {
|
|
||||||
"input": {
|
|
||||||
"camera": {"x": args.xc, "y": args.yc, "z": args.zc},
|
|
||||||
"tcp": {"x": tcp_x, "y": tcp_y, "z": tcp_z, "phi": tcp_phi}
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"base": {"x": xb, "y": yb, "z": zb}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
print(json.dumps(result, indent=2))
|
|
||||||
elif args.quiet:
|
|
||||||
print(f"{xb:.3f} {yb:.3f} {zb:.3f}")
|
|
||||||
else:
|
|
||||||
print("=" * 70)
|
|
||||||
print("相机坐标系到基坐标系变换")
|
|
||||||
print("=" * 70)
|
|
||||||
print(f"\n输入(相机坐标系):")
|
|
||||||
print(f" X_cam = {args.xc:8.3f} mm (向右)")
|
|
||||||
print(f" Y_cam = {args.yc:8.3f} mm (向下)")
|
|
||||||
print(f" Z_cam = {args.zc:8.3f} mm (向前,光轴)")
|
|
||||||
|
|
||||||
print(f"\nTCP 位姿:")
|
|
||||||
print(f" X_tcp = {tcp_x:8.3f} mm")
|
|
||||||
print(f" Y_tcp = {tcp_y:8.3f} mm")
|
|
||||||
print(f" Z_tcp = {tcp_z:8.3f} mm")
|
|
||||||
print(f" φ_tcp = {tcp_phi:8.3f} °")
|
|
||||||
|
|
||||||
if any([args.cam_tx, args.cam_ty, args.cam_tz,
|
|
||||||
args.cam_roll, args.cam_pitch, args.cam_yaw]):
|
|
||||||
print(f"\n相机到 TCP 变换:")
|
|
||||||
print(f" 平移: ({args.cam_tx:.3f}, {args.cam_ty:.3f}, {args.cam_tz:.3f}) mm")
|
|
||||||
print(f" 旋转: roll={args.cam_roll:.3f}°, pitch={args.cam_pitch:.3f}°, yaw={args.cam_yaw:.3f}°")
|
|
||||||
|
|
||||||
print(f"\n输出(机械臂基坐标系):")
|
|
||||||
print(f" X_base = {xb:8.3f} mm")
|
|
||||||
print(f" Y_base = {yb:8.3f} mm")
|
|
||||||
print(f" Z_base = {zb:8.3f} mm")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
print(f"\n使用此坐标控制机械臂:")
|
|
||||||
print(f" python tools/udp_control.py pose \\")
|
|
||||||
print(f" --x {xb:.1f} --y {yb:.1f} --z {zb:.1f} \\")
|
|
||||||
print(f" --phi {tcp_phi:.1f}")
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"错误: {e}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,907 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""CRAIC mechanical arm UDP controller.
|
|
||||||
|
|
||||||
Mechanical structure used here:
|
|
||||||
1. J2/J3/J4 rotate around the Z axis in the XY plane.
|
|
||||||
2. The gripper TCP has a fixed offset relative to the J4 frame: (x4, 0, z4).
|
|
||||||
3. ``phi`` is the TCP yaw in the XY plane and is always ``J2 + J3 + J4``.
|
|
||||||
4. All linear pose and geometry values are millimeters.
|
|
||||||
|
|
||||||
Supports two control modes:
|
|
||||||
1. Direct joint command mode.
|
|
||||||
2. Cartesian TCP pose mode using the inverse kinematics from ``docs/craic.md``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_UDP_IP = "192.168.4.1"
|
|
||||||
DEFAULT_UDP_PORT = 8888
|
|
||||||
|
|
||||||
DEFAULT_HEIGHT_MIN = -290 # 内部 d1 坐标:底部,单位 mm(z 轴朝上)
|
|
||||||
DEFAULT_HEIGHT_MAX = 0 # 内部 d1 坐标:顶部,单位 mm(z 轴朝上)
|
|
||||||
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
|
|
||||||
Z4_OPEN = 55
|
|
||||||
Z4_CLOSED = -100
|
|
||||||
DEFAULT_FIXED_J5 = J5_OPEN
|
|
||||||
# ==== 抓取/释放(由 J6 控制)====
|
|
||||||
# 请填写实际角度值:
|
|
||||||
GRIP_ANGLE = -5 # TODO: 填写抓取时 J6 的角度
|
|
||||||
RELEASE_ANGLE = 80 # TODO: 填写释放时 J6 的角度
|
|
||||||
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(__file__).with_name(".udp_control_state.json")
|
|
||||||
|
|
||||||
|
|
||||||
class ArmControlError(ValueError):
|
|
||||||
"""Raised when the requested arm pose is invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
@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:
|
|
||||||
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:
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Joint4Center:
|
|
||||||
x: float
|
|
||||||
y: float
|
|
||||||
z: float
|
|
||||||
|
|
||||||
|
|
||||||
def default_command_state() -> ArmJointState:
|
|
||||||
return ArmJointState(
|
|
||||||
height=-DEFAULT_HEIGHT_MAX,
|
|
||||||
j2=DEFAULT_ZERO_J2,
|
|
||||||
j3=DEFAULT_ZERO_J3,
|
|
||||||
j4=DEFAULT_ZERO_J4,
|
|
||||||
j5=DEFAULT_FIXED_J5,
|
|
||||||
j6=DEFAULT_FIXED_J6,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
normalized = (angle_deg + 180.0) % 360.0 - 180.0
|
|
||||||
if normalized == -180.0 and angle_deg > 0:
|
|
||||||
return 180.0
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
||||||
def lerp(start: float, end: float, t: float) -> float:
|
|
||||||
return start + (end - start) * t
|
|
||||||
|
|
||||||
|
|
||||||
def lerp_angle_deg(start_deg: float, end_deg: float, t: float) -> float:
|
|
||||||
delta = normalize_angle_deg(end_deg - start_deg)
|
|
||||||
return normalize_angle_deg(start_deg + delta * t)
|
|
||||||
|
|
||||||
|
|
||||||
def tcp_to_joint4_center(geometry: ArmGeometry, pose: ArmPose) -> Joint4Center:
|
|
||||||
"""Project the TCP target back to the J4 rotation center.
|
|
||||||
|
|
||||||
``x4`` and ``z4`` only affect position conversion. They do not affect ``phi``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
phi = math.radians(pose.phi_deg)
|
|
||||||
return Joint4Center(
|
|
||||||
x=pose.x - geometry.x4 * math.cos(phi),
|
|
||||||
y=pose.y - geometry.x4 * math.sin(phi),
|
|
||||||
z=pose.z + geometry.z4,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def forward_kinematics(geometry: ArmGeometry, state: ArmMathState) -> ArmPose:
|
|
||||||
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:
|
|
||||||
joint4_center = tcp_to_joint4_center(geometry, pose)
|
|
||||||
d1 = joint4_center.z
|
|
||||||
|
|
||||||
r2 = joint4_center.x * joint4_center.x + joint4_center.y * joint4_center.y
|
|
||||||
if r2 < 1e-9:
|
|
||||||
raise ArmControlError("目标点过于接近奇异点,无法稳定求解 J2。")
|
|
||||||
|
|
||||||
denom = 2.0 * geometry.l1 * geometry.l2
|
|
||||||
if abs(denom) < 1e-9:
|
|
||||||
raise ArmControlError("机械臂几何参数无效:L1 和 L2 不能为 0。")
|
|
||||||
|
|
||||||
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"目标超出工作空间,关节 4 投影距离为 {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(joint4_center.y, joint4_center.x) - math.atan2(
|
|
||||||
geometry.l2 * s3,
|
|
||||||
geometry.l1 + geometry.l2 * c3,
|
|
||||||
)
|
|
||||||
phi = math.radians(pose.phi_deg)
|
|
||||||
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(cmd)",
|
|
||||||
),
|
|
||||||
j2=clamp_int(
|
|
||||||
math_state.theta2_deg + zero_offsets.j2,
|
|
||||||
limits.j2_min,
|
|
||||||
limits.j2_max,
|
|
||||||
"J2(cmd)",
|
|
||||||
),
|
|
||||||
j3=clamp_int(
|
|
||||||
math_state.theta3_deg + zero_offsets.j3,
|
|
||||||
limits.j3_min,
|
|
||||||
limits.j3_max,
|
|
||||||
"J3(cmd)",
|
|
||||||
),
|
|
||||||
j4=clamp_int(
|
|
||||||
math_state.theta4_deg + zero_offsets.j4,
|
|
||||||
limits.j4_min,
|
|
||||||
limits.j4_max,
|
|
||||||
"J4(cmd)",
|
|
||||||
),
|
|
||||||
j5=clamp_int(j5, limits.joint_min, limits.joint_max, "J5"),
|
|
||||||
j6=clamp_int(j6, limits.joint_min, limits.joint_max, "J6"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_cached_command_state(limits: ArmLimits) -> ArmJointState | None:
|
|
||||||
if not STATE_FILE.exists():
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
|
||||||
return ArmJointState(
|
|
||||||
height=clamp_int(payload["height"], limits.height_min, limits.height_max, "height(cmd)"),
|
|
||||||
j2=clamp_int(payload["j2"], limits.j2_min, limits.j2_max, "J2"),
|
|
||||||
j3=clamp_int(payload["j3"], limits.j3_min, limits.j3_max, "J3"),
|
|
||||||
j4=clamp_int(payload["j4"], limits.j4_min, limits.j4_max, "J4"),
|
|
||||||
j5=clamp_int(payload.get("j5", DEFAULT_FIXED_J5), limits.joint_min, limits.joint_max, "J5"),
|
|
||||||
j6=clamp_int(payload.get("j6", DEFAULT_FIXED_J6), limits.joint_min, limits.joint_max, "J6"),
|
|
||||||
)
|
|
||||||
except (OSError, json.JSONDecodeError, KeyError, TypeError, ArmControlError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def save_cached_command_state(state: ArmJointState) -> None:
|
|
||||||
STATE_FILE.write_text(
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"height": state.height,
|
|
||||||
"j2": state.j2,
|
|
||||||
"j3": state.j3,
|
|
||||||
"j4": state.j4,
|
|
||||||
"j5": state.j5,
|
|
||||||
"j6": state.j6,
|
|
||||||
},
|
|
||||||
ensure_ascii=True,
|
|
||||||
indent=2,
|
|
||||||
),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_start_command_state(
|
|
||||||
limits: ArmLimits,
|
|
||||||
use_state_cache: bool,
|
|
||||||
) -> ArmJointState:
|
|
||||||
if use_state_cache:
|
|
||||||
cached_state = load_cached_command_state(limits)
|
|
||||||
if cached_state is not None:
|
|
||||||
return cached_state
|
|
||||||
return default_command_state()
|
|
||||||
|
|
||||||
|
|
||||||
def interpolate_command_states(
|
|
||||||
start: ArmJointState,
|
|
||||||
end: ArmJointState,
|
|
||||||
steps: int,
|
|
||||||
) -> list[ArmJointState]:
|
|
||||||
if steps <= 1:
|
|
||||||
return [end]
|
|
||||||
|
|
||||||
states: list[ArmJointState] = []
|
|
||||||
for step_index in range(1, steps + 1):
|
|
||||||
t = step_index / steps
|
|
||||||
states.append(
|
|
||||||
ArmJointState(
|
|
||||||
height=int(round(lerp(start.height, end.height, t))),
|
|
||||||
j2=int(round(lerp(start.j2, end.j2, t))),
|
|
||||||
j3=int(round(lerp(start.j3, end.j3, t))),
|
|
||||||
j4=int(round(lerp(start.j4, end.j4, t))),
|
|
||||||
j5=int(round(lerp(start.j5, end.j5, t))),
|
|
||||||
j6=int(round(lerp(start.j6, end.j6, t))),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return states
|
|
||||||
|
|
||||||
|
|
||||||
def interpolate_pose(
|
|
||||||
start: ArmPose,
|
|
||||||
end: ArmPose,
|
|
||||||
t: float,
|
|
||||||
) -> ArmPose:
|
|
||||||
return ArmPose(
|
|
||||||
x=lerp(start.x, end.x, t),
|
|
||||||
y=lerp(start.y, end.y, t),
|
|
||||||
z=lerp(start.z, end.z, t),
|
|
||||||
phi_deg=lerp_angle_deg(start.phi_deg, end.phi_deg, t),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_pose_command_path(
|
|
||||||
start_pose: ArmPose,
|
|
||||||
target_pose: ArmPose,
|
|
||||||
steps: int,
|
|
||||||
geometry: ArmGeometry,
|
|
||||||
limits: ArmLimits,
|
|
||||||
zero_offsets: ArmZeroOffsets,
|
|
||||||
elbow_up: bool,
|
|
||||||
j5: int,
|
|
||||||
j6: int,
|
|
||||||
) -> list[ArmJointState]:
|
|
||||||
if steps <= 1:
|
|
||||||
math_state = inverse_kinematics(
|
|
||||||
geometry=geometry,
|
|
||||||
pose=target_pose,
|
|
||||||
limits=limits,
|
|
||||||
elbow_up=elbow_up,
|
|
||||||
j5=j5,
|
|
||||||
j6=j6,
|
|
||||||
)
|
|
||||||
return [math_to_command_state(math_state, zero_offsets, limits, j5, j6)]
|
|
||||||
|
|
||||||
# Interpolate pose first (movement)
|
|
||||||
path: list[ArmJointState] = []
|
|
||||||
start_command_j5 = None
|
|
||||||
start_command_j6 = None
|
|
||||||
|
|
||||||
for step_index in range(1, steps + 1):
|
|
||||||
t = step_index / steps
|
|
||||||
pose = interpolate_pose(start_pose, target_pose, t)
|
|
||||||
math_state = inverse_kinematics(
|
|
||||||
geometry=geometry,
|
|
||||||
pose=pose,
|
|
||||||
limits=limits,
|
|
||||||
elbow_up=elbow_up,
|
|
||||||
j5=j5,
|
|
||||||
j6=j6,
|
|
||||||
)
|
|
||||||
command = math_to_command_state(math_state, zero_offsets, limits, j5, j6)
|
|
||||||
|
|
||||||
# Store the first command's j5/j6 values
|
|
||||||
if start_command_j5 is None:
|
|
||||||
start_command_j5 = command.j5
|
|
||||||
start_command_j6 = command.j6
|
|
||||||
|
|
||||||
# For all movement steps, use the starting j5/j6 values
|
|
||||||
path.append(ArmJointState(
|
|
||||||
height=command.height,
|
|
||||||
j2=command.j2,
|
|
||||||
j3=command.j3,
|
|
||||||
j4=command.j4,
|
|
||||||
j5=start_command_j5,
|
|
||||||
j6=start_command_j6,
|
|
||||||
))
|
|
||||||
|
|
||||||
# Add final grip/release adjustment if j5 or j6 changed
|
|
||||||
final = path[-1]
|
|
||||||
if final.j5 != j5 or final.j6 != j6:
|
|
||||||
path.append(ArmJointState(
|
|
||||||
height=final.height,
|
|
||||||
j2=final.j2,
|
|
||||||
j3=final.j3,
|
|
||||||
j4=final.j4,
|
|
||||||
j5=j5,
|
|
||||||
j6=j6,
|
|
||||||
))
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def compute_interpolation_steps(duration: float, rate: float) -> int:
|
|
||||||
if duration <= 0.0 or rate <= 0.0:
|
|
||||||
return 1
|
|
||||||
return max(1, int(math.ceil(duration * rate)))
|
|
||||||
|
|
||||||
|
|
||||||
def send_udp_commands(
|
|
||||||
ip: str,
|
|
||||||
port: int,
|
|
||||||
states: list[ArmJointState],
|
|
||||||
dry_run: bool,
|
|
||||||
duration: float,
|
|
||||||
) -> None:
|
|
||||||
if not states:
|
|
||||||
return
|
|
||||||
|
|
||||||
delay = duration / len(states) if len(states) > 1 and duration > 0.0 else 0.0
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
for state in states:
|
|
||||||
print(state.to_udp_message().decode("utf-8").strip())
|
|
||||||
return
|
|
||||||
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
|
||||||
for index, state in enumerate(states):
|
|
||||||
sock.sendto(state.to_udp_message(), (ip, port))
|
|
||||||
if delay > 0.0 and index < len(states) - 1:
|
|
||||||
time.sleep(delay)
|
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="CRAIC 机械臂 UDP 控制程序"
|
|
||||||
)
|
|
||||||
parser.add_argument("--ip", default=DEFAULT_UDP_IP, help="目标 UDP IP")
|
|
||||||
parser.add_argument("--port", type=int, default=DEFAULT_UDP_PORT, help="目标 UDP 端口")
|
|
||||||
parser.add_argument(
|
|
||||||
"--dry-run",
|
|
||||||
action="store_true",
|
|
||||||
help="只打印指令,不实际发送 UDP",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--show-fk",
|
|
||||||
action="store_true",
|
|
||||||
help="输出对应关节角的 TCP 正运动学结果",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--duration",
|
|
||||||
type=float,
|
|
||||||
default=DEFAULT_INTERP_DURATION,
|
|
||||||
help="插值总时长(秒),默认 1.0;设为 0 则直接发送",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--rate",
|
|
||||||
type=float,
|
|
||||||
default=DEFAULT_INTERP_RATE,
|
|
||||||
help="插值发送频率(Hz),默认 20",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--no-state-cache",
|
|
||||||
action="store_true",
|
|
||||||
help="不读取或更新上次发送的关节命令缓存",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--height-min",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_HEIGHT_MIN,
|
|
||||||
help=f"高度下限 (内部 d1 坐标, mm),默认 {DEFAULT_HEIGHT_MIN}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--height-max",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_HEIGHT_MAX,
|
|
||||||
help=f"高度上限 (内部 d1 坐标, mm),默认 {DEFAULT_HEIGHT_MAX}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--joint-min",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_JOINT_MIN,
|
|
||||||
help="关节角下限,默认 -180",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--joint-max",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_JOINT_MAX,
|
|
||||||
help="关节角上限(J5/J6),默认 180",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--j2-min",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_J2_MIN,
|
|
||||||
help=f"J2 下限,默认 {DEFAULT_J2_MIN}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--j2-max",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_J2_MAX,
|
|
||||||
help=f"J2 上限,默认 {DEFAULT_J2_MAX}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--j3-min",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_J3_MIN,
|
|
||||||
help=f"J3 下限,默认 {DEFAULT_J3_MIN}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--j3-max",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_J3_MAX,
|
|
||||||
help=f"J3 上限,默认 {DEFAULT_J3_MAX}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--j4-min",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_J4_MIN,
|
|
||||||
help=f"J4 下限,默认 {DEFAULT_J4_MIN}",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--j4-max",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_J4_MAX,
|
|
||||||
help=f"J4 上限,默认 {DEFAULT_J4_MAX}",
|
|
||||||
)
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="mode", required=True)
|
|
||||||
|
|
||||||
joints = subparsers.add_parser("joints", help="直接发送关节角")
|
|
||||||
joints.add_argument(
|
|
||||||
"--dry-run",
|
|
||||||
action="store_true",
|
|
||||||
help="只打印指令,不实际发送 UDP",
|
|
||||||
)
|
|
||||||
joints.add_argument(
|
|
||||||
"--show-fk",
|
|
||||||
action="store_true",
|
|
||||||
help="输出对应关节角的 TCP 正运动学结果",
|
|
||||||
)
|
|
||||||
joints.add_argument(
|
|
||||||
"--duration",
|
|
||||||
type=float,
|
|
||||||
default=DEFAULT_INTERP_DURATION,
|
|
||||||
help="插值总时长(秒),默认 1.0;设为 0 则直接发送",
|
|
||||||
)
|
|
||||||
joints.add_argument(
|
|
||||||
"--rate",
|
|
||||||
type=float,
|
|
||||||
default=DEFAULT_INTERP_RATE,
|
|
||||||
help="插值发送频率(Hz),默认 20",
|
|
||||||
)
|
|
||||||
joints.add_argument(
|
|
||||||
"--no-state-cache",
|
|
||||||
action="store_true",
|
|
||||||
help="不读取或更新上次发送的关节命令缓存",
|
|
||||||
)
|
|
||||||
joints.add_argument("--height", type=int, required=True, help="内部 d1 坐标 mm (-290=底部, 0=顶部,z 轴朝上)")
|
|
||||||
joints.add_argument("--j2", type=int, required=True, help="UDP 指令里的 J2 命令值")
|
|
||||||
joints.add_argument("--j3", type=int, required=True, help="UDP 指令里的 J3 命令值")
|
|
||||||
joints.add_argument("--j4", type=int, required=True, help="UDP 指令里的 J4 命令值")
|
|
||||||
joints_grip = joints.add_mutually_exclusive_group()
|
|
||||||
joints_grip.add_argument("--up", action="store_true", dest="up", default=None, help="夹爪抬起 (J5=-100°)")
|
|
||||||
joints_grip.add_argument("--down", action="store_false", dest="up", default=None, help="夹爪放下 (J5=81°)")
|
|
||||||
grip_release = joints.add_mutually_exclusive_group()
|
|
||||||
grip_release.add_argument("--grip", action="store_true", help=f"抓取(J6={GRIP_ANGLE}°,待填写)")
|
|
||||||
grip_release.add_argument("--release", action="store_true", help=f"释放(J6={RELEASE_ANGLE}°,待填写)")
|
|
||||||
joints.add_argument("--j6", type=int, default=DEFAULT_FIXED_J6, help="UDP 指令里的 J6 命令值,默认固定 0")
|
|
||||||
joints.add_argument("--l1", type=float, default=DEFAULT_L1, help=f"J2 到 J3 的连杆长度 mm (默认 {DEFAULT_L1})")
|
|
||||||
joints.add_argument("--l2", type=float, default=DEFAULT_L2, help=f"J3 到 J4 的连杆长度 mm (默认 {DEFAULT_L2})")
|
|
||||||
joints.add_argument("--x4", type=float, default=DEFAULT_X4, help=f"J4 到 TCP 的 X 偏移 mm (默认 {DEFAULT_X4})")
|
|
||||||
joints.add_argument("--z4", type=float, default=DEFAULT_Z4, help=f"J4 到 TCP 的 Z 偏移 mm (默认 {DEFAULT_Z4})")
|
|
||||||
|
|
||||||
pose = subparsers.add_parser("pose", help="根据末端位姿逆解后发送")
|
|
||||||
pose.add_argument(
|
|
||||||
"--dry-run",
|
|
||||||
action="store_true",
|
|
||||||
help="只打印指令,不实际发送 UDP",
|
|
||||||
)
|
|
||||||
pose.add_argument(
|
|
||||||
"--show-fk",
|
|
||||||
action="store_true",
|
|
||||||
help="输出对应关节角的 TCP 正运动学结果",
|
|
||||||
)
|
|
||||||
pose.add_argument(
|
|
||||||
"--duration",
|
|
||||||
type=float,
|
|
||||||
default=DEFAULT_INTERP_DURATION,
|
|
||||||
help="插值总时长(秒),默认 1.0;设为 0 则直接发送",
|
|
||||||
)
|
|
||||||
pose.add_argument(
|
|
||||||
"--rate",
|
|
||||||
type=float,
|
|
||||||
default=DEFAULT_INTERP_RATE,
|
|
||||||
help="插值发送频率(Hz),默认 20",
|
|
||||||
)
|
|
||||||
pose.add_argument(
|
|
||||||
"--no-state-cache",
|
|
||||||
action="store_true",
|
|
||||||
help="不读取或更新上次发送的关节命令缓存",
|
|
||||||
)
|
|
||||||
pose.add_argument("--x", type=float, required=True, help="TCP X 坐标 mm")
|
|
||||||
pose.add_argument("--y", type=float, required=True, help="TCP Y 坐标 mm")
|
|
||||||
pose.add_argument("--z", type=float, required=True, help="TCP Z 坐标 mm (-290=底部, 0=顶部, z 轴朝上)")
|
|
||||||
pose.add_argument("--phi", type=float, required=True, help="TCP 偏航角,单位度;等于 J2+J3+J4")
|
|
||||||
pose.add_argument("--l1", type=float, default=DEFAULT_L1, help=f"J2 到 J3 的连杆长度 mm (默认 {DEFAULT_L1})")
|
|
||||||
pose.add_argument("--l2", type=float, default=DEFAULT_L2, help=f"J3 到 J4 的连杆长度 mm (默认 {DEFAULT_L2})")
|
|
||||||
pose.add_argument("--x4", type=float, default=DEFAULT_X4, help=f"J4 到 TCP 的 X 偏移 mm (默认 {DEFAULT_X4})")
|
|
||||||
pose.add_argument("--z4", type=float, default=DEFAULT_Z4, help=f"J4 到 TCP 的 Z 偏移 mm (默认 {DEFAULT_Z4})")
|
|
||||||
pose.add_argument(
|
|
||||||
"--elbow-up",
|
|
||||||
action="store_true",
|
|
||||||
help="使用肘部向上分支,默认使用肘部向下分支",
|
|
||||||
)
|
|
||||||
pose_grip_release = pose.add_mutually_exclusive_group()
|
|
||||||
pose_grip_release.add_argument("--grip", action="store_true", help=f"抓取(J6={GRIP_ANGLE}°,待填写)")
|
|
||||||
pose_grip_release.add_argument("--release", action="store_true", help=f"释放(J6={RELEASE_ANGLE}°,待填写)")
|
|
||||||
pose_grip = pose.add_mutually_exclusive_group()
|
|
||||||
pose_grip.add_argument("--up", action="store_true", dest="up", default=None, help="夹爪抬起 (J5=-100°),覆盖 z 自动判断")
|
|
||||||
pose_grip.add_argument("--down", action="store_false", dest="up", default=None, help="夹爪放下 (J5=81°),覆盖 z 自动判断")
|
|
||||||
pose.add_argument("--j6", type=int, default=DEFAULT_FIXED_J6, help="附加发送的 J6 命令值,默认固定 0")
|
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def geometry_from_args(args: argparse.Namespace, z4: float | None = None) -> ArmGeometry:
|
|
||||||
return ArmGeometry(
|
|
||||||
l1=float(args.l1),
|
|
||||||
l2=float(args.l2),
|
|
||||||
x4=float(args.x4),
|
|
||||||
z4=float(args.z4) if z4 is None else z4,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def limits_from_args(args: argparse.Namespace) -> ArmLimits:
|
|
||||||
if args.height_min > args.height_max:
|
|
||||||
raise ArmControlError("height-min 不能大于 height-max。")
|
|
||||||
if args.joint_min > args.joint_max:
|
|
||||||
raise ArmControlError("joint-min 不能大于 joint-max。")
|
|
||||||
if args.j2_min > args.j2_max:
|
|
||||||
raise ArmControlError("j2-min 不能大于 j2-max。")
|
|
||||||
if args.j3_min > args.j3_max:
|
|
||||||
raise ArmControlError("j3-min 不能大于 j3-max。")
|
|
||||||
if args.j4_min > args.j4_max:
|
|
||||||
raise ArmControlError("j4-min 不能大于 j4-max。")
|
|
||||||
return ArmLimits(
|
|
||||||
height_min=args.height_min,
|
|
||||||
height_max=args.height_max,
|
|
||||||
joint_min=args.joint_min,
|
|
||||||
joint_max=args.joint_max,
|
|
||||||
j2_min=args.j2_min,
|
|
||||||
j2_max=args.j2_max,
|
|
||||||
j3_min=args.j3_min,
|
|
||||||
j3_max=args.j3_max,
|
|
||||||
j4_min=args.j4_min,
|
|
||||||
j4_max=args.j4_max,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_j5(up: bool) -> int:
|
|
||||||
return J5_CLOSED if up else J5_OPEN
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_z4(up: bool) -> float:
|
|
||||||
return Z4_CLOSED if up else Z4_OPEN
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_gripper_from_z(z: float) -> bool:
|
|
||||||
"""Auto-select gripper state based on TCP z coordinate (user mm, z-up).
|
|
||||||
|
|
||||||
- z in [-345, -55] -> down (gripper open, J5=81, Z4=55)
|
|
||||||
- z in [-190, 110] -> up (gripper closed, J5=-100, Z4=-100)
|
|
||||||
- Overlap [-190, -55] -> down (prefer down in intersection)
|
|
||||||
"""
|
|
||||||
return z > -55 # True = up, False = down
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_j6(grip: bool, release: bool, fallback: int) -> int:
|
|
||||||
if grip:
|
|
||||||
return GRIP_ANGLE
|
|
||||||
if release:
|
|
||||||
return RELEASE_ANGLE
|
|
||||||
return fallback
|
|
||||||
|
|
||||||
|
|
||||||
def state_from_joint_args(args: argparse.Namespace, limits: ArmLimits) -> ArmJointState:
|
|
||||||
up = args.up if args.up is not None else False
|
|
||||||
return ArmJointState(
|
|
||||||
height=clamp_int(args.height, limits.height_min, limits.height_max, "height(cmd)"),
|
|
||||||
j2=clamp_int(args.j2, limits.j2_min, limits.j2_max, "J2"),
|
|
||||||
j3=clamp_int(args.j3, limits.j3_min, limits.j3_max, "J3"),
|
|
||||||
j4=clamp_int(args.j4, limits.j4_min, limits.j4_max, "J4"),
|
|
||||||
j5=clamp_int(resolve_j5(up), limits.joint_min, limits.joint_max, "J5"),
|
|
||||||
j6=clamp_int(resolve_j6(args.grip, args.release, args.j6), limits.joint_min, limits.joint_max, "J6"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def print_joint_summary(state: ArmJointState) -> None:
|
|
||||||
print(
|
|
||||||
"UDP command joints:",
|
|
||||||
f"height={state.height}mm",
|
|
||||||
f"J2={state.j2}",
|
|
||||||
f"J3={state.j3}",
|
|
||||||
f"J4={state.j4}",
|
|
||||||
f"J5={state.j5}",
|
|
||||||
f"J6={state.j6}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def print_pose_summary(pose: ArmPose) -> None:
|
|
||||||
print(
|
|
||||||
"TCP pose:",
|
|
||||||
f"x={pose.x:.3f}mm",
|
|
||||||
f"y={pose.y:.3f}mm",
|
|
||||||
f"z={pose.z:.3f}mm",
|
|
||||||
f"phi={pose.phi_deg:.3f}deg",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def print_joint4_center_summary(center: Joint4Center) -> None:
|
|
||||||
print(
|
|
||||||
"J4 center:",
|
|
||||||
f"x={center.x:.3f}mm",
|
|
||||||
f"y={center.y:.3f}mm",
|
|
||||||
f"z={center.z:.3f}mm",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def print_math_summary(state: ArmMathState) -> None:
|
|
||||||
print(
|
|
||||||
"Math joints:",
|
|
||||||
f"d1={state.d1:.3f}mm",
|
|
||||||
f"J2={state.theta2_deg:.3f}",
|
|
||||||
f"J3={state.theta3_deg:.3f}",
|
|
||||||
f"J4={state.theta4_deg:.3f}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def print_interpolation_summary(
|
|
||||||
duration: float,
|
|
||||||
rate: float,
|
|
||||||
steps: int,
|
|
||||||
use_state_cache: bool,
|
|
||||||
) -> None:
|
|
||||||
cache_mode = "on" if use_state_cache else "off"
|
|
||||||
print(
|
|
||||||
"Interpolation:",
|
|
||||||
f"duration={duration:.3f}s",
|
|
||||||
f"rate={rate:.3f}Hz",
|
|
||||||
f"steps={steps}",
|
|
||||||
f"state_cache={cache_mode}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = build_parser()
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
limits = limits_from_args(args)
|
|
||||||
zero_offsets = ArmZeroOffsets()
|
|
||||||
use_state_cache = not args.no_state_cache
|
|
||||||
steps = compute_interpolation_steps(args.duration, args.rate)
|
|
||||||
print_interpolation_summary(args.duration, args.rate, steps, use_state_cache)
|
|
||||||
|
|
||||||
if args.mode == "joints":
|
|
||||||
start_command_state = resolve_start_command_state(limits, use_state_cache)
|
|
||||||
command_state = state_from_joint_args(args, limits)
|
|
||||||
math_state = command_to_math_state(command_state, zero_offsets)
|
|
||||||
start_math_state = command_to_math_state(start_command_state, zero_offsets)
|
|
||||||
command_path = interpolate_command_states(start_command_state, command_state, steps)
|
|
||||||
print("Start state source:", "cache/default")
|
|
||||||
print_joint_summary(start_command_state)
|
|
||||||
print_math_summary(start_math_state)
|
|
||||||
print_joint_summary(command_state)
|
|
||||||
print_math_summary(math_state)
|
|
||||||
|
|
||||||
if args.show_fk:
|
|
||||||
up = args.up if args.up is not None else False
|
|
||||||
z4 = resolve_z4(up)
|
|
||||||
start_pose = forward_kinematics(
|
|
||||||
geometry_from_args(args, z4=z4), start_math_state,
|
|
||||||
)
|
|
||||||
print("Start FK:")
|
|
||||||
print_pose_summary(start_pose)
|
|
||||||
pose = forward_kinematics(
|
|
||||||
geometry_from_args(args, z4=z4), math_state,
|
|
||||||
)
|
|
||||||
print_pose_summary(pose)
|
|
||||||
|
|
||||||
elif args.mode == "pose":
|
|
||||||
if args.up is not None:
|
|
||||||
auto_up = args.up
|
|
||||||
else:
|
|
||||||
auto_up = resolve_gripper_from_z(args.z)
|
|
||||||
z4 = resolve_z4(auto_up)
|
|
||||||
geometry = geometry_from_args(args, z4=z4)
|
|
||||||
j5 = resolve_j5(auto_up)
|
|
||||||
j6 = resolve_j6(args.grip, args.release, args.j6)
|
|
||||||
start_command_state = resolve_start_command_state(limits, use_state_cache)
|
|
||||||
start_math_state = command_to_math_state(start_command_state, zero_offsets)
|
|
||||||
start_pose = forward_kinematics(geometry, start_math_state)
|
|
||||||
target_pose = ArmPose(
|
|
||||||
x=args.x,
|
|
||||||
y=args.y,
|
|
||||||
z=args.z,
|
|
||||||
phi_deg=args.phi,
|
|
||||||
)
|
|
||||||
joint4_center = tcp_to_joint4_center(geometry, target_pose)
|
|
||||||
math_state = inverse_kinematics(
|
|
||||||
geometry=geometry,
|
|
||||||
pose=target_pose,
|
|
||||||
limits=limits,
|
|
||||||
elbow_up=args.elbow_up,
|
|
||||||
j5=j5,
|
|
||||||
j6=j6,
|
|
||||||
)
|
|
||||||
command_state = math_to_command_state(
|
|
||||||
math_state,
|
|
||||||
zero_offsets,
|
|
||||||
limits,
|
|
||||||
j5=j5,
|
|
||||||
j6=j6,
|
|
||||||
)
|
|
||||||
command_path = interpolate_command_states(start_command_state, command_state, steps)
|
|
||||||
print("Start state source:", "cache/default")
|
|
||||||
print_joint_summary(start_command_state)
|
|
||||||
print_math_summary(start_math_state)
|
|
||||||
print("Start FK:")
|
|
||||||
print_pose_summary(start_pose)
|
|
||||||
print_pose_summary(target_pose)
|
|
||||||
print_joint4_center_summary(joint4_center)
|
|
||||||
print_math_summary(math_state)
|
|
||||||
print_joint_summary(command_state)
|
|
||||||
|
|
||||||
if args.show_fk:
|
|
||||||
solved_pose = forward_kinematics(geometry, math_state)
|
|
||||||
print("Solved FK check:")
|
|
||||||
print_pose_summary(solved_pose)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ArmControlError(f"未知模式: {args.mode}")
|
|
||||||
|
|
||||||
final_payload = command_state.to_udp_message()
|
|
||||||
print("Final UDP payload:", final_payload.decode("utf-8").strip())
|
|
||||||
send_udp_commands(args.ip, args.port, command_path, args.dry_run, args.duration)
|
|
||||||
if use_state_cache and not args.dry_run:
|
|
||||||
save_cached_command_state(command_state)
|
|
||||||
if not args.dry_run:
|
|
||||||
print(f"Sent to {args.ip}:{args.port}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
except ArmControlError as exc:
|
|
||||||
print(f"错误: {exc}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
Reference in New Issue
Block a user