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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/57476.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

配置DNS服务的正反向解析

正向解析 安装DNS服务 2.在服务器端 编辑区域配置文件&#xff0c;选择一个解析模版进行修改---------/etc/named.rfc1912.zones 修改第一第三行 编辑数据配置文件&#xff0c;使用cp -a命令完全拷贝一份正向解析模版&#xff08;named.localhost&#xff09;&#xff0c;在…

安装docker服务,配置镜像加速器

文章目录 1.安装docker服务&#xff0c;配置镜像加速器2.下载系统镜像&#xff08;Ubuntu、 centos&#xff09;3.基于下载的镜像创建两个容器 &#xff08;容器名一个为自己名字全拼&#xff0c;一个为首名字字母&#xff09;4.容器的启动、 停止及重启操作5.怎么查看正在运行…

k8s deployment创建pod流程图

参考 k8s 创建pod和deployment的流程 - SoulChild随笔记

微前沿 | 第1期:强可控视频生成;定制化样本检索器;用脑电重建视觉感知;大模型鲁棒性评测

欢迎阅读我们的新栏目——“微前沿”&#xff01; “微前沿”汇聚了微软亚洲研究院最新的创新成果与科研动态。在这里&#xff0c;你可以快速浏览研究院的亮点资讯&#xff0c;保持对前沿领域的敏锐嗅觉&#xff0c;同时也能找到先进实用的开源工具。 本期内容速览 01. 强可…

【详解】文本检测OCR模型的评价指标

关于文本检测OCR模型的评价指标 前言&#xff1a;网上关于评价标准乱七八糟的&#xff0c;有关于单词的&#xff0c;有关于段落的&#xff0c;似乎没见过谁解释一下常见论文中常用的评价指标具体是怎么计算的&#xff0c;比如DBNet&#xff0c;比如RCNN&#xff0c;这似乎好像…

操作系统真题

操作系统真题 考点前驱图真题分页存储管理索引文件结构分段存储管理进程的状态进程的同步和互斥 考点 考试只会考察选择题 前驱图真题 c 这是常考题型 b 分页存储管理 将程序分页 --逻辑地址 将内存分为页框&#xff08;物理块&#xff09; --物理地址 程序页的大小和页框的大小…

修改Jupyter Notebook默认打开路径

这里我是重新下载的anaconda&#xff0c;打开Jupyter之后是默认在C盘的一个路径的&#xff0c;现在我们就来修改一下它的一个默认打开路径&#xff0c;这样在我们后续学习过程中&#xff0c;可以将ipynb后缀的文件放在这个目录下就能查看了。 1、先打开Anaconda Prompt&#x…

常见前端面试之VUE面试题汇总十一

31. Vuex 有哪几种属性&#xff1f; 有五种&#xff0c;分别是 State、 Getter、Mutation 、Action、 Module state > 基本数据(数据源存放地) getters > 从基本数据派生出来的数据 mutations > 提交更改数据的方法&#xff0c;同步 actions > 像一个装饰器&a…

他们朝我扔泥巴(scratch)

前言 纯~~~属~~~虚~~~构~~~&#xff08;同学看完短视频要我做&#xff0c;蟹蟹你&#xff09; 用scratch做的&#xff0c;幼稚得嘞(&#xffe3;_&#xffe3;|||)呵呵&#xff08;强颜欢笑&#xff09; 完成视频 视频试了好久&#xff0c;就是传不上来&#xff0c;私信我加我…

Spring Cloud Nacos 和 Eureka区别,包含实战代码

目录 一、Spring Cloud Eureka详解二、Spring Cloud Nacos详解三、Spring Cloud Nacos和Eureka区别 Spring Cloud Nacos 和 Spring Cloud Eureka 都是 Spring Cloud 微服务框架中的服务注册和发现组件&#xff0c;用于帮助开发者轻松地构建和管理微服务应用。它们之间的主要区别…

【业务功能篇86】微服务-springcloud-系统性能压力测试-jmeter-性能优化-JVM参数调优

系统性能压力测试 一、压力测试 压力测试是给软件不断加压&#xff0c;强制其在极限的情况下运行&#xff0c;观察它可以运行到何种程度&#xff0c;从而发现性能缺陷&#xff0c;是通过搭建与实际环境相似的测试环境&#xff0c;通过测试程序在同一时间内或某一段时间内&…

MySQL项目迁移华为GaussDB PG模式指南

文章目录 0. 前言1. 数据库模式选择&#xff08;B/PG&#xff09;2.驱动选择2.1. 使用postgresql驱动2.1. 使用opengaussjdbc驱动 3. 其他考虑因素4. PG模式4.1 MySQL和OpenGauss不兼容的语法处理建议4.2 语法差异 6. 高斯数据库 PG模式JDBC 使用示例验证6. 参考资料 本章节主要…

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

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

Redis的数据结构与单线程架构

"飞吧&#xff0c;去寻觅红色的流星" Redis中的五种数据结构和编码 Redis是一种通过键值对关系存储数据的软件&#xff0c;在前一篇中&#xff0c;我们可以使用type命令实际返回当前键所对应的数据结构类型&#xff0c;例如: String\list\hash\set等等。 但…

开源项目-数据可视化分析平台

哈喽,大家好,今天给大家带来一个开源项目-数据可视化分析平台。项目通过SpringBoot实现 数据可视化分析平台主要有数据源管理,项目管理,数据集管理,图表管理,看板管理等功能 登录 数据源管理 数据源管理功能可以添加MySQL,Oracle,PostgreSQL等类型的数据源信息 项目…

ResNet详解:网络结构解读与PyTorch实现教程

目录 一、深度残差网络&#xff08;Deep Residual Networks&#xff09;简介深度学习与网络深度的挑战残差学习的提出为什么ResNet有效&#xff1f; 二、深度学习与梯度消失问题梯度消失问题定义为什么会出现梯度消失&#xff1f;激活函数初始化方法网络深度 如何解决梯度消失问…

电脑共享文件夹-实现手机和其他电脑的文件同步更新

一、首先是电脑和手机需要处于同一个路由器下 二、创建一个文件夹&#xff0c;随便命名 三、点击属性-共享-Everyone-添加&#xff08;设置读取/写入&#xff09;-最后点击共享 四、这里要注意&#xff0c;如果电脑之前没设置过共享文件夹会有一个弹窗&#xff0c;点第一个就好…

OpenAI推出ChatGPT企业版,提供更高安全和隐私保障

&#x1f989; AI新闻 &#x1f680; OpenAI推出ChatGPT企业版&#xff0c;提供更高安全和隐私保障 摘要&#xff1a;OpenAI发布了面向企业用户的ChatGPT企业版&#xff0c;用户可以无限制地访问强大的GPT-4模型&#xff0c;进行更深入的数据分析&#xff0c;并且拥有完全控制…

AI助力智能安检,基于图像目标检测实现危险品X光智能安全检测系统

基于AI相关的技术来对一些重复性的但是又比较重要的工作来做智能化助力是一个非常有潜力的场景&#xff0c;关于这方面的项目开发实践在我之前的文章中也有不少的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a;《AI助力智能安检&#xff0c;基于目标检测模型实现…