在机器人开发领域,ROS通过rosbag工具将话题、服务与参数记录为.bag文件,便于后期回放与调试。很多团队希望在不部署Linux与ROS环境的前提下,用C#在Windows工控机或上位机软件中直接读取这些数据包,做可视化或算法验证。理解bag文件的磁盘布局是用C#自主解析的前提。

ROS Bag文件格式基础
ROS Bag并非普通文本或数据库文件,而是一种带版本头的二进制流格式。文件开头是十三字节的魔法字“#ROSBAG V2.0n”,随后由若干记录(Record)组成。每条记录包含操作码(opcode)、数据长度与数据体。常见操作码有0x02(消息数据)、0x03(索引数据)、0x04( chunk信息)、0x05(连接信息)、0x06(文件头信息)等。连接信息记录了话题名称、消息类型与MD5,消息数据则引用连接ID并携带时间戳与序列化后的消息体。
Chunk是bag文件的核心存储单元,多个消息被打包进一个chunk并可整体压缩(如bz2或lz4)。C#解析时应先顺序读取所有记录,把连接信息缓存起来,再在遇到chunk时按需解压并提取内部消息。这样既能避免一次性加载大文件导致内存溢出,也能在只关心某些话题时跳过无关chunk。下面代码展示如何用BinaryReader读取文件头与第一条记录头。
using System;
using System.IO;
class BagReader
{
static void Main()
{
using (var fs = new FileStream("test.bag", FileMode.Open))
using (var br = new BinaryReader(fs))
{
// 读取魔法字
var magic = br.ReadBytes(13);
string magicStr = System.Text.Encoding.ASCII.GetString(magic);
Console.WriteLine(magicStr); // 应输出 #ROSBAG V2.0
// 读取一条记录头:opcode(1字节) + 长度(4字节小端)
byte op = br.ReadByte();
int len = br.ReadInt32();
Console.WriteLine("op=" + op + " len=" + len);
}
}
}
用C#读取连接与消息记录
连接记录(opcode=0x05)的数据体采用键值对格式,键和值都以“长度+字符串”方式存储。典型字段包括topic、type、md5sum、message_definition。我们需要把这些映射保存到一个字典中,以便后续消息反序列化时知道应该使用哪种结构。由于bag内部不使用标准protobuf schema,而是ROS自有的序列化规则,因此消息体要按照对应类型的字段顺序逐个读取。
消息记录(opcode=0x02)数据体前四个字节是连接ID,接着是时间戳(两个32位整数表示秒与纳秒),之后是消息内容。以下示例演示如何跳过chunk压缩直接读取未压缩bag里的连接信息,并输出话题名与类型。真实工程中还需处理压缩块,但逻辑相似。
using System;
using System.IO;
using System.Collections.Generic;
class ConnReader
{
static void Main()
{
var conns = new Dictionary<int, string>();
using (var br = new BinaryReader(File.OpenRead("test.bag")))
{
br.ReadBytes(13); // 跳过魔法字
while (br.BaseStream.Position < br.BaseStream.Length)
{
byte op = br.ReadByte();
int len = br.ReadInt32();
if (op == 0x05) // 连接信息
{
long start = br.BaseStream.Position;
int connId = br.ReadInt32();
// 简易解析:找topic字段
while (br.BaseStream.Position < start + len)
{
int flen = br.ReadInt32();
string field = new string(br.ReadChars(flen));
int vlen = br.ReadInt32();
string val = new string(br.ReadChars(vlen));
if (field == "topic")
conns[connId] = val;
}
}
else
{
br.ReadBytes(len); // 跳过其他记录
}
}
}
foreach (var kv in conns)
Console.WriteLine("conn " + kv.Key + " => " + kv.Value);
}
}
消息反序列化与数据分析
拿到连接对应的消息类型后,就能针对具体msg结构写反序列化方法。例如标准geometry_msgs/Twist包含linear与angular两个Vector3,每个Vector3有三个float64。C#中可用BinaryReader按小端读取double。对于自定义消息,需要根据message_definition生成相应解析类,或者利用反射动态构建。分析阶段常需统计频率、提取轨迹点或导出CSV。
下面示例读取未压缩消息记录,假设已知某连接ID为0的话题是Twist,直接解析线速度与角速度并打印。实际项目可把解析逻辑抽象为委托字典,按connId分发,从而支持多话题混合处理。注意ROS时间戳是UTC秒加纳秒,合并后可转为DateTime做时序分析。
using System;
using System.IO;
class MsgParse
{
static void Main()
{
using (var br = new BinaryReader(File.OpenRead("test.bag")))
{
br.ReadBytes(13);
while (br.BaseStream.Position < br.BaseStream.Length)
{
byte op = br.ReadByte();
int len = br.ReadInt32();
if (op == 0x02)
{
int connId = br.ReadInt32();
uint sec = br.ReadUInt32();
uint nsec = br.ReadUInt32();
if (connId == 0) // 假定为Twist
{
double lx = br.ReadDouble();
double ly = br.ReadDouble();
double lz = br.ReadDouble();
double ax = br.ReadDouble();
double ay = br.ReadDouble();
double az = br.ReadDouble();
Console.WriteLine($"t={sec}.{nsec} lin=({lx},{ly},{lz}) ang=({ax},{ay},{az})");
}
else
{
// 跳过消息体剩余部分
long remain = len - 4 - 4 - 4 - 6 * 8;
br.ReadBytes((int)remain);
}
}
else
{
br.ReadBytes(len);
}
}
}
}
}
处理压缩块与性能建议
当bag文件使用compression字段标记为bz2或lz4时,chunk内的记录是整体压缩的。C#可引用相应压缩库(如SharpZipLib处理bz2,或自行绑定lz4)先将整块解出,再按前述方式遍历内部记录。为了避免重复解压,建议只解压包含目标话题的chunk,并利用文件尾的索引记录快速定位chunk偏移。
在工程化封装时,可设计BagFile类暴露Topics、GetMessages(string topic)等API,内部用懒加载方式读取chunk。对于几百MB以上的数据包,应采用流式枚举而非一次性ToList,以防内存峰值过高。另外,消息定义中的数组与字符串字段带有长度前缀,解析时要严格按ROS规范处理嵌套结构,否则会出现偏移错位。掌握这些细节后,C#完全能胜任离线分析与测试数据提取任务。
小结
通过直接基于二进制格式编写C#解析器,我们摆脱了对ROS运行时的依赖,能够在纯Windows环境下读取机器人记录数据包。核心步骤包括识别魔法字、缓存连接信息、按需解压chunk、按消息类型反序列化。配合合理的流式处理,即可稳定地完成位姿、速度或图像话题的提取与二次分析。