• 340查看
  • 0回复

[软件工程] ROS2实战,自定义接口,让代码更优雅

[复制链接]


该用户从未签到

发表于 30-4-2024 19:49:22 | 显示全部楼层 |阅读模式

汽车零部件采购、销售通信录       填写你的培训需求,我们帮你找      招募汽车专业培训老师


ROS2实战,自定义接口,让代码更优雅w1.jpg

在上一篇文章《ROS2实战,我写了个AEB程序,在CARLA中进行了仿真测试》中,我们创建了两个节点,实现了车辆信息的发布和订阅。但是聪明的你,一定发现当时的程序非常笨重,简单的车辆信息的发送,竟然用到了三个publisher对象,仅仅是因为发送的数据类型不一样,就要换一个publisher,这也太傻了。还好ROS 2提供了用户自定义接口的方法,可以把这些要发送的信息统一定义到一个接口类型中,然后打包发布,打包订阅。

接口:两个程序之间进行信号对接,所采用的统一的数据格式。

接下来我们看看怎么实现用户自定义接口。

我们还是从零做起,先定义工作空间,再定义功能包,再设计代码,再配置代码,最后编译测试。
1、创建ROS 2工作空间

打开终端创建本次演示案例的工作空间:
cd ~/ROS2_study
mkdir ros2_ws_demo006_custom_interface/
cd ros2_ws_demo006_custom_interface
2、创建功能包

为了创建能够在不同功能包之间都通用的接口,我们把接口也独立出来定义成一个只有接口的功能包。进入工作空间,创建src文件夹来存放源码,在src下创建本次演示所需要的用户自定义的接口功能包。
mkdir src
cd src
ros2 pkg create my_custom_interfaces --build-type ament_cmake

my_custom_interfaces 是新包的名称。请注意,它只能是 CMake 包,但这并不限制我们可以在哪种类型的包中使用消息和服务。我们可以在 CMake 包中创建自定义接口,然后在 C++ 或 Python 节点中使用它。

然后我们进入这个包,删除不必要的文件。
cd my_custom_interfaces/
rm -rf include/ src/

然后创建我们需要的接口文件夹(msg),和其内的接口文件(MyCarlaInterface.msg):
mkdir msg
touch msg/MyCarlaInterface.msg

注意,到此时,我们的整个工作空间下的目录还非常简单:
ros2_ws_demo006_custom_interface/
└── src
    └── my_custom_interfaces
        ├── CMakeLists.txt
        ├── msg
        │   └── MyCarlaInterface.msg
        └── package.xml

3 directories, 3 files
3、编辑接口文件

文件目录定义好了以后,就可以开始编辑文件内容了。首先要编辑的是我们自定义的接口文件MyCarlaInterface.msg。我这里内容如下:
string ego_id
float64 ego_speed_x
float64 ego_speed_y
float64 ego_speed_z
float64 ego_position_x
float64 ego_position_y
float64 ego_position_z
string obj_id
float64 obj_speed_x
float64 obj_speed_y
float64 obj_speed_z
float64 obj_position_x
float64 obj_position_y
float64 obj_position_z

这些都是我们在上一节文章中要发布和订阅的车辆信号,我这里统一把他们都定义在这个文件里面了。每个信号的格式都是“类型+变量名”,变量名是用户自己定义的,变量类型可以参考如下列表:
bool
byte
char
float32
float64
int8
uint8
int16
uint16
int32
uint32
int64
uint64
string

注意,上面的这些类型都是最最最基础的类型,其实ROS 2还基于这些基础数据继续封装以提供其他更为复杂数据类型,比如std_msgs、geometry_msgs等,它们其实就是对一些比较典型的接口进行了封装,我们接下来要做的就是根据我们自己的需求,做出跟这个差不多的东西来。
4、配置接口文件

打开CMakeLists.txt文件,添加如下内容:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/MyCarlaInterface.msg"
)

它的作用是定义将我们刚刚创建的接口文件使用rosidl工具编译成可以调用的库文件。

再来看下CMakeLists.txt的完整内容吧:
cmake_minimum_required(VERSION 3.5)
project(tutorial_interfaces)

# Default to C99
if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 99)
endif()

# 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 dependencies
find_package(ament_cmake REQUIRED)

find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/MyCarlaInterface.msg"
)

ament_package()

接下来需要配置下package.xml文件,需要新增以下内容:
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

完整的package.xml文件内容如下:
<?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>my_custom_interfaces</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="goodman@todo.todo">goodman</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>

  <buildtool_depend>rosidl_default_generators</buildtool_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <member_of_group>rosidl_interface_packages</member_of_group>

</package>

然后我们回到工作空间,编译一下这个接口功能包。
cd ~/ROS2_study/ros2_ws_demo006_custom_interface/
colcon build --packages-select my_custom_interfaces

看到屏幕显示的如下编译信息,就是编译成功了。
Starting >>> my_custom_interfaces
Finished <<< my_custom_interfaces [5.04s]                    

Summary: 1 package finished [7.04s]
5、接口文件检查

接下来我们使用ros2命令行工具检查下当前工作空间下的接口属性吧:
source install/setup.bash
ros2 interface show my_custom_interfaces/msg/MyCarlaInterface

我们看到屏幕上打印出如下的内容:
string ego_id
float64 ego_speed_x
float64 ego_speed_y
float64 ego_speed_z
float64 ego_position_x
float64 ego_position_y
float64 ego_position_z
string obj_id
float64 obj_speed_x
float64 obj_speed_y
float64 obj_speed_z
float64 obj_position_x
float64 obj_position_y
float64 obj_position_z

这可不就是我们定义好的接口文件的内容嘛,它现在已经在工作空间的接口库里了,后面我们就能直接调用了。
6、创建新的功能包

接下来就是对上一篇文章中的代码进行基于新接口的改造。

切换到src目录下,创建新的功能包。
cd src
ros2 pkg create aeb_control_pkg --build-type ament_python --dependencies rclpy

此时,src下的目录树如下:
src/
├── aeb_control_pkg
│   ├── aeb_control_pkg
│   │   └── __init__.py
│   ├── package.xml
│   ├── resource
│   │   └── aeb_control_pkg
│   ├── setup.cfg
│   ├── setup.py
│   └── test
│       ├── test_copyright.py
│       ├── test_flake8.py
│       └── test_pep257.py
└── my_custom_interfaces
    ├── CMakeLists.txt
    ├── msg
    │   └── MyCarlaInterface.msg
    └── package.xml

6 directories, 11 files
7、创建节点

我们来继续定义aeb_control_pkg这个功能包下的节点文件,scenario_runner_node.py和controller_node.py。
cd src/aeb_control_pkg/aeb_control_pkg
touch scenario_runner_node.py
touch controller_node.py

在scenario_runner_node.py中填入以下内容:
import traceback

import rclpy
from rclpy.node import Node
from my_custom_interfaces.msg import MyCarlaInterface
import time
import carla
import traceback

class PublisherNode(Node):

    def __init__(self):
        super().__init__('publisher_node')
        #
        self.carla_world = None
        self.connect_to_carla()
        self.ego_car = None
        self.obj_car = None
        self.ego_speed_x = None
        self.obj_speed_x = None
        self.ego_pos_x = None
        self.obj_pos_x = None
        self.actor_list = []
        self.spawn_actor_ego(position=(0, 28, 1))
        self.spawn_actor_obj(position=(50, 28, 1))
        self.set_ego_speed(x=5.5, y=0, z=0)

        self.logger = self.get_logger()

        #
        self.my_publisher = self.create_publisher(MyCarlaInterface, '/carla/info', 10)
        self.timer = self.create_timer(1, self.publish_vehicle_info)

    def publish_vehicle_info(self):
        self.get_carla_data()
        # print(self.ego_pos_x, self.ego_speed_x, self.obj_pos_x, self.obj_speed_x)
        if None not in (self.ego_pos_x, self.ego_speed_x , self.obj_pos_x, self.obj_speed_x):
            my_carla_info = MyCarlaInterface()
            my_carla_info.ego_id = self.ego_car.id
            my_carla_info.obj_id = self.obj_car.id
            my_carla_info.ego_position_x = self.ego_pos_x
            my_carla_info.ego_position_y = 2.0
            my_carla_info.ego_position_z = 0.0
            my_carla_info.obj_position_x = self.obj_pos_x
            my_carla_info.obj_position_y = 2.0
            my_carla_info.obj_position_z = 0.0
            my_carla_info.ego_speed_x = self.ego_speed_x
            my_carla_info.ego_speed_y = 0.0
            my_carla_info.ego_speed_z = 0.0
            my_carla_info.obj_speed_x = self.obj_speed_x
            my_carla_info.obj_speed_y = 0.0
            my_carla_info.obj_speed_z = 0.0
            # to pulish all message...
            self.my_publisher.publish(my_carla_info)

            self.logger.info('Publishing... ego:position_x={},speed_x={},obj:position_x={},speed_x={}'.format(
                round(my_carla_info.ego_position_x,2),
                round(my_carla_info.ego_speed_x,2),
                round(my_carla_info.obj_position_x,2),
                round(my_carla_info.obj_speed_x,2)))
        else:
            print("debug data:", self.ego_pos_x, self.ego_speed_x, self.obj_pos_x, self.obj_speed_x)

    def connect_to_carla(self):
        carla_client = carla.Client(host="192.168.2.6", port=2000)
        carla_client.set_timeout(10)  # second
        try:
            self.carla_world = carla_client.get_world()
        except RuntimeError as e:
            self.get_logger().error('can not connect to CARLA world.')
            raise e

    def spawn_actor_ego(self, position):
        blueprint_library = self.carla_world.get_blueprint_library()
        # for ego car
        my_bp = blueprint_library.filter("model3")[0]
        trans_loc = carla.Location(x=position[0], y=position[1], z=position[2])
        trans_rot = carla.Rotation(pitch=0, yaw=0, roll=0)
        spawn_point = carla.Transform(trans_loc, trans_rot)
        my_car = self.carla_world.spawn_actor(my_bp, spawn_point)
        # print("ego position:", my_car.get_location())
        self.ego_car = my_car
        self.actor_list.append(my_car)

    def spawn_actor_obj(self, position):
        obj_blueprint_library = self.carla_world.get_blueprint_library()
        # for ego car
        obj_my_bp = obj_blueprint_library.filter("mkz_2020")[0]
        obj_trans_loc = carla.Location(x=position[0], y=position[1], z=position[2])
        obj_trans_rot = carla.Rotation(pitch=0, yaw=0, roll=0)
        obj_spawn_point = carla.Transform(obj_trans_loc, obj_trans_rot)
        obj_my_car = self.carla_world.spawn_actor(obj_my_bp, obj_spawn_point)
        self.obj_car = obj_my_car
        self.actor_list.append(obj_my_car)

    def set_ego_speed(self, x, y, z):
        if self.ego_car:
            # print("ego_car:", self.ego_car.id)
            target_speed = carla.Vector3D(x, y, z)
            # print("target_speed:", target_speed)
            self.ego_car.set_target_velocity(target_speed)

    def destroy_actors(self):
        for actor in self.actor_list:
            actor.destroy()
            self.get_logger().info(f"destroy actor: {actor}")

    def get_carla_data(self):
        if self.ego_car:
            ego_loc = self.ego_car.get_location()
            self.ego_pos_x = ego_loc.x
            ego_spd = self.ego_car.get_velocity()
            self.ego_speed_x = ego_spd.x
            # print("current_speed:", ego_spd)
        if self.obj_car:
            obj_loc = self.obj_car.get_location()
            self.obj_pos_x = obj_loc.x
            obj_spd = self.obj_car.get_velocity()
            self.obj_speed_x = obj_spd.x
        # pass

def main():
    try:
        rclpy.init()
        publisher_node = PublisherNode()
        rclpy.spin(publisher_node)
    except:
        traceback.print_exc()
    finally:
        publisher_node.destroy_actors()
        publisher_node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

在controller_node.py中填入以下内容:
import rclpy
from rclpy.node import Node
from my_custom_interfaces.msg import MyCarlaInterface
import carla

class SubscriberNode(Node):

    def __init__(self):
        super().__init__('subscriber_node')
        #
        self.ttc = None
        self.ego_speed_x = None
        self.obj_speed_x = None
        self.ego_position_x = None
        self.obj_position_x = None
        self.logger = self.get_logger()
        self.my_subscription = self.create_subscription(MyCarlaInterface, '/carla/info', self.callback, 10)
        #
        self.carla_world = None
        self.connect_to_carla()
        self.ego_car_id = None

    def callback(self, msg):
        self.logger.info('Received... ego:position_x={},speed_x={},obj:position_x={},speed_x={}'.format(
                round(msg.ego_position_x,2),
                round(msg.ego_speed_x,2),
                round(msg.obj_position_x,2),
                round(msg.obj_speed_x,2)))
        self.ego_car_id = msg.ego_id
        self.ego_speed_x = msg.ego_speed_x
        self.ego_position_x = msg.ego_position_x
        self.obj_speed_x = msg.obj_speed_x
        self.obj_position_x = msg.obj_position_x
        #
        self.calculate_ttc()
        # brake ego
        if self.ttc is not None:
            if 0 < self.ttc < 3:
                if self.ego_car_id is not None:
                    ego_car = self.carla_world.get_actor(self.ego_car_id)
                    brake_control = carla.VehicleControl(brake=1, steer=0, throttle=0)
                    ego_car.apply_control(brake_control)
                    self.logger.info('send braking order to ego car')

    def calculate_ttc(self):
        if None not in (self.ego_speed_x, self.obj_speed_x, self.ego_position_x, self.obj_position_x):
            distance = (self.obj_position_x - self.ego_position_x)
            speed_relative = self.ego_speed_x - self.obj_speed_x
            if speed_relative != 0:
                self.ttc = distance / speed_relative
                # print("ttc:", self.ttc)
                self.logger.info('ttc: {}'.format(self.ttc))
            else:
                self.ttc = 999
        else:
            self.logger.info('ttc: can not be calculated')
            # print(self.ego_speed_x)
            # print(self.obj_speed_x)
            # print(self.ego_position_x)
            # print(self.obj_position_x)
    def connect_to_carla(self):
        carla_client = carla.Client(host="192.168.2.6", port=2000)
        carla_client.set_timeout(10)  # second
        try:
            self.carla_world = carla_client.get_world()
        except RuntimeError as e:
            self.get_logger().error('can not connect to CARLA world.')
            raise e

def main():
    rclpy.init()
    subscriber_node = SubscriberNode()
    rclpy.spin(subscriber_node)
    subscriber_node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

8、配置节点

节点文件编写好之后又到了配置阶段了,我们打开setup.py脚本,将两个节点配置进去。打开配置文件'src/aeb_control_pkg/setup.py',修改’console_scripts‘的内容:加入本次的两个节点,完成后的setup.py完整内容如下:
from setuptools import setup

package_name = 'aeb_control_pkg'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='goodman',
    maintainer_email='goodman@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            "scenario_runner_node = aeb_control_pkg.scenario_runner_node:main",
            "controller_node = aeb_control_pkg.controller_node:main"
        ],
    },
)

到这里,源码内容就已经准备好了。

最后再来看看src下的文件目录:
src/
├── aeb_control_pkg
│   ├── aeb_control_pkg
│   │   ├── controller_node.py
│   │   ├── __init__.py
│   │   └── scenario_runner_node.py
│   ├── package.xml
│   ├── resource
│   │   └── aeb_control_pkg
│   ├── setup.cfg
│   ├── setup.py
│   └── test
│       ├── test_copyright.py
│       ├── test_flake8.py
│       └── test_pep257.py
└── my_custom_interfaces
    ├── CMakeLists.txt
    ├── msg
    │   └── MyCarlaInterface.msg
    └── package.xml

6 directories, 13 files
9、编译和试运行

回到工作空间下,编译功能包:
cd ~/ROS2_study/ros2_ws_demo006_custom_interface/
colcon build

编译过程很快,我这里终端打印的消息长这样:
Starting >>> aeb_control_pkg
Starting >>> my_custom_interfaces            
Finished <<< my_custom_interfaces [2.17s]                                                         
Finished <<< aeb_control_pkg [3.52s]         

Summary: 2 packages finished [6.47s]

这样就表示编译完成啦。

接下来就是测试了。

由于两个ROS 2节点都是CARLA的客户端程序,在运行客户端程序前,我们应该先把CARLA的服务端打开。这样客户端后面才能连接连接到服务端。我这边的CARLA的服务端启动程序如下:
cd path/to/your/carla
./CarlaUE4.sh

然后等CARLA服务端完全运行起来后。我们新打开一个终端,输入节点1启动指令:
source install/setup.bash
ros2 run aeb_control_pkg scenario_runner_node

此时,我们应该能从CARLA的服务端看到两辆车被创建了出来,并且主车逐渐靠近前方的目标车。如果没有下方的节点2启动,则主车会与目标车发生碰撞。

然后在新打开一个终端,赶在碰撞前,把AEB控制节点运行起来:
source install/setup.bash
ros2 run aeb_control_pkg  controller_node

然后我们就能看到主车在撞到目标车前就制动停了下来。



屏幕截图
节点1打印的消息如下:

ROS2实战,自定义接口,让代码更优雅w3.jpg

屏幕截图

节点2打印的消息如下:

ROS2实战,自定义接口,让代码更优雅w4.jpg

屏幕截图10、总结

本节我们通过对车辆之间通信消息的自定义,把所有消息用一个接口文件包装了起来,这样在发布和订阅过程中都更加的优雅,逻辑更加清晰,代码简化了不少。然后基于新的接口文件重新优化了上一节的发布和订阅程序,实现了AEB功能的控制。完结,撒花。

ROS 2中比较多使用这种自定义接口的方法,但是采用protobuf对数据进行发送能更轻量级、也能更定制化,但是我一直没有成功。不知道哪位道友有知道做法的可以一起探讨下。

快速发帖

您需要登录后才可以回帖 登录 | 注册

本版积分规则

QQ|手机版|小黑屋|Archiver|汽车工程师之家 ( 渝ICP备18012993号-1 )

GMT+8, 20-11-2024 17:19 , Processed in 0.314928 second(s), 30 queries .

Powered by Discuz! X3.5

© 2001-2013 Comsenz Inc.