ULog 文件格式
ULog 是用于记录消息的文件格式。该格式具有自描述性,即它包含所记录的格式和 uORB 消息类型。 本文档旨在成为 ULog 文件格式规范文档。 它特别适用于任何有兴趣编写 ULog 解析器/序列化器并且需要对文件进行解码/编码的人。
PX4 使用 ULog 来记录与(但不限于)以下来源相关的 uORB 主题消息:
- 设备输入:传感器、遥控输入等。
- 内部状态:CPU 负载、姿态、扩展卡尔曼滤波器(EKF)状态等。
- 字符串消息:
printf
语句,包括PX4_INFO()
和PX4_ERR()
。
该格式对所有二进制类型使用 小端序 的内存布局(数据类型的最低有效字节 (LSB) 放置在最低内存地址处)。
数据类型
以下二进制类型用于记录日志。它们都与 C 语言中的类型相对应。
类型 | 大小(以字节为单位) |
---|---|
int8_t, uint8_t | 1 |
int16_t, uint16_t | 2 |
int32_t, uint32_t | 4 |
int64_t, uint64_t | 8 |
float | 4 |
double | 8 |
bool, char | 1 |
此外,这些类型可以用作固定大小的数组:例如 float[5]
。
字符串(char[length]
)末尾不包含终止 NULL 字符 '\0'
。
INFO
字符串比较区分大小写,在 添加订阅 时比较消息名称时应考虑这一点。
ULog 文件结构
ULog 文件包含以下三个部分:
----------------------
| 头 |
----------------------
| 定义 |
----------------------
| 数据 |
----------------------
下面对每个部分进行说明。
头部分
头是一个固定大小的部分,具有以下格式(16个字节):
----------------------------------------------------------------------
| 0x55 0x4c 0x6f 0x67 0x01 0x12 0x35 | 0x01 | uint64_t |
| 文件魔法值(7B) | 版本 (1B) | 时间戳 (8B) |
----------------------------------------------------------------------
- 文件魔法值(7 字节):文件类型指示符,内容为 "ULogXYZ",其中 XYZ 是魔法字节序列
0x01 0x12 0x35
。 - 版本(1 字节):文件格式版本(当前为 1)。
- 时间戳(8 字节):
uint64_t
整数,表示日志记录开始的时间,单位为微秒。
定义和数据部分消息头
“定义” 和 “数据” 部分包含许多 消息。每条消息前面都有这个头:
struct message_header_s {
uint16_t msg_size;
uint8_t msg_type;
};
msg_size
是消息的大小(以字节为单位),不包括头。msg_type
定义了消息的内容,是一个单字节。
INFO
下面的消息部分都以前缀字符开头,该字符对应于其 msg_type
。
定义部分
定义部分包含基本信息,如软件版本、消息格式、初始参数值等。
此部分中的消息类型有:
'B': 标志位消息
INFO
此消息必须是紧接在头部分之后的 第一条消息,以便它与文件开头有固定的常量偏移量!
此消息向日志解析器提供有关日志是否可解析的信息。
struct ulog_message_flag_bits_s {
struct message_header_s header; // msg_type = 'B'
uint8_t compat_flags[8];
uint8_t incompat_flags[8];
uint64_t appended_offsets[3]; // 如果设置了附加位,则为附加数据的文件偏移量
};
compat_flags
:兼容标志位- 这些标志表示日志文件中存在与任何 ULog 解析器兼容的功能。
compat_flags[0]
:DEFAULT_PARAMETERS(位 0):如果设置,则日志包含 默认参数消息。
其余位目前未定义,必须设置为 0。 这些位可用于未来与现有解析器兼容的 ULog 更改。 例如,可以通过在标准中定义一个新位来指示添加新的消息类型,现有解析器将忽略该新消息类型。 这意味着如果设置了未知位之一,解析器可以直接忽略这些位。
incompat_flags
:不兼容标志位。incompat_flags[0]
:DATA_APPENDED(位 0):如果设置,则日志包含附加数据,并且至少有一个appended_offsets
不为零。
其余位目前未定义,必须设置为 0。 这可用于引入现有解析器无法处理的重大更改。例如,当一个没有 DATA_APPENDED 概念的旧 ULog 解析器读取较新的 ULog 时,它将停止解析日志,因为日志将包含不符合规范的消息/概念。 如果解析器发现设置了任何未指定的这些位,则必须拒绝解析该日志。
appended_offsets
:附加数据的文件偏移量(从 0 开始)。 如果没有附加数据,则所有偏移量必须为零。 这可以用于在消息中途暂停的情况下可靠地添加数据。 例如,崩溃转储。附加数据的过程应该做到:
- 设置相关的
incompat_flags
位。 - 将当前为 0 的第一个
appended_offsets
设置为不包含附加数据的日志文件长度,因为新数据将从那里开始。 - 附加对数据部分有效的任何类型的消息。
- 设置相关的
在未来的 ULog 规范中,可能会在此消息的末尾附加更多字段。这意味着解析器不能假定此消息的长度是固定的。如果消息比预期的长(当前为 40 字节),则超出的字节必须被忽略。 这意味着解析器不能假定此消息的长度是固定的。 如果 msg_size
比预期的大(当前为 40),则任何额外的字节必须被忽略/丢弃。
'F': 格式消息
格式消息在单个字符串中定义单个消息名称及其内部字段。
struct message_format_s {
struct message_header_s header; // msg_type = 'F'
char format[header.msg_size];
};
format
是一个纯文本字符串,具有以下格式:message_name:field0;field1;
- 可以有任意数量的字段(最少 1 个),用
;
分隔。 message_name
:任意非空字符串,允许的字符为:a-zA-Z0-9_-/
(并且不同于任何 基本类型)。
- 可以有任意数量的字段(最少 1 个),用
一个 field
的格式为:type field_name
,对于数组则使用:type[array_length] field_name
(仅支持固定大小的数组)。 field_name
必须由字符集 a-zA-Z0-9_
中的字符组成。
一个 type
是 基本二进制类型 之一,或者是另一个格式定义的 message_name
(嵌套使用)。
- 一个类型可以在定义之前使用。
- 例如,消息
MessageA:MessageB[2] msg_b
可以在MessageB:uint_8[3] data
之前出现。
- 例如,消息
- 可以有任意嵌套,但 不能有循环依赖
- 例如,
MessageA:MessageB[2] msg_b
和MessageB:MessageA[4] msg_a
是不允许的。
- 例如,
有些字段名是特殊的:
timestamp
:每个带有 订阅消息 的消息格式都必须包含一个时间戳字段(例如,仅作为另一个格式的嵌套定义的一部分使用的消息格式可能不包含时间戳字段)- 其类型必须是
uint64_t
。 - 单位是微秒。
- 对于具有相同
msg_id
(相同订阅)的消息系列,时间戳必须始终单调递增。
- 其类型必须是
_padding{}
:以_padding
开头的字段名(例如_padding[3]
)不应显示,读取器必须忽略其数据。- 写入器可以通过插入这个字段确保正确对齐。
- 如果填充字段是最后一个字段,则可以不记录此字段,以避免写入不必要的数据。
- 这意味着
message_data_s.data
的长度将比填充的大小短。 - 但是当消息在嵌套定义中使用时仍然需要填充。
- 一般来说,消息字段不一定是对齐的(即消息内的字段偏移量不一定是其数据大小的倍数),因此读取器必须始终使用适当的内存复制方法来访问各个字段。
'I': 信息消息
信息消息为任何信息定义了字典类型定义 key
: value
对,包括但不限于硬件版本、软件版本、软件的构建工具链等。
struct ulog_message_info_header_s {
struct message_header_s header; // msg_type = 'I'
uint8_t key_len;
char key[key_len];
char value[header.msg_size-1-key_len];
};
key_len
:键值的长度。key
:以type name
的形式包含键字符串,例如char[value_len] sys_toolchain_ver
。名称的有效字符为:a-zA-Z0-9_-/
。类型可以是 基本类型(包括数组) 之一。value
:包含与key
对应的数据(长度为value_len
),例如9.4.0
。
INFO
信息消息中定义的键必须是唯一的。这意味着不能有多个具有相同键值的定义。
解析器可以将消息信息存储为字典。
预定义的信息消息有:
键 | 描述 | 示例值 |
---|---|---|
char[value_len] sys_name | 系统名称 | "PX4" |
char[value_len] ver_hw | 硬件版本 (主板) | "PX4FMU_V4" |
char[value_len] ver_hw_subtype | 主板子版本 (变化的) | "V2" |
char[value_len] ver_sw | 软件版本 (git 标签) | "7f65e01" |
char[value_len] ver_sw_branch | git 分支 | "master" |
uint32_t ver_sw_release | 软件版本 (见下文) | 0x010401ff |
char[value_len] sys_os_name | 操作系统名称 | "Linux" |
char[value_len] sys_os_ve r | 操作系统版本 (git 标签) | "9f82919" |
uint32_t ver_os_release | 操作系统版本 (见下文) | 0x010401ff |
char[value_len] sys_toolchain | 工具链名称 | "GNU GCC" |
char[value_len] sys_toolchain_ver | 工具链版本 | "6.2.1" |
char[value_len] sys_mcu | 芯片名称和修订 | "STM32F42x, rev A" |
char[value_len] sys_uuid | 飞行器的唯一标识符 (例如 MCU ID) | "392a93e32fa3"... |
char[value_len] log_type | 日志类型 (如果未指定则为完整日志) | "mission" |
char[value_len] replay | 重播日志的文件名如果处于重播模式 | "log001.ulg" |
int32_t time_ref_utc | UTC 时间的秒偏移量 | -3600 |
INFO
value_len
表示 value
的数据大小。这在 key
中进行了描述。
ver_sw_release
和ver_os_release
的格式为:0xAABBCCTT,其中 AA 是 主版本号,BB 是 次版本号,CC 是补丁版本号,TT 是 类型。- 类型 定义如下:
>= 0
:开发版本,>= 64
:alpha 版本,>= 128
:beta 版本,>= 192
:发布候选版本 (RC),== 255
:正式发布版本。 - 例如,
0x010402FF
转换为正式发布版本 v1.4.2。
- 类型 定义如下:
此消息也可以在数据部分中使用(不过这是首选部分)。
'M': 多信息消息
多信息消息与信息消息的目的相同,但用于长消息或具有相同键的多个消息。
struct ulog_message_info_multiple_header_s {
struct message_header_s header; // msg_type = 'M'
uint8_t is_continued; // 可用于数组
uint8_t key_len;
char key[key_len];
char value[header.msg_size-2-key_len];
};
is_continued
可用于拆分消息:如果设置为 1,则它是具有相同键的前一条消息的一部分。
解析器可以将所有多消息信息存储为一个 2D 列表,使用与日志中消息相同的顺序。
有效的名称和类型与信息消息相同。
'P': 参数消息
“定义” 部分中的参数消息定义了开始记录日志时飞行器的参数值。它使用与 信息消息 相同的格式。
struct message_info_s {
struct message_header_s header; // msg_type = 'P'
uint8_t key_len;
char key[key_len];
char value[header.msg_size-1-key_len];
};
如果参数在运行时动态变化,此消息也可以 在数据部分中使用。
数据类型限制为 int32_t
和 float
。名称的有效字符为:a-zA-Z0-9_-/
。
'Q': 默认参数消息
默认参数消息定义了给定飞行器和设置的参数的默认值。
struct ulog_message_parameter_default_header_s {
struct message_header_s header; // msg_type = 'Q'
uint8_t default_types;
uint8_t key_len;
char key[key_len];
char value[header.msg_size-2-key_len];
};
default_types
是一个位字段,定义了该值所属的组。- 至少必须设置一位:
1<<0
:系统范围的默认值。1<<1
:当前配置的默认值(例如,一个机架)。
- 至少必须设置一位:
日志可能不包含所有参数的默认值。 在这些情况下,默认值等于参数值,并且不同的默认类型被独立处理。
此消息也可以在数据部分中使用,并且与参数消息使用相同的数据类型和命名规则。
此部分在第一个 订阅消息 或 日志记录字符串消息 开始之前结束,以先出现的为准。
数据部分
“数据” 部分中的消息类型有:
A
: 订阅消息
按名称订阅一条消息,并给它一个在 记录数据消息 中使用的 ID。 这必须在第一个相应的 记录数据消息 之前出现。
struct message_add_logged_s {
struct message_header