ROS连接STM32

从本章开始,我们就要来对我们的ros上位机进行开发啦! 首先我们需要学习如何通过串口让ROS和STM32进行通信

创建ros工作空间

mkdir -p heima_ws/src

编译工作空间

cd heima_ws
catkin_make

创建包

cd src
catkin_create_pkg zxcar_driver roscpp rospy rosmsg

接收消息

我们首先在Ros中构建一个serial对象,设定相应的参数完成对象的初始化

ser = serial.Serial(port="/dev/ttyUSB0", baudrate=115200)

if not ser.isOpen():
    ser.open()

经过上面的步骤,我们就已经把串口对象给构造好啦!接下来,我们就可以读取STM32所发送过来的数据啦!

只要有数据过来,我们就立马把它读到队列中存起来,并且启动一个线程去解析队列中的数据

    recv_queue = Queue.Queue()
    threading.Thread(target=do_recv).start()
    while not rospy.is_shutdown():
        buff = ser.read(1)
        recv_queue.put(buff)

接下来,我们就可以开始来解析数据啦! 解析数据我们需要按照如下步骤进行:

  1. 找到帧头0
  2. 找到帧头1
  3. 找到类型
  4. 找到数据长度
  5. 根据数据长度读取数据部分
def do_recv():
    """
        找到一帧完整数据 recv_queue中查找
        1. 查找帧头0
        2. 查找帧头1
        3. 查找帧类型
        4. 查找帧的长度
        5. 根据帧长度,读取帧数据
        完整的帧数据
    """
    while True:
        buff = recv_queue.get();
        value = bytearray(buff)[0]

        if value == 0xce: # 找到帧头0
            # 若找到帧头0,则继续判断帧头1
            value = bytearray(recv_queue.get())[0]
            if value == 0xfa: # 若判断成功,head0和head1匹配成功,意味着帧头完全匹配
                # 开始读类型
                ext_type = bytearray(recv_queue.get())[0]
                # 读帧数据长度
                ext_len = bytearray(recv_queue.get())[0]
                # 读数据
                ext_data = []
                while len(ext_data) < ext_len:
                    # 不断往后读取数据
                    value = bytearray(recv_queue.get())[0]
                    ext_data.append(value);

                # 判断帧尾数据
                if ext_data[-1] != 0xad:
                    rospy.logerr("error tail:{}".format(ext_data[-1]))
                else:
                    # 开始解析数据
                    do_parse(ext_type,ext_len,ext_data);

解析数据,并将解析出来的数据封装成消息,发送出去

def do_parse(ext_type,ext_len,ext_data):
    # 将ext_data 封装成完整的数据帧
    ext_data.insert(0, ext_len);
    ext_data.insert(0, ext_type);
    ext_data.insert(0, 0xfa);
    ext_data.insert(0,0xce);
    # CE FA 03 18    76 0E    5C 03 F0 FF 88 3C      E0 FF A2 00 84 00    00 A2 FF E0 00 84      F4 01     20 03
    # 根据数据帧的类型的类型来做对应的解析  0x01 线速度  0x02 电池
    if ext_type == 0x03:
        # 对数据进行拆包
        # 温度
        temperature = struct.unpack('h',bytearray(ext_data[4:6]))[0]

        # 加速度
        ax = struct.unpack('h',bytearray(ext_data[6:8]))[0]
        ay = struct.unpack('h',bytearray(ext_data[8:10]))[0]
        az = struct.unpack('h',bytearray(ext_data[10:12]))[0]
        # 角速度
        gx = struct.unpack('h', bytearray(ext_data[12:14]))[0]
        gy = struct.unpack('h', bytearray(ext_data[14:16]))[0]
        gz = struct.unpack('h', bytearray(ext_data[16:18]))[0]
        # 地磁
        mx = struct.unpack('h', bytearray(ext_data[18:20]))[0]
        my = struct.unpack('h', bytearray(ext_data[20:22]))[0]
        mz = struct.unpack('h', bytearray(ext_data[22:24]))[0]
        # 速度
        velocity = struct.unpack('h', bytearray(ext_data[24:26]))[0]
        angular = struct.unpack('h', bytearray(ext_data[26:28]))[0]

        # 发布陀螺仪的数据
        imu = Imu()
        # 根据芯片手册设定缩放系数
        accel_ratio = 1/16384.0
        imu.linear_acceleration.x = ax*accel_ratio;
        imu.linear_acceleration.y = ay*accel_ratio;
        imu.linear_acceleration.z = az*accel_ratio;

        # °/s 转成弧度/s
        gyro_ratio = 1/65.5/(180/3.1415926);
        imu.angular_velocity.x = gx*gyro_ratio;
        imu.angular_velocity.y = gy*gyro_ratio;
        imu.angular_velocity.z = gz*gyro_ratio;

        imuPublisher.publish(imu)

        # 发布地磁的数据
        mag = MagneticField();
        mag.magnetic_field.x = mx;
        mag.magnetic_field.y = my;
        mag.magnetic_field.z = mz;

        magPublisher.publish(mag);

        # print(velocity,angular)
        # 将小车当前的线速度和角速度发布出去
        twist = Twist()
        twist.linear.x = velocity/1000.0
        twist.angular.z = angular/1000.0
        velPublisher.publish(twist);

发送消息

当大家能够成功接收消息之后,接下来我们就需要来编写消息发送的功能.其实ROS发送给单片机的消息大多是想让单片机去干某件简单的事情,针对我们的机器人移动底盘,ros要发送给它的消息就是小车移动的线速度角速度

这里只需要把消息封装,然后再使用ser.write()函数即可

def cmd_vel_callback(msg):

    rospy.loginfo("cmd_vel:{},{}".format(msg.linear.x,msg.angular.z))
    #     帧头0  帧头1  帧类型
    cmd = [0xce,0xfa,0x05]
    # 添加帧长度
    cmd.append(0x04);

    # 下发线速度和角速度
    velocity = msg.linear.x;
    angular = msg.angular.z;

    # 对数据进行拆解
    velocity_params = bytearray(struct.pack('h',int(velocity*1000)))
    angular_params = bytearray(struct.pack('h',int(angular*1000)))

    cmd.append(velocity_params[0])
    cmd.append(velocity_params[1])

    cmd.append(angular_params[0]);
    cmd.append(angular_params[1]);
    # 添加帧尾
    cmd.append(0xad);

    # [206, 250, 5, 4, 250, 0, 244, 1]
    # 将数据发送给串口
    ser.write(cmd)

执行键盘控制节点,根据提示下发线速度和角速度信息

rosrun teleop_twist_keyboard teleop_twist_keyboard.py 

如果小车没有反应,则观察下位机中的led灯是否在闪烁

完整的示例代码

#! /usr/bin/env python
# encoding: utf-8
import rospy
import serial
import Queue
import threading
import struct
from geometry_msgs.msg import Twist
from sensor_msgs.msg import Imu,MagneticField


def do_parse(ext_type,ext_len,ext_data):
    # 将ext_data 封装成完整的数据帧
    ext_data.insert(0, ext_len);
    ext_data.insert(0, ext_type);
    ext_data.insert(0, 0xfa);
    ext_data.insert(0,0xce);
    # CE FA 03 18    76 0E    5C 03 F0 FF 88 3C      E0 FF A2 00 84 00    00 A2 FF E0 00 84      F4 01     20 03
    # 根据数据帧的类型的类型来做对应的解析  0x01 线速度  0x02 电池
    if ext_type == 0x03:
        # 对数据进行拆包
        # 温度
        temperature = struct.unpack('h',bytearray(ext_data[4:6]))[0]

        # 加速度
        ax = struct.unpack('h',bytearray(ext_data[6:8]))[0]
        ay = struct.unpack('h',bytearray(ext_data[8:10]))[0]
        az = struct.unpack('h',bytearray(ext_data[10:12]))[0]
        # 角速度
        gx = struct.unpack('h', bytearray(ext_data[12:14]))[0]
        gy = struct.unpack('h', bytearray(ext_data[14:16]))[0]
        gz = struct.unpack('h', bytearray(ext_data[16:18]))[0]
        # 地磁
        mx = struct.unpack('h', bytearray(ext_data[18:20]))[0]
        my = struct.unpack('h', bytearray(ext_data[20:22]))[0]
        mz = struct.unpack('h', bytearray(ext_data[22:24]))[0]
        # 速度
        velocity = struct.unpack('h', bytearray(ext_data[24:26]))[0]
        angular = struct.unpack('h', bytearray(ext_data[26:28]))[0]

        # 发布陀螺仪的数据
        imu = Imu()
        # 根据芯片手册设定缩放系数
        accel_ratio = 1/16384.0
        imu.linear_acceleration.x = ax*accel_ratio;
        imu.linear_acceleration.y = ay*accel_ratio;
        imu.linear_acceleration.z = az*accel_ratio;

        # °/s 转成弧度/s
        gyro_ratio = 1/65.5/(180/3.1415926);
        imu.angular_velocity.x = gx*gyro_ratio;
        imu.angular_velocity.y = gy*gyro_ratio;
        imu.angular_velocity.z = gz*gyro_ratio;

        imuPublisher.publish(imu)

        # 发布地磁的数据
        mag = MagneticField();
        mag.magnetic_field.x = mx;
        mag.magnetic_field.y = my;
        mag.magnetic_field.z = mz;

        magPublisher.publish(mag);

        # print(velocity,angular)
        # 将小车当前的线速度和角速度发布出去
        twist = Twist()
        twist.linear.x = velocity/1000.0
        twist.angular.z = angular/1000.0
        velPublisher.publish(twist);

def do_recv():
    """
        找到一帧完整数据 recv_queue中查找
        1. 查找帧头0
        2. 查找帧头1
        3. 查找帧类型
        4. 查找帧的长度
        5. 根据帧长度,读取帧数据
        完整的帧数据
    """
    while True:
        buff = recv_queue.get();
        value = bytearray(buff)[0]

        if value == 0xce: # 找到帧头0
            # 若找到帧头0,则继续判断帧头1
            value = bytearray(recv_queue.get())[0]
            if value == 0xfa: # 若判断成功,head0和head1匹配成功,意味着帧头完全匹配
                # 开始读类型
                ext_type = bytearray(recv_queue.get())[0]
                # 读帧数据长度
                ext_len = bytearray(recv_queue.get())[0]
                # 读数据
                ext_data = []
                while len(ext_data) < ext_len:
                    # 不断往后读取数据
                    value = bytearray(recv_queue.get())[0]
                    ext_data.append(value);

                # 判断帧尾数据
                if ext_data[-1] != 0xad:
                    rospy.logerr("error tail:{}".format(ext_data[-1]))
                else:
                    # 开始解析数据
                    do_parse(ext_type,ext_len,ext_data);


def cmd_vel_callback(msg):

    rospy.loginfo("cmd_vel:{},{}".format(msg.linear.x,msg.angular.z))
    #     帧头0  帧头1  帧类型
    cmd = [0xce,0xfa,0x05]
    # 添加帧长度
    cmd.append(0x04);

    # 下发线速度和角速度
    velocity = msg.linear.x;
    angular = msg.angular.z;

    # 对数据进行拆解
    velocity_params = bytearray(struct.pack('h',int(velocity*1000)))
    angular_params = bytearray(struct.pack('h',int(angular*1000)))

    cmd.append(velocity_params[0])
    cmd.append(velocity_params[1])

    cmd.append(angular_params[0]);
    cmd.append(angular_params[1]);
    # 添加帧尾
    cmd.append(0xad);

    # [206, 250, 5, 4, 250, 0, 244, 1]
    # 将数据发送给串口
    ser.write(cmd)


def onShutdown():
    rospy.loginfo("stop car_driver...")
    twist = Twist()
    twist.linear.x = 0
    twist.angular.z = 0
    cmd_vel_callback(twist)

if __name__ == '__main__':
    # 创建ros节点
    node_name="car_driver_node";
    rospy.init_node(node_name);

    rospy.on_shutdown(onShutdown)

    # 订阅/cmd_vel
    rospy.Subscriber("/cmd_vel",Twist,cmd_vel_callback);
    # 发布者
    velPublisher = rospy.Publisher("/zxcar/get_vel",Twist,queue_size=50);
    # 发布陀螺仪的数据
    imuPublisher = rospy.Publisher("/zxcar/imu",Imu,queue_size=100);
    # 地磁的数据
    magPublisher = rospy.Publisher("/zxcar/magnetic",MagneticField,queue_size=50)

    port = rospy.get_param("~port", "/dev/ttyUSB0")

    # 创建serial 串口通讯对象
    ser = serial.Serial(port=port,baudrate=115200)

    # 判断串口是否打开成功
    if not ser.isOpen():
        ser.open();

    # 创建消息队列,专门用于存储读到的数据
    recv_queue = Queue.Queue();

    # 开启线程,专门用于取队列中查找帧数据
    threading.Thread(target=do_recv).start();
    try:
        # 读取串口中的数据
        while not rospy.is_shutdown():
            buff = ser.read();
            # 读到数据,立马存起来
            recv_queue.put(buff);
        rospy.spin();
    except Exception as e:
        rospy.logerr("car_driver:{}".format(e))

        #exit();
    ser.close();