MCAP存储格式是一种用于机器人数据的标准化容器格式。MCAP(发音为“em-cap”),可以用于异构时间戳数据的模块化容器文件格式。它非常适用于机器人应用,因为它可以在单个文件中记录多个流的结构化和非结构化数据(例如ROS、Protobuf、JSON Schema等)。
使用MCAP存储格式优点:
- 用于在多种序列化格式(Protobuf、MessagePack、JSON等)中存储数据。
- 高性能写入。(采用追加结构,数据可以流式传输到磁盘或网络,无需回溯)
- 解码不需要额外的依赖项。(自包含消息模式)
- 高效的读取和定位。
- 能够轻松集成现有的第三方工具(如Foxglove Studio、Foxglove Data Platform等)。
这里以Protobuf数据保存作为案例:
1. 先给出一个简单的结构体, speed代表车速, acc代表加速度
#include <cstdint>
struct Head{
int id;
int index;
uint64_t timestamp;
};
struct Data{
Head head;
int speed;
float acc;
};
2. 接下来用写一个proto存储
syntax = "proto3";
message Head {
int32 id = 1;
int32 index = 2;
uint64 timestamp = 3;
}
message Data {
Head head = 1;
int32 speed = 2;
float acc = 3;
}
3. 用protobuf序列化, 这里序列化为cpp的
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: mcapTest/data.proto
#ifndef PROTOBUF_mcapTest_2fdata_2eproto__INCLUDED
#define PROTOBUF_mcapTest_2fdata_2eproto__INCLUDED
#include <string>
#include <google/protobuf/stubs/common.h>
#if GOOGLE_PROTOBUF_VERSION < 3003000
#error This file was generated by a newer version of protoc which is
#error incompatible with your Protocol Buffer headers. Please update
#error your headers.
#endif
#if 3003000 < GOOGLE_PROTOBUF_MIN_PROTOC_VERSION
#error This file was generated by an older version of protoc which is
#error incompatible with your Protocol Buffer headers. Please
#error regenerate this file with a newer version of protoc.
#endif
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/arena.h>
#include <google/protobuf/arenastring.h>
#include <google/protobuf/generated_message_table_driven.h>
#include <google/protobuf/generated_message_util.h>
#include <google/protobuf/metadata.h>
#include <google/protobuf/message.h>
#include <google/protobuf/repeated_field.h> // IWYU pragma: export
#include <google/protobuf/extension_set.h> // IWYU pragma: export
#include <google/protobuf/unknown_field_set.h>
// @@protoc_insertion_point(includes)
class Data;
class DataDefaultTypeInternal;
extern DataDefaultTypeInternal _Data_default_instance_;
class Head;
class HeadDefaultTypeInternal;
extern HeadDefaultTypeInternal _Head_default_instance_;
namespace protobuf_mcapTest_2fdata_2eproto {
// Internal implementation detail -- do not call these.
struct TableStruct {
static const ::google::protobuf::internal::ParseTableField entries[];
static const ::google::protobuf::internal::AuxillaryParseTableField aux[];
static const ::google::protobuf::internal::ParseTable schema[];
static const ::google::protobuf::uint32 offsets[];
static void InitDefaultsImpl();
static void Shutdown();
};
void AddDescriptors();
void InitDefaults();
} // namespace protobuf_mcapTest_2fdata_2eproto
// ===================================================================
class Head : public ::google::protobuf::Message /* @@protoc_insertion_point(class_definition:Head) */ {
public:
Head();
virtual ~Head();
Head(const Head& from);
inline Head& operator=(const Head& from) {
CopyFrom(from);
return *this;
}
static const ::google::protobuf::Descriptor* descriptor();
static const Head& default_instance();
static inline const Head* internal_default_instance() {
return reinterpret_cast<const Head*>(
&_Head_default_instance_);
}
static PROTOBUF_CONSTEXPR int const kIndexInFileMessages =
0;
void Swap(Head* other);
// implements Message ----------------------------------------------
inline Head* New() const PROTOBUF_FINAL { return New(NULL); }
Head* New(::google::protobuf::Arena* arena) const PROTOBUF_FINAL;
void CopyFrom(const ::google::protobuf::Message& from) PROTOBUF_FINAL;
void MergeFrom(const ::google::protobuf::Message& from) PROTOBUF_FINAL;
void CopyFrom(const Head& from);
void MergeFrom(const Head& from);
void Clear() PROTOBUF_FINAL;
bool IsInitialized() const PROTOBUF_FINAL;
size_t ByteSizeLong() const PROTOBUF_FINAL;
bool MergePartialFromCodedStream(
::google::protobuf::io::CodedInputStream* input) PROTOBUF_FINAL;
void SerializeWithCachedSizes(
::google::protobuf::io::CodedOutputStream* output) const PROTOBUF_FINAL;
::google::protobuf::uint8* InternalSerializeWithCachedSizesToArray(
bool deterministic, ::google::protobuf::uint8* target) const PROTOBUF_FINAL;
int GetCachedSize() const PROTOBUF_FINAL { return _cached_size_; }
private:
void SharedCtor();
void SharedDtor();
void SetCachedSize(int size) const PROTOBUF_FINAL;
void InternalSwap(Head* other);
private:
inline ::google::protobuf::Arena* GetArenaNoVirtual() const {
return NULL;
}
inline void* MaybeArenaPtr() const {
return NULL;
}
public:
::google::protobuf::Metadata GetMetadata() const PROTOBUF_FINAL;
// nested types ----------------------------------------------------
// accessors -------------------------------------------------------
// int32 id = 1;
void clear_id();
static const int kIdFieldNumber = 1;
::google::protobuf::int32 id() const;
void set_id(::google::protobuf::int32 value);
// int32 index = 2;
void clear_index();
static const int kIndexFieldNumber = 2;
::google::protobuf::int32 index() const;
void set_index(::google::protobuf::int32 value);
// uint64 timestamp = 3;
void clear_timestamp();
static const int kTimestampFieldNumber = 3;
::google::protobuf::uint64 timestamp() const;
void set_timestamp(::google::protobuf::uint64 value);
// @@protoc_insertion_point(class_scope:Head)
private:
::google::protobuf::internal::InternalMetadataWithArena _internal_metadata_;
::google::protobuf::int32 id_;
::google::protobuf::int32 index_;
::google::protobuf::uint64 timestamp_;
mutable int _cached_size_;
friend struct protobuf_mcapTest_2fdata_2eproto::TableStruct;
};
// -------------------------------------------------------------------
class Data : public ::google::protobuf::Message /* @@protoc_insertion_point(class_definition:Data) */ {
public:
Data();
virtual ~Data();
Data(const Data& from);
inline Data& operator=(const Data& from) {
CopyFrom(from);
return *this;
}
static const ::google::protobuf::Descriptor* descriptor();
static const Data& default_instance();
static inline const Data* internal_default_instance() {
return reinterpret_cast<const Data*>(
&_Data_default_instance_);
}
static PROTOBUF_CONSTEXPR int const kIndexInFileMessages =
1;
void Swap(Data* other);
// implements Message ----------------------------------------------
inline Data* New() const PROTOBUF_FINAL { return New(NULL); }
Data* New(::google::protobuf::Arena* arena) const PROTOBUF_FINAL;
void CopyFrom(const ::google::protobuf::Message& from) PROTOBUF_FINAL;
void MergeFrom(const ::google::protobuf::Message& from) PROTOBUF_FINAL;
void CopyFrom(const Data& from);
void MergeFrom(const Data& from);
void Clear() PROTOBUF_FINAL;
bool IsInitialized() const PROTOBUF_FINAL;
size_t ByteSizeLong() const PROTOBUF_FINAL;
bool MergePartialFromCodedStream(
::google::protobuf::io::CodedInputStream* input) PROTOBUF_FINAL;
void SerializeWithCachedSizes(
::google::protobuf::io::CodedOutputStream* output) const PROTOBUF_FINAL;
::google::protobuf::uint8* InternalSerializeWithCachedSizesToArray(
bool deterministic, ::google::protobuf::uint8* target) const PROTOBUF_FINAL;
int GetCachedSize() const PROTOBUF_FINAL { return _cached_size_; }
private:
void SharedCtor();
void SharedDtor();
void SetCachedSize(int size) const PROTOBUF_FINAL;
void InternalSwap(Data* other);
private:
inline ::google::protobuf::Arena* GetArenaNoVirtual() const {
return NULL;
}
inline void* MaybeArenaPtr() const {
return NULL;
}
public:
::google::protobuf::Metadata GetMetadata() const PROTOBUF_FINAL;
// nested types ----------------------------------------------------
// accessors -------------------------------------------------------
// .Head head = 1;
bool has_head() const;
void clear_head();
static const int kHeadFieldNumber = 1;
const ::Head& head() const;
::Head* mutable_head();
::Head* release_head();
void set_allocated_head(::Head* head);
// int32 speed = 2;
void clear_speed();
static const int kSpeedFieldNumber = 2;
::google::protobuf::int32 speed() const;
void set_speed(::google::protobuf::int32 value);
// float acc = 3;
void clear_acc();
static const int kAccFieldNumber = 3;
float acc() const;
void set_acc(float value);
// @@protoc_insertion_point(class_scope:Data)
private:
::google::protobuf::internal::InternalMetadataWithArena _internal_metadata_;
::Head* head_;
::google::protobuf::int32 speed_;
float acc_;
mutable int _cached_size_;
friend struct protobuf_mcapTest_2fdata_2eproto::TableStruct;
};
// ===================================================================
// ===================================================================
#if !PROTOBUF_INLINE_NOT_IN_HEADERS
// Head
// int32 id = 1;
inline void Head::clear_id() {
id_ = 0;
}
inline ::google::protobuf::int32 Head::id() const {
// @@protoc_insertion_point(field_get:Head.id)
return id_;
}
inline void Head::set_id(::google::protobuf::int32 value) {
id_ = value;
// @@protoc_insertion_point(field_set:Head.id)
}
// int32 index = 2;
inline void Head::clear_index() {
index_ = 0;
}
inline ::google::protobuf::int32 Head::index() const {
// @@protoc_insertion_point(field_get:Head.index)
return index_;
}
inline void Head::set_index(::google::protobuf::int32 value) {
index_ = value;
// @@protoc_insertion_point(field_set:Head.index)
}
// uint64 timestamp = 3;
inline void Head::clear_timestamp() {
timestamp_ = GOOGLE_ULONGLONG(0);
}
inline ::google::protobuf::uint64 Head::timestamp() const {
// @@protoc_insertion_point(field_get:Head.timestamp)
return timestamp_;
}
inline void Head::set_timestamp(::google::protobuf::uint64 value) {
timestamp_ = value;
// @@protoc_insertion_point(field_set:Head.timestamp)
}
// -------------------------------------------------------------------
// Data
// .Head head = 1;
inline bool Data::has_head() const {
return this != internal_default_instance() && head_ != NULL;
}
inline void Data::clear_head() {
if (GetArenaNoVirtual() == NULL && head_ != NULL) delete head_;
head_ = NULL;
}
inline const ::Head& Data::head() const {
// @@protoc_insertion_point(field_get:Data.head)
return head_ != NULL ? *head_
: *::Head::internal_default_instance();
}
inline ::Head* Data::mutable_head() {
if (head_ == NULL) {
head_ = new ::Head;
}
// @@protoc_insertion_point(field_mutable:Data.head)
return head_;
}
inline ::Head* Data::release_head() {
// @@protoc_insertion_point(field_release:Data.head)
::Head* temp = head_;
head_ = NULL;
return temp;
}
inline void Data::set_allocated_head(::Head* head) {
delete head_;
head_ = head;
if (head) {
} else {
}
// @@protoc_insertion_point(field_set_allocated:Data.head)
}
// int32 speed = 2;
inline void Data::clear_speed() {
speed_ = 0;
}
inline ::google::protobuf::int32 Data::speed() const {
// @@protoc_insertion_point(field_get:Data.speed)
return speed_;
}
inline void Data::set_speed(::google::protobuf::int32 value) {
speed_ = value;
// @@protoc_insertion_point(field_set:Data.speed)
}
// float acc = 3;
inline void Data::clear_acc() {
acc_ = 0;
}
inline float Data::acc() const {
// @@protoc_insertion_point(field_get:Data.acc)
return acc_;
}
inline void Data::set_acc(float value) {
acc_ = value;
// @@protoc_insertion_point(field_set:Data.acc)
}
#endif // !PROTOBUF_INLINE_NOT_IN_HEADERS
// -------------------------------------------------------------------
// @@protoc_insertion_point(namespace_scope)
// @@protoc_insertion_point(global_scope)
#endif // PROTOBUF_mcapTest_2fdata_2eproto__INCLUDED
4. 创建mcap文件并写入数据
const char* outputFilename = "aaa.mcap";
mcap::McapWriter writer;
{
auto options = mcap::McapWriterOptions("");
const auto res = writer.open(outputFilename, options);
if (!res.ok())
{
std::cerr << "Failed to open " << outputFilename << " for writing: " << res.message
<< std::endl;
return 1;
}
}
mcap::ChannelId channelId;
{
mcap::Schema schema(
"Data", "protobuf",
foxglove::BuildFileDescriptorSet(Data::descriptor()).SerializeAsString());
writer.addSchema(schema);
// choose an arbitrary topic name.
mcap::Channel channel("Data", "protobuf", schema.id);
writer.addChannel(channel);
channelId = channel.id;
}
5. 写入mcap数据到文件
// 创建一个ExampleMessage实例并填充数据。
Data message;
Head head;
head.set_id(1);
head.set_index(3);
head.set_timestamp(2);
message.set_allocated_head(&head);
message.set_speed(80);
message.set_acc(5);
mcap::Timestamp startTime = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
for (uint32_t frameIndex = 0; frameIndex < 100; ++frameIndex) {
mcap::Timestamp cloudTime = startTime + (static_cast<uint64_t>(frameIndex) * 100 * 1000000);
head.set_index(head.index() + 1);
message.set_speed(message.speed() + 1);
message.set_acc(message.acc() + 1);
std::string serialized = message.SerializeAsString();
mcap::Message msg;
msg.channelId = channelId;
msg.sequence = frameIndex;
msg.publishTime = cloudTime;
msg.logTime = cloudTime;
msg.data = reinterpret_cast<const std::byte*>(serialized.data());
msg.dataSize = serialized.size();
const auto res = writer.write(msg);
if (!res.ok()) {
std::cerr << "Failed to write message: " << res.message << "\n";
writer.terminate();
writer.close();
std::ignore = std::remove(outputFilename);
return 1;
}
}
// Write the index and footer to the file, then close it.
writer.close();
6. 编译代码并运行
cmake ..
make
./Demo
最后用fox glove打开文件查看里面的数据:
项目代码:GitHub - yk785496878/mcapTest: 使用mcap存储protobuf数据
conan库构建项目: c++项目起始(五)-CSDN博客