四轮转向轮式里程计设计(python)

写在前面的话

上一篇博客:键盘控制车子四轮转向

这篇文章通过订阅车轮的速度和转向计算得到整车的速度和转向,同时发布轮式里程计信息,需要和 四轮转向键盘控制改进版 ros2(python) 进行配套使用

大部分的车子的底盘控制器其实不需要自己写轮式里程计,因为ros2有很多官方的底盘控制器,比如两轮差速等常见底盘控制器,这些官方底盘控制器会自带轮式里程计。本文用的不是官方的四轮转向底盘,所以需要再写一个轮式里程计。(耗时1-2个星期)

参考教程

官方教程

1 里程计设置
2 Publishing Odometry Information over ROS

参考代码(c++)

Autonomous-Ackerman-Simulation/four_ws_navigation/four_wheel_steering_controller/src

在这里插入图片描述

关键代码解析

订阅车轮速度

订阅话题/forward_velocity_controller/commands,回调函数self.velocity_callback

self.wheel_v_sub = self.create_subscription(Float64MultiArray, '/forward_velocity_controller/commands', self.velocity_callback, 10)

def velocity_callback(self, msg):
    # 处理 '/forward_velocity_controller/commands' 话题的消息
    # self.get_logger().info('Received data from /forward_velocity_controller/commands: %s' % list(msg.data))
    self.wheel_v_array = list(msg.data)

订阅车轮转向

订阅话题/forward_position_controller/commands,回调函数self.position_callback

self.wheel_w_sub = self.create_subscription(Float64MultiArray, '/forward_position_controller/commands', self.position_callback, 10)

def position_callback(self, msg):
    # 处理 '/forward_position_controller/commands' 话题的消息
    # self.get_logger().info('Received data from /forward_position_controller/commands: %s' % list(msg.data))
    self.wheel_w_array = list(msg.data)

订阅四轮转向控制模式

订阅话题/keyboard_control_mode,回调函数self.control_mode_callback

self.mode_sub = self.create_subscription(Int32, '/keyboard_control_mode', self.control_mode_callback, 10)

def control_mode_callback(self, msg):
    # 处理 '/control_4ws_mode 话题的消息
    # self.get_logger().info('Received data from /control_4ws_mode: %s' % msg.data)
    self.control_mode = msg.data

积累速度和转向角

self.current_time = self.get_clock().now()
dt = (self.current_time - self.last_time).nanoseconds / 1e9


self.vx, self.vy, self.vth = self.forward_kinematics(self.control_mode, self.wheel_v_array, self.wheel_w_array)
self.get_logger().info(f'\n self.x:{
      
      self.x}, self.y:{
      
      self.y}, self.th:{
      
      self.th}, dt:{
      
      dt}')
self.get_logger().info(f'\n self.vx:{
      
      self.vx}, self.vy:{
      
      self.vy}, self.vth:{
      
      self.vth}')
#====================== publish odom ====================#

delta_x = (self.vx * math.cos(self.th) - self.vy * math.sin(self.th))*dt
delta_y = (self.vx * math.sin(self.th) + self.vy * math.cos(self.th))*dt
delta_th = self.vth*dt

self.th += delta_th
self.x += delta_x
self.y += delta_y

# self.get_logger().info(f'\n self.x:{self.x}, self.y:{self.y}, self.th:{self.th}')
self.last_time = self.current_time

发布里程计

self.odom_pub = self.create_publisher(Odometry, '/odom', 50)
self.odom_broadcaster = TransformBroadcaster(self)

odom_quat = Quaternion()
# magnitude = math.sqrt(odom_quat.x**2 + odom_quat.y**2 
#                       + math.sin(self.th / 2)**2 + math.cos(self.th / 2)**2)

# 归一化四元数
# odom_quat.z /= magnitude
# odom_quat.w /= magnitude

odom_quat.z = math.sin(self.th / 2)
odom_quat.w = math.cos(self.th / 2)

# Publish the transform
odom_trans = TransformStamped()
odom_trans.header.stamp = self.current_time.to_msg()
odom_trans.header.frame_id = 'odom'
odom_trans.child_frame_id = 'base_link'
odom_trans.transform.translation.x = self.x
odom_trans.transform.translation.y = self.y
odom_trans.transform.translation.z = 0.0
odom_trans.transform.rotation = odom_quat

self.odom_broadcaster.sendTransform(odom_trans)

# Publish the odometry message
odom = Odometry()
odom.header.stamp = self.current_time.to_msg()
odom.header.frame_id = 'odom'
odom.pose.pose.position.x = self.x
odom.pose.pose.position.y = self.y
odom.pose.pose.position.z = 0.0
odom.pose.pose.orientation = odom_quat
odom.child_frame_id = 'base_link'
odom.twist.twist.linear = Vector3(x=self.vx, y=self.vy, z=0.0)
odom.twist.twist.angular = Vector3(x=0.0, y=0.0, z=self.vth)
self.odom_pub.publish(odom)

完整代码

Class Commdar(Node) 通过整车速度和转向控制车轮速度和车轮转向
Class OdometryPublisher(Node) forward_kinematics 函数通过车轮速度和车轮转向计算整车速度和转向

#!/usr/bin/python3
import math
import threading
import rclpy
import numpy as np
from rclpy.node import Node
from std_msgs.msg import Float64MultiArray
from geometry_msgs.msg import Twist
from sensor_msgs.msg import Joy
import sys, select, termios, tty
from nav_msgs.msg import Odometry
import pty
from geometry_msgs.msg import Quaternion, TransformStamped, Twist, Vector3
from nav_msgs.msg import Odometry
from tf2_ros import TransformBroadcaster
from tf2_ros import TransformStamped
import math
from std_msgs.msg import Int32



class Commander(Node):
    '''
    wheel_separation:
        Shortest distance between the left and right wheels. If this parameter is wrong, 
        the robot will not behave correctly in curves.
    wheel_base:
        Distance between front and rear wheels, in meters.
    wheel_radius:
        Radius of a wheel, i.e., wheels size, used for transformation of linear velocity into wheel rotations. 
        If this parameter is wrong the robot will move faster or slower then expected.
    '''

    def __init__(self):
        super().__init__('commander')
        
        self.wheel_seperation = 1.2
        self.wheel_base = 1
        self.wheel_radius = 0.3
        self.wheel_steering_y_offset = 0.03
        self.steering_track = self.wheel_seperation - 2*self.wheel_steering_y_offset #1.14

        self.pos = np.array([0,0,0,0], float)
        self.vel = np.array([0,0,0,0], float) #left_front, right_front, left_rear, right_rear
                
        self.pub_pos = self.create_publisher(Float64MultiArray, '/forward_position_controller/commands', 10)
        self.pub_vel = self.create_publisher(Float64MultiArray, '/forward_velocity_controller/commands', 10)
        self.sub_vel = self.create_subscription(Twist,'/keyboard_vel_msg', self.vel_callback, 10)
        self.sub_mode = self.create_subscription(Int32, '/keyboard_control_mode', self.mode_callback, 10)
        self.sub_vel
        self.sub_mode
        self.vel_msg = Twist()
        self.vel_msg.linear.x = 0.0
        self.vel_msg.linear.y = 0.0
        self.vel_msg.angular.x = 0.0 
        self.vel_msg.angular.y = 0.0 
        self.vel_msg.angular.z = 0.0
        self.control_mode = Int32()
        self.control_mode.data = 1
        self.create_timer(timer_period_sec=0.02, callback=self.timer_callback)
        


    def vel_callback(self, msg):
        self.vel_msg = msg
        return
    
    def mode_callback(self, msg):
        self.control_mode = msg.data
        return 

    def timer_callback(self):
        vel_msg = self.vel_msg
        
        mode_selection = self.control_mode
        
        # opposite phase
        if(mode_selection == 1):
            
            vel_steerring_offset = vel_msg.angular.z * self.wheel_steering_y_offset / self.wheel_radius
            sign = math.copysign(1.0, vel_msg.linear.x)            

            self.vel[0] = sign*math.hypot(vel_msg.linear.x - vel_msg.angular.z*self.steering_track/2, vel_msg.angular.z*self.wheel_base/2)/self.wheel_radius - vel_steerring_offset
            self.vel[1] = sign*math.hypot(vel_msg.linear.x + vel_msg.angular.z*self.steering_track/2, vel_msg.angular.z*self.wheel_base/2)/self.wheel_radius + vel_steerring_offset
            self.vel[2] = sign*math.hypot(vel_msg.linear.x - vel_msg.angular.z*self.steering_track/2, vel_msg.angular.z*self.wheel_base/2)/self.wheel_radius - vel_steerring_offset
            self.vel[3] = sign*math.hypot(vel_msg.linear.x + vel_msg.angular.z*self.steering_track/2, vel_msg.angular.z*self.wheel_base/2)/self.wheel_radius + vel_steerring_offset

            if abs(2.0 * vel_msg.linear.x) > abs(vel_msg.angular.z * self.steering_track) and math.fabs(vel_msg.linear.y) < 0.001:
                front_left_steering = math.atan(vel_msg.angular.z * self.wheel_base / (2.0 * vel_msg.linear.x - vel_msg.angular.z * self.steering_track))
                front_right_steering = math.atan(vel_msg.angular.z * self.wheel_base / (2.0 * vel_msg.linear.x + vel_msg.angular.z * self.steering_track))
                self.pos[0] = front_left_steering
                self.pos[1] = front_right_steering
            elif abs(vel_msg.linear.x) > 0.001 and math.fabs(vel_msg.linear.y) < 0.001:
                front_left_steering = math.copysign(math.pi / 2, vel_msg.angular.z)
                front_right_steering = math.copysign(math.pi / 2, vel_msg.angular.z)
                self.pos[0] = front_left_steering
                self.pos[1] = front_right_steering

            # self.pos[0] = math.atan(vel_msg.angular.z*self.wheel_base/(2*vel_msg.linear.x + vel_msg.angular.z*self.steering_track+0.001))
            # self.pos[1] = math.atan(vel_msg.angular.z*self.wheel_base/(2*vel_msg.linear.x - vel_msg.angular.z*self.steering_track+0.001))
            self.pos[2] = -self.pos[0]
            self.pos[3] = -self.pos[1]
            

        # in-phase
        elif(mode_selection == 2):

            V = math.hypot(vel_msg.linear.x, vel_msg.linear.y)
            sign = np.sign(vel_msg.linear.x)
            
            if(vel_msg.linear.x != 0):
                ang = vel_msg.linear.y / vel_msg.linear.x
            else:
                ang = 0
            
            self.pos[0] = math.atan(ang)
            self.pos[1] = math.atan(ang)
            self.pos[2] = self.pos[0]
            self.pos[3] = self.pos[1]
            
            self.vel[:] = sign*V            
            
        # pivot turn
        elif(mode_selection == 3):
            omega_const = 6  # to make robot rotates a little bit slower
            
            self.pos[0] = -math.atan(self.wheel_base/self.steering_track)
            self.pos[1] = math.atan(self.wheel_base/self.steering_track)
            self.pos[2] = math.atan(self.wheel_base/self.steering_track)
            self.pos[3] = -math.atan(self.wheel_base/self.steering_track)
            
            self.vel[0] = -vel_msg.angular.z/(omega_const * self.wheel_radius)
            self.vel[1] = vel_msg.angular.z/(omega_const * self.wheel_radius)
            self.vel[2] = self.vel[0]
            self.vel[3] = self.vel[1]

        else:
            self.pos[:] = 0
            self.vel[:] = 0

        # fixed by xcg: change the sign of the position
        self.pos = -self.pos
        pos_array = Float64MultiArray(data=self.pos) 
        vel_array = Float64MultiArray(data=self.vel) 
        self.pub_pos.publish(pos_array)
        self.pub_vel.publish(vel_array)
        
        self.pos[:] = 0
        self.vel[:] = 0



class OdometryPublisher(Node):
    def __init__(self):
        super().__init__('odometry_publisher')
        self.wheel_v_sub = self.create_subscription(Float64MultiArray, '/forward_velocity_controller/commands', self.velocity_callback, 10)
        self.wheel_w_sub = self.create_subscription(Float64MultiArray, '/forward_position_controller/commands', self.position_callback, 10)
        self.mode_sub = self.create_subscription(Int32, '/keyboard_control_mode', self.control_mode_callback, 10)
        self.wheel_v_sub  # 防止被 GC 回收
        self.wheel_w_sub
        self.mode_sub

        self.odom_pub = self.create_publisher(Odometry, '/odom', 50)
        self.odom_broadcaster = TransformBroadcaster(self)
        self.get_logger().info('Subscribed to /forward_velocity(position)_controller/commands topic')

        self.x = 0.0
        self.y = 0.0
        self.th = 0.0

        self.vx = 0.0
        self.vy = 0.0
        self.vth = 0.0

        self.wheel_base_ = 1.0 
        self.wheel_seperation = 1.2
        self.steering_track_ = 1.14
        self.wheel_steering_y_offset_ = 0.03
        self.wheel_radius_ = 0.3 #轮子半径30cm


        self.wheel_v_array = [0,0,0,0]
        self.wheel_w_array = [0,0,0,0]
        self.control_mode = 1

        self.current_time = self.get_clock().now()
        self.last_time = self.get_clock().now()

        timer_period = 0.1
        self.timer = self.create_timer(timer_period, self.timer_callback)
    
    def velocity_callback(self, msg):
        # 处理 '/forward_velocity_controller/commands' 话题的消息
        # self.get_logger().info('Received data from /forward_velocity_controller/commands: %s' % list(msg.data))
        self.wheel_v_array = list(msg.data)

    def position_callback(self, msg):
        # 处理 '/forward_position_controller/commands' 话题的消息
        # self.get_logger().info('Received data from /forward_position_controller/commands: %s' % list(msg.data))
        self.wheel_w_array = list(msg.data)
    
    def control_mode_callback(self, msg):
        # 处理 '/control_4ws_mode 话题的消息
        # self.get_logger().info('Received data from /control_4ws_mode: %s' % msg.data)
        self.control_mode = msg.data
    
    def forward_kinematics(self, control_mode, wheel_velocity, wheel_angular):
        # transform wheel_velocity and wheel_angular to the whole car
        fl_speed = wheel_velocity[0]#车轮线速度,没除车轮半径
        fr_speed = wheel_velocity[1]
        rl_speed = wheel_velocity[2]
        rr_speed = wheel_velocity[3]
        fl_steering = wheel_angular[0]#轮子转角
        fr_steering = wheel_angular[1]
        rl_steering = wheel_angular[2]
        rr_steering = wheel_angular[3]

        # 检查转向角是否为 NaN
        if any(math.isnan(steering) for steering in [fl_steering, fr_steering, rl_steering, rr_steering]):
            return

        # 计算前轮转向位置
        front_steering = 0.0
        if abs(fl_steering) > 0.001 or abs(fr_steering) > 0.001:
            # self.get_logger().info(f'\n fl_steering:{fl_steering}, math.tan(fl_steering):{math.tan(fl_steering)}, fr_steering:{fr_steering}')
            if control_mode != 3:
                front_steering = math.atan(
                    2 * math.tan(fl_steering) * math.tan(fr_steering) / (math.tan(fl_steering) + math.tan(fr_steering))#pivot模式下正负为0
                )
            else:
                front_steering = 0.0
        # 计算后轮转向位置
        rear_steering = 0.0
        if abs(rl_steering) > 0.001 or abs(rr_steering) > 0.001:
            if control_mode != 3:
                rear_steering = math.atan(
                    2 * math.tan(rl_steering) * math.tan(rr_steering) / (math.tan(rl_steering) + math.tan(rr_steering))
                )
            else:
                rear_steering = 0.0


        front_tmp = math.cos(front_steering) * (math.tan(front_steering) - math.tan(rear_steering)) / self.wheel_base_
        front_left_tmp = front_tmp / math.sqrt(1 - self.steering_track_ * front_tmp * math.cos(front_steering)
                                            + (self.steering_track_ * front_tmp / 2)**2)
        front_right_tmp = front_tmp / math.sqrt(1 + self.steering_track_ * front_tmp * math.cos(front_steering)
                                            + (self.steering_track_ * front_tmp / 2)**2)
        fl_speed_tmp = fl_speed * (1 / (1 - self.wheel_steering_y_offset_ * front_left_tmp))
        fr_speed_tmp = fr_speed * (1 / (1 - self.wheel_steering_y_offset_ * front_right_tmp))
        front_linear_speed = self.wheel_radius_ * math.copysign(1.0, fl_speed_tmp + fr_speed_tmp) * math.sqrt(
            (fl_speed**2 + fr_speed**2) / (2 + (self.steering_track_ * front_tmp)**2 / 2.0))

        rear_tmp = math.cos(rear_steering) * (math.tan(front_steering) - math.tan(rear_steering)) / self.wheel_base_
        rear_left_tmp = rear_tmp / math.sqrt(1 - self.steering_track_ * rear_tmp * math.cos(rear_steering)
                                            + (self.steering_track_ * rear_tmp / 2)**2)
        rear_right_tmp = rear_tmp / math.sqrt(1 + self.steering_track_ * rear_tmp * math.cos(rear_steering)
                                            + (self.steering_track_ * rear_tmp / 2)**2)
        rl_speed_tmp = rl_speed * (1 / (1 - self.wheel_steering_y_offset_ * rear_left_tmp))
        rr_speed_tmp = rr_speed * (1 / (1 - self.wheel_steering_y_offset_ * rear_right_tmp))
        rear_linear_speed = self.wheel_radius_ * math.copysign(1.0, rl_speed_tmp + rr_speed_tmp) * math.sqrt(
            (rl_speed_tmp**2 + rr_speed_tmp**2) / (2 + (self.steering_track_ * rear_tmp)**2 / 2.0))

        if fl_speed == 0:
            return 0.0, 0.0, 0.0

        if control_mode == 1:#opposite phase
            self.angular_ = (front_linear_speed * front_tmp + rear_linear_speed * rear_tmp) / 2.0
            self.linear_x_ = (front_linear_speed * math.cos(front_steering) + rear_linear_speed * math.cos(rear_steering)) / 2.0
            self.linear_y_ = (front_linear_speed * math.sin(front_steering) - self.wheel_base_ * self.angular_ / 2.0 +
                            rear_linear_speed * math.sin(rear_steering) + self.wheel_base_ * self.angular_ / 2.0) / 2.0
            # print('opposite phase mode...')
        elif control_mode == 2:#in-phase
            c_sign = math.copysign(1.0, rl_speed)
            self.linear_x_ = c_sign * math.sqrt(fl_speed**2 / (1 + math.tan(fl_steering)**2))
            self.linear_y_ = math.tan(fl_steering) * self.linear_x_
            self.angular_ = 0.0
            print('in-phase mode...')
        elif control_mode == 3:
            self.linear_x_ = 0.0
            self.linear_y_ = 0.0
            R = math.sqrt((self.wheel_base_ / 2) ** 2 + (self.steering_track_ / 2) ** 2) + self.wheel_steering_y_offset_
            self.angular_ = - self.wheel_radius_ * fl_speed / R
            print('pivot turn mode...')
        else:
            self.linear_x_ = 0.0
            self.linear_y_ = 0.0
            self.angular_ = 0.0
            print('other mode...')
        # print('\nn调用函数了马???')
        self.get_logger().info(f'\n self.linear_x_:{
      
      self.linear_x_}, self.linear_y_:{
      
      self.linear_y_}, self.angular_:{
      
      self.angular_}, control_mode:{
      
      control_mode}')
            
        return self.linear_x_, self.linear_y_, self.angular_

    def timer_callback(self):
        self.current_time = self.get_clock().now()
        dt = (self.current_time - self.last_time).nanoseconds / 1e9
        
        
        self.vx, self.vy, self.vth = self.forward_kinematics(self.control_mode, self.wheel_v_array, self.wheel_w_array)
        self.get_logger().info(f'\n self.x:{
      
      self.x}, self.y:{
      
      self.y}, self.th:{
      
      self.th}, dt:{
      
      dt}')
        self.get_logger().info(f'\n self.vx:{
      
      self.vx}, self.vy:{
      
      self.vy}, self.vth:{
      
      self.vth}')
        #====================== publish odom ====================#
        
        delta_x = (self.vx * math.cos(self.th) - self.vy * math.sin(self.th))*dt
        delta_y = (self.vx * math.sin(self.th) + self.vy * math.cos(self.th))*dt
        delta_th = self.vth*dt
        
        self.th += delta_th
        self.x += delta_x
        self.y += delta_y
        
        # self.get_logger().info(f'\n self.x:{self.x}, self.y:{self.y}, self.th:{self.th}')
        self.last_time = self.current_time

        odom_quat = Quaternion()
        # magnitude = math.sqrt(odom_quat.x**2 + odom_quat.y**2 
        #                       + math.sin(self.th / 2)**2 + math.cos(self.th / 2)**2)

        # 归一化四元数
        # odom_quat.z /= magnitude
        # odom_quat.w /= magnitude

        odom_quat.z = math.sin(self.th / 2)
        odom_quat.w = math.cos(self.th / 2)

        # Publish the transform
        odom_trans = TransformStamped()
        odom_trans.header.stamp = self.current_time.to_msg()
        odom_trans.header.frame_id = 'odom'
        odom_trans.child_frame_id = 'base_link'
        odom_trans.transform.translation.x = self.x
        odom_trans.transform.translation.y = self.y
        odom_trans.transform.translation.z = 0.0
        odom_trans.transform.rotation = odom_quat

        self.odom_broadcaster.sendTransform(odom_trans)

        # Publish the odometry message
        odom = Odometry()
        odom.header.stamp = self.current_time.to_msg()
        odom.header.frame_id = 'odom'
        odom.pose.pose.position.x = self.x
        odom.pose.pose.position.y = self.y
        odom.pose.pose.position.z = 0.0
        odom.pose.pose.orientation = odom_quat
        odom.child_frame_id = 'base_link'
        odom.twist.twist.linear = Vector3(x=self.vx, y=self.vy, z=0.0)
        odom.twist.twist.angular = Vector3(x=0.0, y=0.0, z=self.vth)
        self.odom_pub.publish(odom)
        


class Joy_subscriber(Node):

    def __init__(self):
        super().__init__('joy_subscriber')
        self.subscription = self.create_subscription(
            Joy,
            'joy',
            self.listener_callback,
            10)
        self.subscription

    def listener_callback(self, data):
        global vel_msg, mode_selection

        if(data.buttons[0] == 1):   # in-phase # A button of Xbox 360 controller
            mode_selection = 2
        elif(data.buttons[4] == 1): # opposite phase # LB button of Xbox 360 controller
            mode_selection = 1
        elif(data.buttons[5] == 1): # pivot turn # RB button of Xbox 360 controller
            mode_selection = 3
        else:
            mode_selection = 4

        vel_msg.linear.x = data.axes[1]*7.5
        vel_msg.linear.y = data.axes[0]*7.5
        vel_msg.angular.z = data.axes[3]*10



if __name__ == '__main__':
    rclpy.init(args=sys.argv)
    
    commander = Commander()#需要, 缺少无法发布odom话题
    # joy_subscriber = Joy_subscriber()
    odom_pub = OdometryPublisher()

    executor = rclpy.executors.MultiThreadedExecutor()
    executor.add_node(commander)
    executor.add_node(odom_pub)
    # executor.add_node(joy_subscriber)
    
    executor_thread = threading.Thread(target=executor.spin, daemon=True)
    executor_thread.start()
    rate = commander.create_rate(2)
    try:
        while rclpy.ok():
            rate.sleep()
    except KeyboardInterrupt:
        pass
    
    rclpy.shutdown()
    executor_thread.join()


完整视频演示

在这里插入图片描述

四轮转向轮式里程计设计 G果-CSDN