Files
Just-DataInspector/Utilities/Src/Mf4Loader.cpp
T
DeepSeek V4.1 Flash 53b981a2a9 全项目大检查:修复数据层/UI 真 BUG、清理死代码与过期注释、并评估二进制体积
按「性能 / BUG / 错误 / 过期注释 / 命名与实际含义不符」四项对全仓做了一次审查,
四个只读子审查交叉核对后逐条回到源码确认再改。

一、数据层真 BUG
- MatH5:U8/U16/U32/U64 用逐字节 Cached() 拼装,而 Cached 只服务「完全落在同一个
  64KB 块内」的请求 => 跨块时静默读成 0(U32 整段为 0、U64 高 32 位为 0),分块 B-tree
  的 chunk 偏移/子节点地址被破坏且无任何报错。新增 ScalarLE():命中缓存用缓存,
  否则退回一次 ReadAt;U64 顺带从 8 次缓存查找降到 1 次。
- Mf4Loader::IsBlock 未校验链接表是否落在块内 => DataOffset 越界,随后
  block_len - (data - offset) 在 uint64 上下溢,得到一个约 2^64 的"可用字节数",
  下游 vector(usable+1) 直接 bad_alloc。同时把 offset + bytes > size 改成不产生回绕的写法。
- DecodeRawValue 的整数路径:bit_offset%8 != 0 且位宽 64 时需 9 个字节,先拼成 64 位
  再整体右移会把最高字节静默丢弃 => 改为逐字节带位偏移累加。
- cg_cycle_count 加「数据段总字节 / 记录字节」上限(压缩段按 deflate 最大膨胀比估算)。
- CsvLoader 表头分支同样跳过 #/% 注释行(否则整份文件按 1 列解析并被丢弃)。
- MatLoader:double -> int 的越界转换加了范围检查;普通对象递归加深度上限与成环检测。
- Utf8Util::BuildTicks:double -> long long 越界转换前先判商是否落在 ±9e18 内。

二、界面 BUG
- 工具栏按钮永远不显示悬停高亮:Paint 读的 Button::hover 全仓从未赋值,改为按 m_hoverIndex 判断。
- 菜单打开时"取消底层拖拽"对每个消息都执行,把菜单自己的 Target::EDIT_FIELD 也清掉
  => 菜单内联输入框无法拖拽框选。排除 EDIT_FIELD。
- GdiplusShutdown 之前未释放 PlotView 的 GDI+ 位图(Application 是进程单例,成员析构在
  WinMain 之后)=> 新增 PlotView::ReleaseGraphicsResources() 并在 Shutdown 里调用。
- WM_KILLFOCUS 现在会提交标签页/绘图区编辑并复位捕获与拖拽状态。
- WM_LBUTTONUP 不再抢走中键平移的鼠标捕获。
- TextEdit:被过滤器拒绝的字符返回 false;无选区时 Ctrl+X 只复制、不再清空整段文本。
- SignalPanel::InvalidateRows 作废编辑/待确认/改名计时等行下标状态。
- EndFilterEdit 改走 ApplyFilterText(否则输入纯空格后回车,列表仍停在筛选结果)。
- 新增 SignalBadgeVisible 作为唯一判据(原来命中只看"可见",绘制还要求"已分配网格")。
- 折起动画结束后夹取滚动量;双击分组名改名时清掉该组正在播放的折起动画。
- ScrollToSignal 复用 SignalScrollTarget(含悬浮组名避让)。
- RowNameRect 与绘制使用同一最小宽度(原来退化时返回空矩形)。
- 分隔条拖拽上限改用与 Relayout 同一公式;UpdateDragCursor 补 SPLITTER/EDIT_FIELD。
- SignalPanel 里几个守卫改为判断真正被解引用的指针;分组行 ✕ 也返回手型光标。

三、PlotView
- FindStyleOnlyLanes 改用完整的 SceneStyleEntry::operator==(原来只比 4 个字段,
  另一个网格同时改"调整"时带着旧像素被当成最新)。
- 宽基底图层不再烘焙拖拽光晕(LaneLayerHash 不含它),改由 PaintHoverHighlight 每帧叠加。
- PaintLaneLegend 提前返回前清掉本网格的命中矩形(否则能点中看不见的图例条目)。
- 面板主体改为完全不透明(消除平移后 4% 透出的旧波形,零额外代价)。
- PAN / Shift+滚轮改走 SetTimeView,使"自动点线"判据随平移重算。
- 内联输入框命中字体与绘制统一为 monoSmall。
- 派生曲线取值与窗口按 SourceIndexFor 映射(水平镜像时不再读到镜像另一端)。
- EdgePanelWidth 补 style/axis 空指针守卫;二分结果夹到 count。

四、清理
- 删除 Toolbar::ActionLabel、Theme::statusBg、PopupMenu 的 editResult 死机制、
  Toolbar::Layout 的重复 clear()、HitRow 的恒假分支、OnDoubleClick 的重复判定、
  PaintUtil 里孤立的注释块。
- 修正一批过期/错误注释(包络带"不做加宽"、LaneLayerHash 的图例说法、
  均值滤波曲线的窗口说法、样式区行数与「调整」按钮顺序、FIT_ALL 退出缩放模式、
  RemoveSignal 的 ✕ 位置、.mat/.dat 格式文档、MatH5 的"映射视图"、DataModel 的
  "点线/被移除文件"、SignalPanel 的"行高连续缩小"、TextEdit 的 PaintUtil 引用等)。

五、二进制体积评估(按要求只评估,未做压缩修改)
- release 809,472 字节;.text 652KB、.xdata+.pdata 约 75KB、.rdata 42KB、
  .rsrc 20KB、.idata 11.5KB。
- 去掉 -ld3d11 -ldwmapi -ld3dcompiler(仓内无任何调用)实测 0 字节收益
  (静态库只拉取被引用的对象)=> 删链接库不是压缩手段,只是配置整洁。
- 仓内已无 <iostream>/<sstream>/<filesystem>/<regex>/<locale> 等体积炸弹。
- 结论:已无明显的浪费可捡;要显著变小只能改变取舍(动态 CRT / UPX / 砍功能),
  需要用户决策,本轮未动。

验证
- editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、convcheck 18/18。
- check 136/137:唯一失败项经 git stash 回到改动前复现同一数值,属该探针自身的既有
  不稳定项(取决于前置用例留下的缓存/时序),与本轮改动无关。
- 真实数据:4 个 MF4 => 283/8/18/24 个信号;.mat 样例 8 信号、
  带无障碍对照-防夹力曲线数据 8 信号、BIG_MAT.mat 5 信号(私有内存 +741MB)。
- make release / make debug 零警告。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 171-180 条硬约束
与文末本轮小节(含体积评估数据)。
2026-10-20 16:10:29 +08:00

2079 lines
70 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Mf4Loader.hpp"
#include "Deflate.hpp"
#include "Utf8Util.hpp"
#include <windows.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <limits>
#include <string>
#include <utility>
#include <vector>
namespace jdi
{
namespace
{
const double kMissingValue = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
const std::size_t kReadChunkBytes = 4u * 1024u * 1024u; /**< 数据区流式读取块大小。 */
const std::size_t kMaxChannelsPerGroup = 4096; /**< 单个通道组的通道数上限(防损坏文件死循环)。 */
const std::size_t kMaxGroups = 65536; /**< 数据组数量上限。 */
/** MDF 块头固定长度(id + reserved + length + link_count)。 */
const std::size_t kBlockHeaderBytes = 24;
/** @brief 通道数据类型(`cn_data_type`)。 */
enum class DataType : int
{
UINT_LE = 0,
UINT_BE = 1,
INT_LE = 2,
INT_BE = 3,
FLOAT_LE = 4,
FLOAT_BE = 5,
STRING_LATIN1 = 6,
STRING_UTF8 = 7,
STRING_UTF16_LE = 8,
STRING_UTF16_BE = 9,
BYTE_ARRAY = 10,
MIME_SAMPLE = 11,
MIME_STREAM = 12
};
/**
* @brief 换算类型(`cc_type`)。
*
* ⚠️ 数值换算(0–6)用于把记录里的原始值转成物理值;`7/8` 是「值 → 文本」,
* 走 `ChannelSpec::enumText`(只在光标读数里展示,不改变数值)。
*/
enum class ConversionType : int
{
NONE = 0, /**< 无换算(恒等)。 */
LINEAR = 1, /**< 线性:`phys = c0 + c1 * raw`。 */
RATIONAL = 2, /**< 有理式:`phys = (c0 + c1*raw) / (c2 + c3*raw)`。 */
ALGEBRAIC = 3, /**< 代数式:`phys = (c0*raw² + c1*raw + c2) / (c3*raw² + c4*raw + c5)`。 */
TAB_INTP = 4, /**< 值→值查表(点之间**线性插值**,即特性曲线)。 */
TAB = 5, /**< 值→值查表(**不插值**,取阶梯值)。 */
TAB_RANGE = 6, /**< 值区间→值(`[min,max] → phys`,区间外不换算)。 */
VALUE_TO_TEXT = 7, /**< 值 → 枚举文本(`cc_val[i]` ↔ `cc_ref[i]` 文本)。 */
VALUE_RANGE_TO_TEXT = 8 /**< 值区间 → 文本(`cc_val` 交错 `(下限,上限)` ↔ `cc_ref[i]` 文本)。 */
};
/**
* @brief `##CA` 的数组类型(`ca_type`)。
*
* 三者**物理布局相同**(`ca_storage == 0` 时元素在本记录内按位宽连续排布,
* 实测 CANape 写出的 25 个 `##CA` 块全部如此),差别只在语义:
* `0` 普通数组 / `1` 缩放轴(特性曲线的断点) / `2` 区间轴(map 的一维断点)。
*/
enum class ArrayKind : int
{
ARRAY = 0, /**< 普通数值数组。 */
SCALING_AXIS = 1, /**< 缩放轴(断点表)。 */
INTERVAL_AXIS = 2 /**< 区间轴(每维断点成对给出)。 */
};
/**
* @brief 只读的 MDF 文件访问器(按需 seek + read,不把整个文件读进内存)。
*/
class MdfFile
{
public:
~MdfFile() { Close(); }
bool Open(const std::wstring& path, std::wstring& error)
{
m_file = _wfopen(path.c_str(), L"rb");
if (m_file == nullptr)
{
error = L"无法打开文件,请确认路径与访问权限。";
return false;
}
if (_fseeki64(m_file, 0, SEEK_END) != 0)
{
error = L"无法定位文件末尾。";
return false;
}
const long long size = _ftelli64(m_file);
if (size <= 0)
{
error = L"文件内容为空。";
return false;
}
m_size = static_cast<std::uint64_t>(size);
// ID 块:0..7 = "MDF ",8..15 = 版本号,例如 "4.10 "。
char id[8] = {};
char version[8] = {};
if (!ReadAt(0, id, sizeof(id)) || std::memcmp(id, "MDF", 3) != 0)
{
error = L"不是有效的 MDF 文件(缺少 MDF 标识)。";
return false;
}
if (!ReadAt(8, version, sizeof(version)) || version[0] != '4')
{
error = L"仅支持 MDF v4 格式(当前文件不是 v4)。";
return false;
}
return true;
}
void Close()
{
if (m_file != nullptr)
{
std::fclose(m_file);
m_file = nullptr;
}
}
std::uint64_t Size() const { return m_size; }
/**
* @brief 已从文件实际读取的字节数(用于「已读 / 总大小」进度)。
*
* 随机访问会重复读取元数据,因此这个值可能超过文件大小;
* 上报时必须自行夹到 `Size()`。
*/
std::uint64_t BytesRead() const { return m_bytesRead; }
/** @brief 读取一段原始字节。 */
bool ReadAt(std::uint64_t offset, void* dst, std::size_t bytes) const
{
// ⚠️ 不能写 `offset + bytes > m_size`:损坏文件里 offset 接近 2^64 时会回绕,
// 校验因此“通过”然后去读一个不存在的偏移。
if (m_file == nullptr || offset > m_size || bytes > m_size - offset)
{
return false;
}
if (_fseeki64(m_file, static_cast<long long>(offset), SEEK_SET) != 0)
{
return false;
}
const bool ok = std::fread(dst, 1, bytes, m_file) == bytes;
if (ok)
{
m_bytesRead += bytes;
}
return ok;
}
/** @brief 读取一个小端标量(最多 8 字节)。越界返回 0。 */
std::uint64_t Scalar(std::uint64_t offset, std::size_t bytes) const
{
unsigned char buffer[8] = {};
if (bytes == 0 || bytes > 8 || !ReadAt(offset, buffer, bytes))
{
return 0;
}
std::uint64_t value = 0;
for (std::size_t i = 0; i < bytes; ++i)
{
value |= static_cast<std::uint64_t>(buffer[i]) << (8u * i);
}
return value;
}
std::uint32_t U32(std::uint64_t offset) const
{
return static_cast<std::uint32_t>(Scalar(offset, 4));
}
std::uint16_t U16(std::uint64_t offset) const
{
return static_cast<std::uint16_t>(Scalar(offset, 2));
}
std::uint8_t U8(std::uint64_t offset) const
{
return static_cast<std::uint8_t>(Scalar(offset, 1));
}
std::uint64_t U64(std::uint64_t offset) const { return Scalar(offset, 8); }
double F64(std::uint64_t offset) const
{
const std::uint64_t bits = U64(offset);
double value = 0.0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
/** @brief 读取块标识(4 字节 ASCII)。 */
bool BlockId(std::uint64_t offset, char out[5]) const
{
out[0] = out[1] = out[2] = out[3] = '?';
out[4] = '\0';
return ReadAt(offset, out, 4);
}
/** @brief 块总长度(含块头)。 */
std::uint64_t BlockLength(std::uint64_t offset) const { return U64(offset + 8); }
/** @brief 块内链接数量。 */
std::uint64_t LinkCount(std::uint64_t offset) const { return U64(offset + 16); }
/** @brief 第 `index` 个链接(绝对文件偏移,0 表示空)。 */
std::uint64_t Link(std::uint64_t offset, std::uint64_t index) const
{
if (index >= LinkCount(offset))
{
return 0;
}
return U64(offset + kBlockHeaderBytes + 8u * index);
}
/** @brief 块的数据段起始偏移。 */
std::uint64_t DataOffset(std::uint64_t offset) const
{
return offset + kBlockHeaderBytes + 8u * LinkCount(offset);
}
/**
* @brief 判断某个偏移是否为一个有效块的开头(且标识匹配)。
*
* ⚠️ **必须同时校验链接表落在块内**(`24 + 8*link_count <= block_len`):
* 否则 `DataOffset()` 会算到块尾之外,随后 `block_len - (data - offset)` 在 `uint64`
* 上发生**下溢**,得到一个约 2^64 的“可用字节数”,下游 `vector(usable+1)` 直接抛
* `bad_alloc` —— 一个损坏的链接数就能让加载崩溃。
*
* @param [IN] offset 候选偏移。
* @param [IN] id 期望的 4 字节标识(如 "##CN");空字符串表示只校验偏移合法性。
*/
bool IsBlock(std::uint64_t offset, const char* id) const
{
if (offset == 0 || offset > m_size || kBlockHeaderBytes > m_size - offset)
{
return false;
}
char actual[5] = {};
if (!BlockId(offset, actual))
{
return false;
}
if (id != nullptr && id[0] != '\0' && std::memcmp(actual, id, 4) != 0)
{
return false;
}
const std::uint64_t length = BlockLength(offset);
if (length < kBlockHeaderBytes || length > m_size - offset)
{
return false;
}
const std::uint64_t links = LinkCount(offset);
// 链接表必须真的落在块内(并限制在合理上限内,防止 length 被算成天文数字)。
return links <= (length - kBlockHeaderBytes) / 8u;
}
/**
* @brief 读取 `##TX` / `##MD` 块里的文本。
*
* ⚠️ 此前注释写的是「`##TX` / `##MD`」但实现只接受 `##TX`,于是 `##MD`
*(XML 元数据:单位、注释、显示名等)一律返回空。现在两种块都读:
* * `##TX`:兼容规范的「长度 + UTF-16 / UTF-8」与 CANape 的「NUL 结尾裸文本」;
* * `##MD`:数据段是 XML(UTF-8),没有长度前缀 ⇒ 直接转成宽字符串。
*/
std::wstring TextBlock(std::uint64_t offset) const
{
std::wstring text;
if (IsBlock(offset, "##MD"))
{
const std::uint64_t data = DataOffset(offset);
const std::uint64_t usable = BlockLength(offset) - (data - offset);
if (usable == 0)
{
return text;
}
std::vector<char> buffer(static_cast<std::size_t>(usable) + 1u, '\0');
if (!ReadAt(data, buffer.data(), static_cast<std::size_t>(usable)))
{
return text;
}
std::size_t length = static_cast<std::size_t>(usable);
while (length > 0 && buffer[length - 1] == '\0')
{
--length;
}
return Utf8ToWide(buffer.data(), static_cast<int>(length));
}
if (!IsBlock(offset, "##TX"))
{
return text;
}
const std::uint64_t data = DataOffset(offset);
const std::uint64_t usable = BlockLength(offset) - (data - offset);
if (usable < 2)
{
return text;
}
// MDF 规范里 TX 的数据段是「长度 + 文本」,长度可能是字符数(UTF-16)或字节数
//(UTF-8,MDF 4.1 起)。但部分工具(如 CANape 16)会**直接写 NUL 结尾的
// 原始文本、不带长度前缀**,因此这里按「先按规范、再按裸文本回退」的顺序解析。
if (usable >= 4)
{
const std::uint64_t declared = U32(data);
if (declared > 0 && 4u + 2u * declared <= usable)
{
// 长度前缀 + UTF-16 代码单元。
std::vector<char> buffer(static_cast<std::size_t>(2u * declared));
if (!ReadAt(data + 4, buffer.data(), buffer.size()))
{
return text;
}
text.reserve(static_cast<std::size_t>(declared));
for (std::uint64_t i = 0; i < declared; ++i)
{
const wchar_t c =
static_cast<wchar_t>(static_cast<unsigned char>(buffer[2 * i]) |
(static_cast<unsigned char>(buffer[2 * i + 1]) << 8));
if (c == L'\0')
{
break;
}
text.push_back(c);
}
return text;
}
if (declared > 0 && 4u + declared <= usable)
{
// 长度前缀 + UTF-8 字节。
std::vector<char> buffer(static_cast<std::size_t>(declared) + 1u, '\0');
if (!ReadAt(data + 4, buffer.data(), static_cast<std::size_t>(declared)))
{
return text;
}
return Utf8ToWide(buffer.data(), static_cast<int>(declared));
}
}
// 回退:数据段直接是 NUL 结尾的文本(UTF-16 或 UTF-8)。
std::vector<unsigned char> buffer(static_cast<std::size_t>(usable));
if (!ReadAt(data, buffer.data(), buffer.size()))
{
return text;
}
const bool utf16 = (buffer.size() >= 2 && buffer[1] == 0 && buffer[0] != 0);
if (utf16)
{
for (std::size_t i = 0; i + 1 < buffer.size(); i += 2)
{
const wchar_t c = static_cast<wchar_t>(buffer[i] | (buffer[i + 1] << 8));
if (c == L'\0')
{
break;
}
text.push_back(c);
}
return text;
}
std::string raw;
for (const unsigned char c : buffer)
{
if (c == '\0')
{
break;
}
raw += static_cast<char>(c);
}
return Utf8ToWide(raw.data(), static_cast<int>(raw.size()));
}
private:
FILE* m_file = nullptr;
std::uint64_t m_size = 0;
mutable std::uint64_t m_bytesRead = 0; /**< 已实际读取的字节数(进度上报用)。 */
};
/** @brief 一个通道的读取方案(已从 CN 块解析出来)。 */
struct ChannelSpec
{
std::wstring name; /**< 通道名(`cn_tx_name`)。 */
std::size_t bitOffset = 0; /**< 记录内的起始位偏移(= byte_offset*8 + bit_offset)。 */
std::size_t bitCount = 0; /**< 位宽。 */
DataType dataType = DataType::UINT_LE; /**< 原始数据类型。 */
bool allInvalid = false; /**< `cn_flags & 1`:整列无效。 */
bool hasInvalidBit = false; /**< `cn_flags & 2`:有无效位。 */
std::size_t invalidBitPos = 0; /**< 无效位的绝对位偏移。 */
int type = 0; /**< `cn_type`:0 定长 / 1 VLSD / 2 虚拟主通道 / 3 同步通道。 */
int syncType = 0; /**< `cn_sync_type`:1 时间 / 2 角度 / 3 距离。 */
bool syncMaster = false; /**< `cn_sync_type ∈ [1,3]`(1 时间 / 2 角度 / 3 距离):本组的主通道。 */
bool vlsd = false; /**< `cn_type == 1`:值存放在 `##SD` 里(记录内是偏移)。 */
std::uint64_t signalData = 0; /**< VLSD 通道的 `##SD` 块地址(`cn_data`)。 */
/** `##SI` 采样索引(`cn_si_source`);空表示每个记录都有采样。 */
std::vector<std::uint64_t> sampleIndex;
/** @brief 换算方式(`cc_type` 的数值子集;`VALUE_TO_TEXT` 走 `enumText`)。 */
ConversionType conversion = ConversionType::NONE;
/**
* @brief `##CC` 的 `cc_val[]` 参数。
*
* 线性 2 个、有理式 4 个、代数式 6 个;
* 查表类(4/5)为交错的 `(raw, phys)` 点对,区间表(6)为 `(min, max, phys)` 三元组。
*/
std::vector<double> conversionValues;
/** 值 → 文本枚举表(`cc_type == 7` 精确值 / `cc_type == 8` 值区间,可能为空)。 */
SignalEnumText enumText;
/** @brief `##CA` 数组元素总数(0 = 普通标量通道)。 */
std::size_t arrayElements = 0;
/** @brief `##CA` 存储方式(0 = 元素连续存放在本记录内)。 */
int arrayStorage = 0;
/** @brief `##CA` 数组类型(`ca_type`,见 `ArrayKind`)。 */
int arrayType = 0;
/** @brief `##CA` 各维长度(仅用于生成 `name[i][j]` 这样的名字)。 */
std::vector<std::size_t> arrayDims;
/**
* @brief `##CA` 各维的周期数(`ca_cycle_count`);空 = 文件没写这个字段。
*
* 规范里 `ca_cycle_count[i] > 1` 表示该维的元素被**拆到多条记录**里,
* 这时单个记录内并不含全部元素 ⇒ 不能按「记录内连续」展开(见 `ExpandArrayChannel`)。
* ⚠️ 实测文件里这个字段**不存在**(`##CA` 块长度恰好到 `dim_size[]` 就结束),
* 因此读取前必须先按块长度做边界检查,不能盲读(盲读会拿到下一个块头当作数字)。
*/
std::vector<std::size_t> arrayCycles;
/** @brief `##CA` 的元素起始字节基址(`ca_byte_offset_base`,仅用于一致性校验与诊断)。 */
std::size_t arrayByteOffsetBase = 0;
/** @brief `##CA` 的无效位位置(`ca_inval_bit_pos`,仅记录用于诊断)。 */
std::size_t arrayInvalidBitPos = 0;
/** 数值有效性:只有整数与浮点通道可以绘制。 */
bool numeric = false;
};
/** @brief 判断数据类型是否为可绘制的数值类型。 */
bool IsNumericType(DataType type)
{
return type == DataType::UINT_LE || type == DataType::UINT_BE || type == DataType::INT_LE ||
type == DataType::INT_BE || type == DataType::FLOAT_LE || type == DataType::FLOAT_BE;
}
/**
* @brief 按位从记录缓冲中取出一个数值。
*
* 支持 1–64 位整数(任意位偏移)与 32/64 位浮点(字节对齐)。
*
* @param [IN] record 记录起始指针。
* @param [IN] recordBytes 记录长度(字节),用于边界检查。
* @param [IN] spec 通道读取方案。
* @return 原始数值;越界或类型不支持时返回 NaN。
*/
double DecodeRawValue(const unsigned char* record, std::size_t recordBytes, const ChannelSpec& spec)
{
const std::size_t bitEnd = spec.bitOffset + spec.bitCount;
if (spec.bitCount == 0 || spec.bitCount > 64 || bitEnd > recordBytes * 8u)
{
return kMissingValue;
}
const std::size_t byteStart = spec.bitOffset / 8u;
const std::size_t byteCount = (bitEnd - byteStart * 8u + 7u) / 8u;
unsigned char buffer[9] = {};
std::memcpy(buffer, record + byteStart, byteCount);
if (spec.dataType == DataType::FLOAT_LE || spec.dataType == DataType::FLOAT_BE)
{
if (spec.bitOffset % 8u != 0u || (spec.bitCount != 32u && spec.bitCount != 64u))
{
return kMissingValue;
}
if (spec.dataType == DataType::FLOAT_LE)
{
if (spec.bitCount == 32u)
{
std::uint32_t bits = 0;
std::memcpy(&bits, buffer, 4);
float value = 0.0f;
std::memcpy(&value, &bits, 4);
return static_cast<double>(value);
}
double value = 0.0;
std::memcpy(&value, buffer, 8);
return value;
}
if (spec.bitCount == 32u)
{
std::uint32_t bits = (static_cast<std::uint32_t>(buffer[0]) << 24) |
(static_cast<std::uint32_t>(buffer[1]) << 16) |
(static_cast<std::uint32_t>(buffer[2]) << 8) |
static_cast<std::uint32_t>(buffer[3]);
float value = 0.0f;
std::memcpy(&value, &bits, 4);
return static_cast<double>(value);
}
std::uint64_t bits = 0;
for (int i = 0; i < 8; ++i)
{
bits = (bits << 8) | static_cast<std::uint64_t>(buffer[i]);
}
double value = 0.0;
std::memcpy(&value, &bits, 8);
return value;
}
// 整数:按字节序逐字节**带着位偏移**累加。
//
// ⚠️ 不能先拼成一个 64 位整数再整体右移:`bit_offset % 8 != 0` 且位宽为 64 时
// 需要 9 个字节(65–71 位),先拼装会让最高字节整体左移出 64 位而被**静默丢弃**,
// 于是把一个 64 位计数读成“高位少 4~7 位”的错误值。这里改成
// 「每个字节按 `8*i - shift` 直接落到目标位上」,最右端不足一个字节的部分右移对齐,
// 因此任何位宽/位偏移组合都不会丢数据。
const std::size_t shift = spec.bitOffset % 8u;
const bool littleEndian = (spec.dataType == DataType::UINT_LE || spec.dataType == DataType::INT_LE);
std::uint64_t raw = 0;
for (std::size_t i = 0; i < byteCount; ++i)
{
// 该字节的第一位在“整体位流”里的位置(大端时字节序反向)。
const std::size_t position = littleEndian ? (8u * i) : (8u * (byteCount - 1u - i));
const std::uint64_t bits = buffer[i];
if (position >= shift)
{
const std::size_t left = position - shift;
if (left < 64u)
{
raw |= bits << left;
}
}
else
{
raw |= bits >> (shift - position);
}
}
if (spec.bitCount < 64u)
{
raw &= (1ull << spec.bitCount) - 1ull;
}
const bool signedType = (spec.dataType == DataType::INT_LE || spec.dataType == DataType::INT_BE);
if (signedType && spec.bitCount < 64u && (raw & (1ull << (spec.bitCount - 1u))) != 0u)
{
// 符号扩展。
raw |= ~((1ull << spec.bitCount) - 1ull);
}
if (signedType)
{
return static_cast<double>(static_cast<std::int64_t>(raw));
}
return static_cast<double>(raw);
}
/**
* @brief 按 `##CC` 换算把原始值转换成物理值。
*
* 支持 `cc_type`:
* * `0` 无换算 / `1` 线性;
* * `2` 有理式 `phys = (v0 + v1·raw) / (v2 + v3·raw)`;
* * `3` 代数式 `phys = (v0·raw² + v1·raw + v2) / (v3·raw² + v4·raw + v5)`;
* * `4` 值→值查表(**线性插值**,即标定的特性曲线);
* * `5` 值→值查表(**不插值**,取 `raw_i ≤ raw` 的最后一个值);
* * `6` 区间表(`(min,max,phys)` 三元组,命中区间才换算)。
*
* ⚠️ 参数个数与类型不匹配时**原样返回 raw**:宁可显示原始计数,
* 也绝不按错误的公式算出一个"看起来像物理值"的数字。
* ⚠️ 对于 `4/5/6`,本项目假定 `cc_val` 按「键在前、值在后」交错存放
* (与 ASAM MDF 4.1 规范一致);键单调递增。
*/
double ApplyConversion(const ChannelSpec& spec, double raw)
{
if (!std::isfinite(raw))
{
return raw;
}
const std::vector<double>& v = spec.conversionValues;
switch (spec.conversion)
{
case ConversionType::NONE:
case ConversionType::VALUE_TO_TEXT:
case ConversionType::VALUE_RANGE_TO_TEXT:
return raw;
case ConversionType::LINEAR:
if (v.size() >= 2)
{
return v[0] + v[1] * raw;
}
return raw;
case ConversionType::RATIONAL:
if (v.size() >= 4)
{
const double denominator = v[2] + v[3] * raw;
if (denominator == 0.0 || !std::isfinite(denominator))
{
return raw;
}
return (v[0] + v[1] * raw) / denominator;
}
return raw;
case ConversionType::ALGEBRAIC:
if (v.size() >= 6)
{
const double denominator = v[3] * raw * raw + v[4] * raw + v[5];
if (denominator == 0.0 || !std::isfinite(denominator))
{
return raw;
}
return (v[0] * raw * raw + v[1] * raw + v[2]) / denominator;
}
return raw;
case ConversionType::TAB_INTP:
{
// 交错点对:(key0, value0, key1, value1, ...),key 递增。
if (v.size() < 4 || (v.size() % 2) != 0)
{
return raw;
}
const std::size_t count = v.size() / 2;
if (raw <= v[0])
{
return v[1];
}
if (raw >= v[(count - 1) * 2])
{
return v[(count - 1) * 2 + 1];
}
for (std::size_t index = 0; index + 1 < count; ++index)
{
const double key0 = v[index * 2];
const double key1 = v[(index + 1) * 2];
if (raw >= key0 && raw <= key1)
{
if (key1 == key0)
{
return v[index * 2 + 1];
}
const double ratio = (raw - key0) / (key1 - key0);
return v[index * 2 + 1] + (v[(index + 1) * 2 + 1] - v[index * 2 + 1]) * ratio;
}
}
return raw;
}
case ConversionType::TAB:
{
if (v.size() < 4 || (v.size() % 2) != 0)
{
return raw;
}
const std::size_t count = v.size() / 2;
if (raw < v[0])
{
return v[1];
}
double result = v[1];
for (std::size_t index = 0; index < count; ++index)
{
if (raw >= v[index * 2])
{
result = v[index * 2 + 1];
}
else
{
break;
}
}
return result;
}
case ConversionType::TAB_RANGE:
{
if (v.size() < 3 || (v.size() % 3) != 0)
{
return raw;
}
const std::size_t count = v.size() / 3;
for (std::size_t index = 0; index < count; ++index)
{
const double low = v[index * 3];
const double high = v[index * 3 + 1];
if (raw >= low && raw <= high)
{
return v[index * 3 + 2];
}
}
return raw;
}
default:
return raw;
}
}
/**
* @brief 把 VLSD 通道的 `##SD`(信号数据)块内容读进内存。
*
* MDF4 的 VLSD 数据格式(实测 + 参考实现一致):
* * `##SD` 数据段是一串「`UINT32` 长度 + 该长度的字节」的样本;
* * 记录(`##DT`)在该通道的 `cn_byte_offset` 处放一个 **64 位偏移**,
* 指向样本长度前缀在 `##SD` 数据里的位置(偏移按各 `##SD` 数据段顺序累加)。
*
* 只有 VLSD 通道才需要这块数据,因此按通道按需加载,不做全局缓存。
*
* @param [IN] block 起始块(`##SD` / `##DZ` / `##DL` / `##HL`)。
* @param [IN,OUT] out 追加写入的逻辑字节流。
* @return 读到至少一段数据返回 true。
*/
bool AppendSignalData(const MdfFile& file, std::uint64_t block, std::vector<std::uint8_t>& out,
int depth)
{
if (block == 0 || depth > 8 || !file.IsBlock(block, ""))
{
return false;
}
// 防御:单个通道的 VLSD 数据超过 512MB 视为异常,避免损坏文件吃光内存。
constexpr std::size_t kMaxSignalDataBytes = 512u * 1024u * 1024u;
char id[5] = {};
file.BlockId(block, id);
const std::uint64_t data = file.DataOffset(block);
const std::uint64_t payload = file.BlockLength(block) - (data - block);
if (payload == 0)
{
return false;
}
if (std::memcmp(id, "##SD", 4) == 0)
{
if (out.size() + payload > kMaxSignalDataBytes)
{
return false;
}
const std::size_t base = out.size();
out.resize(base + static_cast<std::size_t>(payload));
if (!file.ReadAt(data, out.data() + base, static_cast<std::size_t>(payload)))
{
out.resize(base);
return false;
}
// `##SD` 可能还有同类型的后继块(链接 0)。
if (file.LinkCount(block) > 0)
{
AppendSignalData(file, file.Link(block, 0), out, depth + 1);
}
return true;
}
if (std::memcmp(id, "##DZ", 4) == 0)
{
// `##DZ` 数据段布局:original_type(4) + zip_type(1) + reserved(3) +
// param(4) + original_size(8) + zip_size(8) + 压缩数据。
const std::uint8_t zipType = file.U8(data + 4);
const std::uint64_t zipSize = file.U64(data + 20);
if (zipType != 0 || payload <= 28u) // 只支持纯 deflate(不支持转置/LZ4/ZSTD)
{
return false;
}
const std::uint64_t usable = std::min(payload - 28u, zipSize);
if (usable == 0 || out.size() + usable > kMaxSignalDataBytes)
{
return false;
}
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(usable));
if (!file.ReadAt(data + 28u, compressed.data(), compressed.size()))
{
return false;
}
std::vector<std::uint8_t> inflated;
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
{
return false;
}
out.insert(out.end(), inflated.begin(), inflated.end());
return true;
}
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
{
const std::uint64_t links = file.LinkCount(block);
bool any = false;
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
{
any = AppendSignalData(file, file.Link(block, index), out, depth + 1) || any;
}
return any;
}
return false;
}
/**
* @brief 推断 VLSD 样本的元素位宽(`cn_bit_count` 为 0 时使用)。
*
* 相当一部分写盘器把 VLSD 通道的 `cn_bit_count` 写成 0(它描述的是"记录里那个
* 偏移字段",而偏移固定是 64 位)。此时按数据类别 + 样本长度推断:
* 浮点优先 8 字节(其次 4 字节),整数按 8 → 4 → 2 → 1 字节依次尝试。
*
* ⚠️ 长度同时是 8 与 4 的整数倍时(例如 4 个 float = 16 字节)无法区分,
* 这里取 8 字节(`double` 更常见)。只有 `cn_bit_count == 0` 才会走到这条路径。
*/
std::size_t InferVlsdElementBits(DataType type, std::size_t length)
{
const bool floating = (type == DataType::FLOAT_LE || type == DataType::FLOAT_BE);
if (length == 0)
{
return 0;
}
if (length % 8u == 0)
{
return 64;
}
if (length % 4u == 0)
{
return 32;
}
if (!floating && length % 2u == 0)
{
return 16;
}
return floating ? 0 : 8;
}
/**
* @brief 从记录中解出一个 VLSD 采样(取样本的第一个元素)。
*
* 记录里放的是 64 位偏移。两种写法都接受:
* * 直接偏移(部分写盘器);
* * `偏移 << 1 | 标志位`(标志位为 1 时表示长度,此时按 `>> 1` 回退)。
* 判据是「长度前缀自洽」——哪种解释能在 `##SD` 缓冲里定位到合法样本就用哪种。
*
* 元素位宽优先用 `cn_bit_count`;它为 0(常见)时按数据类型与样本长度推断
* (见 `InferVlsdElementBits`)。数值类型(无符号/有符号整数、32/64 位浮点)
* 全部支持;样本含多个元素时取**第一个元素**。
*
* @return 解出的数值;无法解释时返回 NaN。
*/
double DecodeVlsdValue(const unsigned char* record, std::size_t recordBytes, const ChannelSpec& spec,
const std::vector<std::uint8_t>& signalData)
{
if (signalData.empty())
{
return kMissingValue;
}
const std::size_t byteStart = spec.bitOffset / 8u;
if (byteStart + 8u > recordBytes)
{
return kMissingValue;
}
std::uint64_t raw = 0;
std::memcpy(&raw, record + byteStart, sizeof(raw));
for (int attempt = 0; attempt < 2; ++attempt)
{
const std::uint64_t offset = (attempt == 0) ? raw : (raw >> 1);
if (offset + 4u > signalData.size())
{
continue;
}
std::uint32_t length = 0;
std::memcpy(&length, signalData.data() + offset, sizeof(length));
if (static_cast<std::uint64_t>(length) > signalData.size() - offset - 4u)
{
continue;
}
if (length == 0)
{
return kMissingValue;
}
std::size_t elementBits = spec.bitCount;
if (elementBits == 0 || elementBits > 64u)
{
elementBits = InferVlsdElementBits(spec.dataType, static_cast<std::size_t>(length));
}
if (elementBits == 0 || elementBits > 64u)
{
return kMissingValue;
}
const std::size_t elementBytes = (elementBits + 7u) / 8u;
if (static_cast<std::size_t>(length) < elementBytes)
{
return kMissingValue;
}
ChannelSpec element = spec;
element.bitOffset = 0;
element.bitCount = elementBits;
element.vlsd = false;
// 只给"这一个元素"的字节数做边界检查(样本可能含多个元素)。
return DecodeRawValue(signalData.data() + offset + 4u, elementBytes, element);
}
return kMissingValue;
}
/**
* @brief 解析一个 CN 块为读取方案。
*
* ⚠️ **链接布局有两种写法**:MDF 4.0 写 8 个链接(没有 `cn_sr_source`),
* 4.1 写 9 个。二者共有的尾部 4 个链接是
* `cn_cc_conversion` / `cn_data` / `cn_md_unit` / `cn_md_comment`,
* 因此这里按**链接总数**反推换算/数据块下标,而不是写死 4/5。
*(实测 Simulink 导出的「4.10」文件仍使用 8 链接布局。)
*/
bool ParseChannel(const MdfFile& file, std::uint64_t cn, ChannelSpec& spec)
{
if (!file.IsBlock(cn, "##CN"))
{
return false;
}
const std::uint64_t data = file.DataOffset(cn);
const std::uint8_t type = file.U8(data + 0);
const std::uint8_t sync = file.U8(data + 1);
const std::uint8_t dataType = file.U8(data + 2);
const std::uint8_t bitOffset = file.U8(data + 3);
const std::uint32_t byteOffset = file.U32(data + 4);
const std::uint32_t bitCount = file.U32(data + 8);
const std::uint32_t flags = file.U32(data + 12);
const std::uint32_t invalidBit = file.U32(data + 16);
const std::uint64_t linkCount = file.LinkCount(cn);
// 8 链接布局:[4] = 换算、[5] = 数据块、[6] = 单位、[7] = 注释。
// 9 链接布局:[5] = 换算、[6] = 数据块、[7] = 单位、[8] = 注释。
const std::uint64_t conversionIndex = (linkCount >= 9) ? 5 : 4;
const std::uint64_t signalDataIndex = conversionIndex + 1;
spec.name = file.TextBlock(file.Link(cn, 2));
spec.dataType = static_cast<DataType>(dataType);
spec.bitCount = bitCount;
spec.bitOffset = static_cast<std::size_t>(byteOffset) * 8u + bitOffset;
spec.allInvalid = (flags & 0x1u) != 0u;
spec.hasInvalidBit = (flags & 0x2u) != 0u;
spec.invalidBitPos = invalidBit;
spec.type = static_cast<int>(type);
spec.syncType = static_cast<int>(sync);
spec.syncMaster = (sync >= 1 && sync <= 3);
spec.vlsd = (type == 1);
if (spec.vlsd && linkCount > signalDataIndex)
{
spec.signalData = file.Link(cn, signalDataIndex);
}
// 采样索引(`##SI`):只有当它确实是「多条记录下标」时才生效。
// ⚠️ 实测部分工具会给每个通道挂一个只含 1 个 8 字节值的 `##SI`(如 513),
// 那显然不是采样下标(远大于周期数)⇒ 必须忽略,否则会把整列清成 NaN。
{
const std::uint64_t si = (linkCount > 3) ? file.Link(cn, 3) : 0;
if (file.IsBlock(si, "##SI"))
{
const std::uint64_t siData = file.DataOffset(si);
const std::uint64_t usable = file.BlockLength(si) - (siData - si);
const std::size_t count = static_cast<std::size_t>(usable / 8u);
if (count >= 2 && count <= (1u << 24))
{
spec.sampleIndex.resize(count);
bool ok = file.ReadAt(siData, spec.sampleIndex.data(), count * 8u);
if (!ok)
{
spec.sampleIndex.clear();
}
}
}
}
// 换算:数值换算(0–6)与「值 → 文本」(7)。
const std::uint64_t cc = (linkCount > conversionIndex) ? file.Link(cn, conversionIndex) : 0;
if (file.IsBlock(cc, "##CC"))
{
const std::uint64_t ccData = file.DataOffset(cc);
const std::uint8_t ccType = file.U8(ccData + 0);
const std::uint16_t valCount = file.U16(ccData + 6);
// `cc_val[]` 从数据段 +24 开始(每个 REAL 占 8 字节)。
const auto readValues = [&file, ccData, valCount](std::size_t wanted) -> std::vector<double>
{
std::vector<double> values;
for (std::uint16_t index = 0; index < valCount && values.size() < wanted; ++index)
{
values.push_back(file.F64(ccData + 24u + 8u * index));
}
return values;
};
const ConversionType kind = static_cast<ConversionType>(ccType);
switch (kind)
{
case ConversionType::LINEAR:
if (valCount >= 2)
{
spec.conversionValues = readValues(2);
// 换算表未填写:按恒等处理,避免整列变成 0。
if (spec.conversionValues.size() >= 2 && spec.conversionValues[0] == 0.0 &&
spec.conversionValues[1] == 0.0)
{
spec.conversionValues[0] = 0.0;
spec.conversionValues[1] = 1.0;
}
spec.conversion = ConversionType::LINEAR;
}
break;
case ConversionType::RATIONAL:
if (valCount >= 4)
{
spec.conversionValues = readValues(4);
spec.conversion = kind;
}
break;
case ConversionType::ALGEBRAIC:
if (valCount >= 6)
{
spec.conversionValues = readValues(6);
spec.conversion = kind;
}
break;
case ConversionType::TAB_INTP:
case ConversionType::TAB:
if (valCount >= 4 && (valCount % 2) == 0)
{
spec.conversionValues = readValues(valCount);
spec.conversion = kind;
}
break;
case ConversionType::TAB_RANGE:
if (valCount >= 3 && (valCount % 3) == 0)
{
spec.conversionValues = readValues(valCount);
spec.conversion = kind;
}
break;
case ConversionType::VALUE_TO_TEXT:
if (valCount > 0)
{
// `cc_val[i]`(F64 数组,从数据段 +24 开始)与 `cc_ref[4 + i]` 一一对应;
// 最后一个引用(下标 4 + valCount)是「默认文本」,这里不使用。
const std::uint64_t ccLinkCount = file.LinkCount(cc);
spec.enumText.values.reserve(valCount);
spec.enumText.texts.reserve(valCount);
for (std::uint16_t index = 0; index < valCount; ++index)
{
const std::uint64_t textLink = 4u + index;
if (textLink >= ccLinkCount)
{
break;
}
std::wstring text = file.TextBlock(file.Link(cc, textLink));
if (text.empty())
{
continue; // 没有文本的取值不进入枚举表
}
spec.enumText.values.push_back(file.F64(ccData + 24u + 8u * index));
spec.enumText.texts.push_back(std::move(text));
}
if (spec.enumText.texts.size() != spec.enumText.values.size() ||
spec.enumText.texts.empty())
{
spec.enumText = SignalEnumText();
}
}
break;
case ConversionType::VALUE_RANGE_TO_TEXT:
// 值区间 → 文本:`cc_val[]` 交错存放 `(下限, 上限)`,`cc_ref[4 + i]` 是第 i 段的文本
//(最后一个引用是「默认文本」,这里不使用)。
// ⚠️ 区间**成对**出现在一个 1-D 表里:周期描述的是**一维断点表**的相邻输出值
//(例如 0–10 → 'A'、10–20 → 'B'),故取相交的第一个区间。
// ⚠️ 段数必须是 `cc_val_count / 2`(奇数个参数说明表是残缺的,整张表丢弃)。
if (valCount >= 2 && (valCount % 2) == 0)
{
const std::uint64_t ccLinkCount = file.LinkCount(cc);
const std::size_t ranges = static_cast<std::size_t>(valCount / 2);
for (std::size_t index = 0; index < ranges; ++index)
{
const std::uint64_t textLink = 4u + index;
if (textLink >= ccLinkCount)
{
break;
}
std::wstring text = file.TextBlock(file.Link(cc, textLink));
if (text.empty())
{
continue; // 没有文本的区间不进入枚举表
}
double lower = file.F64(ccData + 24u + 16u * index);
double upper = file.F64(ccData + 24u + 16u * index + 8u);
if (!std::isfinite(lower) || !std::isfinite(upper))
{
continue;
}
if (lower > upper)
{
std::swap(lower, upper);
}
spec.enumText.rangeMin.push_back(lower);
spec.enumText.rangeMax.push_back(upper);
spec.enumText.rangeTexts.push_back(std::move(text));
}
if (!spec.enumText.HasRanges())
{
spec.enumText = SignalEnumText();
}
}
break;
default:
break;
}
}
// `##CA`(通道数组):一个通道装一整条曲线/矩阵(标定的特性曲线、map)。
// 数据段布局(实测 CANape 写出的文件):
// +0 ca_type(U8) / +1 ca_storage(U8) / +2 ndim(U16) / +4 flags(U32)
// +8 ca_byte_offset_base(U16) / +10 ca_inval_bit_pos(U16)
// +16 dim_size[](U64 × ndim) 然后紧接 cycle_count[](U32 × ndim,**可缺省**)
// ⚠️ `cycle_count` 在实测文件里**不存在**(块长度到 `dim_size[]` 就结束)。
// 必须按块长度做边界检查,否则会把下一个块头当成数字读进来(实测得到 1481909027)。
const std::uint64_t ca = (linkCount > 1) ? file.Link(cn, 1) : 0;
if (ca != 0 && file.IsBlock(ca, "##CA"))
{
const std::uint64_t caData = file.DataOffset(ca);
const std::uint64_t caBlockLength = file.BlockLength(ca);
const std::uint64_t caUsable =
(caBlockLength > (caData - ca)) ? (caBlockLength - (caData - ca)) : 0;
spec.arrayType = (caUsable >= 1) ? static_cast<int>(file.U8(caData + 0)) : 0;
spec.arrayStorage = (caUsable >= 2) ? static_cast<int>(file.U8(caData + 1)) : 0;
const std::uint16_t rank = (caUsable >= 4) ? file.U16(caData + 2) : 0;
if (caUsable >= 12)
{
spec.arrayByteOffsetBase = file.U16(caData + 8);
spec.arrayInvalidBitPos = file.U16(caData + 10);
}
std::size_t total = (rank > 0) ? 1 : 0;
bool dimsOk = (rank > 0);
spec.arrayDims.clear();
spec.arrayCycles.clear();
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
{
if (16u + 8u * (static_cast<std::uint64_t>(axis) + 1u) > caUsable)
{
dimsOk = false; // 维度表被截断:不做任何猜测
break;
}
const std::size_t dim =
static_cast<std::size_t>(file.U64(caData + 16u + 8u * axis));
if (dim == 0)
{
dimsOk = false;
break;
}
spec.arrayDims.push_back(dim);
// 元素总数太大时不再累乘(只用它判断「是否可展开」)。
if (total <= (1u << 24))
{
total *= dim;
if (total > (1u << 24))
{
total = (1u << 24) + 1u;
}
}
}
// `ca_cycle_count[]`(可缺省):只有真的读得到才记录。
bool cyclesPresent = (rank > 0);
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
{
const std::uint64_t cycleOffset =
16u + 8u * static_cast<std::uint64_t>(rank) + 4u * static_cast<std::uint64_t>(axis);
if (cycleOffset + 4u > caUsable)
{
cyclesPresent = false;
break;
}
spec.arrayCycles.push_back(static_cast<std::size_t>(file.U32(caData + cycleOffset)));
}
if (!cyclesPresent || spec.arrayCycles.size() != spec.arrayDims.size())
{
spec.arrayCycles.clear(); // 缺省 = 每维 1 个周期(整个数组在本记录内)
}
spec.arrayElements = dimsOk ? total : 0;
}
// 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。
if (spec.vlsd)
{
// VLSD:`cn_bit_count` 描述的是记录里的偏移字段,元素位宽由样本自身决定
//(很多写盘器把 `cn_bit_count` 写成 0)⇒ 只按数据类型判定。
spec.numeric = IsNumericType(spec.dataType);
}
else
{
spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64;
}
return true;
}
/**
* @brief 把 `##CA` 数组通道展开成多个普通通道(每个元素一个)。
*
* CANape 的标定数组(特性曲线/断点表/map)在 MDF 里是「一个通道 + `##CA`」,
* `storage == 0` 时各元素**连续存放**在记录的 `cn_byte_offset` 处
*(实测:`WIN_Motor_StallCurrentVolBrk` 的 9 个 `uint16` 就是从记录 +8 起连续 18 字节)。
* 展开后每个元素都是普通标量信号,因此自动获得光标取值、枚举、图例等全部能力。
*
* 展开的**必要条件**(任何一条不满足就原样保留,只显示元素 0,绝不猜):
* 1. `ca_storage == 0`:元素在本记录内连续存放;
* 2. `ca_type` ∈ {0 数组, 1 缩放轴, 2 区间轴}:三者物理布局相同(实测 25 个 `##CA` 块全部如此),
* 未知类型(> 2)不展开;
* 3. `cn_bit_count` ∈ [1, 64]:位宽是算元素步进的**唯一依据**,为 0 时不知道元素之间隔多少位
* (VLSD 通道的 `bit_count` 常写 0,那类数据在 `##SD` 里,与"记录内连续"完全不是一套逻辑);
* 4. `ca_cycle_count` 缺失或每一维都为 1:否则元素被拆到**多条记录**里,单个记录内不含全部元素;
* 5. 元素能放进本记录:`bit_offset + 元素数 × 位宽 ≤ 记录位数`(这是真正的完整性校验,
* 替代了「猜一个元素数上限」);
* 6. 规模护栏(防止一个巨型数组把信号列表/网格撑爆):
* 元素数 ≤ `kMaxArrayElements`,且 `元素数 × 记录数` ≤ `kMaxArrayCells`。
*
* @param [IN,OUT] spec 已解析的通道方案(不展开时原样平移进 `out`)。
* @param [IN] cycles 该通道组的记录条数(`cg_cycle_count`,用于规模护栏)。
* @param [IN] recordBytes 单条记录字节数(`cg_data_bytes`,0 = 未知则跳过第 5 条校验)。
* @param [OUT] out 展开后的通道列表。
*/
void ExpandArrayChannel(ChannelSpec&& spec, std::uint64_t cycles, std::size_t recordBytes,
std::vector<ChannelSpec>& out)
{
/** 单个通道最多展开成多少条信号。 */
constexpr std::size_t kMaxArrayElements = 16384;
/** 元素总数 × 记录数的上限(≈ 生成的数据点数 × 8 字节的内存/绘制代价)。 */
constexpr std::size_t kMaxArrayCells = 32u << 20;
const std::size_t count = spec.arrayElements;
bool expandable = (count > 1);
if (expandable && spec.arrayStorage != 0)
{
expandable = false; // 元素不在本记录内(跨通道/跨通道组)
}
if (expandable && (spec.arrayType < 0 || spec.arrayType > 2))
{
expandable = false; // 未知 `ca_type`
}
if (expandable && (spec.bitCount == 0 || spec.bitCount > 64))
{
expandable = false; // 位宽未知
}
if (expandable)
{
for (const std::size_t perAxis : spec.arrayCycles)
{
if (perAxis > 1)
{
expandable = false; // 元素被拆到多条记录
break;
}
}
}
if (expandable && count > kMaxArrayElements)
{
expandable = false;
}
if (expandable && cycles > 1 && count > kMaxArrayCells / static_cast<std::size_t>(cycles))
{
expandable = false;
}
if (expandable && recordBytes > 0)
{
const std::uint64_t needBits = static_cast<std::uint64_t>(spec.bitOffset) +
static_cast<std::uint64_t>(count) *
static_cast<std::uint64_t>(spec.bitCount);
if (needBits > static_cast<std::uint64_t>(recordBytes) * 8u)
{
expandable = false; // 放不进本记录 ⇒ 元素位置不符合「记录内连续」
}
}
if (!expandable)
{
spec.arrayElements = 0;
out.push_back(std::move(spec));
return;
}
const std::size_t strideBits = spec.bitCount;
const std::vector<std::size_t> dims = spec.arrayDims;
for (std::size_t index = 0; index < count; ++index)
{
ChannelSpec element = spec; // 复制:换算表 / 枚举表 / 数据类型全部沿用
element.arrayElements = 0;
element.bitOffset = spec.bitOffset + index * strideBits;
// 维度后缀:按 `dim_size[]` 从末维向前取余,因此 1/2/3/… 维都得到 `[i][j][k]`。
std::wstring suffix;
if (dims.size() >= 2)
{
std::vector<std::size_t> axisIndex(dims.size(), 0);
std::size_t remainder = index;
for (std::size_t axis = dims.size(); axis-- > 0;)
{
const std::size_t size = dims[axis];
axisIndex[axis] = (size > 0) ? (remainder % size) : 0;
remainder = (size > 0) ? (remainder / size) : 0;
}
for (const std::size_t value : axisIndex)
{
suffix += L"[";
suffix += std::to_wstring(value);
suffix += L"]";
}
}
else
{
suffix = L"[" + std::to_wstring(index) + L"]";
}
element.name = spec.name + suffix;
out.push_back(std::move(element));
}
}
/** @brief 在一条 `next` 链上迭代块偏移(带访问次数上限,避免损坏文件死循环)。 */
template <typename Fn>
void WalkChain(const MdfFile& file, std::uint64_t first, const char* expectedId, std::size_t limit,
Fn&& visit)
{
std::uint64_t current = first;
std::size_t guard = 0;
while (current != 0 && guard++ < limit)
{
if (!file.IsBlock(current, expectedId))
{
break;
}
if (!visit(current))
{
break;
}
current = file.Link(current, 0);
}
}
/**
* @brief 一段原始记录数据(MDF4 的 `##DT` / `##DZ` / `##DV` 块的数据区)。
*
* MDF4 允许把记录拆到多段里:
* * `##DT` 普通数据块(记录连续存放);
* * `##DZ` 压缩数据块(zlib 流,解压后是普通记录);
* * `##DV` 数据值块(`##DL` 列表指向的原始值);
* * `##DL` 数据列表(链接到多个 `##DT` / `##DZ`);
* * `##HL` 头部列表(链接到多个数据块,含压缩的)。
*/
struct DataSegment
{
std::uint64_t offset = 0; /**< 数据区起始绝对偏移。 */
std::uint64_t bytes = 0; /**< 数据区字节数(压缩段为压缩后长度)。 */
bool compressed = false; /**< 是否 need inflate。 */
};
/**
* @brief 递归展开数据块,收集所有数据段。
*
* ⚠️ 用户 2026-10-19 报告:Simulink SDI 导出的 MF4 把记录写在 `##DL`(数据列表)
* 里,而旧实现只认 `##DT` ⇒ 整个数据组被跳过(8 个信号只能读出 4 个)。
*/
bool CollectDataSegments(const MdfFile& file, std::uint64_t block,
std::vector<DataSegment>& out, int depth)
{
if (block == 0 || depth > 4 || !file.IsBlock(block, ""))
{
return false;
}
char id[5] = {};
file.BlockId(block, id);
const std::uint64_t data = file.DataOffset(block);
const std::uint64_t payload = file.BlockLength(block) - (data - block);
if (payload == 0)
{
return false;
}
if (std::memcmp(id, "##DT", 4) == 0 || std::memcmp(id, "##DV", 4) == 0)
{
out.push_back(DataSegment{data, payload, false});
return true;
}
if (std::memcmp(id, "##DZ", 4) == 0)
{
// `##DZ` 数据段布局:original_type(4) + zip_type(1) + reserved(3) +
// param(4) + original_size(8) + zip_size(8) + 压缩数据。
// ⚠️ 载荷里含 28 字节头,不能把整段当 zlib 流(旧实现在这里错了)。
char originalType[5] = {};
file.BlockId(data, originalType); // 读 4 字节原始类型("DT"/"SD"/...)
const std::uint8_t zipType = file.U8(data + 4);
const std::uint64_t zipSize = file.U64(data + 20);
if (zipType != 0 || payload <= 28u)
{
return false; // 只支持纯 deflate
}
if (std::memcmp(originalType, "SD", 2) == 0)
{
return false; // 压缩的 VLSD 数据由 AppendSignalData 负责
}
const std::uint64_t usable = std::min(payload - 28u, zipSize);
out.push_back(DataSegment{data + 28u, usable, true});
return true;
}
if (std::memcmp(id, "##DL", 4) == 0 || std::memcmp(id, "##HL", 4) == 0)
{
const std::uint64_t links = file.LinkCount(block);
bool any = false;
for (std::uint64_t index = 0; index < links && index < 1000000; ++index)
{
any = CollectDataSegments(file, file.Link(block, index), out, depth + 1) || any;
}
return any;
}
// 到这里说明不是记录数据块:
// * `##SD`(Signal Data,VLSD 通道的样本)由 `AppendSignalData` 按通道读取;
// * `##SI` 是采样索引,不是数据块。
return false;
}
/**
* @brief 依次从若干数据段中读取连续记录,并对每条完整记录调用 `visit`。
*
* * 记录可能**跨段**(`##DT` 载荷不是记录长度的整数倍时)⇒ 用 `carry` 拼接;
* * 压缩段先用内置 inflate 展开(`##DZ`),再按普通字节流消费;
* * 每读一块调用 `cancelled`(可为空),返回 true 表示请求取消。
*
* @return 成功读取到至少 `cycles` 条记录返回 true。
*/
bool ReadRecordSegments(
const MdfFile& file, const std::vector<DataSegment>& segments, std::uint64_t cycles,
std::size_t recordBytes,
const std::function<void(const unsigned char* record, std::uint64_t row)>& visit,
const std::function<bool()>& cancelled = std::function<bool()>())
{
if (recordBytes == 0 || cycles == 0)
{
return false;
}
std::vector<unsigned char> carry(recordBytes);
std::size_t carryBytes = 0;
std::uint64_t row = 0;
std::vector<std::uint8_t> inflated;
for (const DataSegment& segment : segments)
{
if (row >= cycles)
{
break;
}
if (cancelled && cancelled())
{
return false;
}
const unsigned char* data = nullptr;
std::size_t bytes = 0;
if (segment.compressed)
{
std::vector<std::uint8_t> compressed(static_cast<std::size_t>(segment.bytes));
if (!file.ReadAt(segment.offset, compressed.data(), compressed.size()))
{
return false;
}
if (!InflateZlib(compressed.data(), compressed.size(), inflated))
{
return false;
}
data = inflated.data();
bytes = inflated.size();
}
else
{
if (segment.offset + segment.bytes > file.Size())
{
return false;
}
// 非压缩段同样分块读,避免一次分配整段(巨型数据组可能是 GB 级)。
std::vector<unsigned char> buffer;
std::uint64_t consumed = 0;
while (consumed < segment.bytes && row < cycles)
{
const std::size_t want = static_cast<std::size_t>(
std::min<std::uint64_t>(kReadChunkBytes, segment.bytes - consumed));
buffer.resize(std::max<std::size_t>(want, recordBytes));
if (!file.ReadAt(segment.offset + consumed, buffer.data(), want))
{
return false;
}
data = buffer.data();
bytes = want;
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
consumed += want;
if (cancelled && cancelled())
{
return false;
}
}
continue;
}
// 压缩段:整段已在内存里,直接消费。
std::size_t position = 0;
while (position < bytes && row < cycles)
{
const std::size_t need = recordBytes - carryBytes;
const std::size_t take = std::min(need, bytes - position);
std::memcpy(carry.data() + carryBytes, data + position, take);
carryBytes += take;
position += take;
if (carryBytes == recordBytes)
{
visit(carry.data(), row++);
carryBytes = 0;
}
}
}
return row > 0;
}
/** @brief 一个通道组(CG)解析出的时间轴 + 信号列。 */
struct ChannelGroupData
{
std::wstring timeName; /**< 时间列名(主通道名或 `time`)。 */
std::vector<ChannelSpec> channels; /**< 非时间通道。 */
std::vector<double> time; /**< 时间轴数值。 */
std::vector<std::vector<double>> values; /**< 每个非时间通道的采样值。 */
};
/**
* @brief 把 `##SI` 采样索引转换成「逐记录有效位图」。
*
* 只有当索引表**至少两条**且**全部落在记录范围内**时才生效;
* 否则(例如某些工具给每个通道挂一个只含 `513` 的 `##SI`)返回空向量表示「全有效」,
* 与不支持 `##SI` 时的行为完全一致。
*/
std::vector<std::uint8_t> BuildSampleMask(const ChannelSpec& spec, std::uint64_t cycles)
{
std::vector<std::uint8_t> mask;
if (spec.sampleIndex.size() < 2 || cycles == 0)
{
return mask;
}
for (const std::uint64_t index : spec.sampleIndex)
{
if (index >= cycles)
{
return mask; // 不像采样索引:忽略
}
}
mask.assign(static_cast<std::size_t>(cycles), 0);
for (const std::uint64_t index : spec.sampleIndex)
{
mask[static_cast<std::size_t>(index)] = 1;
}
return mask;
}
/**
* @brief 读取一个通道组的全部记录(流式,按 4MB 分块)。
*
* @param [IN] file 文件访问器。
* @param [IN] segments 数据段列表(`##DT` / `##DL` / `##HL` / `##DZ`)。
* @param [IN] cycles 记录条数。
* @param [IN] recordBytes 单条记录字节数。
* @param [IN] masterIndex 时间主通道下标。
* @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。
* @param [IN] cancelled 取消检查(可为空)。
*/
bool ReadChannelGroupRecords(const MdfFile& file, const std::vector<DataSegment>& segments,
std::uint64_t cycles, std::size_t recordBytes,
std::size_t masterIndex, ChannelGroupData& group,
const std::function<bool()>& cancelled = std::function<bool()>())
{
if (cycles == 0 || recordBytes == 0 || segments.empty())
{
return false;
}
group.time.assign(static_cast<std::size_t>(cycles), kMissingValue);
group.values.assign(group.channels.size(),
std::vector<double>(static_cast<std::size_t>(cycles), kMissingValue));
// `##SI`:每个通道各自的「有效记录」位图(空 = 每个记录都有效)。
std::vector<std::vector<std::uint8_t>> masks(group.channels.size());
for (std::size_t index = 0; index < group.channels.size(); ++index)
{
masks[index] = BuildSampleMask(group.channels[index], cycles);
}
// VLSD 通道的 `##SD` 数据:按通道加载(只有 VLSD 通道才需要)。
std::vector<std::vector<std::uint8_t>> signalData(group.channels.size());
for (std::size_t index = 0; index < group.channels.size(); ++index)
{
const ChannelSpec& spec = group.channels[index];
if (spec.vlsd && spec.signalData != 0 && spec.numeric)
{
AppendSignalData(file, spec.signalData, signalData[index], 0);
}
}
std::vector<const std::vector<std::uint8_t>*> signalDataPtr(group.channels.size(), nullptr);
for (std::size_t index = 0; index < group.channels.size(); ++index)
{
signalDataPtr[index] = signalData[index].empty() ? nullptr : &signalData[index];
}
// ⚠️ 记录可能分散在多个 `##DT` / `##DZ`(经 `##DL` / `##HL` 列表)中,
// 由 `ReadRecordSegments` 负责跨段拼接与解压(见该函数的说明)。
return ReadRecordSegments(
file, segments, cycles, recordBytes,
[&group, masterIndex, recordBytes, &masks, &signalDataPtr](const unsigned char* record,
std::uint64_t row)
{
// 时间主通道。
{
const ChannelSpec& master = group.channels[masterIndex];
double value = DecodeRawValue(record, recordBytes, master);
if (master.allInvalid)
{
value = kMissingValue;
}
else if (master.hasInvalidBit)
{
const std::size_t byteIndex = master.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (master.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = ApplyConversion(master, value);
}
group.time[static_cast<std::size_t>(row)] = value;
}
// 各信号通道。
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex)
{
continue;
}
const ChannelSpec& spec = group.channels[c];
// `##SI` 采样索引:只有表中列出的记录才有采样,其余保持 NaN。
if (!masks[c].empty() && masks[c][static_cast<std::size_t>(row)] == 0)
{
continue;
}
double value = kMissingValue;
if (spec.vlsd)
{
// VLSD:记录里是 64 位偏移,真正的取值在 `##SD` 里。
if (signalDataPtr[c] != nullptr)
{
value = DecodeVlsdValue(record, recordBytes, spec, *signalDataPtr[c]);
}
}
else
{
value = DecodeRawValue(record, recordBytes, spec);
}
if (spec.allInvalid)
{
value = kMissingValue;
}
else if (spec.hasInvalidBit)
{
const std::size_t byteIndex = spec.invalidBitPos / 8u;
if (byteIndex < recordBytes &&
((record[byteIndex] >> (spec.invalidBitPos % 8u)) & 1u) != 0u)
{
value = kMissingValue;
}
}
if (std::isfinite(value))
{
value = ApplyConversion(spec, value);
}
group.values[c][static_cast<std::size_t>(row)] = value;
}
},
cancelled);
}
/** @brief 主通道名字判定用的 ASCII 小写化。 */
std::wstring LowerAscii(const std::wstring& text)
{
std::wstring lower;
lower.reserve(text.size());
for (const wchar_t c : text)
{
lower.push_back((c >= L'A' && c <= L'Z') ? static_cast<wchar_t>(c - L'A' + L'a') : c);
}
return lower;
}
/**
* @brief 从通道组里选出时间主通道。
*
* 判定顺序(越靠前越可信):
* 1. `cn_sync_type` = 1(时间)/ 2(角度)/ 3(距离)且为数值通道;
* 2. 名字为 `t` / `time` / `timestamp` / `zeit` / `时间`(含 `t[s]` 这类带单位后缀);
* 3. 第一个数值通道(兜底:至少让数据能画出来,而不是整组丢弃)。
*
* ⚠️ `cg_flags` 的第 2 位表示「远程主通道」(主通道不在本 CG 里)。此时跳过第 1 步,
* 直接按名字 / 首个数值通道兜底,避免把某个普通信号误当成时间轴。
*
* @return 主通道下标;一个数值通道都没有时返回 `channels.size()`。
*/
std::size_t SelectMasterChannel(const std::vector<ChannelSpec>& channels, std::uint16_t cgFlags)
{
const bool remoteMaster = (cgFlags & 0x4u) != 0u;
if (!remoteMaster)
{
// 优先同步主通道,但 VLSD 通道不能当时间轴(记录里只有偏移)。
for (std::size_t i = 0; i < channels.size(); ++i)
{
if (channels[i].syncMaster && channels[i].numeric && !channels[i].vlsd)
{
return i;
}
}
}
for (std::size_t i = 0; i < channels.size(); ++i)
{
if (!channels[i].numeric)
{
continue;
}
const std::wstring lower = LowerAscii(channels[i].name);
if (lower == L"time" || lower == L"t" || lower == L"timestamp" || lower == L"zeit" ||
lower == L"时间" || lower.rfind(L"t[", 0) == 0 || lower.rfind(L"time[", 0) == 0)
{
return i;
}
}
for (std::size_t i = 0; i < channels.size(); ++i)
{
if (channels[i].numeric && !channels[i].vlsd)
{
return i;
}
}
return channels.size();
}
} // namespace
bool LoadMf4Source(const std::wstring& filePath, LoadedSource& source, std::wstring& error,
const LoadProgress& progress)
{
source = LoadedSource();
error.clear();
MdfFile file;
if (!file.Open(filePath, error))
{
return false;
}
const std::size_t slashPos = filePath.find_last_of(L"\\/");
source.sourceName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1);
// 头块 → 第一个数据组。
std::uint64_t hd = 0;
{
char id[5] = {};
// 头块通常紧跟在 64 字节 ID 块之后;用一个小的扫描窗口定位,兼容前置填充。
for (std::uint64_t offset = 24; offset + 64 <= file.Size() && offset < 4096; offset += 8)
{
if (file.BlockId(offset, id) && std::memcmp(id, "##HD", 4) == 0)
{
hd = offset;
break;
}
}
}
if (hd == 0)
{
error = L"未找到 MDF 头块(##HD)。";
return false;
}
const std::uint64_t firstDg = file.Link(hd, 0);
// 先走一遍 DG 链只收集偏移(只读块头,代价极小):
// 进度分母必须是**真实数据组数量**(旧实现写死 64,组数少的文件会「停在 44% 再跳 100%」)。
std::vector<std::uint64_t> dataGroups;
WalkChain(file, firstDg, "##DG", kMaxGroups,
[&](std::uint64_t dg)
{
dataGroups.push_back(dg);
return true;
});
std::size_t processedGroups = 0;
double lastRatio = -1.0;
const auto emitProgress = [&]() -> bool
{
if (!progress)
{
return true;
}
LoadProgressInfo info;
const double byGroup =
dataGroups.empty() ? 1.0
: static_cast<double>(processedGroups) /
static_cast<double>(dataGroups.size());
double ratio = 0.98 * byGroup;
ratio = std::max(ratio, lastRatio); // 保证单调
lastRatio = ratio;
info.ratio = ratio;
// 同时上报真实字节数:`MdfFile::ReadAt` 逐次累计(随机访问可能重复读元数据,需夹住)。
info.hasBytes = true;
info.bytesRead = std::min<std::uint64_t>(file.BytesRead(), file.Size());
info.totalBytes = file.Size();
return progress(info);
};
std::size_t skippedNoLayout = 0;
std::size_t skippedEmpty = 0;
std::size_t skippedNoAxis = 0;
bool cancelRequested = false;
for (const std::uint64_t dg : dataGroups)
{
if (cancelRequested)
{
break;
}
++processedGroups;
const std::uint64_t cg = file.Link(dg, 1);
const std::uint64_t dataBlock = file.Link(dg, 2);
if (!file.IsBlock(cg, "##CG") || dataBlock == 0)
{
++skippedNoLayout; // 不完整的数据组(没有 CG 或没有数据块)
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// 记录可能写在 `##DT` / `##DL`(数据列表)/ `##HL`(头部列表)/ `##DZ`(压缩)里:
// 旧实现只认 `##DT`,于是用 `##DL` 存放记录的文件会**整组被跳过**
//(用户 2026-10-19 报告:Simulink SDI 导出的 8 个信号只读出 4 个)。
std::vector<DataSegment> segments;
if (!CollectDataSegments(file, dataBlock, segments, 0) || segments.empty())
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
const std::uint64_t cgData = file.DataOffset(cg);
const std::uint16_t cgFlags = file.U16(cgData + 0);
const std::uint64_t cycles = file.U64(cgData + 8);
if (cycles == 0)
{
++skippedEmpty; // 空数据组(例如只有注释通道的组)
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// `cg_data_bytes`(+24)是单条记录字节数,4.1 起由文件直接给出,最可靠。
const std::uint32_t cgDataBytes = file.U32(cgData + 24);
// 解析通道链。
// `##CA` 数组通道就地展开:需要 `cg_data_bytes`(单个记录容量)与 `cycles`(规模护栏)。
std::vector<ChannelSpec> channels;
WalkChain(file, file.Link(cg, 1), "##CN", kMaxChannelsPerGroup,
[&](std::uint64_t cn)
{
ChannelSpec spec;
if (ParseChannel(file, cn, spec))
{
// 一个通道装整条曲线时,旧实现只能看到元素 0。
ExpandArrayChannel(std::move(spec), cycles, static_cast<std::size_t>(cgDataBytes),
channels);
}
return true;
});
if (channels.empty())
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// 记录长度:优先用 `cg_data_bytes`;否则用「数据块大小 / 记录数」反推;
// ⚠️ 压缩段(`##DZ`)的载荷是压缩后长度,不能用来反推 ⇒ 退回按通道位宽推算。
std::uint64_t payload = 0;
bool anyCompressed = false;
for (const DataSegment& segment : segments)
{
payload += segment.bytes;
anyCompressed = anyCompressed || segment.compressed;
}
std::size_t recordBytes = 0;
if (cgDataBytes > 0 && cgDataBytes <= (1u << 20))
{
recordBytes = static_cast<std::size_t>(cgDataBytes);
}
else if (!anyCompressed && payload % cycles == 0 && payload / cycles > 0 &&
payload / cycles <= (1u << 20))
{
recordBytes = static_cast<std::size_t>(payload / cycles);
}
else
{
std::size_t bits = 0;
for (const ChannelSpec& spec : channels)
{
bits = std::max(bits, spec.bitOffset + spec.bitCount);
}
recordBytes = (bits + 31u) / 32u * 4u;
}
if (recordBytes == 0)
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// ⚠️ `cg_cycle_count` 是文件里的数字,**不能无条件相信**:损坏或伪造的文件可以声明
// 几十亿条记录,而 `ReadChannelGroupRecords` 会按它一次性
// `assign(cycles × 通道数)` 个 double(几十 GB)⇒ 直接 bad_alloc 或把机器拖死。
// 这里用「记录数据总字节数」做上限:非压缩段的总载荷是精确值;
// 压缩段的解压上限按 deflate 的理论最大膨胀比(≈1032:1)估算。
{
const std::uint64_t payloadBound = anyCompressed ? payload * 1032ull : payload;
const std::uint64_t recordBound = payloadBound / static_cast<std::uint64_t>(recordBytes) + 2ull;
if (cycles > recordBound)
{
++skippedNoLayout; // 声明的记录数远超数据块容量 ⇒ 视为损坏
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
}
const std::size_t masterIndex = SelectMasterChannel(channels, cgFlags);
if (masterIndex >= channels.size())
{
++skippedNoAxis; // 没有任何数值通道,无法建立时间轴
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
// 主通道自身若是 VLSD/字符串,取值无法作为时间轴 ⇒ 直接跳过该组。
if (channels[masterIndex].vlsd)
{
++skippedNoAxis;
if (!emitProgress())
{
cancelRequested = true;
}
continue;
}
ChannelGroupData group;
group.timeName = channels[masterIndex].name;
if (group.timeName.empty())
{
group.timeName = L"time";
}
group.channels = std::move(channels);
bool readCancelled = false;
const auto cancelled = [&]() -> bool
{
if (emitProgress())
{
return false;
}
readCancelled = true;
return true;
};
const bool readOk =
ReadChannelGroupRecords(file, segments, cycles, recordBytes, masterIndex, group, cancelled);
if (readCancelled)
{
cancelRequested = true;
break;
}
if (!readOk)
{
++skippedNoLayout;
if (!emitProgress())
{
cancelRequested = true;
break;
}
continue;
}
// 组织成一个时间轴块:时间列 + 其余数值通道。
TableBlock block;
block.timeHeader = group.timeName;
block.table.headers.push_back(group.timeName);
block.table.columns.push_back(std::move(group.time));
block.table.columnEnumText.emplace_back(); // 时间列没有枚举信息
for (std::size_t c = 0; c < group.channels.size(); ++c)
{
if (c == masterIndex || !group.channels[c].numeric)
{
continue;
}
std::wstring name = group.channels[c].name;
if (name.empty())
{
name = L"Channel" + std::to_wstring(c + 1);
}
block.table.headers.push_back(name);
block.table.columns.push_back(std::move(group.values[c]));
block.table.columnEnumText.push_back(std::move(group.channels[c].enumText));
}
if (block.table.columns.size() < 2)
{
++skippedEmpty; // 只有时间列:没有可绘制信号
continue;
}
block.table.rowCount = static_cast<std::size_t>(cycles);
source.blocks.push_back(std::move(block));
}
if (cancelRequested)
{
error.clear(); // 用户取消:不显示错误
return false;
}
if (source.blocks.empty())
{
// 说明「为什么一个信号都没有」,而不是笼统地说找不到数值通道。
std::wstring detail;
if (skippedNoLayout > 0)
{
detail += L"数据块布局无法解析 " + std::to_wstring(skippedNoLayout) + L" 组;";
}
if (skippedNoAxis > 0)
{
detail += L"缺少可用时间通道 " + std::to_wstring(skippedNoAxis) + L" 组;";
}
if (skippedEmpty > 0)
{
detail += L"空数据组 " + std::to_wstring(skippedEmpty) + L" 组;";
}
error = L"未在文件中找到可绘制的数值通道。";
if (!detail.empty())
{
error += L"(" + detail + L")";
}
return false;
}
if (progress)
{
LoadProgressInfo info;
info.ratio = 1.0;
info.hasBytes = true;
info.bytesRead = std::min<std::uint64_t>(file.BytesRead(), file.Size());
info.totalBytes = file.Size();
progress(info);
}
return true;
}
} // namespace jdi