- 2025-03-31
-
发表了主题帖:
ROS2智能机器人开发实践-Yolov5部署-深度学习
# ROS2智能机器人开发实践-Yolov5部署-深度学习
## 1.首先要下载模型
```
# 首先执行git clone指令拉取YOLOv5的代码,然后切换到v2.0的分支以适应板端支持的算子
git clone https://github.com/ultralytics/yolov5.git
git checkout v2.0
```
## 2.然后在虚拟机配置环境
```
# 创建一个名为 yolov5 的新 Conda 环境,并指定 Python 版本为 3.7
$ conda create -n yolov5 python=3.7
# 激活名为 yolov5 的 Conda 环境
$ conda activate yolov5
# 在激活的环境中安装 PyTorch、torchvision、 torchaudio,以及对应的 CUDA 11.7 支持
$ conda install pytorch torchvision torchaudio pytorch-cuda=11.7 -c pytorch -c nvidia
# 安装 requirements.txt 文件中列出的所有 Python 包
$ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装 apex 库
$ pip install apex -i https://pypi.tuna.tsinghua.edu.cn/simple
```
> 建议使用虚拟环境,有效管理环境安装位置
## 3.接着使用精灵助手标注数据集
> 在制作数据集之前,需要先启动自己的小车,录制一段视频
> ROS2可以使用如下指令`ros2 run image_view video_recorder --ros-args -r __ns:=/camera/rgb -p image:="image_raw" -p filename:="/home/jetsonnano/videos/rgb_video.avi" -p fps:=30
`
1. - -ros-args :用于指定 ROS 2 的参数。
2. -r __ns:=/camera/rgb :将命名空间设置为 /camera/rgb ,以便正确订阅 /camera/rgb/image_raw 。
3. -p image: = “image_raw” :指定要订阅的图像话题名称。
4. -p filename:=“/home/jetsonnano/videos/rgb_video.avi” :指定输出视频文件的路径和名称。
5. -p fps:=30 :设置视频的帧率为 30。
> ROS1可以使用`rosrun image_view video_recorder image:=/camera/rgb/image_raw _filename:=/home/jetsonnano/videos/rgb_video.avi _fps:=30`
#### 之后进行标注,需要注意以下几点:
1. 要将图像中肉眼可以识别的东西都标注上
2. 糊的不能再糊的就不管了
3. 如果有的图像中没有识别内容也要放到数据集中,让他识别背景(当然也可以去除)
#### 4.接着改写detect.py文件
```python
#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Run inference on images, videos, directories, streams, etc.
Usage:
$ python path/to/detect.py --weights yolov5s.pt --source 0 # webcam
img.jpg # image
vid.mp4 # video
path/ # directory
path/*.jpg # glob
'https://youtu.be/Zgi9g1ksQHc' # YouTube
'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream
"""
import argparse
import os
import sys
from pathlib import Path
import cv2
import torch
import torch.backends.cudnn as cudnn
FILE = Path(__file__).resolve()
ROOT = FILE.parents[0] # YOLOv5 root directory
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
from models.common import DetectMultiBackend
from utils.datasets import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams
from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr,
increment_path, non_max_suppression, print_args, scale_coords, strip_optimizer, xyxy2xywh)
from utils.plots import Annotator, colors, save_one_box
from utils.torch_utils import select_device, time_sync
import rospy
from limo_deeplearning.msg import identify
import numpy
import time
from cv_bridge import CvBridge, CvBridgeError
from sensor_msgs.msg import Image
#import numpy as np
def img_publisher(imga):
#rospy.init_node('img_publish',anonymous=True)
#print(imga)
#img_list = imga.tolist()
bridge = CvBridge()
img_pub = rospy.Publisher('/image', Image, queue_size=1)
#Image = img
rate = rospy.Rate(10)
try:
img_pub.publish(bridge.cv2_to_imgmsg(imga, "bgr8"))
except CvBridgeError as e:
print (e)
#img_pub.publish(imga)
rospy.loginfo("Publish image success!")
rate.sleep()
def identify_publisher(names,classes,number,position,acc):
# ROS节点初始化
#rospy.init_node('identify_publisher', anonymous=True)
# 创建一个Publisher,发布名为/identify_info的topic,消息类型为learning_topic::Person,队列长度10
identify_info_pub = rospy.Publisher('/identify_info', identify, queue_size=3)
#设置循环的频率
rate = rospy.Rate(10)
#global same
#acc = acc.cpu().numpy() #tensor to numpy
#number = number.cpu().numpy()
#while not rospy.is_shutdown():
# 初始化learning_topic::Person类型的消息
identify_msg = identify()
identify_msg.results = names;
identify_msg.classes = classes;
identify_msg.area = number;
identify_msg.position = position;
identify_msg.acc = acc;
#identify_msg.same = same;
identify_info_pub.publish(identify_msg)
#print(type(names), type(classes), type(number), type(position), type(acc))
print (position)
#print(position)
rospy.loginfo("Publish identifiy message [%s, %d, %d, %f]", identify_msg.results, identify_msg.classes, identify_msg.area, identify_msg.acc)
rate.sleep()
if cv2.waitKey(1) == ord('q'):
#break
rospy.on_shutdown(identify_publisher)
#rospy.signal_shutdown("closed!")
@torch.no_grad()
def run(weights=ROOT / 'yolov5s.pt', # model.pt path(s)
source=ROOT / 'data/images', # file/dir/URL/glob, 0 for webcam
imgsz=(640, 640), # inference size (height, width)
conf_thres=0.25, # confidence threshold
iou_thres=0.45, # NMS IOU threshold
max_det=1000, # maximum detections per image
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu
view_img=False, # show results
save_txt=False, # save results to *.txt
save_conf=False, # save confidences in --save-txt labels
save_crop=False, # save cropped prediction boxes
nosave=False, # do not save images/videos
classes=None, # filter by class: --class 0, or --class 0 2 3
agnostic_nms=False, # class-agnostic NMS
augment=False, # augmented inference
visualize=False, # visualize features
update=False, # update all models
project=ROOT / 'runs/detect', # save results to project/name
name='exp', # save results to project/name
exist_ok=False, # existing project/name ok, do not increment
line_thickness=3, # bounding box thickness (pixels)
hide_labels=False, # hide labels
hide_conf=False, # hide confidences
half=False, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference
):
source = str(source)
save_img = not nosave and not source.endswith('.txt') # save inference images
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file)
if is_url and is_file:
source = check_file(source) # download
# Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model
device = select_device(device)
model = DetectMultiBackend(weights, device=device, dnn=dnn)
stride, names, pt, jit, onnx, engine = model.stride, model.names, model.pt, model.jit, model.onnx, model.engine
imgsz = check_img_size(imgsz, s=stride) # check image size
# Half
half &= (pt or jit or engine) and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
if pt or jit:
model.model.half() if half else model.model.float()
# Dataloader
if webcam:
view_img = check_imshow()
cudnn.benchmark = True # set True to speed up constant image size inference
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt)
bs = len(dataset) # batch_size
else:
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt)
bs = 1 # batch_size
vid_path, vid_writer = [None] * bs, [None] * bs
# Run inference
model.warmup(imgsz=(1, 3, *imgsz), half=half) # warmup
dt, seen = [0.0, 0.0, 0.0], 0
for path, im, im0s, vid_cap, s in dataset:
t1 = time_sync()
im = torch.from_numpy(im).to(device)
im = im.half() if half else im.float() # uint8 to fp16/32
im /= 255 # 0 - 255 to 0.0 - 1.0
if len(im.shape) == 3:
im = im[None] # expand for batch dim
t2 = time_sync()
dt[0] += t2 - t1
# Inference
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
pred = model(im, augment=augment, visualize=visualize)
t3 = time_sync()
dt[1] += t3 - t2
# NMS
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
dt[2] += time_sync() - t3
# Second-stage classifier (optional)
# pred = utils.general.apply_classifier(pred, classifier_model, im, im0s)
# Process predictions
#global same
for i, det in enumerate(pred): # per image
seen += 1
#same = same + 1
if webcam: # batch_size >= 1
p, im0, frame = path, im0s.copy(), dataset.count
s += f'{i}: '
else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
p = Path(p) # to Path
save_path = str(save_dir / p.name) # im.jpg
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # im.txt
s += '%gx%g ' % im.shape[2:] # print string
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh
imc = im0.copy() if save_crop else im0 # for save_crop
img_publisher(imc)
#print('imc:', imc)
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
if len(det):
# Rescale boxes from img_size to im0 size
det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()
# Print results
for c in det[:, -1].unique():
n = (det[:, -1] == c).sum() # detections per class
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " # add to string
# Write results
for *xyxy, conf, cls in reversed(det):
if save_txt: # Write to file
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
with open(txt_path + '.txt', 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n')
if save_img or save_crop or view_img: # Add bbox to image
c = int(cls) # integer class
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}')
annotator.box_label(xyxy, label, color=colors(c, True))
if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True)
center = (xyxy[2]-xyxy[0])*(xyxy[3]-xyxy[1]) # area of the box
identifies = names[c]
number = c
xyxy = [po.cpu().numpy() for po in xyxy] #tensor to numpy
conf = conf.item() # tensor to float
center = int(center.item())
#identify_publisher(names[c],c,center)
#print(cls,conf)
identify_publisher(identifies,number,center,xyxy,conf)
# Print time (inference-only)
LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)')
# Stream results
im0 = annotator.result()
if view_img:
cv2.imshow(str(p), im0)
cv2.waitKey(1) # 1 millisecond
# Save results (image with detections)
if save_img:
if dataset.mode == 'image':
cv2.imwrite(save_path, im0)
else: # 'video' or 'stream'
if vid_path != save_path: # new video
vid_path = save_path
if isinstance(vid_writer, cv2.VideoWriter):
vid_writer.release() # release previous video writer
if vid_cap: # video
fps = vid_cap.get(cv2.CAP_PROP_FPS)
w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
else: # stream
fps, w, h = 30, im0.shape[1], im0.shape[0]
save_path += '.mp4'
vid_writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
vid_writer.write(im0)
# Print results
t = tuple(x / seen * 1E3 for x in dt) # speeds per image
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t)
if save_txt or save_img:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
if update:
strip_optimizer(weights) # update model (to fix SourceChangeWarning)
def parse_opt():
parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'runs/train/exp/weights/best.pt', help='model.pt path(s)')
#parser.add_argument('--source', type=str, default=ROOT / 'data/test', help='file/dir/URL/glob, 0 for webcam') #摄像头测试
#parser.add_argument('--source', type=str, default='4.mp4', help='source')
parser.add_argument('--source', type=str, default='0', help='source')
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640,480], help='inference size h,w')
parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold')
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--view-img', action='store_true', help='show results')
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes')
parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
parser.add_argument('--augment', action='store_true', help='augmented inference')
parser.add_argument('--visualize', action='store_true', help='visualize features')
parser.add_argument('--update', action='store_true', help='update all models')
parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')
parser.add_argument('--name', default='exp', help='save results to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(FILE.stem, opt)
return opt
def main(opt):
global same
same = 0
check_requirements(exclude=('tensorboard', 'thop'))
run(**vars(opt))
if __name__ == "__main__":
try:
rospy.init_node('identify', anonymous=True)
rospy.loginfo("Starting identify")
opt = parse_opt()
main(opt)
if cv2.waitKey(1) == ord('q'):
#break
cv2.destroyAllWindows()
except KeyboardInterrupt:
print ("Shutting down traffic_light node.")
cv2.destroyAllWindows()
```
#### 修改过后即可发布识别到的类别话题!
> 注意1. `parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')`
> 2.`parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')`
> 3.`parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'runs/train/exp/weights/best.pt', help='model.pt path(s)')`
> 4.`parser.add_argument('--source', type=str, default='4.mp4', help='source')`
#### *half参数*可以优化模型,在执行时要加上以减少内存消耗
#### 如果要使用tensorrt加速(如果有jetsonnano)可以使用export.py文件导出engine权重文件
#### 使用python3 detect.py --half 推理模型
[localvideo]1e0987d56e714ee2d63d27fbd2abcf7b[/localvideo]
-
发表了主题帖:
ROS2智能机器人开发实践——底盘运动学模型
ROS2底盘模型运动学全解析:两轮差速、阿克曼、全向轮原理与代码实现
大家好!今天和大家深入探讨ROS机器人中常见的三种底盘模型:两轮差速、阿克曼转向和全向轮模型。本文将从运动学原理出发,结合公式推导和代码示例,帮助大家理解如何通过正逆运动学实现底盘控制。文章末尾还会附上完整的代码仓库链接,方便大家实践!
1. 两轮差速模型
1.1 正运动学:从轮速到整体速度
两轮差速模型通过左右轮的转速差异实现转向,其核心公式为:
线速度:左右轮速度的平均值
角速度:右轮速度减左轮速度,除以轮间距
代码实现(C++示例):
void kinematic_forward(float wheel1_speed, float wheel2_speed, float &linear_speed, float &angular_speed) {
linear_speed = (wheel1_speed + wheel2_speed) / 2.0;
angular_speed = (wheel2_speed - wheel1_speed) / wheel_distance_;
}
1.2 逆运动学:从目标速度到轮速
给定线速度vv和角速度ωω,计算左右轮目标转速:
代码实现:
void kinematic_inverse(float linear_speed, float angular_speed, float &out_wheel1_speed, float &out_wheel2_speed) {
out_wheel1_speed = linear_speed - (angular_speed * wheel_distance_) / 2.0;
out_wheel2_speed = linear_speed + (angular_speed * wheel_distance_) / 2.0;
}
2. 阿克曼转向模型
阿克曼模型常见于汽车底盘,通过转向轮的角度差实现转弯。
2.1 正运动学
假设前轮转向角为θ,前后轮轴距为L,转弯半径为R:
整体线速度vv与角速度ω的关系为:
2.2 逆运动学
根据目标线速度和转弯半径,计算转向角和轮速:
转向角:
轮速:内外轮需差速,内轮速度低于外轮。
代码片段(简化版):
def ackerman_inverse(v, omega, wheelbase):
theta = math.atan(wheelbase * omega / v)
inner_wheel_speed = v * (1 - wheelbase * omega / (2 * v))
outer_wheel_speed = v * (1 + wheelbase * omega / (2 * v))
return theta, inner_wheel_speed, outer_wheel_speed
3. 全向轮模型(麦克纳姆轮)
全向轮模型通过多组斜置滚轮实现平面全向移动,典型应用为麦克纳姆轮。
3.1 正运动学
已知四个轮子的线速度v1,v2,v3,v44,计算整体速度:
其中KK为轮子安装位置的几何参数。
3.2 逆运动学
给定 , , 计算各轮目标速度:
代码示例(ROS2节点中的速度转换):
void cmd_vel_callback(const geometry_msgs::Twist::ConstPtr& msg) {
float v1 = msg->linear.x - msg->linear.y - WHEEL_K * msg->angular.z;
float v2 = msg->linear.x + msg->linear.y - WHEEL_K * msg->angular.z;
float v3 = -msg->linear.x + msg->linear.y + WHEEL_K * msg->angular.z;
float v4 = -msg->linear.x - msg->linear.y + WHEEL_K * msg->angular.z;
// 转换为电机转速并发送
}
本文详细讲解了三种底盘模型的运动学原理及代码实现,核心要点:
两轮差速:简单易用,适合室内机器人。
阿克曼:适用于高速场景,如自动驾驶汽车。
全向轮:灵活性高,适合狭窄空间作业。
-
发表了主题帖:
ROS2智能机器人开发与实践——ROS2机器人仿真
# ROS2智能机器人开发与实践——ROS2机器人仿真
> 这一部分介绍一下ROS2机器人的仿真,包括urdf建模、gazebo仿真、rviz显示等等
> 我们可以使用solidowrks中ROS提供的插件`export as urdf` 将我们建好的模型转换成合适的urdf包
## 1.urdf模型导出(基于solid works软件导出)
### 1.1首先再模型上设立坐标系
### 1.2在sw的工具中找到export_as_urdf插件,匹配坐标系和模型,最终导出urdf功能包
### 1.3最终得到的功能包如下:
> 但是我们导出来的模型ROS1可以稍微修改一下后使用,但是对ROS2来说则不行
> 可以参考如下链接修改目标文件夹以满足ROS2的要求
[https://zhuanlan.zhihu.com/p/465398486](https://zhuanlan.zhihu.com/p/465398486)
## 2.gazebo仿真
### 2.1下载功能包
> `git clone https://gitee.com/guyuehome/ros2_21_tutorials.git`
### 2.2运行gazebo仿真
> `ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py `
> 注意虚拟机使用时要注意关闭`3d图形加速`,否则图像会出现闪烁
### 2.3RGB相机仿真与可视化
#### 加载模型
> `ros2 launch learning_gazebo_harmonic load_mbot_camera_into_gazebo_harmonic.launch.py`
#### 运行rviz2
> `ros2 run rviz2 rviz2`并添加image组件
#### 还可以加载三维相机
> `ros2 launch learning_gazebo_harmonic load_mbot_rgbd_into_gazebo_harmonic.launch.py `
### 2.4激光雷达仿真与可视化
> `ros2 launch learning_gazebo_harmonic load_mbot_lidar_into_gazebo_harmonic.launch.py`
- 2025-03-30
-
发表了主题帖:
ROS2智能机器人开发与实践——ROS2工具
# ROS2智能机器人开发与实践——ROS2工具
#### 今天讲一下ROS2中的开放工具,包括launch脚本文件、tf坐标系、gazebo仿真平台、rviz可视化平台、rosbag数据记录与回放、rqt模块化可视化工具箱。
## 1.launch脚本文件
> 相较于ROS1来说ROS2的launch文件使用了python语言,使其可以变得更加灵活。同时也可以对各个节点进行更加细致的操作
#### 比如,ROS1的launch文件可能是这样的:
```
```
#### 而ROS2的launch文件如下
```
import os
from ament_index_python.packages import get_package_share_directory # 查询功能包路径的方法
from launch import LaunchDescription # launch文件的描述类
from launch_ros.actions import Node # 节点启动的描述类
def generate_launch_description(): # 自动生成launch文件的函数
rviz_config = os.path.join( # 找到配置文件的完整路径
get_package_share_directory('learning_launch'),
'rviz',
'turtle_rviz.rviz'
)
return LaunchDescription([ # 返回launch文件的描述信息
Node( # 配置一个节点的启动
package='rviz2', # 节点所在的功能包
executable='rviz2', # 节点的可执行文件名
name='rviz2', # 对节点重新命名
arguments=['-d', rviz_config] # 加载命令行参数
)
])
```
> 可以看得出来ROS2的launch文件更发杂也更加灵活,学会编写launch文件可以实现一键启动等想法
#### 如果想具体了解launch脚本文件的写法和多样的用法可以访问如下两个官方的链接
[https://docs.ros.org/en/humble/Tutorials/Launch/Launch-Main.html](https://docs.ros.org/en/humble/Tutorials/Launch/Launch-Main.html)
[https://docs.ros.org/en/humble/Tutorials/Launch/Using-ROS2-Launch-For-Large-Projects.html](https://docs.ros.org/en/humble/Tutorials/Launch/Using-ROS2-Launch-For-Large-Projects.html)
## 2.tf坐标系

#### tf坐标系的具体用法有以下这些:
1. 查看tf树
2. 查询坐标变换信息
3. 坐标可视化
- 静态tf广播
- 动态tf广播
- tf监听
> 接下来演示一下:如何进行`海龟跟随`
效果如下:
#### 具体的实现代码(python)
```
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2 TF示例-通过坐标变化实现海龟跟随功能
"""
import math
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
import tf_transformations # TF坐标变换库
from tf2_ros import TransformException # TF左边变换的异常类
from tf2_ros.buffer import Buffer # 存储坐标变换信息的缓冲类
from tf2_ros.transform_listener import TransformListener # 监听坐标变换的监听器类
from geometry_msgs.msg import Twist # ROS2 速度控制消息
from turtlesim.srv import Spawn # 海龟生成的服务接口
class TurtleFollowing(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self.declare_parameter('source_frame', 'turtle1') # 创建一个源坐标系名的参数
self.source_frame = self.get_parameter( # 优先使用外部设置的参数值,否则用默认值
'source_frame').get_parameter_value().string_value
self.tf_buffer = Buffer() # 创建保存坐标变换信息的缓冲区
self.tf_listener = TransformListener(self.tf_buffer, self) # 创建坐标变换的监听器
self.spawner = self.create_client(Spawn, 'spawn') # 创建一个请求产生海龟的客户端
self.turtle_spawning_service_ready = False # 是否已经请求海龟生成服务的标志位
self.turtle_spawned = False # 海龟是否产生成功的标志位
self.publisher = self.create_publisher(Twist, 'turtle2/cmd_vel', 1) # 创建跟随运动海龟的速度话题
self.timer = self.create_timer(1.0, self.on_timer) # 创建一个固定周期的定时器,控制跟随海龟的运动
def on_timer(self):
from_frame_rel = self.source_frame # 源坐标系
to_frame_rel = 'turtle2' # 目标坐标系
if self.turtle_spawning_service_ready: # 如果已经请求海龟生成服务
if self.turtle_spawned: # 如果跟随海龟已经生成
try:
now = rclpy.time.Time() # 获取ROS系统的当前时间
trans = self.tf_buffer.lookup_transform( # 监听当前时刻源坐标系到目标坐标系的坐标变换
to_frame_rel,
from_frame_rel,
now)
except TransformException as ex: # 如果坐标变换获取失败,进入异常报告
self.get_logger().info(
f'Could not transform {to_frame_rel} to {from_frame_rel}: {ex}')
return
msg = Twist() # 创建速度控制消息
scale_rotation_rate = 1.0 # 根据海龟角度,计算角速度
msg.angular.z = scale_rotation_rate * math.atan2(
trans.transform.translation.y,
trans.transform.translation.x)
scale_forward_speed = 0.5 # 根据海龟距离,计算线速度
msg.linear.x = scale_forward_speed * math.sqrt(
trans.transform.translation.x ** 2 +
trans.transform.translation.y ** 2)
self.publisher.publish(msg) # 发布速度指令,海龟跟随运动
else: # 如果跟随海龟没有生成
if self.result.done(): # 查看海龟是否生成
self.get_logger().info(
f'Successfully spawned {self.result.result().name}')
self.turtle_spawned = True
else: # 依然没有生成跟随海龟
self.get_logger().info('Spawn is not finished')
else: # 如果没有请求海龟生成服务
if self.spawner.service_is_ready(): # 如果海龟生成服务器已经准备就绪
request = Spawn.Request() # 创建一个请求的数据
request.name = 'turtle2' # 设置请求数据的内容,包括海龟名、xy位置、姿态
request.x = float(4)
request.y = float(2)
request.theta = float(0)
self.result = self.spawner.call_async(request) # 发送服务请求
self.turtle_spawning_service_ready = True # 设置标志位,表示已经发送请求
else:
self.get_logger().info('Service is not ready') # 海龟生成服务器还没准备就绪的提示
def main(args=None):
rclpy.init(args=args) # ROS2 Python接口初始化
node = TurtleFollowing("turtle_following") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
```
## 3.gazebo仿真平台
> 相较于ROS1,ROS2对gazebo惊醒了重新设计与升级;这里只展示如何在官网上下载gazebo,后续的使用则在后面进行展示
#### 参考以下官方链接
[https://gazebosim.org/docs/harmonic/install_ubuntu/](https://gazebosim.org/docs/harmonic/install_ubuntu/)
```
#安装依赖项
sudo apt-get update
sudo apt-get install curl lsb-release gnupg
#安装Gazebo Harmonic
sudo curl https://packages.osrfoundation.org/gazebo.gpg --output /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
sudo apt-get update
sudo apt-get install gz-harmonic
#最后安装jazzy gz安装包
sudo apt install ros-jazzy-ros-gz
#需要注意的是:
#根据 Gazebo 官方文档,ROS 2 Jazzy(LTS 版本)仅支持 Gazebo Harmonic(Gazebo 11),而 Ionic 版本(Gazebo 12)与 Jazzy 的兼容性被标记为**❌(不推荐)**。所以下载的时候要看清楚版本再下载
```
## 4.rviz可视化平台
> rviz平台ROS2和ROS1版本的使用方法差不多,所以这里不会过多赘述

## 5.rosbag数据记录与回放
同样的ROS1和ROS2的使用方法大差不差
1. 数据记录`rosbag record`
2. 数据回放`rosbag play`
> 值得注意的是,rosbag可以对图像话题进行录制,然后再会放出来,相当于进行了一次`录像`操作
## 6.rqt模块化可视化工具箱
#### rqt工具箱主要是这几个方面的使用
##### rqt安装
```
sudo apt install ros-jazzy-rqt
```
1. 日志显示
`ros2 run rqt_console rqt_console`

2. 图像显示
`ros2 run rqt_image_view rqt_image_view`

3. 发布图像话题/服务数据

4. 绘制数据曲线

5. 数据包管理

6. 节点可视化

## 最后,希望大家可以学习一下git开发工具,并使用vscode进行开发
- 2025-03-18
-
发表了主题帖:
《ROS2智能机器人开发与实践》——ROS核心(节点、话题、服务、DDS通信协议等)
# 《ROS2智能机器人开发与实践》——ROS核心(节点、话题、服务、DDS通信协议等)
------------
###### 今天主要是分享一下ROS2的核心包括节点、话题、服务和DDS通信协议的内容
###### 其中会对官方和本书的相关细节进行比对
###### 本文章都以C++为例
## 一、节点
### 1、ROS2中的节点具有以下几个特点
1. 每个节点是一个进程
2. 每个节点都是一个可以独立运行的可执行文件
3. 每个节点可使用不同的编程语言
4. 每个节点可分布式运行在不同的主机中
5. 每个节点需要唯一的名称
### 2、节点编程方法(C++)
#### a.C++程序编写

```cpp
#include "rclcpp/rclcpp.hpp"
/***
创建一个HelloWorld节点, 初始化时输出“hello world”日志
***/
class HelloWorldNode : public rclcpp::Node
{
public:
HelloWorldNode()
: Node("node_helloworld_class") // ROS2节点父类初始化
{
while(rclcpp::ok()) // ROS2系统是否正常运行
{
RCLCPP_INFO(this->get_logger(), "Hello World"); // ROS2日志输出
sleep(1); // 休眠控制循环时间
}
}
};
// ROS2节点主入口main函数
int main(int argc, char * argv[])
{
// ROS2 C++接口初始化
rclcpp::init(argc, argv);
// 创建ROS2节点对象并进行初始化
rclcpp::spin(std::make_shared());
// 关闭ROS2 C++接口
rclcpp::shutdown();
return 0;
}
```
> ** 可以看的出来ROS2对节点的编程推荐`面向对象编程`的编程方法**
> 同时ROS2的编程难度更大、C++的语法更新、所以使用python也不失为一种方法
#### b.设置编译选项(编译接口)
```
find_package(rclcpp REQUIRED)
add_executable(node_helloworld_class src/node_helloworld_class.cpp)
ament_target_dependencies(node_helloworld_class rclcpp)
install(TARGETS
node_helloworld_class
DESTINATION lib/${PROJECT_NAME})
```
前三行相对于ROS1没什么太大区别,主要是多了最后这个
> 将编译生成的可执行文件拷贝到install安装空间的lib文件夹下
>我们知道ROS2相对于ROS1的工作空间构建方式变了,其中多了install文件夹 所以要注意最后一行
#### c.编译运行
```
#进入工作空间后
colcon build #编译工作空间
ros2 run learning_node_cpp node_helloworld_class # (功能包 可执行文件)
ros2 node list # 查看节点列表
ros2 node info # 查看节点信息
```
## 二、话题

### 1.rqt_graph
```
ros2 run turtlesim turtlesim_node
ros2 run turtlesim turtle_teleop_key
rqt_graph
```
我们将使用 `rqt_graph` 来可视化不断变化的节点和主题,以及它们之间的联系。

#### 话题的特点:
- 多对多通信
- 异步通信
- 消息接口
> 话题通信的一个特性,就是`异步通信`所谓异步,只要是指发布者发出数据后,并不知道订阅者什么时候可以收到。所以ROS2的后台会有一个轮询机制,当发现有数据进入消息队列时,就会触发回调函数
### 2.发布话题
```cpp
/***
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-发布图像话题
***/
#include
#include
#include
#include
#include "rclcpp/rclcpp.hpp" // ROS2 C++接口库
#include "std_msgs/msg/string.hpp" // 字符串消息类型
using namespace std::chrono_literals;
class PublisherNode : public rclcpp::Node
{
public:
PublisherNode()
: Node("topic_helloworld_pub") // ROS2节点父类初始化
{
// 创建发布者对象(消息类型、话题名、队列长度)
publisher_ = this->create_publisher("chatter", 10);
// 创建一个定时器,定时执行回调函数
timer_ = this->create_wall_timer(
500ms, std::bind(&PublisherNode::timer_callback, this));
}
private:
// 创建定时器周期执行的回调函数
void timer_callback()
{
// 创建一个String类型的消息对象
auto msg = std_msgs::msg::String();
// 填充消息对象中的消息数据
msg.data = "Hello World";
// 发布话题消息
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", msg.data.c_str());
// 输出日志信息,提示已经完成话题发布
publisher_->publish(msg);
}
rclcpp::TimerBase::SharedPtr timer_; // 定时器指针
rclcpp::Publisher::SharedPtr publisher_; // 发布者指针
};
// ROS2节点主入口main函数
int main(int argc, char * argv[])
{
// ROS2 C++接口初始化
rclcpp::init(argc, argv);
// 创建ROS2节点对象并进行初始化
rclcpp::spin(std::make_shared());
// 关闭ROS2 C++接口
rclcpp::shutdown();
return 0;
}
```
>都是面向对象编程
### 3.订阅话题
```cpp
/***
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-订阅“Hello World”话题消息
***/
#include
#include "rclcpp/rclcpp.hpp" // ROS2 C++接口库
#include "std_msgs/msg/string.hpp" // 字符串消息类型
using std::placeholders::_1;
class SubscriberNode : public rclcpp::Node
{
public:
SubscriberNode()
: Node("topic_helloworld_sub") // ROS2节点父类初始化
{
subscription_ = this->create_subscription(
"chatter", 10, std::bind(&SubscriberNode::topic_callback, this, _1)); // 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
}
private:
void topic_callback(const std_msgs::msg::String & msg) const // 创建回调函数,执行收到话题消息后对数据的处理
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str()); // 输出日志信息,提示订阅收到的话题消息
}
rclcpp::Subscription::SharedPtr subscription_; // 订阅者指针
};
// ROS2节点主入口main函数
int main(int argc, char * argv[])
{
// ROS2 C++接口初始化
rclcpp::init(argc, argv);
// 创建ROS2节点对象并进行初始化
rclcpp::spin(std::make_shared());
// 关闭ROS2 C++接口
rclcpp::shutdown();
return 0;
}
```
## 3.服务

> 服务可以实现类似你问我答的`同步通信`效果
### a.服务的特点
##### 服务的特点:
- 同步通信
- 一对多通信(服务端唯一)
- 服务接口
### b.客户端
```cpp
/***
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-发送两个加数,请求加法器计算
***/
#include "rclcpp/rclcpp.hpp" // ROS2 C++接口库
#include "learning_interface/srv/add_two_ints.hpp" // 自定义的服务接口
#include
#include
#include
using namespace std::chrono_literals;
int main(int argc, char **argv)
{
// ROS2 C++接口初始化
rclcpp::init(argc, argv);
if (argc != 3) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: service_adder_client X Y");
return 1;
}
// 创建ROS2节点对象并进行初始化
std::shared_ptr node = rclcpp::Node::make_shared("service_adder_client");
// 创建服务客户端对象(服务接口类型,服务名)
rclcpp::Client::SharedPtr client =
node->create_client("add_two_ints");
// 创建服务接口数据
auto request = std::make_shared();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
// 循环等待服务器端成功启动
while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}
// 异步方式发送服务请求
auto result = client->async_send_request(request);
// 接收服务器端的反馈数据
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
// 将收到的反馈信息打印输出
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
}
// 关闭ROS2 C++接口
rclcpp::shutdown();
return 0;
}
```
### c. 服务端
```cpp
/***
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-提供加法器的服务器处理功能
***/
#include "rclcpp/rclcpp.hpp" // ROS2 C++接口库
#include "learning_interface/srv/add_two_ints.hpp" // 自定义的服务接口
#include
// 创建回调函数,执行收到请求后对数据的处理
void adderServer(const std::shared_ptr request,
std::shared_ptr response)
{
// 完成加法求和计算,将结果放到反馈的数据中
response->sum = request->a + request->b;
// 输出日志信息,提示已经完成加法求和计算
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
request->a, request->b);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
// ROS2节点主入口main函数
int main(int argc, char **argv)
{
// ROS2 C++接口初始化
rclcpp::init(argc, argv);
// 创建ROS2节点对象并进行初始化
std::shared_ptr node = rclcpp::Node::make_shared("service_adder_server");
// 创建服务器对象(接口类型、服务名、服务器回调函数)
rclcpp::Service::SharedPtr service =
node->create_service("add_two_ints", &adderServer);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");
// 循环等待ROS2退出
rclcpp::spin(node);
// 关闭ROS2 C++接口
rclcpp::shutdown();
}
```
### d.命令行操作
```
ros2 service list # 查看服务列表
ros2 service type # 查看服务数据类型
ros2 service call # 发送服务请求
```
## 4.通信接口


> 同ROS1一样ROS2同样有的可自定义的通信接口,这些通信接口大大拓展了ROS的应用场景
> 具体的自定义接口的方法可以看官方文档或者
## 5.动作

> 动作是一种应用层的通信机制,其底层是基于话题和服务实现的
### a. 服务端
```cpp
/***
@作者: 古月居(www.guyuehome.com)
@说明: ROS2动作示例-负责执行圆周运动动作的服务端
***/
#include
#include "rclcpp/rclcpp.hpp" // ROS2 C++接口库
#include "rclcpp_action/rclcpp_action.hpp" // ROS2 动作类
#include "learning_interface/action/move_circle.hpp" // 自定义的圆周运动接口
using namespace std;
class MoveCircleActionServer : public rclcpp::Node
{
public:
// 定义一个自定义的动作接口类,便于后续使用
using CustomAction = learning_interface::action::MoveCircle;
// 定义一个处理动作请求、取消、执行的服务器端
using GoalHandle = rclcpp_action::ServerGoalHandle;
explicit MoveCircleActionServer(const rclcpp::NodeOptions & action_server_options = rclcpp::NodeOptions())
: Node("action_move_server", action_server_options) // ROS2节点父类初始化
{
using namespace std::placeholders;
this->action_server_ = rclcpp_action::create_server( // 创建动作服务器(接口类型、动作名、回调函数)
this->get_node_base_interface(),
this->get_node_clock_interface(),
this->get_node_logging_interface(),
this->get_node_waitables_interface(),
"move_circle",
std::bind(&MoveCircleActionServer::handle_goal, this, _1, _2),
std::bind(&MoveCircleActionServer::handle_cancel, this, _1),
std::bind(&MoveCircleActionServer::handle_accepted, this, _1));
}
private:
rclcpp_action::Server::SharedPtr action_server_; // 动作服务器
// 响应动作目标的请求
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr goal_request)
{
RCLCPP_INFO(this->get_logger(), "Server: Received goal request: %d", goal_request->enable);
(void)uuid;
// 如请求为enable则接受运动请求,否则就拒绝
if (goal_request->enable)
{
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
else
{
return rclcpp_action::GoalResponse::REJECT;
}
}
// 响应动作取消的请求
rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr goal_handle_canceled_)
{
RCLCPP_INFO(this->get_logger(), "Server: Received request to cancel action");
(void) goal_handle_canceled_;
return rclcpp_action::CancelResponse::ACCEPT;
}
// 处理动作接受后具体执行的过程
void handle_accepted(const std::shared_ptr goal_handle_accepted_)
{
using namespace std::placeholders;
// 在线程中执行动作过程
std::thread{std::bind(&MoveCircleActionServer::execute, this, _1), goal_handle_accepted_}.detach();
}
void execute(const std::shared_ptr goal_handle_)
{
const auto requested_goal = goal_handle_->get_goal(); // 动作目标
auto feedback = std::make_shared(); // 动作反馈
auto result = std::make_shared(); // 动作结果
RCLCPP_INFO(this->get_logger(), "Server: Executing goal");
rclcpp::Rate loop_rate(1);
// 动作执行的过程
for (int i = 0; (i < 361) && rclcpp::ok(); i=i+30)
{
// 检查是否取消动作
if (goal_handle_->is_canceling())
{
result->finish = false;
goal_handle_->canceled(result);
RCLCPP_INFO(this->get_logger(), "Server: Goal canceled");
return;
}
// 更新反馈状态
feedback->state = i;
// 发布反馈状态
goal_handle_->publish_feedback(feedback);
RCLCPP_INFO(this->get_logger(), "Server: Publish feedback");
loop_rate.sleep();
}
// 动作执行完成
if (rclcpp::ok())
{
result->finish = true;
goal_handle_->succeed(result);
RCLCPP_INFO(this->get_logger(), "Server: Goal succeeded");
}
}
};
// ROS2节点主入口main函数
int main(int argc, char * argv[])
{
// ROS2 C++接口初始化
rclcpp::init(argc, argv);
// 创建ROS2节点对象并进行初始化
rclcpp::spin(std::make_shared());
// 关闭ROS2 C++接口
rclcpp::shutdown();
return 0;
}
```
### b. 客户端
```cpp
/***
@作者: 古月居(www.guyuehome.com)
@说明: ROS2动作示例-请求执行圆周运动动作的客户端
***/
#include
#include "rclcpp/rclcpp.hpp" // ROS2 C++接口库
#include "rclcpp_action/rclcpp_action.hpp" // ROS2 动作类
#include "learning_interface/action/move_circle.hpp" // 自定义的圆周运动接口
using namespace std;
class MoveCircleActionClient : public rclcpp::Node
{
public:
// 定义一个自定义的动作接口类,便于后续使用
using CustomAction = learning_interface::action::MoveCircle;
// 定义一个处理动作请求、取消、执行的客户端类
using GoalHandle = rclcpp_action::ClientGoalHandle;
explicit MoveCircleActionClient(const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions())
: Node("action_move_client", node_options) // ROS2节点父类初始化
{
this->client_ptr_ = rclcpp_action::create_client( // 创建动作客户端(接口类型、动作名)
this->get_node_base_interface(),
this->get_node_graph_interface(),
this->get_node_logging_interface(),
this->get_node_waitables_interface(),
"move_circle");
}
// 创建一个发送动作目标的函数
void send_goal(bool enable)
{
// 检查动作服务器是否可以使用
if (!this->client_ptr_->wait_for_action_server(std::chrono::seconds(10)))
{
RCLCPP_ERROR(this->get_logger(), "Client: Action server not available after waiting");
rclcpp::shutdown();
return;
}
// 绑定动作请求、取消、执行的回调函数
auto send_goal_options = rclcpp_action::Client::SendGoalOptions();
using namespace std::placeholders;
send_goal_options.goal_response_callback =
std::bind(&MoveCircleActionClient::goal_response_callback, this, _1);
send_goal_options.feedback_callback =
std::bind(&MoveCircleActionClient::feedback_callback, this, _1, _2);
send_goal_options.result_callback =
std::bind(&MoveCircleActionClient::result_callback, this, _1);
// 创建一个动作目标的消息
auto goal_msg = CustomAction::Goal();
goal_msg.enable = enable;
// 异步方式发送动作的目标
RCLCPP_INFO(this->get_logger(), "Client: Sending goal");
this->client_ptr_->async_send_goal(goal_msg, send_goal_options);
}
private:
rclcpp_action::Client::SharedPtr client_ptr_;
// 创建一个服务器收到目标之后反馈时的回调函数
void goal_response_callback(GoalHandle::SharedPtr goal_message)
{
if (!goal_message)
{
RCLCPP_ERROR(this->get_logger(), "Client: Goal was rejected by server");
rclcpp::shutdown(); // Shut down client node
}
else
{
RCLCPP_INFO(this->get_logger(), "Client: Goal accepted by server, waiting for result");
}
}
// 创建处理周期反馈消息的回调函数
void feedback_callback(
GoalHandle::SharedPtr,
const std::shared_ptr feedback_message)
{
std::stringstream ss;
ss get_logger(), "%s", ss.str().c_str());
}
// 创建一个收到最终结果的回调函数
void result_callback(const GoalHandle::WrappedResult & result_message)
{
switch (result_message.code)
{
case rclcpp_action::ResultCode::SUCCEEDED:
break;
case rclcpp_action::ResultCode::ABORTED:
RCLCPP_ERROR(this->get_logger(), "Client: Goal was aborted");
rclcpp::shutdown(); // 关闭客户端节点
return;
case rclcpp_action::ResultCode::CANCELED:
RCLCPP_ERROR(this->get_logger(), "Client: Goal was canceled");
rclcpp::shutdown(); // 关闭客户端节点
return;
default:
RCLCPP_ERROR(this->get_logger(), "Client: Unknown result code");
rclcpp::shutdown(); // 关闭客户端节点
return;
}
RCLCPP_INFO(this->get_logger(), "Client: Result received: %s", (result_message.result->finish ? "true" : "false"));
rclcpp::shutdown(); // 关闭客户端节点
}
};
// ROS2节点主入口main函数
int main(int argc, char * argv[])
{
// ROS2 C++接口初始化
rclcpp::init(argc, argv);
// 创建一个客户端指针
auto action_client = std::make_shared();
// 发送动作目标
action_client->send_goal(true);
// 创建ROS2节点对象并进行初始化
rclcpp::spin(action_client);
// 关闭ROS2 C++接口
rclcpp::shutdown();
return 0;
}
```
### c. 命令行操作
```
$ ros2 action list # 查看服务列表
$ ros2 action info # 查看服务数据类型
$ ros2 action send_goal # 发送服务请求
```
## 6.参数
> 参数是ROS2模块化封装的重要功能,可以利用全局参数可视化显示以及动态调整
> 默认情况下,某一个参数被修改后,使用它的节点并不会立刻被同步,ROS2提供了动态配置的机制,需要在程序中进行设置,通过回调函数的方法,动态调整参数
关于参数,具体可以看[https://docs.ros.org/en/jazzy/Concepts/Basic/About-Parameters.html#parameters](https://docs.ros.org/en/jazzy/Concepts/Basic/About-Parameters.html#parameters)
## 7.DDS

DS的全称是Data Distribution Service,也就是数据分发服务,2004年由对象管理组织OMG发布和维护,是一套专门为实时系统设计的数据分发/订阅标准,最早应用于美国海军, 解决舰船复杂网络环境中大量软件升级的兼容性问题,现在已经成为强制标准。
DDS强调以数据为中心,可以提供丰富的服务质量策略,以保障数据进行实时、高效、灵活地分发,可满足各种分布式实时通信应用需求。
### 在命令行中配置DDS
```
$ ros2 topic pub /chatter std_msgs/msg/Int32 "data: 42" --qos-reliability best_effort
$ ros2 topic echo /chatter --qos-reliability reliable
$ ros2 topic echo /chatter --qos-reliability best_effort
$ ros2 topic info /chatter --verbose #查看节点的Qos策略
```
## 8.分布式通信
> 分布式通信是解决使用一台计算机算力不足的情况
> 有几个实用的例子,也是我自己在使用的:
> 一台电脑运行执行器节点 另一台电脑读取数据并使用QT可视化数据
> 一台电脑运行节点 另一台使用rviz等等
- 2025-02-10
-
发表了主题帖:
《ROS2智能机器人开发实践》——阅读测评
# 《ROS2智能机器人开发实践》阅读心得
各位,新年伊始,给各位拜个晚年了!
非常荣幸能够参加这次阅读活动,自从收到书之后,由于过年,年后的拜年,走亲戚,以及与好友聚会等原因,让我很少拥有自己的时间,加之毕业设计的开题报告,以及一些其他专业书籍的阅读,让我这段时间有点忙忙的感觉哈哈哈哈。
但是想着既然参加了这个活动就一定要坚持下去,一定要开个好开头。接下来的一段时间我会持续跟进我的阅读进度,和大家分享我的理解,对比书的优劣,和大家一起进步!我的毕业设计就是做的移动机器人,不过可惜的是,我用的是ROS1,虽然ROS1和ROS2相比变化很大,但是我相信未来是ROS2的,我也会在有ROS1的基础上,持续跟进ROS2的学习。如果大家可以持续跟帖,我会将我的学习方法,遇到的问题分享给大家,菜鸟可以跟着我学会从官方文档入手,辅助这本书学习ROS2,也希望大佬可以对我批评指正!还请轻点喷,感谢感谢!
# 一、学习内容
这次的学习内容有点少,而且章节不整,希望大家可以多多包涵!谢谢大家喔,后续更新不会这个样子了!
## 1.第一章: ROS:智能机器人的灵魂
关于为什么ROS2和ROS1架构上的区别,我就不说了,感兴趣的可以看一下B站小虎哥的视频:
https://www.bilibili.com/video/BV15D4y167Hv?spm_id_from=333.788.player.switch&vd_source=d338071225639d42527e58e702fb919d
ROS2相较于ROS1,实时性得到了很大的提高,节点间的耦合大大减少,跨平台能力大大增强,我看到过他们用Esp32系列的板子,装了MicroROS系统,但是这个系统跟ROS2的写法还是有一定区别的。
ROS2相对于ROS1的具体差别表现在以下方面:
引用书中说法
>1.重新设计了系统架构。ROS2的节点拜托了ROSMaster的集中控制,实现了真正的分布式通信,借助于全新的通信框架DDS、Zenoh,为所有节点的通讯提供了可靠的保障。
2.重新设计了软件API。ROS1原本的API已经无法满足需求了,ROS2结合C++最新标准和python3语言特性,设计了更加通用的API,当然,也提供了移植教程
3.优化升级了编译系统。ROS1中的编译问题很多,相信大家在rosbuild和catkin的问题上有很多吧,没少骂过这个系统哈哈哈。ROS2进行了优化,优化后的编译系统叫做ament和colcon,它们提供了更加稳定、高效的编译和链接过程,减少了出错的可能。
ROS2的安装大家参考小鱼一键安装: `wget http://fishros.com/install -O fishros && . fishros`
我安装的是jazzy(Ubuntu24.04)版本,多么优雅的一个版本!
虚拟机大家可以下载vmware然后跟着以下的链接进行下载
https://blog.csdn.net/2301_80224556/article/details/142675808
**一些基本概念我就不赘述了**
## 接下来开始我们的操作,开始编译我们的工作空间吧!
>我推荐不管学ROS1还是ROS2,都要从官方文档入门,网上的视频再好,也是别人笑话过的东西,当然我们刚入手的时候可以通过它们进行大概的了解!
下面给出jazzy版本的文档链接
https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html
>值得一提的是 ROS2命令行的操作机制与linux相同,不过所有操作都集成在ROS2的总命令中,
如`ros2 run turtlesim turtlrsim_node`,"ros2"后边第一个参数表示不同的操作目的,如node表示对节点的操作,topic表示对话题的操作,后边还可以添加一系列参数
#### 考虑到这并不是一个操作手册,所以我会尽量将重点放在书外的思考和学习中,剩下的包括运行节点程序,查看节点信息,话题信息,数据的录制和播放等就不再过多说明,大家在一些网站上都可以找到。
## 第二章:ROS2核心原理:构建机器人的基石
### 1.编译工作空间
>由于使用colcon的编译方式,ROS2工作空间编译后会出现四个文件夹分别是:build、log、install、src
相对于ROS1来说,少了devvel文件夹多了install和log,前两者功能类似,都是存放编译生成的可执行文件等
##### 官方文档:
>准备
```shell
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://github.com/ros/ros_tutorials.git -b jazzy
```
###### 接下来是安装依赖库
>官方文档:
```shell
rosdep install -i --from-path src --rosdistro jazzy -y
```
本书:
```shell
rosdep install --from-paths src --ignore-src -r -y
```
总结最简单:
```shell
rosdep install -i --from-paths src -y
```
`--from-path ` :指定一个路径, `rosdep` 会从该路径下的文件夹中查找 ROS 包。
`src ` :通常是指 ROS 工作空间的 src 目录,该目录包含所有的 ROS 包。
`--rosdistro `:指定 ROS 发行版(如 jazzy 、 foxy 、 galactic 等), rosdep 会根据指定的发行版来解析依赖项。
`-i `:忽略已安装的依赖项,只安装尚未安装的依赖项,这意味着 rosdep 不会解析 src 目录中的包的依赖项,而是只解析其他路径中的包的依赖项。和`--ignore-src`一样。
`-y ` :自动确认安装,无需手动输入 y 。
###### 下载ROS2编译器colcon:
>本书:
```shell
sudo apt install python3-colcon-ros
```
官方:
```shell
sudo apt install python3-colcon-common-extensions
```
`python3-colcon-ros是python3-colcon-common-extensions的一个分支,专门针对ros的扩展包
###### 编译工作空间:
>本书:
```shell
cd ~/ros2_ws/
colcon build
```
官方:
```shell
colcon build --symlink-install
```
` --symlink-install` 选项会使用符号链接(symlink)将安装目录中的文件指向源代码目录中的文件。
这意味着,当你修改源代码目录中的文件时,安装目录中的文件也会自动更新,无需重新编译。•适用于开
发阶段,可以快速测试代码的修改。
• 当你修改源代码时,安装目录中的文件会自动更新,因为它们是通过符号链接指向源代码的。
• 这意味着你不需要重新编译整个项目来测试代码的修改,只需重新编译修改的部分即可。
###### 添加环境变量:
```shell
#一次性
source ~/dev_ws/install/setup.bash
#永久,加入~/.bashrc中
echo "source ~/dev_ws/install/setup.bash" >> ~/.bashrc
//注意不是devel而是install
```
### 2.创建一个功能包
>注意什么样的文件夹才是功能包呢,一定是包含cmakelist.txt以及package.xml的才是!
###### 创建:
>本书
```shell
cd ros2_ws/src
ros2 pkg create --build-type ament_cmake learning_pkg_cpp //C++
ros2 pkg create --build-type ament_cmake learning_pkg_cpp //python
```
官方
```shell
ros2 pkg create --build-type ament_cmake --license Apache-2.0
//--license Apache-2.0是开源声明
ros2 pkg create --build-type ament_python --license Apache-2.0
```
***可以知道相比于ROS1通用结构的功能包类型,ROS2对功能包的类型进行了明确的划分,有助于复杂系统的编译和链接***
其他的一些cmakelist.txt以及package.xml的文件内容,感兴趣大家可以自己去官方文档查询:
https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.html
# 二、阅读感受
>我还是坚持,ros最好的教程是官方的wiki和document。在我读的ROS1的书和这本书中我都没有看到对ROS1官方文档的解读。我理解这是因为书的受众是初学者,但是如果可以有对ros官方文档的提炼和解读,可以更好的培养我们独立自主的学习能力。这只是一点点小的意见,个人看法,不喜勿喷哇,谢谢!
整本书开头也是循序渐进,对于初学者很受用。
这次是因为过年,开题等原因,读的确实少,后面会继续加油,认真读完这本书,并做一个较好的测评!
注:免责声明:以上都是个人看法和个人见解,欢迎大佬们批评指正,喷的轻一点!
- 2025-01-20
-
回复了主题帖:
读书活动入围名单:《ROS2智能机器人开发实践》
个人信息无误,确认可以完成阅读分享计划