当前位置: 首页 > news >正文

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

文章目录

  • 一、激光雷达驱动
  • 二、IMU驱动
    • 2.1 上位机配置
    • 4.2 IMU校准
    • 4.3 安装ROS驱动
  • 三、CAN驱动
  • 四、相机驱动
    • 4.1 安装驱动
    • 4.2 修改相机参数
  • 五、GNSS驱动


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

一、激光雷达驱动

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

  • 官方驱动下载地址:https://github.com/RoboSense-LiDAR/rslidar_sdk
  • 官方雷达ROS2消息类型下载地址:https://github.com/RoboSense-LiDAR/rslidar_msg
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 resultdef calculate_speed(linear_speed):EV_Speed_H = int((linear_speed * 185)) // 256EV_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_Ldata_EV1['EV_Speed_H'] = EV_Speed_Hdef calculate_angle(linear_speed, angular_speed):# 转弯的角度 = arctan(( 角速度 / 线速度 ) * 车长 ) Steer_Angle = -math.atan((angular_speed/linear_speed)*1) * radian2angle * 4.5print('Steer_Angle:%f' % Steer_Angle)# 实际测试下来,方向盘转动的角度范围是-27.5~27.5,对应的取值范围是924~1124  27.5*3.6=99Steer_Angle_H = int((Steer_Angle * 3.6) + 1024) // 256Steer_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_Hdata_EPS2['Steer_Angle_L'] = Steer_Angle_Ldef calculate_angle(angular_rad):Steer_Angle = -angular_rad * radian2angleprint("Steer_Angle:", Steer_Angle)# 实际测试下来,方向盘转动的角度范围是-27.5~27.5,对应的取值范围是924~1124  27.5*3.6=99Steer_Angle_H = int((Steer_Angle * 3.6) + 1024) // 256Steer_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_Hdata_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']) / 185Steer_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 / 1if (message['Gear'] == 1):feedback_twist_linear_x = feedback_twist_linear_xelif (message['Gear'] == 2):feedback_twist_linear_x = -feedback_twist_linear_xelse: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_valueself.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 00udp_send_can(drive_by_wire_command)# 2. 接收autoware传来的线速度和角速度EV_Speed = msg.longitudinal.speed# angular_velocity = msg.lateral.steering_tire_rotation_rateangular_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'] = 1calculate_speed(EV_Speed)# calculate_angle(EV_Speed, angular_velocity)calculate_angle(angular_rad)elif (EV_Speed < 0):data_EV1['Gear'] = 2calculate_speed(-EV_Speed)# calculate_angle(-EV_Speed, angular_velocity)calculate_angle(angular_rad)else:data_EV1['Gear'] = 0calculate_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 = data1msg.angular.z = data2self.publisher_data.publish(msg)def pubilsh_control_mode(self, data):msg = ControlModeReport()msg.mode = dataself.publisher_control_mode.publish(msg)def pubilsh_gear(self, data):msg = GearReport()msg.report = dataself.publisher_gear.publish(msg)def pubilsh_steering(self, data):msg = SteeringReport()msg.steering_tire_angle = dataself.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 = data1msg.longitudinal_velocity = data2msg.lateral_velocity = data3msg.heading_rate = data4self.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 setuppackage_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='car@todo.todo',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_directorydef 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.0io_method: "mmap"# 坐标系frame_id: "camera"# 像素格式pixel_format: "mjpeg2rgb"  # see usb_cam/supported_formats for list of supported formats# 像素尺寸image_width: 1920image_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: truewhite_balance: 4000# 曝光autoexposure: trueexposure: 100# 聚焦autofocus: falsefocus: -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 osfrom ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Nodedef 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); // 超时定义,单位:msser.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);           // 设置循环频率为100Hzser.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;}} // 校验和字节是hexelse{memset(OneFrame, 0, sizeof(OneFrame));StateParser = 0;}break;// 等待结束标志<CR>=0x0dcase 5:OneFrame[CntByte] = rbuf[i];CntByte++; // 指向下一个空位if (rbuf[i] == '\r'){StateParser = 6;}else{memset(OneFrame, 0, sizeof(OneFrame));StateParser = 0;}break;// 等待结束标志<LF>=0x0acase 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即可,这里取200char 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

相关文章:

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

文章目录 一、激光雷达驱动二、IMU驱动2.1 上位机配置4.2 IMU校准4.3 安装ROS驱动 三、CAN驱动四、相机驱动4.1 安装驱动4.2 修改相机参数 五、GNSS驱动 本文介绍了 Autoware.universe 各个传感器ROS2驱动&#xff0c;本系列其他文章&#xff1a; Autoware.universe部署01&…...

Spring boot如何工作

越来越方便了 java技术生态发展近25年&#xff0c;框架也越来越方便使用了&#xff0c;简直so easy&#xff01;&#xff01;&#xff01;我就以Spring衍生出的Spring boot做演示&#xff0c;Spring boot会让你开发应用更快速。 快速启动spring boot 请参照官网 Spring | Quic…...

代码随想录打卡—day45—【DP】— 8.29 完全背包应用

1 70. 爬楼梯&#xff08;完全背包版&#xff09; 70. 爬楼梯 完全背包装满的选法排列的套路&#xff0c;AC代码&#xff1a; class Solution { public:/*完全背包的思路:1 2是两个物体 可以无限取*/int dp[50]; // 能爬到第i楼的选法的排列数/*dp[j] dp[j - i];dp[0] 1fo…...

2023.8.28日论文阅读

文章目录 NestFuse: An Infrared and Visible Image Fusion Architecture based on Nest Connection and Spatial/Channel Attention Models(2020的论文)本文方法 LRRNet: A Novel Representation Learning Guided Fusion Network for Infrared and Visible Images本文方法学习…...

HAproxy(四十七)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、概述 1.1 简介 1.2 核心功能 1.3 关键特性 1.4 应用场景 二、安装 1.内核配置 2.编译安装 ​3. 建立配置文件 4. 添加为系统服务 5. 添加3和5运行级别下自启动…...

Java实战场景下的ElasticSearch

文章目录 前言一、环境准备二、RsetAPI操作索引库1.创建索引库2.判断索引库是否存在3.删除索引库 二、RsetAPI操作文档1.新增文档2.单条查询3.删除文档4.增量修改5.批量导入6.自定义响应解析方法 四、常用的查询方法1.MatchAll():查询所有2.matchQuery():单字段查询3.multiMatc…...

拓世科技集团 | “书剑人生”李步云学术思想研讨会暨李步云先生九十华诞志庆

2023年&#xff0c;中国改革开放迎来了45周年&#xff0c;改革春风浩荡&#xff0c;席卷神州大地&#xff0c;45年间&#xff0c;中国特色社会主义伟大事业大步迈入崭新境界&#xff0c;一路上结出了饶为丰硕的果实。中华民族在这45年间的砥砺前行&#xff0c;不仅使中国的经济…...

前端须知名词解释

目录 一、多维转一维 二、一维转多维 一维转多维——使用场景&#xff1a;分页 三、判断当前元素是否为数组 四、判断当前元素是否是空对象 五、数字分割符&#xff1a;提高数字可读性 六、模糊盒子&#xff08;怪异盒子&#xff09;与标准盒模型 七、css的filter属性 …...

React性能优化之memo缓存函数

React是一个非常流行的前端框架&#xff0c;但是在处理大型应用程序时&#xff0c;性能可能会成为一个问题。为了解决这个问题&#xff0c;React提供了一个称为memo的功能&#xff0c;它可以缓存函数并避免不必要的重新渲染。 memo是React中的一个高阶组件&#xff08;HOC&…...

2023年高教社杯 国赛数学建模思路 - 案例:ID3-决策树分类算法

文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法&#xff0c;就是频繁模…...

C# Emgu.CV 条码检测

效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using Emgu.CV; using Emgu.CV.Util; using static Emgu.C…...

VueRouter的基本使用

路由的基本使用 文章目录 路由的基本使用01-VueRouterVueRouter的使用 &#xff08; 5 2&#xff09;综合代码 拓展&#xff1a;组件存放问题 什么是路由呢&#xff1f; 在生活中的路由&#xff1a;设备和IP的映射关系 在Vue中&#xff1a;路径 和 组件 的 映射 关系。 01-Vu…...

网工笔记:快速认识7类逻辑接口

逻辑接口是指能够实现数据交换功能但物理上不存在、需要通过配置建立的接口。逻辑接口需要承担业务传输。 下面是我整理了7款常见的逻辑接口。 接口类型 描述 Eth-Trunk接口 具有二层特性和三层特性的逻辑接口&#xff0c;把多个以太网接口在逻辑上等同于一个逻辑接口&…...

MySQL中的free链表,flush链表,LRU链表

一、free链表 1、概述 free链表是一个双向链表数据结构&#xff0c;这个free链表里&#xff0c;每个节点就是一个空闲的缓存页的描述数据块的地址&#xff0c;也就是说&#xff0c;只要你一个缓存页是空闲的&#xff0c;那么他的描述数据块就会被放入这个free链表中。 刚开始数…...

mac使用VsCode远程连接服务器总是自动断开并要求输入密码的解决办法

在mac中使用vscode远程连接服务器&#xff0c;时常会出现自动断开并要求重新输入服务器密码的问题&#xff0c;接下来让我们来解决它&#xff1a; 1、首先&#xff0c;在本地创建公钥&#xff1a; ssh-keygen 这条命令执行之后&#xff0c;出现提示直接回车即可&#xff1b;直…...

Python爬虫分布式架构 - Redis/RabbitMQ工作流程介绍

在大规模数据采集和处理任务中&#xff0c;使用分布式架构可以提高效率和可扩展性。本文将介绍Python爬虫分布式架构中常用的消息队列工具Redis和RabbitMQ的工作流程&#xff0c;帮助你理解分布式爬虫的原理和应用。 为什么需要分布式架构&#xff1f; 在数据采集任务中&#…...

【ES】笔记-集合介绍与API

集合是一种不允许值重复的顺序数据结构。 通过集合我们可以进行并集、交集、差集等数学运算&#xff0c; 还会更深入的理解如何使用 ECMAScript 2015(ES2015)原生的 Set 类。 构建数据集合 集合是由一组无序且唯一(即不能重复)的项组成的。该数据结构使用了与有限集合相同的数…...

Spring Boot(Vue3+ElementPlus+Axios+MyBatisPlus+Spring Boot 前后端分离)【五】

&#x1f600;前言 本篇博文是关于Spring Boot(Vue3ElementPlusAxiosMyBatisPlusSpring Boot 前后端分离)【五】&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章…...

二、Tomcat 安装集

一、Tomcat—Docker 1. 拉取镜像 # 1、拉取镜像&#xff08;tomcat版本8&#xff0c;jre版本8&#xff09;。 docker pull tomcat:8-jre82. 启动容器 # 2、启动一个tomcat容器。 docker run -id --name tomcat -p 8080:8080 镜像ID # 3、宿主机里新建/root/tomcat目录&#x…...

CentOS 上通过 NFS 挂载远程服务器硬盘

NFS&#xff08;Network File System&#xff09;是一种用于在不同的计算机系统之间共享文件和目录的协议。它允许一个计算机系统将其文件系统的一部分或全部内容暴露给其他计算机系统&#xff0c;使其能够像访问本地文件一样访问这些内容。在这篇博客中&#xff0c;我们将介绍…...

微信小程序中的 广播监听事件

定义 WxNotificationCenter.js 文件&#xff1b; /*** author: Di (微信小程序开发工程师)* organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)* 垂直微信小程序开发交流社区* * github地址: https://github.com/icindy/WxNotificationCenter…...

Quickstart: MinIO for Linux

单节点部署教程 1.安装Minio服务端 //wget下载二进制文件 wget https://dl.min.io/server/minio/release/linux-amd64/minio //赋予权限 chmod x minio //将minio可执行文件移入usr/local/bin目录下&#xff0c;使得minio可以全局执行 sudo mv minio /usr/local/bin/ 2.启动Mi…...

Java中word转Pdf工具类

背景&#xff1a; 最近做的一个项目中&#xff0c;对于word转Pdf用的地方很多&#xff0c;特此记录 搭建总图&#xff1a; 代码部分&#xff1a; 1.需要的jar包&#xff1a; aspose-words-15.8.0-jdk16.jar 注&#xff1a;下载好这个jar包后&#xff0c;在项目的根目录新建一…...

【conda install】网络慢导致报错CondaHTTPError: HTTP 000 CONNECTION FAILED for url

⭐⭐问题&#xff1a; 部署安装环境经常会出现由于网络慢问题&#xff0c;导致conda安装不了库&#xff0c;报错如下&#xff1a; Solving environment: failedCondaHTTPError: HTTP 000 CONNECTION FAILED for url <https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/…...

2023-8-28 图中点的层次(树与图的广度优先遍历)

题目链接&#xff1a;图中点的层次 #include <iostream> #include <cstring> #include <algorithm>using namespace std;const int N 100010;int h[N], e[N], ne[N], idx; int n, m; int q[N], d[N];void add(int a, int b) {e[idx] b, ne[idx] h[a], h…...

设计模式(一)

1、适配器模式 &#xff08;1&#xff09;概述 适配器中有一个适配器包装类Adapter&#xff0c;其包装的对象为适配者Adaptee&#xff0c;适配器作用就是将客户端请求转化为调用适配者中的接口&#xff1b;当调用适配器中的方法时&#xff0c;适配器内部会调用适配者类的方法…...

Prometheus关于微服务的监控

在微服务架构下随着服务越来越多,定位问题也变得越来越复杂,因此监控服务的运行状态以及针对异常状态及时的发出告警也成为微服务治理不可或缺的一环。服务的监控主要有日志监控、调用链路监控、指标监控等几种类型方式,其中指标监控在整个微服务监控中比重最高,也是实际生…...

CSS实现白天/夜晚模式切换

目录 功能介绍 示例 原理 代码 优化 总结 功能介绍 在网页设计和用户体验中&#xff0c;模式切换功能是一种常见的需求。模式切换可以为用户提供不同的界面外观和布局方案&#xff0c;以适应其个人偏好或特定环境。在这篇博客中&#xff0c;我们将探索如何使用纯CSS实现一…...

selenium实现输入数字字母验证码

思路 1. 登录url 2. 获取验证码坐标 3. 根据桌标截图验证码 4. 对验证码进行识别 5. 自动输入验证码 测试代码 import os import time from io import BytesIO from PIL import Image from selenium import webdriver from selenium.webdriver.common.by import By impo…...

Docker的运用

文章目录 一、 Docker介绍二、Docker常用命令三、Docker 部署微服务项目四、Docker 使用场景五、Docker模拟场景5.1 模拟部署Nacos5.2 模拟部署Mongodb5.3 模拟部署RabbitMQ 一、 Docker介绍 Docker是一种开源软件平台&#xff0c;用于在不同的操作系统&#xff08;如Windows、…...