自动驾驶消息传输机制-LCM

需要用到LCM消息通讯,遂研究下。

这里写目录标题

  • 1 LCM简介
  • 2. LCM源码分析
  • 3 LCM C++教程与实例
    • 3.1 安装配置及介绍
    • 3.2 创建类型定义
    • 3.3 初始化LCM
    • 3.4 发布publish一个消息
    • 3.5 订阅和接收一个消息
    • 3.6 LCM进程间通讯
    • 3.7 注意事项?
      • 3.7.1 当数据结构定义的是数组时,生成的头文件中会用vector来定义这个数据。进行赋值时需要先 resize vector 的长度。不然会填充数据时会报错。
  • 4 LCM 发 收 录 读
    • 4.1 发、收
    • 4.2 录、读
  • 5 ROS与LCM

1 LCM简介

LCM(Lightweight Communications and Marshalling)是一组用于消息传递和数据编组的库和工具,其基于UDP传输的属性,传输速度较快,其目标是高带宽和低延迟的实时系统。它提供了一种发布/订阅消息传递模型以及带有各种编程语言C++、Java、python等应用程序绑定的自动编组/解组代码生成,LCM通过将消息封装在不同的Channel中进行通信,这点类似于ROS中的Topic。
适用于高速自动驾驶场景的LCM通信协议,其特点是轻量化、传输速度快,易封装。
LCM是去ROS化的优秀选择

LCM官方:Github:lcm-proj/lcm
LCM论文:LCM: Lightweight Communications and Marshalling
LCM技术报告:LCM技术报告

2. LCM源码分析

LCM的底层代码都是使用C语言进行编写的,但是充斥着面向对象的风格,甚至实现了一个虚函数表来抽象不同通信通信方式的底层实现。因此,以面向对象的方式来分析代码,忽略C语言实现细节,直接将对某个结构体进行操作的独立函数视为该结构体的成员函数进行分析,且忽略指针,编写类图来分析C代码。

LCM提供了多种底层通信方式,包括tcp、udp、memq、file等,通过解析url来决定使用何种通信方式。主结构体为lcm_t。其类图如下图所示
在这里插入图片描述
我们可以看到一共有五种通信方式分别是lcm_udpm_t,lcm_memq_t,lcm_mpudpm_t,lcm_tcpq_t,lcm_logprov_t。

内存队列通信 lcm_memq_t
memq的通信方式用内存队列进行通信(Memory Queue),只能进行不同线程间的通信,所有的消息都在同一个内存队列中,所有的异步通信方式是linux的管道(pipe)。每个channel的所有handler顺序执行,当所有的handler执行完毕后,lcm负责销毁msg的内存。此外,不同channel也是顺序执行的,因为lcm要求同时只能有一个线程在执行lcm_handle。这就意味着所有handler都是顺序执行的。如果某个handler运行的时间超长,那么第接下来的handler拿到消息的时候,消息可能已经失去时效性。如果要实现不同的handler并行执行,用户需要书写不少额外的代码。

#include <chrono>
#include <iostream>
#include <lcm/lcm-cpp.hpp>
#include <thread>

#include "exlcm/example_t.hpp"

void publisher(lcm::LCM &lcm_in)
{
    for (int i = 0; i < 10; ++i) 
    {
        exlcm::example_t my_data;
        my_data.timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
                                std::chrono::steady_clock::now().time_since_epoch())
                                .count();
        my_data.num_ranges = 15;
        my_data.ranges.resize(my_data.num_ranges);
        for (int i = 0; i < my_data.num_ranges; i++)
            my_data.ranges[i] = i;

        my_data.name = "example string";
        my_data.enabled = true;
        lcm_in.publish("EXAMPLE", &my_data);
        lcm_in.publish("LONG", &my_data);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void example_subscriber(lcm::LCM &lcm_in, int id)
{
    lcm::LCM::HandlerFunction<exlcm::example_t> func;
    func = [id](const lcm::ReceiveBuffer *rbuf, const std::string &channel,
                const exlcm::example_t *msg) 
    {
        const auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
                             std::chrono::steady_clock::now().time_since_epoch())
                             .count();
        std::cout <<"EXAMPLE " << id << " received message at  " << msg->timestamp << ". Time passed "
                  << now - msg->timestamp << "ms" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    };
    lcm_in.subscribe("EXAMPLE", func);
    while (0 == lcm_in.handle()) {
        std::cout << "EXAMPLE thread " << id << " handled the message." << std::endl;
    }
}

void long_subscriber(lcm::LCM &lcm_in)
{
    lcm::LCM::HandlerFunction<exlcm::example_t> func;
    func = [](const lcm::ReceiveBuffer *rbuf, const std::string &channel,
                const exlcm::example_t *msg) 
    {
        const auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
                             std::chrono::steady_clock::now().time_since_epoch())
                             .count();
        std::cout << "LONG received message at  " << msg->timestamp << ". Time passed "
                  << now - msg->timestamp << "ms" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    };
    lcm_in.subscribe("LONG", func);
    while (0 == lcm_in.handle()) 
    {
        std::cout << "LONG thread " << " handled the message." << std::endl;
    }
}

int main(int argc, char **argv)
{
    lcm::LCM lcm_in("memq://");
    if (!lcm_in.good())
        return 1;

    std::thread pub_thread(publisher, std::ref(lcm_in));
    std::thread sub_thread_1(example_subscriber, std::ref(lcm_in), 1);
    std::thread sub_thread_2(example_subscriber, std::ref(lcm_in), 2);
    std::thread sub_thread_3(long_subscriber, std::ref(lcm_in));

    pub_thread.join();
    sub_thread_1.join();
    sub_thread_2.join();
    return 0;
}

文件通信 lcm_logprov_t
该种通信方式利用文件进行通信,可以读也可以写,其实就是log replay的底层实现。一共有读写两种模式。写模式比较简单,就是往文件中写消息。读模式比较复杂,除了主线程,还有一个读文件线程,两个线程通过管道进行通信。主线程读完一个event之后,会根据log播放速度计算下一个event的应该触发的时间,让读线程sleep一段时间,sleep结束后就触发下一个event。此外,它还支持从某一个时刻开始播放log。

tcp通信 lcm_tcpq_t
采用TCP套接字进行通信。其C/C++代码只提供了TCP的客户端,publish/subscribe方法都会尝试和服务器端建立连接,然后进行magic number校验,以确认是LCM的服务器。publish方法会直接发送消息。而subscribe方法则是将自己监听那些消息告诉给服务器,但是客户端并不检查是否成功发送注册小写给服务器端,这一点非常奇怪。由于每次handle或者publish消息都需要重新建立TCP连接,这会大大限制低延时和高速通信,不推荐使用。lcm-jave提供了服务器实现,可以参考。

udp通信 lcm_udpm_t
采用UDP套接字继续通信。采用UDP组播的方式进行通信,组播的方式在有多个接受者的情况下只复制一份数据,提高传送效率。它是内部是双线程方案,一个主线程,一个reader线程,两个线程通过pipe通信,一个管道被用来通知主线程消息到达,另一个被主线程用来quit读线程。主线程负责发消息和处理正常的lcm逻辑,还可以向reader线程发送quit的信号。reader线程负责不断地接收并组装消息,一旦消息组装完毕且主线程没有消息可用,那么通知主线程处理消息。

发送消息 Publish
一次只允许一个线程发送消息。如果消息的大小很小,直接可以用一个packet发送完,相对简单。如果消息比较大,需要将消息切割为很多个fragment,fragment会打上序列号,接收端通过序列号组装消息。第一个fragment比较特别,有channel信息。

订阅消息 Subscribe
订阅消息就是加入组播群中去。它首先会检查reader线程是否被建立,如果没有建立,有建立reader线程。创建套接字然后加入组播群。

读取消息 Read
每读完一个UDP packet,如果是短消息,直接将其读取放入message queue;如果是长消息,那么读取的是消息的一个fragment,会将其送入一个缓存结构中,该结构提前分配好消息的内存,只需往其中memcpy即可,如果收到一个不是同一个消息的fragment,直接将老的弃掉。每一个发送消息的端口都有自己的消息缓存结构,这样就不会冲突。一但消息接受完毕,就会将其送入message queue中。为了避免调用过多的malloc,它还设计了一个ringbuffer来快速分配内存。

ring buffer的设计
它设计了一个很巧妙的ring buffer,提前分配好所需内存空间,然后在该内存中分配所需数据块,还提供了简单的数据校检,下图是它的类图。
在这里插入图片描述
RingBuffer的内存状态如下图所示:
在这里插入图片描述
多端口UDP通信 lcm_mpudpm_t
在上一个lcm_udpm_t中,所有channel都通过同一个端口进行收发,但是一个进程所需的消息经常来自于多个端口。lcm_mpudpm_t允许我们使用多个端口进行UDP通信。我们需要为每个channel指定一个端口,一个端口可以对应多个channel。我们不需要提前指定每个channel对应的端口,该通信方式会通过channel名的hash来选定,如果哈希冲突,那么同一个端口就会发送多个channel消息,而且会将该消息传播给其他客户端进行同步。其实就是实现了端口自发现功能。

发送消息 Publish
实际消息发送和lcm_udpm_t一样,但是添加了一个端口分配和同步。根据哈希值来分配端口。然后将新的channel和端口的映射消息组播给其他节点,进行同步。

订阅消息 Subscribe
和lcm_udpm_t不同的地方是如果该channel没有分配端口,它会发送组播消息CHANNEL_TO_PORT_MAP_REQUEST_CHANNEL要求获得最新的端口映射表,将订阅的操作缓存起来,其实就是询问有没有publisher在发送消息。直到获得最新的端口映射表之后,才会真正执行订阅操作。

读取消息 Read
它会逐一读取每个端口,然后组装消息,根据不同的消息类型进行不同操作,主要分为内部消息和普通消息。内部消息就是CHANNEL_TO_PORT_MAP_REQUEST_CHANNEL、CHANNEL_TO_PORT_MAP_UPDATE_CHANNEL和SELF_TEST_CHANNEL。

总结
至此,我们看完了lcm的通信架构,C/C++源码阅读完毕,lcm-spy等工具没有介绍。总的来说,这个框架简洁直观,代码的可读性高,功能齐全,实现了低延时的去中心化的点对点通信,支持序列化和反序列化代码自动生成,支持多种通信方式,甚至提供了节点自发现的功能,是一个极佳的轻量级通信架构。

3 LCM C++教程与实例

3.1 安装配置及介绍

在/usr/local/bin目录下安装lcm-gen, lcm-logger, lcm-logplayer, lcm-logplayer-gui, lcm-spy可执行程序
在/usr/local/lib目录下安装liblcm.la, liblcm.so, liblcm.so.1, liblcm.so.1.3.3库文件
在/usr/local/include目录下安装eventlog.h, lcm_coretypes.h, lcm-cpp.hpp, lcm-cpp-impl.hpp, lcm.h头文件配置头文件, 在home/igs/.profile中添加以下内容后, 重启:

#glib c++
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/include/glib-2.0:/usr/lib/x86_64-linux-gnu/glib-2.0/include
#lcm
#export CLASSPATH=.:$CLASSPATH:/program/3rd/lcm/lcm-1.2.1/lcm-java/build/lcm
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
#glib c
export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/include/glib-2.0:/usr/lib/x86_64-linux-gnu/glib-2.0/include

为lcm创建一个ld.so.conf文件
export LCM_INSTALL_DIR=/usr/local/lib
sudo sh -c “echo $LCM_INSTALL_DIR > /etc/ld.so.conf.d/lcm.conf”
更新sudo ldconfig
配置pkgconfig来查找lcm.pc
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$LCM_INSTALL_DIR/pkgconfig

C++ API 提供了三个类两个结构体封装LCM:

class lcm::LCM
struct lcm::ReceiveBuffer
class lcm::Subscription
struct lcm::LogEvent
class lcm::LogFile

3.2 创建类型定义

(type definition)
由于类型定义与编程语言类型无关,故第一步对所有编程语言都一样; 类型规则的定义与C语言非常像, 下面是一个名为example_t的示例类型, 创建一个名为 example_t.lcm 的文件

文件名: example_t.lcm
package exlcm;
struct example_t
{
int64_t timestamp;
double position[3];
double orientation[4];
int32_t num_ranges;
int16_t ranges[num_ranges];
string name;
boolean enabled;
}

更多类型规范,下表是基本类型规范:

typeDescription
int8_t8-bit signed integer
int16_t16-bit signed integer
int32_t32-bit signed integer
int64_t64-bit signed integer
float32-bit IEEE floating point value
double64-bit IEEE floating point value
stringUTF-8 string
booleantrue/false logical value
byte8-bit value
除此之外还可以定义数组;

生成特定编程语言的绑定(bindings):

lcm-gen -x example_t.lcm
运行lcm-gen -h 获取命令行帮助

运行之后,会在当前文件夹生成一个exlcm/example_t.hpp 的文件, 它包含了之前定义类型的同名类:

class example_t
{
    public:
        int64_t    timestamp;
        double     position[3];
        double     orientation[4];
        int32_t    num_ranges;
        std::vector< int16_t > ranges;
        std::string name;
        int8_t     enabled;
}

3.3 初始化LCM

任何使用LCM的应用程序第一步就是初始化LCM库, 下面是一个例子:

#include <lcm/lcm-cpp.hpp>
 
int main(int argc, char ** argv)
{
    lcm::LCM lcm;
    if(!lcm.good())
        return 1;
    // Your application goes here
    return 0;
}

3.4 发布publish一个消息

// send_message.cpp
#include <lcm/lcm-cpp.hpp>
#include "exlcm/example_t.hpp"
 
int main(int argc, char **argv)
{
    lcm::LCM lcm;
    if (!lcm.good())
        return 1;
 
    exlcm::example_t my_data;
    my_data.timestamp = 0;
 
    my_data.position[0] = 1;
    my_data.position[1] = 2;
    my_data.position[2] = 3;
 
    my_data.orientation[0] = 1;
    my_data.orientation[1] = 0;
    my_data.orientation[2] = 0;
    my_data.orientation[3] = 0;
 
    my_data.num_ranges = 15;
    my_data.ranges.resize(my_data.num_ranges);
    for (int i = 0; i < my_data.num_ranges; i++)
        my_data.ranges[i] = i;
 
    my_data.name = "example string";
    my_data.enabled = true;
 
    lcm.publish("EXAMPLE", &my_data);
 
    return 0;
}

lcm::LCM::publish() 会将数据序列化为字节流,并使用LCM将数据包传输到任何感兴趣的接收者, 字符串 EXAMPLE 是频道名称, 是随每个数据包一起发送的字符串.

3.5 订阅和接收一个消息

subscribe and receive
下面是一个接收 EXAMPLE 的例子:

// listener.cpp
#include <stdio.h>
 
#include <lcm/lcm-cpp.hpp>
#include "exlcm/example_t.hpp"
 
class Handler 
{
  public:
    ~Handler() {}
    void handleMessage(const lcm::ReceiveBuffer *rbuf, const std::string &chan,
                       const exlcm::example_t *msg)
    {
        int i;
        printf("Received message on channel \"%s\":\n", chan.c_str());
        printf("  timestamp   = %lld\n", (long long) msg->timestamp);
        printf("  position    = (%f, %f, %f)\n", msg->position[0], msg->position[1],
               msg->position[2]);
        printf("  orientation = (%f, %f, %f, %f)\n", msg->orientation[0], msg->orientation[1],
               msg->orientation[2], msg->orientation[3]);
        printf("  ranges:");
        for (i = 0; i < msg->num_ranges; i++)
            printf(" %d", msg->ranges[i]);
        printf("\n");
        printf("  name        = '%s'\n", msg->name.c_str());
        printf("  enabled     = %d\n", msg->enabled);
    }
};
 
int main(int argc, char **argv)
{
    lcm::LCM lcm;
    if (!lcm.good())
        return 1;
 
    Handler handlerObject;
    lcm.subscribe("EXAMPLE", &Handler::handleMessage, &handlerObject);
 
    while (0 == lcm.handle())//阻塞的
    {
        // Do nothing
    }
 
    return 0;
}

lcm用handle()函数来处理消息回调(接受到消息后就执行回调函数)
lcm.handle() 是阻塞的。只能放到单独的线程里执行。
lcm.handleTimeout(10) 可以超时后返回,然后执行后面的代码。设置的时间的单位是毫秒。

文件结构:

example_t.lcm
listener.cpp
Makefile
read_log.cpp
send_message.cpp

CMakeList.txt

ROS1?
find_package(lcm REQUIRED)
include(${LCM_USE_FILE})
target_link_libraries(${PROJECT_NAME} lcm)

ROS2?
set(dependencies
“geometry_msgs”
“nav_msgs”
“rclcpp”
“sensor_msgs”
“tf2”
“tf2_msgs”
“OpenCV”
“PCL”
# “lcm” #不要在ament_target_dependencies中加入lcm,不起作用的
“cv_bridge”
“image_transport”
)
ament_target_dependencies(${EXEC_NAME} ${dependencies} )
# 不加的话会报undefined reference to `lcm_destroy’
target_link_libraries(${EXEC_NAME} lcm) # 使用target_link_libraries添加lcm依赖

lcm-gen -x *.lcm;
在当前文件夹运行 make, 会出现三个可执行文件:listener, read_log, send_message;
运行 ./listener 再开一个窗口运行 ./send_message 可以看到 listener 窗口接收到信息:

3.6 LCM进程间通讯

当需要通信的进程分别在两台机器里时,需要设置如下环境变量(两台电脑命令行窗口都运行这条)。

export LCM_DEFAULT_URL=udpm://239.255.76.67:7667?ttl=1
如果需要通信的两个进程在同一台主机上则不需要运行上面的命令

如果需要在两个docker间实现通信,需要在同一网络下启动这两个容器。

#以相同的网络启动容器
docker run -it --name test_docker0 --network nat shoufei/kinetic:latest /bin/bash
docker run -it --name test_docker1 --network nat shoufei/kinetic:latest /bin/bash

注意上面的两个docker启动命令均添加了–network nat参数。其中nat是用下面的命令建立的网络接口。

docker network create nat

docker中也需要执行上面设置环境变量的命令

3.7 注意事项?

3.7.1 当数据结构定义的是数组时,生成的头文件中会用vector来定义这个数据。进行赋值时需要先 resize vector 的长度。不然会填充数据时会报错。

void TestNode::test_tof_callback(
  const sensor_msgs::msg::PointCloud2::SharedPtr msg)
{
  testlcm::tof_points_t data;
  
  size_t size = msg->height * msg->width;

  pcl::PCLPointCloud2 pcl_pc2;
  pcl_conversions::toPCL(*msg,pcl_pc2);
  pcl::PointCloud<pcl::PointXYZ>::Ptr temp_cloud(new pcl::PointCloud<pcl::PointXYZ>);
  pcl::fromPCLPointCloud2(pcl_pc2,*temp_cloud);

  data.timestamp = msg->header.stamp.nanosec * 0.001;
  data.num_points = size;
  data.xs.resize(data.num_points);//当数据结构定义的是数组时,生成的头文件中会用vector来定义这个数据。进行赋值时需要先resize vector的长度。不然会填充数据时会报错。
  data.ys.resize(data.num_points);
  data.zs.resize(data.num_points);
  for(uint32_t i = 0; i < size; ++i)
  {
      data.xs[i] = temp_cloud->points[i].x;
      data.ys[i] = temp_cloud->points[i].y;
      data.zs[i] = temp_cloud->points[i].z;
  }
  RCLCPP_INFO(this->get_logger(), "got test tof points");
  lcm.publish(REAR_TOF_POINTS_MSG, &data);
}

4 LCM 发 收 录 读

4.1 发、收

step1:定义结构体
将要发送的数据,定义在一个结构体中,写在以 .lcm 结尾的文件中。比如:

struct example_t
{
    int64_t  timestamp;
    double   position[3];
    double   orientation[4]; 
    int32_t  num_ranges;
    int16_t  ranges[num_ranges];
    string   name;
    boolean  enabled;
}

其中,数据类型需要使用lcm支持的类型,如下:
在这里插入图片描述
定义好数据结构后,生成对应语言使用的头文件。使用如下指令:
在这里插入图片描述
没有问题的话就会生成example_t.hpp,这个文件是不允许修改的。

step2:LCM初始化
数据结构的头文件生成后,在主程序代码里#include进来

#include <lcm/lcm-cpp.hpp>
#include “example_t.hpp"

定义lcm的对象并初始化

lcm::LCM lcm;
if(!lcm.good())
return 1;

初始化干了什么呢?我们进去看一下:

inline bool
LCM::good() const
{ return this->lcm != NULL; }

step3:赋值并发送
在程序中定义一个数据结构的对象,并在相应的位置进行赋值

example_t my_data;
my_data.timestamp = 0;
my_data.position[0] = 1;
my_data.position[1] = 2;
my_data.position[2] = 3;

发送:

lcm.publish(“EXAMPLE”, &my_data);

其中"EXAMPLE"为通道名,可以自由定义,接收的时候也要指定,二者保持一致才能收到。
发送的数据较多时,可以定义多个数据结构,相应会有多个lcm生成的头文件。用不同的通道名来区分他们。发送时也可灵活选择对哪些通道的数据进行发送。
至此,lcm发送的方式就结束了,还是很简洁的。整个发送代码如下:

 	  #include <lcm/lcm-cpp.hpp>
     #include "exlcm/example_t.hpp"
     
     int main(int argc, char ** argv)
     {
         lcm::LCM lcm;
         if(!lcm.good())
             return 1;
        exlcm::example_t my_data;
        my_data.timestamp = 0;
    
        my_data.position[0] = 1;
        my_data.position[1] = 2;
        my_data.position[2] = 3;
        
        lcm.publish("EXAMPLE", &my_data);
        return 0;
    }

接收消息
消息接收主要利用lcm.subscribe()函数,函数定义如下:

LCM::subscribe(const std::string& channel,
    void (MessageHandlerClass::*handlerMethod)(const ReceiveBuffer* rbuf, const std::string& channel, const MessageType* msg),
    MessageHandlerClass* handler)

上面需要3个参数,分别是(通道名、回调函数、句柄对象)。
接收时第一个参数为topic name, 第二个参数为自定义类::处理函数, 第三个参数为自定义类的实例化.
subscribe()还有另一种重载形式,其中回调函数只要2个参数,不用绑定具体的类对象。

LCM::subscribe(const std::string& channel,
    void (MessageHandlerClass::*handlerMethod)(const ReceiveBuffer* rbuf, const std::string& channel))

step1:LCM初始化

 #include <stdio.h>
 #include <lcm/lcm-cpp.hpp>
#include "exlcm/example_t.hpp"

int main(int argc, char** argv)
	 {
        lcm::LCM lcm;
        if(!lcm.good())
            return 1;
      }

step2:定义句柄及回调函数
句柄类可以如下定义,其中handleMessage即为subscribe时使用的回调函数:

class Handler 
     {
         public:
             ~Handler() {}
     
            void handleMessage(const lcm::ReceiveBuffer* rbuf,
                    const std::string& chan, 
                    const exlcm::example_t* msg)
            {
                int i;
                printf("Received message on channel \"%s\":\n", chan.c_str());
                printf("  timestamp   = %lld\n", (long long)msg->timestamp);
                printf("  position    = (%f, %f, %f)\n",
                        msg->position[0], msg->position[1], msg->position[2]);
                printf("\n");
            }
    };

上面代码将接收到的内容打印出来,可以看到,使用了之前定义的消息类型example_t

step3:接收消息
定义句柄对象,调用subscribe()函数接收。其中lcm.handle()

Handler handlerObject;
lcm.subscribe("EXAMPLE", &Handler::handleMessage, &handlerObject);

while(0 == lcm.handle());

LCM自动解码消息,再传给回调函数,回调函数可以识别消息类型。因为回调函数在lcm.handle()方法中调度,所以不需要并发执行,这些都在一个单线程中完成。
调用lcm.handle()非常重要,函数会保持阻塞直到有任务需要做。lcm.handle()为阻塞函数,类似于ros::spinOnce(), 当需要处理时跳至处理函数, 无详细接收时阻塞等待. 若不想阻塞等待可以用其他函数???

完整的接收代码如下,代码只会接收通道名为”EXAMPLE"的消息。

	1 #include <stdio.h>
    2 #include <lcm/lcm-cpp.hpp>
    3 #include "exlcm/example_t.hpp"
    4 
    5 class Handler 
    6 {
    7     public:
    8         ~Handler() {}
    9 
   10         void handleMessage(const lcm::ReceiveBuffer* rbuf,
   11                 const std::string& chan, 
   12                 const exlcm::example_t* msg)
   13         {
   14             int i;
   15             printf("Received message on channel \"%s\":\n", chan.c_str());
   16             printf("  timestamp   = %lld\n", (long long)msg->timestamp);
   17             printf("  position    = (%f, %f, %f)\n",
   18                     msg->position[0], msg->position[1], msg->position[2]);
   28         }
   29 };
   30 
   31 int main(int argc, char** argv)
   32 {
   33     lcm::LCM lcm;
   34     if(!lcm.good())
   35         return 1;
   36 
   37     Handler handlerObject;
   38     lcm.subscribe("EXAMPLE", &Handler::handleMessage, &handlerObject);
   39 
   40     while(0 == lcm.handle());
   41 
   42     return 0;
   43 }

一个lcm对象可以有无限个接收器,如果有多个通道的消息需要接收,可以

lcm.subscribe("AAAA", &Handler::handleMessage, &handlerObject);
lcm.subscribe("BBBB", &Handler::handleMessage, &handlerObject);

或者用下面方式接收所有通道

lcm.subscribe(".*", &Handler::handleMessage, &handlerObject);

参考CMake

cmake_minimum_required(VERSION 3.10)  
    project(LCM)  
    set(CMAKE_CXX_STANDARD 11)  
    find_package(lcm)#引入lcm pkg  
    include_directories("../type")#引入用户自定义数据类型  

    add_executable(LCM_Client send_msg.cpp)#分别链接发送与接收程序, 不可链接为同一工程.  
    target_link_libraries(LCM_Client lcm)  

    add_executable(LCM_Server listener.cpp)  
    target_link_libraries(LCM_Server lcm)

在引入用户自定义数据时可利用include_directories(“…/type”)将自定义数据hpp所在的文件夹包含, 在cpp程序中已该包含路径为起点引入hpp文件.

4.2 录、读

自己写的终究是不如官方发布的,https://github.com/lcm-proj/lcm/tree/master/lcm-logger
再官方github上找到了用lcm录数据并播放的源码,提供的功能也比较丰富,重要是可以按照信号发送的频率录下来,同时按频率播放,复现当时的场景。简单介绍一下。
源码编译生产2个可执行文件,lcm-logger和lcm-logplayer。
lcm-logger程序提供了命令行参数选择,生成的log是以.00 , .01为后缀的log文件,把后缀替换为.log也是可以使用的。
一般用到的参数有:
–channel:选择录制的lcm信息通道,看源码只能跟一个通道,不能同时选择录2个及以上的通道。如果不选择,会录制所有收到的通道信息。
–split-mb:设置一个大小,达到后自动新建一个新的log,可以避免log太大带来的不方便。
–invert-channels:选择不录某个通道。
如果只需要录制特定的某几个通道,可以修改源码这部分:

    if(logger.invert_channels) {
        // if inverting the channels, subscribe to everything and invert on the
        // callback
        lcm_subscribe(logger.lcm, ".*", message_handler, &logger);
        char *regexbuf = g_strdup_printf("^%s$", chan_regex);
        GError *rerr = NULL;
        logger.regex = g_regex_new(regexbuf, (GRegexCompileFlags) 0, (GRegexMatchFlags) 0, &rerr);
        if(rerr) {
            fprintf(stderr, "%s\n", rerr->message);
            g_free(regexbuf);
            return 1;
        }
        g_free(regexbuf);
    } else {
        // otherwise, let LCM handle the regex
        //lcm_subscribe(logger.lcm, chan_regex, message_handler, &logger);
        修改这部分
	lcm_subscribe(logger.lcm, "LOGITECH_IMAGE", message_handler, &logger);
	lcm_subscribe(logger.lcm, "RMVCU", message_handler, &logger);
    }

原来是如果不设置–invert-channels,将会录下选择的某个通道或者所有通道。修改后,如果不设置–invert-channels,录制特定的两个通道。
使用时可以:

./lcm-logger 录制能收到的所有lcm通道,以默认方式命名log文件
./lcm-logger xxx.log 录制能收到的所有lcm通道,以设置的名称命名log文件
./lcm-logger --channel=xxx 录制通道名为xxx的信息,以默认方式命名log文件

lcm-logplayer提供了对录制log的播放,通过lcm把log中的信息再发送出来,同样提供了命令行参数:

Options:\n
-v, --verbose Print information about each packet.\n
-s, --speed=NUM Playback speed multiplier. Default is 1.0.\n
-e, --regexp=EXPR GLib regular expression of channels to play.\n
-l, --lcm-url=URL Play logged messages on the specified LCM URL.\n
-h, --help Shows some help text and exits.\n
\n", cmd);

-v是选择是否把通道信息打印出来
-s是选择发送速度,一般是使用默认速度,还原录制时的场景。
使用时:
./lcm-logplayer -v xxx.log 以默认速度播放xxx.log,同时打印出每次播放内容

5 ROS与LCM

深入地研究ROS的数据传递,因为这关系到能不能真正把ROS用于实际的工程项目中。如果工程只用ROS搭建,那么是不需要用到命题所说的LCM的。但如果ROS只是整体系统的一部分,则需要利用内存共享或者网络传输等方式与其他程序进行数据交互,这时候LCM就是一种比较简单的选择
我们已经知道ROS归根结底是一个多进程的管理工具,因此我们可以把上面的工作分成两步:1.在Ubuntu下把LCM搭建起来,可以直接发送和接收数据;2.把上面做完的用LCM发送和接收的代码移植到ROS中

//后续补充

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

unity学习(41)——创建(create)角色脚本(panel)——UserHandler(收)+CreateClick(发)——创建发包!

1.客户端的程序结构被我精简过&#xff0c;现在去MessageManager.cs中增加一个UserHandler函数&#xff0c;根据收到的包做对应的GameInfo赋值。 2.在Model文件夹下新增一个协议文件UserProtocol&#xff0c;内容很简单。 using System;public class UserProtocol {public co…

2024牛客寒假算法基础集训营1(补题)

文章目录 ABCDEFGHIJKL A n的范围很小暴力直接 O ( n 3 ) O(n^3) O(n3)直接做就行。 我还傻的统计了一下前后缀&#xff0c;不过怎么写都行这道题。 #include <bits/stdc.h> #define int long long #define rep(i,a,b) for(int i (a); i < (b); i) #define fep(i,…

图片生成 Stable Diffusion Web 安装教程

一 Stable Diffusion Web介绍 1 什么是stable diffussion web &#xff1f; Stable Diffusion Web 是一个基于 Stable Diffusion 模型开发的图形用户界面&#xff08;GUI&#xff09;应用程序&#xff0c;它允许用户通过简单的网页交互方式来利用人工智能技术进行艺术创作和图像…

2024数字中国创新大赛·数据要素赛道“能源大数据应用赛”正式上线!参赛指南请查收

近日&#xff0c;由国网福建电力承办的2024数字中国创新大赛能源大数据应用赛正式上线发布。赛事按照数字中国建设、能源革命的战略要求&#xff0c;围绕能源数据要素x、能源数字技术、能源商业模式等热点设置赛题&#xff0c;诚邀社会各界为加快建成新型电力系统出谋划策&…

LVGL 环境搭建-基于WSL

背景说明 小白刚开始接触LVGL&#xff0c;前些日子狠心花198元入手了一块堪称LVGL 入门利器~HMI-Board 开发板&#xff0c;虽然有RT-Thread 集成好的LVGL 环境&#xff0c;只需要几个步骤就能成功把lvgl 的示例运行起来&#xff0c;对于爱折腾的我来说&#xff0c;过于简单也并…

亿道信息新品EM-T195轻薄型工业平板,隆重登场!

EM-T195是一款轻巧但坚固的平板电脑&#xff0c;仅 650克重、10.5mm毫米厚&#xff0c;即使没有额外的便携配件进行辅助&#xff0c;您也可以轻松将其长时间随身携带。耐用性外壳完全密封&#xff0c;防尘防潮&#xff1b;出色的坚固性和可靠性&#xff0c;使T195天生适合在苛刻…

Java技术发展历程中的六大春天:从Web开发到大数据战略

Java技术发展历程中的六大春天&#xff1a;从Web开发到大数据战略 Six Springs in the Development Journey of Java Technology: From Web Development to Big Data Strategy 自Java诞生以来&#xff0c;其发展历程中出现了多个关键的“春天”时刻&#xff0c;每一段历程都伴随…

使用Node.js开发一个文件上传功能

在现代 Web 应用程序开发中&#xff0c;文件上传是一个非常常见且重要的功能。今天我们将通过 Node.js 来开发一个简单而强大的文件上传功能。使用 Node.js 来处理文件上传可以带来许多好处&#xff0c;包括简单的代码实现、高效的性能和灵活的配置选项。 首先&#xff0c;我们…

springboot+vue+mysql+easyexcel实现文件导出+导出的excel单元格添加下拉列表

Excel导出 EasyExcel官方文档 官方文档本身写的非常详细&#xff0c;我就是根据官方文档内的写Excel里web中的写实现的导出 后端 对象 需要写一个实体类 其中涉及到一些用到的EasyExcel的注解 ColumnWidth(20) 列宽设为20&#xff0c;自定义的&#xff0c;放在实体类上面是…

Java玩转《啊哈算法》之模拟链表

人应该支配习惯&#xff0c;而绝不是让习惯支配人。一个人要是不能改掉坏习惯&#xff0c;那么他就一文不值。 目录 缘代码地址模拟链表创建遍历打印插入插入优化 完整代码 缘 各位小伙伴们好呀&#xff01;本人最近看了下《啊哈算法》&#xff0c;写的确实不错。 但稍显遗憾…

文生图项目总结

文生图 功能点 页面进来获取背景图url和图片宽高&#xff08;根据比例和手机屏幕处理过的宽高&#xff09;渲染图片&#xff08;背景图最后生成图片模糊&#xff0c;换成img展示解决&#xff09;添加多个文字&#xff0c;编辑文字内容&#xff0c;拖拽改变文字位置&#xff0c…

计算机网络:IP

引言&#xff1a; IP协议是互联网协议族中的核心协议之一&#xff0c;负责为数据包在网络中传输提供路由寻址。它定义了数据包如何在互联网上从源地址传输到目的地址的规则和流程。IP协议使得各种不同类型的网络设备能够相互通信&#xff0c;实现了全球范围内的信息交换。 目录…

HTML-基础标签

1. HTML初识 1.1 什么是HTML HTML&#xff08;英文Hyper Text Markup Language的缩写&#xff09;中文译为“超文本标签语言”&#xff0c;是用来描述网页的一种语言。所谓超文本&#xff0c;因为它可以加入图片、声音、动画、多媒体等内容&#xff0c;不仅如此&#xff0c;它还…

nginx---------------重写功能 防盗链 反向代理 (五)

一、重写功能 rewrite Nginx服务器利用 ngx_http_rewrite_module 模块解析和处理rewrite请求&#xff0c;此功能依靠 PCRE(perl compatible regular expression)&#xff0c;因此编译之前要安装PCRE库&#xff0c;rewrite是nginx服务器的重要功能之一&#xff0c;重写功能(…

数据结构(C语言)代码实现(九)——迷宫探路表达式求值

目录 参考资料 迷宫探路 顺序栈头文件SqStack.h 顺序栈函数实现SqStack.cpp 迷宫探路主函数 表达式求值 链式顺序栈头文件LinkStack.h 链式顺序栈函数实现LinkStack.cpp 表达式求值主函数 测试结果 参考资料 数据结构严蔚敏版 2021-9-22【数据结构/严蔚敏】【顺序…

Django学习笔记-django使用pandas将上传的数据存到MySQL

1.models中创建与excel表结构相同模型 2.模型映射 python manage.py makemigrations myapp01,python manage.py migrate 3.创建index,添加form,enctype使用multipart/form-data 4.urls中导入views,填写路由 5.views中创建index 6.如果为GET请求,直接返回index.html,如果为PO…

历史新知网:寄快递寄个电脑显示器要多少钱?

以下文字信息由&#xff08;新史知识网&#xff09;编辑整理发布。 让我们赶紧来看看吧&#xff01; 问题1&#xff1a;快递寄电脑显示器要多少钱&#xff1f; 此物有多重&#xff1f; 顺丰寄就可以了&#xff0c;但是必须是原包装的&#xff0c;不然不好寄。 问题2&#xff1…

阿里云中小企业扶持权益,助力企业开启智能时代创业新范式

在数字化浪潮的推动下&#xff0c;中小企业正面临着转型升级的重要关口。阿里云深知中小企业的挑战与机遇&#xff0c;特别推出了一系列中小企业扶持权益&#xff0c;旨在帮助企业以更低的成本、更高的效率拥抱云计算&#xff0c;开启智能时代创业的新范式。 一、企业上云权益…

光伏预测 | Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测

光伏预测 | Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测 目录 光伏预测 | Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测预测效果基本描述模型简介程序设计参考资料 预测效果 基本描述 Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测 运行环境: Matla…

【初中生讲机器学习】12. 似然函数和极大似然估计:原理、应用与代码实现

创建时间&#xff1a;2024-02-23 最后编辑时间&#xff1a;2024-02-24 作者&#xff1a;Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏&#xff0c;很高兴遇见你~ 我是 Geeker_LStar&#xff0c;一名初三学生&#xff0c;热爱计算机和数学&#xff0c;我们一起加…
最新文章