本文主要介绍LCM通讯的基本使用,内容主要整理自官网
https://lcm-proj.github.io/lcm/index.html
LCM,即Library for Communication and Marshalling
,是一组用于消息传递与数据封装的库和工具,它主要的面向对象是要求高带宽、低延迟的实时系统。
它使用publish/subscribe的方式进行进程间通信,和ROS的话题有点类似。
既然涉及进程间通信,那么使用什么编程语言就无关紧要了,即不同编程语言编写的程序都可以互相通信。
LCM主要具有以下特点:
-
低延迟进程间通信
-
使用UDP组播的高效广播机制
-
类型安全的编组/解组
-
用户友好的日志记录和回放功能
-
点对点通信
-
无需守护进程
-
依赖少
本文介绍LCM的基本使用。本文环境为ubuntu20.04
LCM下载
进入官网,下载安装包
https://github.com/lcm-proj/lcm/releases
环境构建
首先安装LCM的依赖:
sudo apt install build-essential cmake libglib2.0-dev
然后将包放到自己的目录下解压,执行以下步骤:
mkdir build
cd build
cmake ..
make
sudo make install
至此环境构建完成。
简单程序编写
简单体验下LCM的通讯方式,此处使用C++
语言,使用官网例程做示范。
-
选择一个路径创建项目的文件夹
-
定义消息类型,创建发布方、接收方,编写
CMakeLists
文件四个文件的内容如下:
-
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; }
-
lcm_publisher.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_receiver.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());return 0; }
-
CMakeLists.txt
cmake_minimum_required(VERSION 3.1)project(lcm_cpp_example)find_package(lcm REQUIRED) include(${LCM_USE_FILE})# Put all message definition files in the type directory in one list FILE(GLOB example_message_definitions "${CMAKE_CURRENT_LIST_DIR}/../types/*.lcm")# Generate headers from message definition lcm_wrap_types(CPP_HEADERS cpp_headers${example_message_definitions})# Create library from all the messages lcm_add_library(example_messages-cpp CPP ${cpp_headers}) target_include_directories(example_messages-cpp INTERFACE$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)# Create executables for the three example programs, linking all of them to our # messages library and lcmadd_executable(lcm_receiver "lcm_receiver.cpp") lcm_target_link_libraries(lcm_receiver example_messages-cpp ${LCM_NAMESPACE}lcm)add_executable(lcm_publisher "lcm_publisher.cpp") lcm_target_link_libraries(lcm_publisher example_messages-cpp ${LCM_NAMESPACE}lcm)
以上文件中头文件
exlcm/example_t.hpp
是缺失的,这时我们在目录下执行lcm-gen -x example_t.lcm
,则可以看到当前目录下生成了exlcm
文件夹,下面自动生成了example_t.hpp
example_t.hpp
的内容如下:/** THIS IS AN AUTOMATICALLY GENERATED FILE. DO NOT MODIFY* BY HAND!!** Generated by lcm-gen**/#ifndef __exlcm_example_t_hpp__ #define __exlcm_example_t_hpp__#include <lcm/lcm_coretypes.h>#include <vector> #include <string>namespace exlcm {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;public:/*** Encode a message into binary form.** @param buf The output buffer.* @param offset Encoding starts at thie byte offset into @p buf.* @param maxlen Maximum number of bytes to write. This should generally be* equal to getEncodedSize().* @return The number of bytes encoded, or <0 on error.*/inline int encode(void *buf, int offset, int maxlen) const;/*** Check how many bytes are required to encode this message.*/inline int getEncodedSize() const;/*** Decode a message from binary form into this instance.** @param buf The buffer containing the encoded message.* @param offset The byte offset into @p buf where the encoded message starts.* @param maxlen The maximum number of bytes to read while decoding.* @return The number of bytes decoded, or <0 if an error occured.*/inline int decode(const void *buf, int offset, int maxlen);/*** Retrieve the 64-bit fingerprint identifying the structure of the message.* Note that the fingerprint is the same for all instances of the same* message type, and is a fingerprint on the message type definition, not on* the message contents.*/inline static int64_t getHash();/*** Returns "example_t"*/inline static const char* getTypeName();// LCM support functions. Users should not call theseinline int _encodeNoHash(void *buf, int offset, int maxlen) const;inline int _getEncodedSizeNoHash() const;inline int _decodeNoHash(const void *buf, int offset, int maxlen);inline static uint64_t _computeHash(const __lcm_hash_ptr *p); };int example_t::encode(void *buf, int offset, int maxlen) const {int pos = 0, tlen;int64_t hash = getHash();tlen = __int64_t_encode_array(buf, offset + pos, maxlen - pos, &hash, 1);if(tlen < 0) return tlen; else pos += tlen;tlen = this->_encodeNoHash(buf, offset + pos, maxlen - pos);if (tlen < 0) return tlen; else pos += tlen;return pos; }int example_t::decode(const void *buf, int offset, int maxlen) {int pos = 0, thislen;int64_t msg_hash;thislen = __int64_t_decode_array(buf, offset + pos, maxlen - pos, &msg_hash, 1);if (thislen < 0) return thislen; else pos += thislen;if (msg_hash != getHash()) return -1;thislen = this->_decodeNoHash(buf, offset + pos, maxlen - pos);if (thislen < 0) return thislen; else pos += thislen;return pos; }int example_t::getEncodedSize() const {return 8 + _getEncodedSizeNoHash(); }int64_t example_t::getHash() {static int64_t hash = static_cast<int64_t>(_computeHash(NULL));return hash; }const char* example_t::getTypeName() {return "example_t"; }int example_t::_encodeNoHash(void *buf, int offset, int maxlen) const {int pos = 0, tlen;tlen = __int64_t_encode_array(buf, offset + pos, maxlen - pos, &this->timestamp, 1);if(tlen < 0) return tlen; else pos += tlen;tlen = __double_encode_array(buf, offset + pos, maxlen - pos, &this->position[0], 3);if(tlen < 0) return tlen; else pos += tlen;tlen = __double_encode_array(buf, offset + pos, maxlen - pos, &this->orientation[0], 4);if(tlen < 0) return tlen; else pos += tlen;tlen = __int32_t_encode_array(buf, offset + pos, maxlen - pos, &this->num_ranges, 1);if(tlen < 0) return tlen; else pos += tlen;if(this->num_ranges > 0) {tlen = __int16_t_encode_array(buf, offset + pos, maxlen - pos, &this->ranges[0], this->num_ranges);if(tlen < 0) return tlen; else pos += tlen;}char* name_cstr = const_cast<char*>(this->name.c_str());tlen = __string_encode_array(buf, offset + pos, maxlen - pos, &name_cstr, 1);if(tlen < 0) return tlen; else pos += tlen;tlen = __boolean_encode_array(buf, offset + pos, maxlen - pos, &this->enabled, 1);if(tlen < 0) return tlen; else pos += tlen;return pos; }int example_t::_decodeNoHash(const void *buf, int offset, int maxlen) {int pos = 0, tlen;tlen = __int64_t_decode_array(buf, offset + pos, maxlen - pos, &this->timestamp, 1);if(tlen < 0) return tlen; else pos += tlen;tlen = __double_decode_array(buf, offset + pos, maxlen - pos, &this->position[0], 3);if(tlen < 0) return tlen; else pos += tlen;tlen = __double_decode_array(buf, offset + pos, maxlen - pos, &this->orientation[0], 4);if(tlen < 0) return tlen; else pos += tlen;tlen = __int32_t_decode_array(buf, offset + pos, maxlen - pos, &this->num_ranges, 1);if(tlen < 0) return tlen; else pos += tlen;if(this->num_ranges) {this->ranges.resize(this->num_ranges);tlen = __int16_t_decode_array(buf, offset + pos, maxlen - pos, &this->ranges[0], this->num_ranges);if(tlen < 0) return tlen; else pos += tlen;}int32_t __name_len__;tlen = __int32_t_decode_array(buf, offset + pos, maxlen - pos, &__name_len__, 1);if(tlen < 0) return tlen; else pos += tlen;if(__name_len__ > maxlen - pos) return -1;this->name.assign(static_cast<const char*>(buf) + offset + pos, __name_len__ - 1);pos += __name_len__;tlen = __boolean_decode_array(buf, offset + pos, maxlen - pos, &this->enabled, 1);if(tlen < 0) return tlen; else pos += tlen;return pos; }int example_t::_getEncodedSizeNoHash() const {int enc_size = 0;enc_size += __int64_t_encoded_array_size(NULL, 1);enc_size += __double_encoded_array_size(NULL, 3);enc_size += __double_encoded_array_size(NULL, 4);enc_size += __int32_t_encoded_array_size(NULL, 1);enc_size += __int16_t_encoded_array_size(NULL, this->num_ranges);enc_size += this->name.size() + 4 + 1;enc_size += __boolean_encoded_array_size(NULL, 1);return enc_size; }uint64_t example_t::_computeHash(const __lcm_hash_ptr *) {uint64_t hash = 0x1baa9e29b0fbaa8bLL;return (hash<<1) + ((hash>>63)&1); }}#endif
-
测试
创建build
文件夹,进入,执行以下命令:
cd build
cmake ..
make
可以看到当前目录下生成了对应的可执行文件:
运行lcm_receiver
重新开一个终端运行lcm_publisher
,可以看到:
通信成功。
小结
本文主要介绍了机器人中LCM通讯的入门使用。感兴趣的朋友可以去官网进一步学习。
实际上,LCM和ROS的话题通讯方式在使用上十分类似,不过,正如开头所说,LCM的面向对象是要求高带宽、低延迟的实时系统,如机械臂的控制系统;与之相比ROS的通信机制更复杂,通信延时会更高一点,在这一点上LCM比ROS1的表现更好。
但是ROS有更强大的生态社区,在做一些复杂的功能时,ROS有更丰富的工具帮助快速搭建系统,如
rviz
、rqt
等。因此二者也是各有优劣。