本文主要介绍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 lcm add_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 these inline 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
等。因此二者也是各有优劣。