跳到主要内容

Blackbox日志记录内部结构

Blackbox 旨在以接近最大速率记录飞控的原始内部状态。通过记录 Blackbox 日志记录关键飞行系统的原始输入和输出,旨在允许离线台式仿真、调试、 以及使用从实际飞行中收集的数据测试飞行控制算法。

典型的日志记录机制可能会在一个样本中捕获 30 个不同的状态变量(平均每帧 28 个字节) 频率为900Hz。这大约是每秒 25,000 字节,即典型 8-N-1 串行编码的 250,000 波特率。

参考文献​

请参阅源代码以澄清本文档中未明确的任何内容:

记录周期​

Blackbox 专为基于“主循环”概念的飞控而设计。在每个主循环期间 迭代时,飞控将从传感器读取一些状态,运行一些飞行控制算法,并产生一些 输出。对于每个循环迭代,都会执行 Blackbox“记录迭代”。这将读取数据 在主循环执行期间存储并将该数据记录到附加的记录设备中。数据将包括 算法输入,例如传感器和遥控数据、飞行控制算法的中间结果以及算法输出 例如电机命令。

日志框架类型​

记录到日志中的每个事件都被打包为“日志帧”。 Blackbox 仅使用少数不同类型的 日志帧。每种帧类型均由单个大写字母标识。

主框架:I、P​

最基本的记录帧类型是“主帧”。这些记录了飞控的主要状态(RC 输入、陀螺仪、飞行控制算法中间体、电机输出),并在每次记录期间记录 迭代。

每个主框架必须至少包含两个字段,“loopIteration”记录当前主循环的索引 迭代(第一次记录的迭代从零开始),以及记录迭代开始的时间戳的“时间” 主循环以微秒为单位(这不需要从零开始,在 Cleanflight 上它代表系统正常运行时间)。

主机有“I”和“P”两种。 “I”或“帧内”帧就像视频关键帧。它们可以被解码 不参考任何先前的帧,因此它们允许在日志损坏时重新同步日志解码。 “P” 或“帧间”帧使用引用先前记录的帧的编码,以减少所需的数据速率。 当一个帧间帧无法解码时,所有后续帧间帧都将无法解码,直到下一个帧内帧。

GPS 帧:G、H​

由于 GPS 更新频率很低,因此 GPS 数据记录在其自己的专用帧中。这些都会被记录下来 GPS 数据发生变化(不一定与每个主机一起变化)。与主框架一样,GPS 框架也有自己的 帧内/帧间编码系统。

“H”或“home”框架记录参考点的纬度/经度。 “G”或“GPS”帧记录了当前状态 基于参考点的 GPS 系统(当前位置、海拔高度等)。参考点可以更新 (不经常)在飞行期间,并在发生变化时记录下来。

为了允许“G”帧在“H”更新从日志中丢失的情况下继续解码,“H”帧被 定期记录,即使它没有改变(例如,每 10 秒)。这限制了不可读的“G”帧的持续时间 这将是由于单个错过的“H”更改而导致的。

慢帧:S​

某些飞控状态的更新频率非常低(大约每分钟一次或两次)。记录事实 该数据在每次记录迭代期间都没有更新,这会浪费带宽,因此这些帧 仅当“慢”状态实际发生变化时才会记录。所有慢帧都记录为帧内。帧间编码方案不能用于慢帧,因为 损坏的帧会导致所有后续帧间无法解码。由于慢帧很少被写入,因此一 缺少慢速帧可能会使慢速状态几分钟失效。

在 Cleanflight 上,慢帧当前用于记录用户选择的飞行模式和当前失控保护等数据 状态。

事件框架:E​

某些飞控数据更新频率很低或存在时间很短,因此我们不会将其记录为飞行 控制器“状态”。相反,我们将其记录为状态 transition 。该数据记录在“E”或“事件”帧中。每场活动 帧有效负载以单字节“事件类型”字段开始。其余有效负载的格式未编码在 飞行日志,因此其解释取决于作者和解码器的协议。

例如,Cleanflight 记录的一个事件是用户调整了系统设置(例如 PID 设置) 使用 Cleanflight 的飞行调整功能。事件负载记录了调整的设置以及新值 用于设置。

由于这些设置更新非常罕见,因此将设置视为“状态”并记录以下事实是浪费的: 在每次日志记录迭代期间该设置都没有更改。定期记录系统是不可行的 设置使用帧内/帧间方案,因为帧内太大。相反,我们只记录 转换为事件,接受这些事件中的任何一个将被损坏/缺失在日志中的小概率,并且 让日志读者决定他们愿意在多大程度上假设设置的状态 成功解码的转换事件之间的差异确实没有改变。

日志字段格式​

对于给定帧类型中的每个字段,都有关联的名称、预测器和编码。

当写入字段时,会为该字段计算所选的预测变量,然后从 原始字段值。最后,编码器用于将值转换为要写入日志设备的字节。

场预测器​

预测器的工作是使要编码的值尽可能接近零。预测器可以基于 上一帧中字段的值,或其他一些值,例如固定值或记录在 日志标题。例如,Cleanflight 中“I”帧内的电池电压值使用的参考电压为 作为预测器记录为标头的一部分。这假设电池电压大致类似于 飞行的初始电池组电压(例如 4S 电池电压可能会在整个飞行过程中处于一个小范围内) 飞行)。在“P”帧间,电池电压将使用之前记录的电压作为预测值,因为 连续电压读数之间的相关性很高。

这些预测器目前可用:

预测零 (0)​

该预测器是空预测器,它根本不修改字段值。这是字段的常见选择 已经接近于零,或者没有更好的历史记录可用(例如,在可能不依赖于 字段的先前值)。

预测最后一个值 (1)​

这是帧间最常见的预测器。该字段的最后记录值将用作预测变量,并且 从原始字段值中减去。对于不经常更改的字段,这将使它们的编码值变为 通常为零。大多数场都具有一定的时间相关性,因此该预测器应该减少除 最喧闹的领域。

预测直线 (2)​

该预测器假设当前测量值与前一个测量值之间的斜率类似于 前一次测量值与前一次测量值之间的斜率。这对于以稳定速度增长的领域来说很常见, 例如“时间”字段。预测变量是history_age_2 - 2 * history_age_1。

预测平均值 2 (3)​

该预测变量是该字段之前记录的两个值的平均值(即 (history_age_1 + history_age_2) / 2)。当场中存在显着的随机噪声时使用它,这意味着最近的平均值 历史比前一个值本身更能预测下一个值(例如,在陀螺仪中) 或电机测量)。

预测 minthrottle (4)​

该预测器减去日志标头中包含的“minthrottle”值。在 Cleanflight 中,电机始终 当飞行器武装时,位于 [minthrottle ... maxthrottle] 范围内,因此该预测器用于第一个 帧内运动值。

预测电机[0] (5)​

该预测器设置为 motor[0] 的值,该值是在当前帧中较早解码的。它用于 第一个电机之后的每个电机的帧内,因为电机命令通常位于紧密的分组中。

预测增量 (6)​

该预测器假设每次主循环迭代该字段都会增加 1 个单位。这是用来 预测 loopIteration 字段,每次循环迭代该字段都会增加 1。

预测主坐标 (7)​

该预测器设置为 GPS 主坐标(已登录)的相应纬度或经度字段 前面的“H”帧)。如果不存在前面的“H”帧,则该值被标记为无效。

预测 1500 (8)​

该预测器设置为固定值 1500。最好在帧内记录伺服值,因为这些值 通常位于 1500us 的中点附近。

预测 vbatref (9)​

该预测器设置为写入日志标头中的“vbatref”字段。记录帧内电池时使用 Cleanflight 中的电压,因为这些电压预计与在 武装。

预测最后一个主机时间 (10)​

该预测器设置为主机中最后记录的 time 字段。这在预测时间戳时使用 非主框架(例如,可能会记录主循环周期中发生的事件的时间,例如 GPS 阅读)。

现场编码器​

字段编码器的工作是使用更少的位来表示比接近零的值更接近零的值。 离零更进一步。 Blackbox 支持一系列不同的编码器,应根据每个字段的顺序选择编码器 以最小化编码数据的大小。最佳编码器的选择基于值的概率分布 哪些要被编码。例如,如果一个字段几乎总是零,那么应该为其选择一种编码 可以将该情况编码为非常少量的位,例如一位。相反,如果字段通常为 8-16 位大, 使用为零值提供特殊短编码表示的编码器是浪费的,因为 这将增加较大值的编码长度。

这些编码器目前可用:

无符号变量字节 (1)​

这是最直接的编码。该编码使用编码字节的低 7 位来存储低 7 位 字段值的位。如果需要超过 7 位来存储该编码字节,则该编码字节的高位设置为 1 值。如果该值确实超过 7 位,则该值的低 7 位(已写入日志)将从中删除 值(通过右移),编码过程再次以新值开始。

这可以用以下算法表示:

while (value > 127) {
writeByte((uint8_t) (value | 0x80)); // Set the high bit to mean "more bytes follow"
value >>= 7;
}
writeByte(value);

以下是使用可变字节编码编码的一些示例值:

输入值输出编码
10x01
4242 0x2A
127127 0x7F
128128 0x80 0x01
129129 0x81 0x01
2345623456 0xA0 0xB7 0x01

有符号变量字节 (0)​

此编码应用预处理步骤将负值折叠为正值,然后得到无符号的结果 数字使用无符号可变字节编码进行编码。折叠是通过“ZigZag”编码完成的,即 代表者:

unsigned32 = (signed32 << 1) ^ (signed32 >> 31)

ZigZag 编码优于简单地将有符号整数转换为无符号整数,因为转换会导致小负数看起来是非常大的无符号整数,导致编码长度同样很大。 ZigZag 编码确保接近零的值在编码后仍然接近零。

以下是使用 ZigZag 编码进行编码的一些示例整数:

输入值ZigZag 编码
00
-11
12
-23
21474836474294967294
-21474836484294967295

负 14 位 (3)​

该值被取反,被视为无符号 14 位整数,然后使用无符号可变字节编码进行编码。这个 Cleanflight 中对电池组电压使用了奇怪的编码。这是因为电池电压是使用 14 位 ADC,带有一个预测器,该预测器在准备期间设置为电池电压,预计该电压高于任何 飞行期间经历的电压。减去预测器后,电池电压几乎肯定会低于 零。

当电压远低于初始电压时,这会导致编码值较小,但代价是非常大 如果电压上升高于初始值,则编码值。

Elias delta 无符号 32 位 (4)​

由于此编码会生成比特流,因此这是唯一编码值可能不是完整的编码 字节数。如果在下一个非 Elias Delta 字段到达时比特流未在字节边界上对齐, 或者到达帧的末尾,最后一个字节用零填充,字节对齐流。该编码需要 由于写入比特流时涉及位处理,因此比其他编码需要更多的 CPU 时间。

当选择该编码器对 Cleanflight 帧间的所有值进行编码时,可节省约 10% 的带宽 与使用其他编码的混合相比,但使用太多的 CPU 时间而不实用。

基本编码算法在维基百科上定义。鉴于这些 效用函数:

/* Write `bitCount` bits from the least-significant end of the `bits` integer to the bitstream. The most-significant bit
* will be written first
*/
void writeBits(uint32_t bits, unsigned int bitCount);

/* Returns the number of bits needed to hold the top-most 1-bit of the integer 'i'. 'i' must not be zero. */
unsigned int numBitsToStoreInteger(uint32_t i);

这是我们的 Elias Delta 参考实现:

// Value must be more than zero
void writeU32EliasDeltaInternal(uint32_t value)
{
unsigned int valueLen, lengthOfValueLen;

valueLen = numBitsToStoreInteger(value);
lengthOfValueLen = numBitsToStoreInteger(valueLen);

// Use unary to encode the number of bits we'll need to write the length of the value
writeBits(0, lengthOfValueLen - 1);

// Now write the length of the value
writeBits(valueLen, lengthOfValueLen);

// Having now encoded the position of the top bit of value, write its remaining bits
writeBits(value, valueLen - 1);
}

为此,我们添加一个包装器,允许对值零和 MAXINT 进行编码:

void writeU32EliasDelta(uint32_t value)
{
/* We can't encode value==0, so we need to add 1 to the value before encoding
*
* That would make it impossible to encode MAXINT, so use 0xFFFFFFFF as an escape
* code with an additional bit to choose between MAXINT-1 or MAXINT.
*/
if (value >= 0xFFFFFFFE) {
// Write the escape code
writeU32EliasDeltaInternal(0xFFFFFFFF);
// Add a one bit after the escape code if we wanted "MAXINT", or a zero if we wanted "MAXINT - 1"
writeBits(value - 0xFFFFFFFE, 1);
} else {
writeU32EliasDeltaInternal(value + 1);
}
}

以下是 writeU32EliasDelta 生成的一些参考编码位模式:

输入值编码位串
01
10100
20101
301100
401101
501110
601111
700100000
800100001
900100010
1010 00100011
1111 00100100
1212 00100101
1300100110
1414 00100111
1515 001010000
225225 00010001100010
4294967292000001000001111111111111111111111111111101
4294967293000001000001111111111111111111111111111110
42949672940000010000011111111111111111111111111111110
42949672950000010000011111111111111111111111111111111

请注意,非常常见的零值编码为单个位,中等大小的值(例如 225)编码为 14 位( 比写入普通 8 位值多 4 位的开销),典型的 32 位值(如 4294967293)编码为 42 位, 10 位的开销。

Elias delta 有符号 32 位 (5)​

首先使用 ZigZag 编码将该值转换为无符号,然后应用无符号 Elias-delta 编码。

TAG8_8SVB (6)首先,写入 8 位(一个字节)标头。当相应的字段(来自一个​

最多 8 个字段)设置为零,否则该位设置为 1。标头中的最低有效位对应 到要写入的第一个字段。该标头后面仅跟随非零字段的值,写入 使用有符号变量字节编码。

这种编码对于飞控不经常更新的帧间字段组是首选。 这意味着他们的预测通常是完美的,因此每个字段要编码的值通常会 为零。这对于 RC 输入和气压计读数等值很常见,这些值仅在主数据的一小部分中更新。 循环迭代。

例如,给定要编码的这些字段值:

0, 0, 4, 0, 8

这将被编码:

0b00010100, 0x04, 0x08

TAG2_3S32 (7)​

写入 2 位标头,后跟 3 个带符号字段值,每个字段值最多 32 位。标头值基于 要编码的三个值的最大大小(以位为单位)如下:

标头值最大字段值大小领域范围
02位[-2...1]
14位[-8...7]
26位[-32...31]
3高达 32 位[-2147483648...2147483647]

如果这三个值中的任何一个需要超过 6 位进行编码,则将第二个 6 位标头值写入较低的位置。 初始头字节的位。第二个标头对于每个编码值都有 2 位,代表有多少个编码值。 需要字节来编码该值。标头的最低有效位表示第一个字段,即 编码。每种可能性的值如下:

标头值场地大小领域范围
01 字节[-127...128]
12 个字节[-32768...32767]
23字节[-8388608...8388607]
34 字节[-2147483648...2147483647]

该标头后面按顺序跟随实际字段值,首先写入最低有效字节,使用字节 标头中指定的长度。

因此,将它们放在一起,这些编码的位模式是可能的,其中“0”和“1”表示固定为这些位 值,“A”、“B”、“C”代表第一、第二、第三字段,“s”代表次要字段的比特 任何字段大于 6 位的情况下的标头:

00AA BBCC,
0100 AAAA BBBB CCCC
10AA AAAA 00BB BBBB 00CC CCCC
11ss ssss (followed by fields of byte lengths specified in the "s" header)

这种编码对于 3 轴陀螺仪等领域非常有用,这些领域通常很小并且通常具有类似的 幅度。

TAG8_4S16 (8)​

写入 8 位标头,后跟 4 个带符号字段值,每个字段值最多 16 位。 8位头值有2位 对于每个编码字段(最低有效位代表第一个字段),它代表 对该字段进行编码所需的位数如下:

标头值字段值大小领域范围
00 位[0...0]
14位[-8...7]
28位[-128...127]
316位[-32768...32767]

该标头后面按顺序跟随实际字段值,就像输出流是比特流一样编写,其中 第一个字段的最高有效位以第一个写入字节的最高有效位结束。如果数量 写入的半字节为奇数,最后一个字节的最低有效半字节设置为零。

例如,给定这些字段值:

13, 0, 4, 2

从允许的字段值大小中进行选择,它们可以分别使用这么多位进行编码:

8, 0, 4, 4

这些长度的相应标头值为:

2, 0, 1, 1

因此标头和字段将一起编码为:

0b01010010, 0x0D, 0x42

空 (9)此编码不会将任何字节写入文件。当预测器总是能够完美预测时使用它​

字段的值,因此余数始终为零。实际上,这仅用于“loopIteration”字段 帧间,根据记录的帧在帧序列中的位置始终是完全可预测的 标题中的“P 间隔”设置。

日志文件结构​

日志记录会话以日志开始标记开始,然后是描述日志格式的标头部分,然后是 记录有效负载数据,最后是可选的“记录结束”事件(“E”帧)。

单个日志文件可以由一个或多个日志记录会话组成。每个会话之前和之后可以有任何 非Blackbox数据量。 Blackbox 日志解码工具会忽略此数据。这允许记录设备 由 Blackbox 和其他一些系统(例如 MSP)交替使用,无需启动 每个单独的活动都有单独的日志文件。

日志开始标记​

日志开始标记为“H Product:Blackbox Flight data recorder by Nicholas Sherlock\n”。这个标记是 如果日志从文件的中途开始,则用于发现飞行日志的开头。因为它是如此之长 字符串,它不会意外出现在来自其他日志设备用户的任何随机字节序列中。

日志头​

标头由一系列纯 ASCII 文本行组成。每个标题行的格式为 H fieldname:value 并以 '\n' 结尾。整个标头没有终止符将其与日志有效负载分开 (当行不以“H”字符开头时,标题隐式结束)。

标头可以包含以下一些字段:

数据版本(必填)​

当 Blackbox 标头的解释由于 Blackbox 规范更新而发生变化时,日志版本为 递增以允许解码器向后兼容:

H Data version:2

记录间隔​

并非每个主循环迭代都需要导致 Blackbox 日志记录迭代。当循环迭代未被记录时, Blackbox 不会被调用,不会从飞控读取任何状态,也不会向日志中写入任何内容。两个标题行 包括以记录哪些主循环迭代将被记录:

#####我间隔

此标头指出哪个主循环迭代会将“I”帧内记录到日志中。如果主循环迭代 索引可被 32 整除的将被记录为“I”帧,标头将为:

H I interval: 32

Blackbox 看到的第一个主循环迭代将使用索引 0 进行编号,因此第一个主循环迭代将 始终记录为帧内。

P 区间​

并非每个“P”帧间都需要记录。 Blackbox 将记录一部分迭代,以便得出总数 记录的主机的一部分到用户选择的部分。该分数称为记录率。尽可能最小的 记录速率为 (1/I interval),对应于在“I”间隔仅记录“I”帧并丢弃所有帧 其他循环迭代。最大记录速率为 1/1,其中每个不是“I”帧的主循环迭代是 记录为“P”帧。标头以 numerator/denominator 格式记录日志记录率分数,如下所示:

H P interval:1/2

应简化 num/denom 给出的测井分数(即,应将 1/3 的测井率改为 2/6) 被使用)。

给定记录速率 num/denom 和 I 帧间隔 I_INTERVAL,迭代记录的帧类型 索引 iteration 由下式给出:

if (iteration % I_INTERVAL == 0)
return 'I';

if ((iteration % I_INTERVAL + num - 1) % denom < num)
return 'P';

return '.'; // i.e. don't log this iteration

对于 32 的 I 间隔,这些是在某些不同 P 测井速率下生成的测井模式。

记录率主框架图案实际记录的部分
1 / 32我……………………………………我0.03
1 / 6我…P…P…P…P…P.I…P…P…P…P…P…P.I0.19
1 / 21 / 2 I.P.P.P.P.P.P.P.P.P.P.P.P.P.P.P.I.P.P.P.P.P.P.P.P.P.P.P.P.P.P.P.I0.50
2 / 32 / 3 PP.PP.PP.PP.PP.PP.PP.PP.PP.PP.I.PP.PP.PP.PP.PP.PP.PP.PP.PP.PP.I0.66
5 / 6I.PPPPPP.PPPPPP.PPPPPP.PPPPPP.PPPPPP.I.PPPPPP.PPPPPP.PPPPPP.PPPPPP.PPPPPP.I0.81
1 / 11 / 1 IPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPI1.00

固件类型(可选)​

由于 Blackbox 记录内部飞控状态,因此记录数据的解释将取决于 知道哪个飞控记录了它。为了适应这种情况,应记录飞控的名称:

H Firmware type:Cleanflight

应包含更多详细信息,以帮助缩小精确的飞控版本范围(但这些不是必需的):

H Firmware revision:c49bd40
H Firmware date:Aug 28 2015 16:49:11

字段 X 名称(必填)​

此标头是“X”帧类型中字段名称的逗号分隔列表:

H Field I name:loopIteration,time,axisP[0],axisP[1]...

解码器假设“P”帧类型中的字段都与“I”帧中的字段具有相同的名称,因此 不需要提供“Field P name”标头。

字段 X 签名(可选)​

这是一个以逗号分隔的整数列表,当相应字段的值应为时,这些整数被设置为“1” 解码后解释为有符号,否则为“0”:

H Field I signed:0,0,1,1...

字段 X 预测器(必需)​

这是一个以逗号分隔的整数列表,用于指定指定帧类型中每个字段的预测变量:

H Field I predictor:0,0,0,0...

字段 X 编码(必填)​

这是一个以逗号分隔的整数列表,指定指定帧类型中每个字段使用的编码:

H Field X encoding:1,1,0,0...

vbatref​

该标头提供预测器 #9 将使用的参考电压。

最小油门​

该标头提供了在武装时由 Cleanflight 发送到 ESC 的最小值,由预测器 #4 使用。

附加标头​

解码器会忽略它不理解的标头,因此您可以自由添加所需的任何标头,以便 正确解释记录值的含义。

例如,要创建 RC 摇杆和电机百分比的图形显示,Blackbox 渲染工具需要 附加标头“rcRate”和“maxthrottle”。为了转换原始陀螺仪、加速度计和电压读数 转换为现实世界单位时,Blackbox 解码器需要校准常数“gyro.scale”、“acc_1G”和“vbatscale”。 这些标头可能如下所示:

H rcRate:100
H maxthrottle:1980
H gyro.scale:0x3d79c190
H acc_1G:4096
H vbatscale:110

日志负载​

日志有效负载是记录帧的串联序列。日志有效负载中存在的每个帧类型必须 先前已在日志标头中进行了描述(带有框架 X 名称等标头)。每个帧都以一个单独的帧开始 大写字母指定帧的类型(I、P 等),其后紧跟帧的字段数据。那里 没有帧长度字段、校验和或尾部。

通过获取原始字段数据数组、计算每个字段的预测变量、减去该字段数据来编码字段数据 预测器,然后将字段编码器按顺序应用于每个字段,将它们序列化到日志中。

例如,假设我们正在帧内编码三个字段,对每个字段使用零预测器 (#0), 并使用无符号变量字节编码(#1)对值进行编码。对于这些字段值:

1, 2, 3

我们将对帧进行编码:

'I', 0x01, 0x02, 0x03

想象一下,我们正在帧间编码一系列运动命令。我们将使用之前的电机命令作为 预测器,并使用有符号变量字节编码对结果值进行编码。电机命令值见 之前记录的迭代是:

1430, 1500, 1470, 1490

本次迭代中要记录的电机命令是:

1635, 1501, 1469, 1532

减去每个字段的预测变量后,我们将得到:

205, 1, -1, 42

我们将对每个字段应用 ZigZag 编码,这将为我们提供:

410, 2, 1, 84
```我们将使用无符号变量字节编码将结果值写入日志,这将为我们提供:

'P', 0x9A, 0x03, 0x02, 0x01, 0x54


### 日志结束标记

日志结束标记是类型为 0xFF 的可选事件(“E”)帧,其有效负载是字符串“End of log\0”。的
有效负载确保随机数据不会偶然看起来像日志结束标记。此次活动标志着圆满结束
日志的。日志应忽略直到下一个日志开始标记(或文件结尾)之前的所有后续字节
解码器。

'E', 0xFF, "End of log�", 0x00


## 日志验证

记录过程中日志遭受的任何损坏绝大多数是由于字节序列被丢弃造成的。
由于缓冲区溢出而导致日志记录设备。因此,Blackbox 日志不会包含任何校验和(字节是
在不改变消息长度的情况下,预计不会被记录设备损坏)。由于紧
记录的带宽要求,既不记录帧长度字段也不记录帧尾部,这将允许
检测丢失字节。

相反,解码器使用启发式方法来检测损坏的帧。解码器从日志中读取整个帧
(对每个字段使用解码器,该解码器是标头中指定的编码器的对应部分),然后检查
查看紧随帧之后的字节(应该是下一帧的开始)是否是可识别的
帧类型字节(例如“I”、“P”、“E”等)。如果接下来的字节表示有效的帧类型,则假设
解码的帧是正确的长度(因此不太可能从中删除随机范围的字节,这将
可能改变了帧长度)。否则,该帧将被拒绝,并查找有效的帧类型字节
紧接在被拒绝的帧的帧起始字节之后。被拒绝的帧会导致所有后续的
帧间帧也会被拒绝,直到下一个帧内帧。

如果“loopIteration”或“time”字段发生不合理的跳跃或移动,帧也会被拒绝。
全部倒退。这足以检测几乎所有日志损坏。