【ROS2總結】點激光掃描儀數據發佈(驅動編寫)


> 說明:
> 本文首發於 Playfish Blog,轉載請保留鏈接。

前言


在上一篇博客中,介紹瞭如何在ROS1下編寫激光測距傳感器節點,在此基礎上我們下面講述如何在ROS2下編程來創建激光測距傳感器節點。

在接下來的博客中將根據本身經驗來編寫一些ROS相關內容,權當是作爲記憶來分享。


ROS2驅動——點激光掃描儀



下面將講述使用ROS官方未有的傳感器來作爲樣例,我使用的是萊旭光電的CHT-10點激光傳感器作爲 介紹,首先這款傳感器通信協議部分比較簡單,基本上打開串口就可以讀出數據,在將數據進行轉換就可以得到想要的距離,比較方便,免去複雜的部分,對教程也有幫助,畢竟教程主要講述的是ROS2驅動部分。
爲了不妨礙與上一篇的銜接問題,這裏我們重新開始從頭介紹,大家就可以不用看上一篇博客。


相關ROS2驅動代碼可以在github上找到進行下載:https://github.com/Playfish/cht10_node/tree/ros2


CHT-10是點激光,掃描範圍是0.05-10m,真實的應該是0.15-10m,10m未測試,不過0.15盲區還是有的。

下圖是CHT-10的通信協議部分介紹:



可以看到想要的數據在9~12位(四字節),得到的數據需要除以1000才能得到真實的距離(米),ROS消息中基本單位是米。

### 消息選定

關於選擇什麼消息作爲該傳感器的ROS2消息,這個相對來說不是很重要,不過作爲通用項來說,儘量向通用靠近(即使用ROS官方消息定義,而不是自己定義消息)。目前激光消息的話,有兩個選擇,第一個是sensor_msgs/LaserScan消息類型,不過該消息類型適合有角度的傳感器,即180°或360°的2D雷達或激光,而目前使用的激光是單點類型,和激光筆差不多,只有一個點,那麼可以選擇sensor_msgs/Range消息類型,該消息類型適用於超聲波傳感器、紅外傳感器單點類型,CHT-10就用這個類型。
注意:在ROS2中,頭文件將採用<消息包>/<通信機制>/消息名稱.hpp形式,拿本教程爲例,
ROS1ROS2
sensor_msgs/Range.hsensor_msgs/msg/range.hpp


關於該ROS2消息,官方暫未提供API文檔,大家可以查看該文件來查看API,基本語法與ROS1一致。

準備工作


傳感器選型選定、通信消息選定,那麼接下來還需要做:

  • Frame_id:Frame在ROS1中作用至關重要,消息將和TF綁定纔可以讀取數據,在這裏作爲通用可配置,暫定內容爲:laser,用戶可自定義設置(通過ROS2 Parameters設置)。
  • 串口:避免和其他傳感器串口衝突,因此在這裏預留出一個串口設置參數,用戶可以自定義設置(通過ROS2 Parameters設置)。
  • 話題:消息內容需要通過話題發佈,並且話題需要唯一,不然容易崩潰,在這裏選擇話題爲“range”

當然以上部分可以不考慮,這個是作爲通用型需要考慮的,暫時可以先忘記以上部分,下面開啓代碼部分

測試代碼編寫


在這部分,主要先編寫測試代碼,如果測試代碼無問題,就可以寫到node中,測試代碼和ROS無關係,只是通過串口讀取數據,並且在終端顯示出來,整體思路如下:

傳感器選擇 -> 測試程序測試(類似串口助手) -> ROS2 node綁定(將測試程序讀取部分加入到ROS2中)

下面是測試程序部分,也可以在cht10_node/test中找到,名爲test_cht10.cpp
#include <string>
#include <cht10_node/serial_func.hpp>
#include <cstdlib>

#include <iostream>
#include <stdint.h>
#define BUFSIZE 17

int main(int argc, char** argv){
  cht10_serial_func::Cht10Driver cht10driver_;

  std::string serialNumber_;
  serialNumber_ = "/dev/ttyUSB0";
  int baudRate_ = 115200;

  std::stringstream ostream;
  int fd, len, rcv_cnt;
  bool success_flag;
  char buf[40], temp_buf[BUFSIZE],result_buf[BUFSIZE];
  unsigned int laser_data=0;
  char data_buf[4];
  rcv_cnt = 0;
  success_flag = false;
  memset(buf, 0xba, sizeof(buf));
  memset(temp_buf, 0xba, sizeof(temp_buf));
  memset(result_buf, 0xba, sizeof(result_buf));

  fd = open(serialNumber_.c_str(), O_RDWR | O_NOCTTY | O_NDELAY );
  if(fd < 0){
    std::cout<<"Open Serial: "<<serialNumber_.c_str()<<" Error!";
    exit(0);
  }
 
  cht10driver_.UART0_Init(fd,baudRate_,0,8,1,'N');
  while(1){
    len = cht10driver_.UART0_Recv(fd, buf,40);
    if(len>0){
      for(int i = 0; i < len; i++){
        if(rcv_cnt<=BUFSIZE){  
          result_buf[rcv_cnt++] = buf[i];
          if(rcv_cnt == BUFSIZE){
            success_flag = true;
          }
        }//end if
        else{
          /****
          *  checkout received data
          */
          success_flag = false;

          for(int count = 0; count < 4; count++){
            data_buf[count] = result_buf[9+count];
          }
          sscanf(data_buf, "%x", &laser_data);
          std::cout<<"sensor data:"<<laser_data<<", Distance: "<<(double)laser_data/1000<<std::endl;
          /****
           *  data writing end
           */
          if('~' == buf[i]){
            rcv_cnt = 0;
            result_buf[rcv_cnt++] = buf[i];
          }
        }//end else
      }//end for    
    }
  }
}

我將串口部分封裝成一個類名爲Cht10Driver,該類包含串口讀/寫部分、初始化。

通過使用ament build可以得到一個名爲test_cht10的可執行文件,使用./test_cht10可以運行,將CHT-10插入到電腦上,並且電腦只有一個ttyUSB0,就可以讀取數據,數據將顯示爲sensor data: 傳感器毫米數據, Distance: 傳感器米數據。

ROS2驅動編寫


上部分講述了測試程序編寫,主要作用是通過串口讀取傳感器數據,那麼得到傳感器數據之後,就可以將傳感器數據填充到ROS消息中,然後通過話題形式發佈出去,如下是將傳感器數據填充到ROS消息並生成節點的主要部分:

完整代碼可以查看: cht10_node.cpp

/**
 * @file /cht10_node/src/cht10/cht10_node.cpp
 *
 * @brief Implementation for dirver with read data from Cht10 nodelet
 *
 * @author Carl
 *
 **/

/*****************************************************************************
 ** Includes
 *****************************************************************************/
#include <chrono>
#include <cstdio>
#include <memory>
#include <string>

#include <rclcpp/rclcpp.hpp>
#include <rcutils/logging_macros.h>

#include <functional>

#include <std_msgs/msg/string.hpp>
#include <sensor_msgs/msg/range.hpp>

#include <cht10_node/cht10_node.h>

#define ROS_ERROR RCUTILS_LOG_ERROR
#define ROS_INFO RCUTILS_LOG_INFO
#define ROS_ERROR_THROTTLE(sec, ...) RCUTILS_LOG_ERROR_THROTTLE(RCUTILS_STEADY_TIME, sec, __VA_ARGS__)

using namespace cht10_serial_func;

  Cht10Func::Cht10Func(rclcpp::Node::SharedPtr & node):node_(node),
serialNumber_("/dev/USB0"),
frame_id("laser"){

    msg_ = std::make_shared<sensor_msgs::msg::Range>();

    scan_pub_ = node_->create_publisher<sensor_msgs::msg::Range>("range");

    parameter_service_ = std::make_shared<rclcpp::ParameterService>(node_);

    node_->get_parameter("serialNumber", serialNumber_);
    node_->get_parameter("baudRate", baudRate_);
    node_->get_parameter("frame_id", frame_id);

    rcv_cnt = 0;
    success_flag = 0;

    fd = open(serialNumber_.c_str(), O_RDWR | O_NOCTTY | O_NDELAY );
    if(fd < 0){
      ROS_ERROR("Open Serial: %s Error!",serialNumber_.c_str());
      exit(0);
    }

    memset(buf, 0, sizeof(buf));
    memset(temp_buf, 0, sizeof(temp_buf));
    memset(result_buf, 0, sizeof(result_buf));
    Cht10driver_.UART0_Init(fd,baudRate_,0,8,1,'N');
    ROS_INFO("Open serial: [ %s ], successfully, with idex: %d.", serialNumber_.c_str() ,fd);

    update();
  }

  Cht10Func::~Cht10Func(){
  }

  double Cht10Func::data_to_meters(unsigned int &data, int scale){
    return (double)data/scale;
  }

  void Cht10Func::publish_scan(double nodes, builtin_interfaces::msg::Time start,
                    std::string frame_id){

    float final_range;
    std::shared_ptr<sensor_msgs::msg::Range> range_msg;
    range_msg->field_of_view = 0.05235988;
    range_msg->max_range = 10.0;
    range_msg->min_range = 0.05;
    range_msg->header.frame_id = frame_id;
    range_msg->radiation_type = sensor_msgs::msg::Range::INFRARED;
    if(nodes > range_msg->max_range){
      final_range = std::numeric_limits<float>::infinity();
    }else if(nodes < range_msg->min_range){
      final_range = -std::numeric_limits<float>::infinity();
    }else{
      final_range = nodes;
    }
    range_msg->header.stamp = start;
    range_msg->range = final_range;

    scan_pub_->publish(range_msg);

  }

  bool Cht10Func::get_scan_data(){
    len = Cht10driver_.UART0_Recv(fd, buf,40);
    if(len>0){
      for(int i = 0; i < len; i++){
        if(rcv_cnt<=BUFSIZE){  
          result_buf[rcv_cnt++] = buf[i];
          if(rcv_cnt == BUFSIZE){
            success_flag = true;
          }
        }//end if
        else{
          /****
          *  checkout received data
          */
          success_flag = false;
           for(int count = 0; count < 4; count++){
            data_buf[count] = result_buf[9+count];
          }
          sscanf(data_buf, "%x", &laser_data);
          
          //std::cout<<"sensor data:"<<laser_data<<std::endl;

          /****
           *  data writing end
           */
          if('~' == buf[i]){
            rcv_cnt = 0;
            result_buf[rcv_cnt++] = buf[i];
          }
        }//end else
      }//end for    
    }
    return success_flag;
  }

  void Cht10Func::update(){
    rcv_cnt = 0;
    success_flag = 0;
    laser_data = 0;
    memset(buf, 0, sizeof(buf));
    memset(temp_buf, 0, sizeof(temp_buf));
    memset(result_buf, 0, sizeof(result_buf));
    ROS_INFO("Begin receive data from %s, with idex: %d.",serialNumber_.c_str(),fd);
    fd = open(serialNumber_.c_str(), O_RDWR | O_NOCTTY | O_NDELAY );
    Cht10driver_.UART0_Init(fd,baudRate_,0,8,1,'N');
    // Create a function for when messages are to be sent.
    auto publish_message =
      [this]() -> void
      {
//        start_scan_time =
        rclcpp::Time(start_scan_time);
        success_flag = get_scan_data();
          
        //Send data
        publish_scan(data_to_meters(laser_data,SCALE),
                         start_scan_time, frame_id);
        countSeq++;
      };

    ROS_INFO("Shotdown and close serial: %s.", serialNumber_.c_str());
    Cht10driver_.UART0_Close(fd);

    // Use a timer to schedule periodic message publishing.
    timer_ = node_->create_wall_timer(300ms, publish_message);
  }


其中get_scan_data()函數,就是測試代碼內容,得到數據後將數據發送到publish_scan()函數中,就可以發佈傳感器數據了。

編寫可執行程序節點,創建名爲cht10_node_ros.cpp,內容爲:

#include <cht10_node/cht10_node.h>

int main(int argc, char * argv[])
{
  // Initialize any global resources needed by the middleware and the client library.
  // You must call this before using any other part of the ROS system.
  // This should be called once per process.
  rclcpp::init(argc, argv);

  // Create a node.
  rclcpp::Node::SharedPtr node = rclcpp::Node::make_shared("cht10_node");

  cht10_serial_func::Cht10Func cht(node);

  // spin will block until work comes in, execute work as it becomes available, and keep blocking.
  // It will only be interrupted by Ctrl-C.
  rclcpp::spin(node);

  rclcpp::shutdown();
  return 0;
}


測試


代碼完成後,按照格式進行編寫CMakeList、package文件,運行catkin_make就可以得到名爲cht10_node.so動態庫,運行驅動節點,命令如下:

ros2 run cht10_node cht10_node_ros 
如果一切政策,運行ros2 topic list 可以得到/range話題。

使用ros2 topic echo /range就可以得到傳感器的ROS2消息內容,包括傳感器距離。

目前ROS2功能還未完全開發完畢,一些圖形化還需藉助ROS1來完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章