herowuxun

  • 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坐标系 ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.2_TF/image-20220528142052573.png) #### 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版本的使用方法差不多,所以这里不会过多赘述 ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.5_Rviz/image-20220528151929609.jpg) ## 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` ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.6_Rqt/image-20220528153332794.png) 2. 图像显示 `ros2 run rqt_image_view rqt_image_view` ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.6_Rqt/image-20220528153354370.png) 3. 发布图像话题/服务数据 ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.6_Rqt/image-20220528153406339.png) 4. 绘制数据曲线 ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.6_Rqt/image-20220528153414784.png) 5. 数据包管理 ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.6_Rqt/image-20220528153423185.png) 6. 节点可视化 ![](https://book.guyuehome.com/ROS2/3.%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7/image/3.6_Rqt/image-20220528153433016.png) ## 最后,希望大家可以学习一下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++程序编写 ![](https://docs.ros.org/en/jazzy/_images/Nodes-TopicandService.gif) ```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 # 查看节点信息 ``` ## 二、话题 ![](https://docs.ros.org/en/jazzy/_images/Topic-MultiplePublisherandMultipleSubscriber.gif) ### 1.rqt_graph ``` ros2 run turtlesim turtlesim_node ros2 run turtlesim turtle_teleop_key rqt_graph ``` 我们将使用 `rqt_graph` 来可视化不断变化的节点和主题,以及它们之间的联系。 ![](https://docs.ros.org/en/jazzy/_images/rqt_graph.png) #### 话题的特点: - 多对多通信 - 异步通信 - 消息接口 > 话题通信的一个特性,就是`异步通信`所谓异步,只要是指发布者发出数据后,并不知道订阅者什么时候可以收到。所以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.服务 ![](https://docs.ros.org/en/jazzy/_images/Service-MultipleServiceClient.gif) > 服务可以实现类似你问我答的`同步通信`效果 ### 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.通信接口 ![](https://book.guyuehome.com/ROS2/2.%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5/image/2.6_%E9%80%9A%E4%BF%A1%E6%8E%A5%E5%8F%A3/image-20220528001533911.png) ![](https://book.guyuehome.com/ROS2/2.%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5/image/2.6_%E9%80%9A%E4%BF%A1%E6%8E%A5%E5%8F%A3/image-20220528001633925.png) > 同ROS1一样ROS2同样有的可自定义的通信接口,这些通信接口大大拓展了ROS的应用场景 > 具体的自定义接口的方法可以看官方文档或者 ## 5.动作 ![](https://docs.ros.org/en/jazzy/_images/Action-SingleActionClient.gif) > 动作是一种应用层的通信机制,其底层是基于话题和服务实现的 ### 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 ![](https://book.guyuehome.com/ROS2/2.%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5/image/2.10_DDS/image-20220528020829304.jpg) 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智能机器人开发实践》

    个人信息无误,确认可以完成阅读分享计划

最近访客

< 1/1 >

统计信息

已有11人来访过

  • 芯积分:29
  • 好友:--
  • 主题:6
  • 回复:1

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言