网站开发做美工,年利润三十万小作坊,新增网站和新增接入,推荐网站建设需要用到LCM消息通讯#xff0c;遂研究下。 这里写目录标题 1 LCM简介2. LCM源码分析3 LCM C教程与实例3.1 安装配置及介绍3.2 创建类型定义3.3 初始化LCM3.4 发布publish一个消息3.5 订阅和接收一个消息3.6 LCM进程间通讯3.7 注意事项#xff1f;3.7.1 当数据结构定义的是数… 需要用到LCM消息通讯遂研究下。 这里写目录标题 1 LCM简介2. LCM源码分析3 LCM C教程与实例3.1 安装配置及介绍3.2 创建类型定义3.3 初始化LCM3.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简介
LCMLightweight Communications and Marshalling是一组用于消息传递和数据编组的库和工具其基于UDP传输的属性传输速度较快其目标是高带宽和低延迟的实时系统。它提供了一种发布/订阅消息传递模型以及带有各种编程语言C、Java、python等应用程序绑定的自动编组/解组代码生成LCM通过将消息封装在不同的Channel中进行通信这点类似于ROS中的Topic。 适用于高速自动驾驶场景的LCM通信协议其特点是轻量化、传输速度快易封装。 LCM是去ROS化的优秀选择
LCM官方Githublcm-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_tlcm_memq_tlcm_mpudpm_tlcm_tcpq_tlcm_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.hppvoid publisher(lcm::LCM lcm_in)
{for (int i 0; i 10; i) {exlcm::example_t my_data;my_data.timestamp std::chrono::duration_caststd::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::HandlerFunctionexlcm::example_t func;func [id](const lcm::ReceiveBuffer *rbuf, const std::string channel,const exlcm::example_t *msg) {const auto now std::chrono::duration_caststd::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::HandlerFunctionexlcm::example_t func;func [](const lcm::ReceiveBuffer *rbuf, const std::string channel,const exlcm::example_t *msg) {const auto now std::chrono::duration_caststd::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发送完相对简单。如果消息比较大需要将消息切割为很多个fragmentfragment会打上序列号接收端通过序列号组装消息。第一个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; } 更多类型规范,下表是基本类型规范:
typeDescriptionint8_t8-bit signed integerint16_t16-bit signed integerint32_t32-bit signed integerint64_t64-bit signed integerfloat32-bit IEEE floating point valuedouble64-bit IEEE floating point valuestringUTF-8 stringbooleantrue/false logical valuebyte8-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.hppint main(int argc, char ** argv)
{lcm::LCM lcm;if(!lcm.good())return 1;// Your application goes herereturn 0;
}3.4 发布publish一个消息
// send_message.cpp
#include lcm/lcm-cpp.hpp
#include exlcm/example_t.hppint 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.hppclass 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_URLudpm://239.255.76.67:7667?ttl1 如果需要通信的两个进程在同一台主机上则不需要运行上面的命令 如果需要在两个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::PointCloudpcl::PointXYZ::Ptr temp_cloud(new pcl::PointCloudpcl::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这个文件是不允许修改的。
step2LCM初始化 数据结构的头文件生成后在主程序代码里#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.hppint 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))step1LCM初始化 #include stdio.h#include lcm/lcm-cpp.hpp
#include exlcm/example_t.hppint 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.h2 #include lcm/lcm-cpp.hpp3 #include exlcm/example_t.hpp4 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// callbacklcm_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 --channelxxx 录制通道名为xxx的信息以默认方式命名log文件lcm-logplayer提供了对录制log的播放通过lcm把log中的信息再发送出来同样提供了命令行参数 Options:\n -v, --verbose Print information about each packet.\n -s, --speedNUM Playback speed multiplier. Default is 1.0.\n -e, --regexpEXPR GLib regular expression of channels to play.\n -l, --lcm-urlURL 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中
//后续补充