确证第25个字节:一份关于潜水电脑日志逆向工程的实证分析

在 上一篇帖子 中,我曾指出,Shearwater 潜水样本的第 25 字节 是 GF99(即实时读取的组织负荷与减压模型极限之间的接近程度);第 24 字节代表减压上限;第 18 字节为电池电压;而第 26 至 27 字节则对应于 @+5 的预测值——也就是如果你再停留五分钟,返回水面所需的时间。然而,Shearwater 从未对这些数据进行过任何记录或验证。也就是说,这一说法与背后的证据完全不相称,“在几次潜水中,曲线看起来确实很合理”这种说法,其实不过是将你的组织负荷错误地解读了整整一年,直到你才意识到这一点。
因此,底时 将每一个字节映射都视为一个假设,并提供了一套验证工具——“底部时间验证”, 旨在针对商店中每一条潜水记录,逐一检验并推翻所有这些假设。 这套工具由四个独立的“预言者”组成,其中第五个预言者后来被发现来自外部。 但这些预言者所给出的结果,无一能真正反映我对于一次合理潜水的主观判断。
预言者 1:供应商的另一种渲染方式
Shearwater Cloud 可以将潜水数据导出为 XML 格式。虽然该 XML 数据存在一定程度的失真——它缺少了我本应反向还原的那些通道信息——但其实际承载的通道数据, 却源自 Shearwater 自己编写的代码,而该代码正是基于相同的原始日志进行解析的。 因此,这些数据可以作为每次采样对应的“样本关键值”,用于记录已知的偏移量。 如果我的解码器与他们的导出工具在 371 号样本的深度上产生了分歧,那么问题就出在了我的解码器身上。
这套工具会根据每个 XML 文件的开始时间,将文件与对应的潜水记录配对,并逐个比较九个通道的数据:
XML 字段
解码后的列
容差
当前深度
depth_m
0.051 米
tts_min
tts_min
0.001
首次停顿深度
stop_depth_m
0.11 米
O2 分数 / He 分数
o2_pct, he_pct
0.001
平均 PPO2
avg_ppo2
0.011
水温
temp_c
0.51°C
电池电压
battery_v
0.011 V
当前 NDL
ndl_min
0.001
之所以设置容差,是因为两种渲染方式的四舍五入规则并不一致。 日志中以分米为单位存储深度,而 XML 文件则以浮点数的形式输出深度,因此哪怕只有半分米的误差,也足以在四舍五入时掩盖真实误差。 第 18 字节同样理所当然地出现在这里。XML 文件中的电池电压,等于每一份样本中该字节的数值除以 100,也正是这一计算过程,让原本只是猜测的数值,最终被精确地映射到了相应的深度上。
在使用 Shearwater 潜水电脑进行的 142 次潜水中,我将每一份 XML 文件与对应的潜水记录一一对应起来:
XML 对比:142 次潜水,最差偏差为 0.0000,出现在第 13 次[86]。depth_m
每一条潜水记录中,所有共享的样本都完全一致。最严重的偏差不过是一个微小的四舍五入误差,几乎可以忽略不计,甚至会被显示为零。
预言者 2:供应商自身的计算方法
XML 文件无法对 GF99 进行确切的验证,因为 GF99 是其遗漏的字段之一。然而,Shearwater Cloud 会为每条潜水计算一个名为 “EndGF99”的汇总统计值——即上升至水面时的梯度因子——并将该值存储在数据库中, 与原始的二进制数据一同保存。Shearwater 的代码正是基于我正在解码的同一组样本,计算出了这个数值。 因此,如果第 25 字节真的就是 GF99,那么我在潜水最后几分钟内解码得到的第 25 字节数值的最大值,应当等于其 EndGF99 值。
在全部 142 次 Shearwater 潜水记录中:
偏差的中位数为零。最严重的偏差仅出现于一个 GF 点,且在两次潜水中均有体现。在一次潜水中,Cloud 记录的深度为 5,而我记录的深度为 6;而在另一次潜水中,Cloud 的 EndGF99 值为 0,而我的尾部记录的值却为 1。这两处读数都位于尺度的最低端,组织仅略高于 Bühlmann 限值的几个百分点,因此仅仅相差一个点,就相当于两次以同样的方式完成了干净的上升。 我选择以过去 60 个样本(约十分钟)作为窗口,而非直接使用最后一份样本,因为 GF99 在上升的最后 1 米过程中始终在变化,而 Cloud 和我的快照未必能恰好在同一瞬间达成一致。
预言者 3:物理规律
有些检测根本不需要依赖供应商。如果解码后的上限深度竟然比显示的停顿深度还要深, 那这显然是毫无意义的。减压停止点只落在 3 米的网格上(3 米、6 米、9 米……),而停顿点则是被推移到下一层网格深度的上限,因此解码后的上限绝不可能低于解码后的停顿深度。 这一恒定性在商店中所有的 33,773 个样本中均得到了完美体现,没有出现任何违规情况。 如果我在任意一个字段中选择了错误的字节,那么这一检查就会立刻显露出问题——因为任意一个字节都没有理由在长达一百小时的潜水过程中, 依然保持与相邻字节的量化一致性。
0xFF 这个哨兵在物理层面也同样表现得十分稳定。 在我的 101.5 米潜水中,第 25 字节从第 10 秒一直读到第 1050 秒,持续了一个完整的循环,之后便再未出现过任何读数。 这段区间涵盖了下降阶段以及深潜部分。 在整个过程中,组织始终处于环境压力之下,没有任何超饱和现象需要报告。 当物理规律表明梯度因子开始出现时,这个哨兵便会自动关闭。
预言者 4:另一只手腕
我用两台设备记录每一次潜水:一台是 Garmin Descent Mk3i,另一台是 Perdix。据我所知,这两台设备在固件、传感器以及供应商方面均互不相同。 在匹配器完成时钟同步后,这两款设备都声称能够描述同一具身体,在同一水柱中进行测量, 因此它们的深度通道必须保持一致。对于所有 84 对匹配的潜水记录,这套工具会取对齐时间戳下的深度绝对差的中位数:
双机一致性:84 次匹配,中位数为 0.162 米,最差为 0.840 米
容差随深度而变化,最大值为 0.6 米,或最大深度的 4.5%——这是因为这两台设备分别依据自身假定的水密度(淡水、海水,或 EN13319 潜水计标准)将压力转换为深度,而这两种水密度的差异最高可达 3%。
诚实地说,两台毫不相关仪器之间 16 厘米的中位数偏差,远比我原本预期的物理规律要好——毕竟,物理规律的起点是“假设一种密度”。
第 25 字节还可能是什么?
如果某个字节仅仅与 GF99 相关,那么它在粗略的检查中很可能被忽略。 因此,每一种活体替代方案都需要单独加以验证。 其中最强有力的,是 SurfGF——Shearwater 的另一项梯度指标,其计算结果与我此刻立即上升时的超饱和度完全一致。 两者在上升至水面的瞬间达到一致,因此仅靠 EndGF99 这一预言者是无法区分它们的。而哨兵却能分辨出来。 SurfGF 的定义清晰明确,它在下降和底部阶段不断攀升,而这正是第 25 字节在上升过程中花费 1,040 秒,读取 0xFF 的时间。CNS——也就是氧气毒性计时器——已经被证明两次无效。 它早已在第 23 字节中被记录下来,而且在潜水过程中从未降低; 而第 25 字节则会在每一次长时间的停顿中逐渐衰减。 计时器、计数器以及类似电池的慢速通道,也都通过了同样的形状测试。 GF99 是我所知唯一一个在深度处未定义的量——它在上升过程中苏醒,在停顿时衰减, 在上升时出现峰值,并且在 Shearwater 自身的 EndGF99 中,竟在 142 次潜水中出现了 142 次。
预言者 5:供应商自身的来源
上述四个预言者本质上都是行为性的。它们通过字节 25 的行为表现来作出判断。后来又多出了一位第五个预言者,它则从结构层面来评判字节 25。Shearwater Cloud 将其解析器封装为 .NET 程序集 DiveLogParser.dll,而 macOS 应用程序则携带与 Windows 版本相同的托管文件,因此我可以读取负责解码这些记录的类(怎么做在上一篇帖子里)。 该类将字节 25 命名为 GF99_OFFSET,将字节 24 命名为 DECOCEILING_OFFSET,从而一次性确认了全部四个经验性的字节。
此外,它还比哨兵更深入地解决了 SurfGF 的问题。解析方法仅在减压模型使用梯度因子时,才会将字节 25 视为 GF99,并将其作为 DCIEM 中安全上升深度的分数来读取——DCIEM 是一种不使用梯度因子的减压模型。像 SurfGF 这样的通用梯度数值,绝不会走这条分支,而这条分支会将字节 25 特别地标记为 GF99,并填补行为预言者留下的那一点空白。 这也是我所掌握的最强有力的证据,也正因如此,我才将其放在最后。 如果我先阅读了源代码,我本可以信任这些名称,而跳过那些本会让我误判 DCIEM 情况的检查。源代码会告诉你这些字节究竟叫什么名字。 唯有数据本身才能告诉你,你的代码是否正确。
第 11 字节,连任何一个预言者都无法触及
第 11 字节击败了所有四个行为预言者。在 33,773 个样本中,它在 32,544 个样本中仍为非零值,在深潜过程中介于 82 到 94 之间,并且与任何导出的字段、汇总统计值、不变量,乃至第二台设备都没有关联。 很长一段时间里,它以原始形式存在于一个额外的 JSON 列中,未加任何标签,成为商店中 392,959 个未解码负载中的一个……