Autoware.universe部署04:universe传感器ROS2驱动


本文介绍了 Autoware.universe 各个传感器ROS2驱动,本系列其他文章:
Autoware.universe部署01:Ubuntu20.04安装Autoware.universe并与Awsim联调
Autoware.universe部署02:高精Lanelet2地图的绘制
Autoware.universe部署03:与Carla(二进制版)联调

一、激光雷达驱动

以速腾32线激光雷达为例:
(1) 建立工作空间,下载两个功能包:

mkdir -p ~/rs_driver/src
cd ~/rs_driver/

(2)速腾聚创雷达ROS1,ROS2代码都是同一个链接,所以将ununtu18.04里面用的驱动拿了过来,然后打开senser_driver/src/rslidar_sdk/CmakeLists.txt,选择CLOCON编译方式
在这里插入图片描述
(3)修改参数配置
主要是修改为你的lidar类型,坐标系以及点云话题:
在这里插入图片描述

(4)将下载的两个功能包一起放到src下,将原来的package.xml文件重命名为package.xml.bak备份,把package_ros2.xml文件重命名为package.xml,然后编译:

#编译
cd ~/rs_driver/
colcon build

(5)设备连接与网络配置

  • 准备一套速腾聚创16线激光雷达(包括激光雷达、电源盒子、航插头以及网线);
  • 本文使用的PC系统是Ubuntu 18.04系统,也可使用Ubuntu 16.04或Ubuntu 20.04;
  • 准备AC 220V电源或DC 12V电源;

如下图所示,将雷达一端的航插头接口与雷达电源盒子的航插头接口两个,对准红点接好;
在这里插入图片描述
电源盒子接上电源,接上网线(网线一端接入到PC,一端接入到电源盒子)如下图:
在这里插入图片描述
雷达点云盒子连接雷达、点云以及网线,网线另一端连接计算机(或者通过交换机转接),设置网络为静态IP,IP地址:192.168.1.102,子网掩码:255.255.255.0
在这里插入图片描述
(6)驱动雷达

source install/setup.bash
ros2 launch rslidar_sdk start.py

在打开的rviz2中,Frame坐标系改成velodyne,点云话题选择/points_raw,可以成功显示雷达点云
在这里插入图片描述

二、IMU驱动

本文设备为亚伯智能十轴IMU惯导模块

2.1 上位机配置

(1) 安装串口驱动
打开WIndows,在配套的资料中找到CP2102_USB驱动.zip文件,下载到本地电脑并解压,双击CP210xVCPInstaller_x64.exe文件打开安装程序,然后跟着提示一直点击下一步,直到完成即可。
(2)连接上位机
解压资料中的IMU标准上位机(V6.2.60).zip压缩包,进入上位机软件找到MiniIMU.exe。双击打开上位机软件,可以看到提示未能搜索到设备,关闭提示框。如果已经连接IMU模块会自动连接上模块。

将IMU模块通过USB-TypeC数据线连接到电脑上。然后点击然后点击菜单栏的‘自动监测设备’。连接成功,可以看到角度X、角度Y、角度Z有数据变化。如果连接不成功,请确认是否已经安装好串口驱动,或者换个USB口试试。
在这里插入图片描述

IMU模块出厂已经烧录好固件,连接上位机后可以用上位机查看IMU模块的当前姿态。
点击’三维‘,可以看到弹出一个窗口,默认会展示一台汽车模型,当我们改变IMU模块的姿态时,模型的姿态会跟着IMU模块的变化。Y轴为车头,IMU模块也应Y轴向前放置。
在这里插入图片描述
(3)参数配置
点击菜单栏上的‘配置’,会弹出一个窗口,查看右下角的状态,一定要是‘在线’才是正确的,如果出现‘离 线’则说明没连接上IMU模块。
通讯速率:串口通讯速率,默认9600,可选择其他波特率(4800~921600)。
回传速率:串口回传数据的速率,默认为10Hz,可修改为0.2Hz~200Hz。10HZ指的是1S回传10个数据包,按默认回传1个数据包是11个字节。
:如果需要200HZ的回传速率,则只能勾选三个量,比如“加速度”,“角速度”,“角度”。
:如果回传内容较多,同时通信的波特率又较低的情况下,可能没法传输这么多数据,此时模块会自动降频,并以允许的最大输出速率进行输出。简单点说 就是回传速率高的话,波特率也要设置高一点,一般用115200。这里我们波特率改为480600,回传速率改为100Hz
在这里插入图片描述

设置串口输出的内容,串口输出内容可查看协议文件解析数据。
注意:勾选上“GPS原始”之后模块只输出GPS原始的信息了,其它数据都不会输出。

4.2 IMU校准

(1)加速度计校准
将IMU模块平放在桌子或者其他设备上,如果发现‘角度X‘和’角度Y‘大于1°,那么需要进行加速度计校准。点击菜单栏中的’配置‘打开配置界面,保证IMU模块平放的情况下,点击’加速度‘,然后再点击’设置角度参考‘。
在这里插入图片描述
校准之后可以看到‘角度X‘和’角度Y‘接近于0°
在这里插入图片描述

(2)IMU模块上电后,打开上位机显示3D模型,转动模块Z轴航向角,3D模型抖动,或者反应迟钝,请在上
位机进行磁力校准设备。
点击配置界面中的‘磁场’,会弹出校准磁场的界面。磁场校准有多种校准方式,比较常规的校准方式为球形拟合法。
在这里插入图片描述

分别校准X/Y/Z轴,看chartYZ/chartXZ/chartXY变化。将IMU模块水平放置。然后缓慢绕X/Y/Z轴旋转360°以
上,chart界面蓝色数据分布在绿线旁为正常。为了数据更加准确,可多转几圈。
在这里插入图片描述

XYZ三轴都校准完成后点击‘结束校准’。注意,在校准Y轴时,只看chartXZ的数据就好,其他两个视图也有数据,不需要关心。同理其他两个轴也是一样。
(3)陀螺仪校准
陀螺仪默认开启自动校准功能,不需要额外设置。保持开启陀螺仪自动校准功能即可。
在这里插入图片描述

4.3 安装ROS驱动

(1)安装依赖:

pip3 install pyserial

(2)在配套资料中找到ROS2驱动包wit_ros2_imu,放入工作空间编译,遇到以下警告:
在这里插入图片描述

根据提示将setup.cfg的横线改为下划线:
在这里插入图片描述

(3)绑定端口
为了防止多个usb设备同时插入的时候,系统识别错误,我们给该模块的串口名字绑定成/dev/imu_usb,终端输入

cd src/wit_ros_imu
sudo chmod 777 bind_usb.sh
sudo sh bind_usb.sh

重新插拔连接IMU模块的USB数据线。以生效绑定的端口,输入以下指令检测绑定端口是否成功,

ll /dev/imu_usb

在这里插入图片描述
不一定是ttyUSB0,只要显示是USB设备就行了。我们下面安装的GNSS模块也有一个串口,这里可以将其也给绑定。首先通过插拔两个端口,我们可以lsusb查看端口信息:
在这里插入图片描述
其中GNSS的为:

0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC

IMU模块的为:

10c4:ea60 Silicon Labs CP210x UART Bridge

然后创建.rules文件(或者直接在上面IMU的文件中修改),填写以下内容

KERNEL=="ttyUSB*", ATTRS{
    
    idVendor}=="10c4", ATTRS{
    
    idProduct}=="ea60", MODE:="0777", SYMLINK+="imu_usb"
KERNEL=="ttyUSB*", ATTRS{
    
    idVendor}=="0403", ATTRS{
    
    idProduct}=="6001", MODE:="0777", SYMLINK+="gnss"

在这里插入图片描述

然后:

sudo cp imu_usb.rules /etc/udev/rules.d
service udev reload
service udev restart

输入以下指令检测绑定端口是否成功,

ll /dev/imu_usb
ll /dev/gnss

在这里插入图片描述

记得把GNSS端口改成我们绑定的
(4)修改波特率
程序默认是使用9600的波特率,我们在上位机修改了波特率460800,那么则需要修改源码中的波特率,源码修改波特率的位置是,wit_ros2_imu/wit_ros2_imu/wit_ros2_imu.py

#149行
def driver_loop(self, port_name):
# 打开串口
    try:
        wt_imu = serial.Serial(port="/dev/imu_usb", baudrate=460800, timeout=0.5)

把9600改成上位机上修改的波特率,然后保存后退出,最后回到工作空间目录下进行编译即可。
(5)运行测试

source install/setup.bash
ros2 launch wit_ros2_imu rviz_and_imu.launch.py

报下面的错误,是因为launch语法的问题,可能是官方使用的ROS版本比较老
在这里插入图片描述

修改launch,并重新编译:
在这里插入图片描述
再次运行
在这里插入图片描述

查看IMU话题:
ros2 topic echo /imu/data_raw
在这里插入图片描述

三、CAN驱动

接收autoware.universe的控制话题,并下发到底盘控制实车运动,同时接收底盘反馈的车的速度信息,发送给Autoware进行位姿初始化,编写了ROS2版本的控制底盘程序(CAN协议不同不能通用,只能作为参考):

# -*- coding: utf-8 -*-
import math
import time
import socket
import cantools
import threading
import rclpy
from rclpy.node import Node
from builtin_interfaces.msg import Time
from binascii import hexlify
from geometry_msgs.msg import TwistStamped, Twist
from autoware_auto_control_msgs.msg import AckermannControlCommand
from autoware_auto_vehicle_msgs.msg import ControlModeReport, GearReport, SteeringReport, VelocityReport

# 弧度转角度系数
radian2angle = 57.29577951308232

# 创建socket套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # AF_INET表示使用ipv4,默认不变,SOCK_DGRAM表示使用UDP通信协议

# 绑定端口port
local_addr = ("192.168.1.102", 8882)  # 本地ip,端口号
udp_socket.bind(local_addr)  # 绑定端口

# 指定要接收的前五个字节的CAN协议数据
EXPECTED_DATA = bytes([0x08, 0x00, 0x00, 0x01, 0x16])

# 档位+车速0指令:08 00 00 00 E2 01 00 00 00 00 00 00 00
drive_by_wire_command = bytes([0x08, 0x00, 0x00, 0x00, 0xE2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# 控制方向盘转到100度,转速100
test1 = bytes([0x08, 0x00, 0x00, 0x04, 0x69, 0x20, 0x00, 0x00, 0x04, 0x64, 0x00, 0x64, 0x24])
# 控制方向盘转到0度,转速50
test2 = bytes([0x08, 0x00, 0x00, 0x04, 0x69, 0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x32, 0x16])

data_EV1 = {
    
    'Gear': 0, 'Invalid': 0, 'EV_Speed_L': 0, 'EV_Speed_H' : 0, 
            'Stay0': 0, 'Stay1': 0, 'Stay2': 0, 'Stay3': 0, 'Stay4': 0}

data_EPS2 = {
    
    'Work_mode': 32, 'Stay0': 0, 'Stay1': 0, 'Steer_Angle_H':0, 
             'Steer_Angle_L':0, 'Angle_alignment': 0, 'Angular_velocity': 100}

message_EV1_front = bytes([0x08, 0x00, 0x00, 0x00, 0xE2]) # EV1的帧信息与帧ID
message_EPS2_front = bytes([0x08, 0x00, 0x00, 0x04, 0x69]) # EPS2的帧信息与帧ID

# 计算异或校验值
def Calculate_XOR_value(dict_data):
    # 提取所有值
    values = list(dict_data.values())

    # 计算异或校验码
    result = values[0]
    for value in values[1:]:
        result ^= value

    # 返回
    return result

def calculate_speed(linear_speed):
    EV_Speed_H = int((linear_speed * 185)) // 256
    EV_Speed_L = int((linear_speed * 185)) % 256
    # print('EV_Speed_H:%f' % EV_Speed_H)
    # print('EV_Speed_L:%f' % EV_Speed_L)
    data_EV1['EV_Speed_L'] = EV_Speed_L
    data_EV1['EV_Speed_H'] = EV_Speed_H

def calculate_angle(linear_speed, angular_speed):
    # 转弯的角度 = arctan(( 角速度 / 线速度 ) * 车长 ) 
    Steer_Angle = -math.atan((angular_speed/linear_speed)*1) * radian2angle * 4.5
    print('Steer_Angle:%f' % Steer_Angle)

    # 实际测试下来,方向盘转动的角度范围是-27.5~27.5,对应的取值范围是924~1124  27.5*3.6=99
    Steer_Angle_H = int((Steer_Angle * 3.6) + 1024) // 256
    Steer_Angle_L = int((Steer_Angle * 3.6) + 1024) % 256
    # print('Steer_Angle_H:%f' % Steer_Angle_H)
    # print('Steer_Angle_L:%f' % Steer_Angle_L)
    data_EPS2['Steer_Angle_H'] = Steer_Angle_H
    data_EPS2['Steer_Angle_L'] = Steer_Angle_L

def calculate_angle(angular_rad):
    Steer_Angle = -angular_rad * radian2angle
    print("Steer_Angle:", Steer_Angle)
    # 实际测试下来,方向盘转动的角度范围是-27.5~27.5,对应的取值范围是924~1124  27.5*3.6=99
    Steer_Angle_H = int((Steer_Angle * 3.6) + 1024) // 256
    Steer_Angle_L = int((Steer_Angle * 3.6) + 1024) % 256
    # print('Steer_Angle_H:%f' % Steer_Angle_H)
    # print('Steer_Angle_L:%f' % Steer_Angle_L)
    data_EPS2['Steer_Angle_H'] = Steer_Angle_H
    data_EPS2['Steer_Angle_L'] = Steer_Angle_L

# udp向底盘发送can协议
def udp_send_can(message_name):
    udp_socket.sendto(message_name, ("192.168.1.10", 6666))

# 处理接收到的CAN消息的函数
def process_can_message(data,node):
    # 解码CAN消息
    can_data = list(data[5:])  # 从第6个字节开始是CAN数据
    message = node.db.decode_message("VCU", can_data)

    # 打印解码结果
    # print(message)
    # print('MP_AP:', message['MP_AP'])
    # print('Gear:', message['Gear'])
    # print('Driver_Break:', message['Driver_Break'])
    # print('Steer_Angle_L:', message['Steer_Angle_L'])
    # print('Steer_Angle_H:', message['Steer_Angle_H'])
    # print('DM_Speed_L:', message['DM_Speed_L'])
    # print('DM_Speed_H:', message['DM_Speed_H'])

    # 处理CAN中接收到的数据,转化成线速度和角速度
    feedback_twist_linear_x = (message['DM_Speed_H'] * 256 + message['DM_Speed_L']) / 185

    Steer_Angle = (message['Steer_Angle_H'] * 256 + message['Steer_Angle_L'] - 1024) / 3.6
    # 角速度 = tan(转向角度) * 线速度 / 前后轮轴距
    feedback_twist_angular_z = math.tan(Steer_Angle / radian2angle) * feedback_twist_linear_x / 1

    if (message['Gear'] == 1):
        feedback_twist_linear_x = feedback_twist_linear_x
    elif (message['Gear'] == 2):
        feedback_twist_linear_x = -feedback_twist_linear_x
    else:
        feedback_twist_linear_x = 0.0

    # 发布处理后的Twist消息到另一个话题
    node.publish_data(feedback_twist_linear_x, feedback_twist_angular_z)
    node.pubilsh_control_mode(1)
    node.pubilsh_gear(2)
    node.pubilsh_steering(-Steer_Angle / radian2angle)
    node.pubilsh_velocity("base_link", feedback_twist_linear_x, 0.0, 0.0)

# 接收数据的线程函数
def receive_data(node):
    while rclpy.ok():
        data, addr = udp_socket.recvfrom(13)
        # print(hexlify(data).decode('ascii'))

        # 确保接收到的数据满足预期的CAN数据
        if data[:5] == EXPECTED_DATA:
            # 在新的线程中处理CAN消息,以保证实时性
            threading.Thread(target=process_can_message, args=(data,node)).start()


class TopicSubscriberPublisher(Node):
    def __init__(self):
        super().__init__('cmd_vel_to_can')
        # 加载dbc文件
        self.declare_parameter("dbc")
        dbcfile = self.get_parameter("dbc").get_parameter_value().string_value
        self.db = cantools.database.load_file(dbcfile)

        self.subscriber = self.create_subscription(AckermannControlCommand, 'control/command/control_cmd', self.sub_callback, 10)
        # self.publisher = self.create_publisher(Twist, 'twist_cmd_feedback', self.pub_callback, 10)
        self.publisher_data = self.create_publisher(Twist, 'twist_cmd_feedback', 10)
        self.publisher_control_mode = self.create_publisher(ControlModeReport, '/vehicle/status/control_mode', 10)
        self.publisher_gear = self.create_publisher(GearReport, '/vehicle/status/gear_status', 10)
        self.publisher_steering = self.create_publisher(SteeringReport, '/vehicle/status/steering_status', 10)
        self.publisher_velocity = self.create_publisher(VelocityReport, '/vehicle/status/velocity_status', 10)
        # self.get_logger().info('TopicSubscriberPublisher node initialized')

    def sub_callback(self, msg):
        # 1. 发送档位+车速0指令:08 00 00 00 E2 01 00 00 00 00 00 00 00
        udp_send_can(drive_by_wire_command)

        # 2. 接收autoware传来的线速度和角速度
        EV_Speed = msg.longitudinal.speed
        # angular_velocity = msg.lateral.steering_tire_rotation_rate
        angular_rad = msg.lateral.steering_tire_angle
        # print('EV_Speed:%f' % EV_Speed)
        # print('angular_velocity:%f' % angular_velocity)
        print('angular_rad:%f' % angular_rad)

        # 3. 计算档位、速度、角度
        if (EV_Speed > 0):
            data_EV1['Gear'] = 1
            calculate_speed(EV_Speed)
            # calculate_angle(EV_Speed, angular_velocity)
            calculate_angle(angular_rad)

        elif (EV_Speed < 0):
            data_EV1['Gear'] = 2
            calculate_speed(-EV_Speed)
            # calculate_angle(-EV_Speed, angular_velocity)
            calculate_angle(angular_rad)

        else:
            data_EV1['Gear'] = 0
            calculate_speed(0)
            # calculate_angle(1, angular_velocity)
            calculate_angle(angular_rad)

        # 4. 发送can消息
        message_EV1 = self.db.encode_message("EV1", data_EV1)
        message_linear_velocity = message_EV1_front + message_EV1
        # print(hexlify(message_linear_velocity).decode('ascii'))
        udp_send_can(message_linear_velocity)

        # 计算异或校验码
        Check = Calculate_XOR_value(data_EPS2)
        data_EPS2.update({
    
    'Check' : Check})
        message_EPS2 = self.db.encode_message("EPS2", data_EPS2)
        message_angle = message_EPS2_front + message_EPS2
        # print(hexlify(message_angle).decode('ascii'))
        udp_send_can(message_angle)

    def publish_data(self, data1, data2):
        msg = Twist()
        msg.linear.x = data1
        msg.angular.z = data2
        self.publisher_data.publish(msg)
    
    def pubilsh_control_mode(self, data):
        msg = ControlModeReport()
        msg.mode = data
        self.publisher_control_mode.publish(msg)

    def pubilsh_gear(self, data):
        msg = GearReport()
        msg.report = data
        self.publisher_gear.publish(msg)

    def pubilsh_steering(self, data):
        msg = SteeringReport()
        msg.steering_tire_angle = data
        self.publisher_steering.publish(msg)

    def pubilsh_velocity(self, data1, data2, data3, data4):
        msg = VelocityReport()
        # 获取当前时间
        # 秒
        sec_ = int(time.time())
        # 纳秒
        nanosec_ = int((time.time()-sec_)*1e9)
        msg.header.stamp = Time(sec = sec_,nanosec = nanosec_)
        msg.header.frame_id = data1
        msg.longitudinal_velocity = data2
        msg.lateral_velocity = data3
        msg.heading_rate = data4
        self.publisher_velocity.publish(msg)

def main():
    # 初始化
    rclpy.init()

    # 新建一个节点
    node = TopicSubscriberPublisher()

    # 启动接收CAN数据的线程
    threading.Thread(target=receive_data, args=(node,)).start()

    # 保持节点运行,检测是否收到退出指令(Ctrl+C)
    rclpy.spin(node)

    # 清理并关闭ros2节点
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

编写setup.py和launch文件

from setuptools import setup

package_name = 'can_ros2_bridge'

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']),
        ('share/' +package_name, ['launch/can.launch.py']),
        ('share/' +package_name, ['config/CarCAN.dbc']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='car',
    maintainer_email='[email protected]',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    # 可执行文件
    entry_points={
    
    
        'console_scripts': [
            'cmd_vel_to_can = can_ros2_bridge.send_message:main',
        ],
    },
)
from launch import LaunchDescription
from launch_ros.actions import Node
import os
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    config = os.path.join(
      get_package_share_directory('can_ros2_bridge'),
      'CarCAN.dbc'
      )
    can_ros2_bridge = Node(
        package='can_ros2_bridge',
        executable='cmd_vel_to_can',
        name='can',
        parameters=[{
    
    'dbc': config},],
        output="both"
    )

    return LaunchDescription(
        [
            can_ros2_bridge,
        ]
    )

修改静态IP(注意关掉WIFI),启动CAN,能成功控制底盘

四、相机驱动

4.1 安装驱动

本文使用的相机没有官方驱动,采用的相机驱动源码地址:https://github.com/ros-drivers/usb_cam/tree/ros2,将代码下载下来放到工作空间src编译运行:

colcon build
source install/setup.bash
ros2 run usb_cam usb_cam_node_exe --ros-args --params-file src/usb_cam-ros2/config/params_1.yaml
# 或者
ros2 launch usb_cam camera.launch.py #但是这个运行可能有一些问题,我们在下一节重新写一个

再打开一个节点,显示图像:

ros2 run usb_cam show_image.py

如果是外置的相机,此时大概率无法驱动,因为相机默认挂载点是/dev/video0(但它一般是电脑自带的摄像头),查看相机挂载地址:

ls /dev/

在这里插入图片描述
可以通过插拔相机判断挂载地址,我们是/dev/video2,在参数文件中修改video_device为/dev/video2,再次驱动即可看到外置相机的图像
在这里插入图片描述
然后再次运行,可以驱动相机了,相机话题为/image_raw

4.2 修改相机参数

上面简单的运行实际上可能无法适配你的相机(可以驱动但是效果很差),我们需要修改参数,新建一个参数文件(例如config/JR_HF868.yaml),内容如下:

/**:
    ros__parameters:
      # 设备挂载点
      video_device: "/dev/video2"
      # 帧率
      framerate: 30.0
      io_method: "mmap"
      # 坐标系
      frame_id: "camera"
      # 像素格式
      pixel_format: "mjpeg2rgb"  # see usb_cam/supported_formats for list of supported formats
      # 像素尺寸
      image_width: 1920
      image_height: 1080
      # 相机名称
      camera_name: "JR_HF868"
      # 标定参数
      camera_info_url: "package://usb_cam/config/camera_info.yaml"
      # 亮度
      brightness: 50
      # 对比度
      contrast: 50
      # 饱和度
      saturation: 50
      # 清晰度
      sharpness: 80
      # 增益
      gain: -1
      # 白平衡
      auto_white_balance: true
      white_balance: 4000
      # 曝光
      autoexposure: true
      exposure: 100
      # 聚焦
      autofocus: false
      focus: -1

其中有几点需要注意的:
(1)将相机分辨率修改为1920*1080(或者你的相机支持的);
(2)将设备挂载点改成/dev/video2(或者自己查到的);
(3)默认的pixel_format,当相机运动过快,图片的运动畸变比较大,发现在运行相机节点的时候,会打印出相机支持的一些参数:
在这里插入图片描述

我们的相机在YUYV 4:2:2: 1920 x 1080这个参数下只支持6 Hz的帧率,相机在Motion-JPEG: 1920 x 1080这个参数下支持30 Hz的帧率,查找senser_driver/src/usb_cam-ros2/include/usb_cam/usb_cam.hpp文件,可以找到驱动支持的像素格式,有如下几种
在这里插入图片描述

修改pixel_format参数,改成mjpeg2rgb
(4)修改亮度,对比度,饱和度等参数
新写一个启动文件(launch/JR_HF868.launch.py):

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
 
def generate_launch_description():
   config = os.path.join(
      get_package_share_directory('usb_cam'),
      'config',
      'JR_HF868.yaml'
      )
   return LaunchDescription([
      Node(
         package='usb_cam',
         executable='usb_cam_node_exe',
         name='usb_cam_node_exe',
         parameters=[config]
      ),
   ])

然后再重新编译,运行节点,现在相机的图像像素比较高,而且快速运动的时候畸变也小:

source install/setup.bash
ros2 launch usb_cam JR_HF868.launch.py

五、GNSS驱动

GNSS是可选的,这里使用的是华测M620RTK模块驱动。
由于ROS2没有再封装串口库serial,因此需要手动安装serial:

git clone https://github.com/ZhaoXiangBox/serial
cd serial && mkdir build
cmake .. && make
sudo make install

Cmake配置:

set(CMAKE_INSTALL_RPATH /usr/local/lib)
find_package(serial REQUIRED)
ament_target_dependencies(exe "serial")

接下来编写串口通信,读取GNSS数据(根据CHCCGI610的ROS1代码修改而来)

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "sensor_msgs/msg/nav_sat_fix.hpp"
#include <serial/serial.h>

// $GPGGA
// 例:$GPGGA,073217.00,3109.93434012,N,12117.26502692,E,4,48,0.59,25.441,M,11.090,M,2.8,0000*4F
// 字段0:$GPGGA,语句ID,表明该语句为Global Positioning System Fix Data(GGA)GPS定位信息
// 字段1:UTC 时间,hhmmss.sss,时分秒格式
// 字段2:纬度ddmm.mmmm,度分格式(前导位数不足则补0)
// 字段3:纬度N(北纬)或S(南纬)
// 字段4:经度dddmm.mmmm,度分格式(前导位数不足则补0)
// 字段5:经度E(东经)或W(西经)
// 字段6:GPS状态,0=未定位,1=单点定位,2=伪距/SBAS,3=无效PPS,4=RTK固定,5=RTK浮动
// 字段7:正在使用的卫星数量
// 字段8:HDOP水平精度因子(0.5 - 99.9)
// 字段9:海拔高度(-9999.9 — +99999.9)
// 字段10:M为单位米
// 字段11:地球椭球面相对大地水准面的高度
// 字段12:M为单位米
// 字段13:差分时间(从最近一次接收到差分信号开始的秒数,如果不是差分定位将为空)
// 字段14:差分站ID号0000 - 1023(前导位数不足则补0,如果不是差分定位将为空)*校验值

class GNSSPublisher : public rclcpp::Node
{
    
    
public:
    GNSSPublisher(const char *nodeName) : Node(nodeName),
                                          StateParser(0), CntByte(0), CntDelimiter(0), tmpint(0)
    {
    
    
        port_name = this->declare_parameter("~port_name", "/dev/ttyUSB0");
        PosDelimiter[15] = {
    
    0};
        temp_field[30] = {
    
    0};
        gnss_pub_ = this->create_publisher<sensor_msgs::msg::NavSatFix>("/sensing/gnss/ublox/nav_sat_fix", 10);

        try
        {
    
    
            // 设置串口属性,并打开串口
            ser.setPort(port_name);
            ser.setBaudrate(460800);
            serial::Timeout to = serial::Timeout::simpleTimeout(1000); // 超时定义,单位:ms
            ser.setTimeout(to);
            ser.open();
        }
        catch (serial::IOException &e)
        {
    
    
            std::cout << port_name + " open failed, please check the permission of port ,run command \"sudo chmod 777 " + port_name + "\" and try again!" << std::endl;
            getchar();
            rclcpp::shutdown();
        }

        // 检测串口是否已经打开,并给出提示信息
        if (ser.isOpen())
        {
    
    
            std::cout << port_name + " open successed!" << std::endl;
        }
        else
        {
    
    
            std::cout << port_name + " open failed!" << std::endl;
            getchar();
            rclcpp::shutdown();
        }

        rclcpp::Rate loop_rate(100);           // 设置循环频率为100Hz
        ser.flushInput();                      // 在开始正式接收数据前先清除串口的接收缓冲区
        memset(OneFrame, 0, sizeof(OneFrame)); // 清空gps字符串
        int framecnt = 0;
        CntByte = 0; // 指向OneFrame的第一个位置

        while (rclcpp::ok())
        {
    
    
            int i, j;
            int start; // 当前位置
            int pos;   // 下一个分隔符的位置
            int numinbuf;
            int numgetted;
            auto gnss_msg = std::make_unique<sensor_msgs::msg::NavSatFix>();

            try
            {
    
    
                numinbuf = ser.available(); // available()返回从串口缓冲区读回的字符数
                                            // std::cout<<"串口缓冲区的数据有"<<numinbuf<<"个"<<std::endl;
                                            // initrd.img.oldCLEAR();
                                            // printf("bytes in buf = %d\n",numinbuf);
            }
            catch (serial::IOException &e)
            {
    
    
                std::cout << "Port crashed! Please check cable!" << std::endl;
                getchar();
                rclcpp::shutdown();
            }

            if (numinbuf > 0) // 串口缓冲区有数据
            {
    
    
                numgetted = ser.read(rbuf, numinbuf); // 串口缓冲区数据读到rbuf中
                if (numgetted == numinbuf)            // 取回的数据个数与缓冲区中有的数据个数相同,说明读串口成功
                {
    
    
                    for (int i = 0; i < numgetted; i++) // 对收到的字符逐个处理
                    {
    
    
                        // 在一帧数据的接收过程中,只要遇到非$GPCHC帧头就重新开始
                        // 此处理具有最高优先级,会重置状态机
                        if (rbuf[i] == '$' && rbuf[i + 3] != 'G' && rbuf[i + 4] != 'G' && rbuf[i + 5] != 'A')
                        {
    
    
                            memset(OneFrame, 0, sizeof(OneFrame));
                            StateParser = 0;
                            break; // 中断循环
                        }
                        // 正常处理过程
                        switch (StateParser)
                        {
    
    
                        // 等待语句开始标志'$'
                        case 0:
                            if (rbuf[i] == '$' && rbuf[i + 3] == 'G' && rbuf[i + 4] == 'G' && rbuf[i + 5] == 'A') // 收到语句开始标志
                            {
    
    
                                memset(OneFrame, 0, sizeof(OneFrame));
                                OneFrame[0] = '$';
                                CntByte = 1; // 开始对帧长度的计数
                                StateParser = 1;
                            }
                            break;

                        // 寻找帧头"$GPGGA,"
                        case 1:
                            OneFrame[CntByte] = rbuf[i];
                            CntByte++; // 指向下一个空位

                            if (rbuf[i] == ',')
                            {
    
    
                                if (strncmp(OneFrame, "$GPGGA,", 7) == 0)
                                {
    
    
                                    CntDelimiter = 0;              // 分隔符计数从0开始
                                    PosDelimiter[0] = CntByte - 1; // 记录分隔符在OneFrame中的位置
                                    // std::cout<<"PosDelimiter[0]"<<PosDelimiter[0]<<std::endl;
                                    StateParser = 2;
                                    // std::cout<<"寻找帧头$GPGGA完成"<<std::endl;
                                }
                                else // 帧头错误
                                {
    
    
                                    memset(OneFrame, 0, sizeof(OneFrame));
                                    StateParser = 0;
                                    // std::cout<<"寻找帧头$GPGGA失败"<<std::endl;
                                }
                            }
                            break;

                        // 接收各数据域
                        case 2:
                            // std::cout<<"开始接受各个数据域"<<std::endl;
                            OneFrame[CntByte] = rbuf[i];
                            // std::cout<<"接受字符"<<rbuf[i]<<std::endl;
                            CntByte++; // 指向下一个空位

                            if (rbuf[i] == ',' || rbuf[i] == '*')
                            {
    
    
                                CntDelimiter++; // 分隔符计数
                                // std::cout<<"分隔符个数:"<<CntDelimiter<<std::endl;
                                PosDelimiter[CntDelimiter] = CntByte - 1; // 记下分隔符位置
                                // std::cout<<"PosDelimiter["<<CntDelimiter<<"]"<<PosDelimiter[CntDelimiter]<<std::endl;
                                field_len[CntDelimiter - 1] = PosDelimiter[CntDelimiter] - PosDelimiter[CntDelimiter - 1] - 1;
                                // std::cout<<"第"<<CntDelimiter<<"段数据长"<<field_len[CntDelimiter]<<std::endl;
                                if (CntDelimiter == 14) // 0-14,共15个分隔符,开始数据解析
                                {
    
    
                                    // 计算出每个字段的长度
                                    for (int j = 0; j <= 13; j++) // 0-13,22个字段
                                    {
    
    
                                        field_len[j] = PosDelimiter[j + 1] - PosDelimiter[j] - 1;
                                        // std::cout<<"第"<<j<<"段数据长"<<field_len[j]<<std::endl;
                                    }

                                    if (field_len[1] > 0)
                                    {
    
    
                                        memset(temp_field, 0, sizeof(temp_field));
                                        strncpy(temp_field, &OneFrame[PosDelimiter[1] + 1], field_len[1]);
                                        int temp = (int)(atof(temp_field) / 100);
                                        gnss_msg->latitude = temp + (atof(temp_field) - temp * 100) / 60;
                                    }

                                    if (field_len[3] > 0)
                                    {
    
    
                                        memset(temp_field, 0, sizeof(temp_field));
                                        strncpy(temp_field, &OneFrame[PosDelimiter[3] + 1], field_len[3]);
                                        int temp = (int)(atof(temp_field) / 100);
                                        gnss_msg->longitude = temp + (atof(temp_field) - temp * 100) / 60;
                                    }

                                    if (field_len[5] > 0)
                                    {
    
    
                                        memset(temp_field, 0, sizeof(temp_field));
                                        strncpy(temp_field, &OneFrame[PosDelimiter[5] + 1], field_len[5]);
                                        gnss_msg->status.status = atof(temp_field);
                                    }

                                    if (field_len[6] > 0)
                                    {
    
    
                                        memset(temp_field, 0, sizeof(temp_field));
                                        strncpy(temp_field, &OneFrame[PosDelimiter[6] + 1], field_len[6]);
                                        gnss_msg->status.service = atof(temp_field);
                                    }

                                    if (field_len[7] > 0)
                                    {
    
    
                                        memset(temp_field, 0, sizeof(temp_field));
                                        strncpy(temp_field, &OneFrame[PosDelimiter[7] + 1], field_len[7]);
                                        gnss_msg->position_covariance[0] = pow(atof(temp_field), 2);
                                        gnss_msg->position_covariance[4] = pow(atof(temp_field), 2);
                                        gnss_msg->position_covariance[8] = pow(atof(temp_field), 2);
                                    }

                                    if (field_len[8] > 0)
                                    {
    
    
                                        memset(temp_field, 0, sizeof(temp_field));
                                        strncpy(temp_field, &OneFrame[PosDelimiter[8] + 1], field_len[8]);
                                        gnss_msg->altitude = atof(temp_field);
                                    }

                                    StateParser = 3;
                                }
                            }
                            break;

                            // 校验和第一个字符
                        case 3:
                            OneFrame[CntByte] = rbuf[i];
                            CntByte++;                                                                                            // 指向下一个空位
                            if (rbuf[i - 1] == '*' && ((rbuf[i] >= '0' && rbuf[i] <= '9') || (rbuf[i] >= 'A' && rbuf[i] <= 'F'))) // 校验和字节应是一个十六进制数
                            {
    
    
                                StateParser = 4;
                            }
                            else
                            {
    
    
                                memset(OneFrame, 0, sizeof(OneFrame));
                                StateParser = 0;
                            }
                            break;

                            // 校验和第二个字符
                        case 4:
                            OneFrame[CntByte] = rbuf[i];
                            CntByte++; // 指向下一个空位

                            if ((rbuf[i] >= '0' && rbuf[i] <= '9') || (rbuf[i] >= 'A' && rbuf[i] <= 'F')) // 校验和字节应是一个十六进制数
                            {
    
    
                                // 检查校验
                                cscomputed = GetXorChecksum((char *)(OneFrame + 1), CntByte - 4); // 计算得到的校验,除去$*hh<CR><LF>共6个字符
                                csreceived = 0;                                                   // 接收到的校验
                                strtemp[0] = OneFrame[CntByte - 2];
                                strtemp[1] = OneFrame[CntByte - 1];
                                strtemp[2] = '\0';                  // 字符串结束标志
                                sscanf(strtemp, "%x", &csreceived); // 字符串strtemp转换为16进制数

                                // 检查校验是否正确
                                if (cscomputed != csreceived) // 校验和不匹配
                                {
    
    
                                    memset(OneFrame, 0, sizeof(OneFrame));
                                    StateParser = 0;
                                }
                                else // 校验和匹配
                                {
    
    
                                    StateParser = 5;
                                }
                            } // 校验和字节是hex
                            else
                            {
    
    
                                memset(OneFrame, 0, sizeof(OneFrame));
                                StateParser = 0;
                            }

                            break;

                            // 等待结束标志<CR>=0x0d
                        case 5:
                            OneFrame[CntByte] = rbuf[i];
                            CntByte++; // 指向下一个空位
                            if (rbuf[i] == '\r')
                            {
    
    
                                StateParser = 6;
                            }
                            else
                            {
    
    
                                memset(OneFrame, 0, sizeof(OneFrame));
                                StateParser = 0;
                            }
                            break;

                        // 等待结束标志<LF>=0x0a
                        case 6:
                            OneFrame[CntByte] = rbuf[i];
                            gnss_msg->header.stamp = this->get_clock()->now(); // ros时刻
                            gnss_msg->header.frame_id = "gnss_link";
                            gnss_pub_->publish(std::move(gnss_msg)); // 发布nav消息
                            // std::cout<<"发布成功"<<std::endl;

                            memset(OneFrame, 0, sizeof(OneFrame));
                            StateParser = 0;

                            break;

                        default:
                            memset(OneFrame, 0, sizeof(OneFrame));
                            StateParser = 0;
                            break;
                        } // switch(StateParser)
                    }     // for(int i=0; i<numgetted; i++)
                }         // if(numgetted == numinbuf)
            }
            loop_rate.sleep();
        }
    }

private:
    // 全局变量
    serial::Serial ser;      // 声明串口对象
    int StateParser;         // 解析处理状态机状态
    int CntByte;             // 用于记录OneFrame中的实际数据长度
    int PosDelimiter[15];    // 用于记录分隔符位置
    int field_len[14];       // 字符串长度
    int CntDelimiter;        // 分隔符计数
    unsigned char rbuf[500]; // 接收缓冲区,要足够大,需要通过测试得出
    char OneFrame[250];      // 存放一帧数据,长度大于115即可,这里取200
    char str[3];
    unsigned int tmpint;
    int cscomputed; // 计算得到的校验,除去$*hh<CR><LF>共6个字符
    int csreceived; // 接收到的校验
    char strtemp[3];
    char temp_field[30];
    std::string port_name;

    /*****************************
    功能:计算校验,字符串中所有字符的异或
    返回:返回一个无符号整数
    输入参数:<1>字符串指针,<2>字符串长度(指有效长度,不包括字符串结束标志)
    输出参数:校验结果
    ******************************/
    unsigned int GetXorChecksum(const char *pch, int len)
    {
    
    
        unsigned int cs = 0;
        int i;

        for (i = 0; i < len; i++)
            cs ^= pch[i];

        return cs;
    }
    rclcpp::Publisher<sensor_msgs::msg::NavSatFix>::SharedPtr gnss_pub_;
};

int main(int argc, char *argv[])
{
    
    
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<GNSSPublisher>("gnss_driver_exe"));
    rclcpp::shutdown();
    return 0;
}

启动,再订阅GNSS数据可以看到GNSS数据

source install/setup.bash
ros2 launch m620_driver m620.launch.py

猜你喜欢

转载自blog.csdn.net/zardforever123/article/details/132529492
今日推荐