Files
Just-DataInspector/Utilities/Src/Mf4Loader.cpp
T
DeepSeek-V4.1-Flash 181f95cc3a MF4 补 ##SD/##SI/VLSD + 信号枚举文本 enumText + .mat 改随机读(流式进度)
1) 修复上轮记录的两个已知问题
   - MatLoader: LoadPlainObject 不再为拿 dims 调 ReadDataset(整块解码第二次),
     改用 ReadDatasetInfo;普通 v7.3 大矩阵的时间/内存不再翻倍。
   - MdfFile::TextBlock 注释与实现对齐:真的支持 ##MD(UTF-8 XML,无长度前缀)。

2) 信号枚举信息(以 TestData/NEW-MF4.mf4 为目标)
   - 新增 SignalEnumText(DataTable.hpp):values[] + texts[] + Empty()/Find(),
     命名用 enumText —— 它描述"取值代表哪个状态",不是单位。
   - DataTable::columnEnumText 与 columns 同下标;SignalData::enumText 由
     BuildSignalGroup 移动赋值;其它格式(CSV/.mat)保持为空。
   - MF4 cc_type = 7:cc_val[i](F64,数据段 +24)↔ cc_ref[4 + i](##TX/##MD 文本),
     最后一个引用是默认文本,不使用。
   - 界面只在**光标取值标注**追加 " = <枚举文本>"(等号两侧各一个空格、无尖括号),
     查表用采样原值(ApplyDrawTransform 之前);常规状态显示一个像素都不变。
   - 实测:NEW-MF4.mf4 8 信号 / 4 条带枚举(32+5+32+5),MF4-Record.MF4 另有 2 种(12/32)。

3) MF4 支持范围
   - ##SI 采样索引:只有索引 ≥2 条且全部 < 周期数才生效(逐记录有效性位图)。
     实测部分工具给每个通道挂只含常量 513/4 的 ##SI,必须忽略,否则整列变 NaN。
   - ##SD + VLSD(cn_type == 1):##SD 数据段是「UINT32 长度 + 字节」样本序列,
     记录里放 64 位偏移;AppendSignalData 递归展开 SD/DZ/DL/HL(单通道上限 512MB),
     DecodeVlsdValue 先按直接偏移、失败再按 >>1 回退,取样本第一个元素。
   - CN 的 cn_data / md_unit / md_comment 下标按链接总数反推:8 链接(MDF 4.0 风格)
     = 换算[4]/数据[5]/单位[6]/注释[7],9 链接(4.1)= [5]/[6]/[7]/[8]。
     实测 Simulink 导出的“4.10”仍写 8 链接,不能写死 Link(cn,4/5)。
   - 主通道:cn_sync_type 1/2/3(时间/角度/距离)→ 名字 t/time/timestamp/zeit/时间
     (含 t[...])→ 第一个数值通道兜底;cg_flags 第 2 位(远程主通道)跳过 sync 判定。
   - 记录长度优先 cg_data_bytes(CG 数据段 +24);旧实现把 U8(cgData) 当
     cg_record_id_len,其实是 cg_flags 低字节(总线事件位)。
   - 进度:先走 DG 链得到真实组数(不再写死 64),hasBytes = true,bytesRead 取
     MdfFile::ReadAt 累计(夹到文件大小);4MB 分块读取检查取消。
   - ##DZ:数据段前有 28 字节头(original_type/zip_type/param/original_size/zip_size),
     旧实现整段当 zlib 流是错的;只支持纯 deflate(zip_type = 0)。
   - 一个信号都没有时,错误信息附带「布局无法解析 / 缺少时间通道 / 空数据组」计数。

4) .mat:内存映射 → 随机读 + 元数据块缓存(消除 SEH 风险)
   - MatHdf5::Open 改为 CreateFileW(FILE_FLAG_SEQUENTIAL_SCAN) + ReadFile;
     删除 MappedBytes,改为 ReadAt / Cached(16 × 64KB LRU + 上次命中槽)/ Tag /
     ReadData / ReadCString。I/O 失败只会变成 false,不再触发页访问异常。
   - 数据读取:连续布局按 4MB 切片;分块布局按需 + 64KB 对齐。
     固定 4MB 预读在交错存放的分块数据集上会把读盘量放大到 2.2 倍(2874MB/1293MB),
     改后 1301MB。
   - 关键提速:dims=[N]、chunk=[32768,1] 归一化后 rank=2、每行 1 字节,逐行算坐标
     是 12.9 亿次迭代;新增「rank == 2 且 chunkDims[1] == dims[1]」快路径(整块一次
     sink)。BIG_MAT.mat(1.29GB / 5 × 16,142,471):21.5s → 7.9s,私有内存 +741MB。
   - 进度:Open 立刻上报「0 / 总大小」,之后每读一块回调 ReadProgress(bytesRead,total)
     → MatLoader 映射到 0.02–0.98 并单调夹紧(字节进度与逐流解码取较大值),收尾 1.0。
     遮罩层百分比不再一上来就 100%;取消会中止(StreamDataset 返回 false),不留 NaN 空洞。
   - 连续布局数据不足(文件被截断)现在明确失败,不再静默截短。

验证(全部离屏:无可见窗口、不注入鼠标键盘屏幕)
- 新增 .preflight/perf/loadercheck(数据层)32/32:
  NEW-MF4 8 信号 / 4 条枚举 / 枚举文本逐条核对 / 9396 与 94 行;
  MF4-Record 124 信号 / 3 轴 / 4 条枚举;data-sample.mat 4 组 8 信号 / 行数;
  BIG_MAT.mat 5 信号 × 16,142,471 + 进度(total 已知、ratio 单调、bytes 递增、
  起点 ≤0.05、终值 1.0、更新 ≥10 次)。
- 新增 .preflight/perf/enumui(真实 Application + SW_HIDE)12/12:
  只显示 gVR_WinOpenReq[0],光标落在枚举采样点上 —— 标签宽度 265px;
  临时清空 enumText 后 25px;差值 240px 与 " = WinOpenReqAtDrvr_Idle" 的
  UiMeasureTextWidth 完全一致;恢复后宽度回到 265px。
- 回归:datacheck 69/69、matcheck 21/21、matui 15/15(loading bytes 达到总大小)、
  verify 49/49。
- make release / make debug 零警告。Build/Release/TargetCheck.exe = 820,224 字节
  (上轮 785,920,+34KB)。(Target.exe 正在被用户运行的程序占用,链接步骤未覆盖它。)
2026-10-20 01:56:47 +08:00

1500 lines
48 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 <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`)中本项目实现的子集。 */
enum class ConversionType : int
{
NONE = 0, /**< 无换算(恒等)。 */
LINEAR = 1, /**< 线性:`phys = c0 + c1 * raw`。 */
VALUE_TO_TEXT = 7, /**< 值 → 枚举文本(`cc_val[i]` ↔ `cc_ref[i]` 文本)。 */
VALUE_RANGE_TO_TEXT = 8 /**< 值区间 → 文本(未支持,按恒等处理)。 */
};
/**
* @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
{
if (m_file == nullptr || offset + bytes > m_size)
{
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 判断某个偏移是否为一个有效块的开头(且标识匹配)。
* @param [IN] offset 候选偏移。
* @param [IN] id 期望的 4 字节标识(如 "##CN");空字符串表示只校验偏移合法性。
*/
bool IsBlock(std::uint64_t offset, const char* id) const
{
if (offset == 0 || offset + kBlockHeaderBytes > m_size)
{
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;
}
return BlockLength(offset) >= kBlockHeaderBytes && offset + BlockLength(offset) <= m_size;
}
/**
* @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`:本组的主通道。 */
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;
bool linearConversion = false; /**< 是否带线性换算。 */
double offset = 0.0; /**< 线性换算常数项。 */
double factor = 1.0; /**< 线性换算系数(无法换算时为 1)。 */
SignalEnumText enumText; /**< 值 → 文本枚举表(`cc_type == 7`,可能为空)。 */
/** 数值有效性:只有整数与浮点通道可以绘制。 */
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 位无符号数,再按位偏移右移并解释符号。
std::uint64_t raw = 0;
if (spec.dataType == DataType::UINT_LE || spec.dataType == DataType::INT_LE)
{
for (std::size_t i = byteCount; i-- > 0;)
{
raw = (raw << 8) | static_cast<std::uint64_t>(buffer[i]);
}
}
else
{
for (std::size_t i = 0; i < byteCount; ++i)
{
raw = (raw << 8) | static_cast<std::uint64_t>(buffer[i]);
}
}
const std::size_t shift = spec.bitOffset % 8u;
raw >>= shift;
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 把 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 采样(取样本的第一个元素)。
*
* 记录里放的是 64 位偏移。两种写法都接受:
* * 直接偏移(部分写盘器);
* * `偏移 << 1 | 标志位`(标志位为 1 时表示长度,此时按 `>> 1` 回退)。
* 判据是「长度前缀自洽」——哪种解释能在 `##SD` 缓冲里定位到合法样本就用哪种。
*
* @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() || spec.bitCount == 0)
{
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;
}
const std::size_t elementBytes = (spec.bitCount + 7u) / 8u;
if (length < elementBytes)
{
return kMissingValue;
}
ChannelSpec element = spec;
element.bitOffset = 0;
element.bitCount = std::min<std::size_t>(spec.bitCount, static_cast<std::size_t>(length) * 8u);
element.vlsd = false;
return DecodeRawValue(signalData.data() + offset + 4u, length, 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();
}
}
}
}
// 换算:支持「无换算」「线性」与「值 → 文本」三类。
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);
if (ccType == static_cast<std::uint8_t>(ConversionType::LINEAR) && valCount >= 2)
{
spec.linearConversion = true;
spec.offset = file.F64(ccData + 24);
spec.factor = file.F64(ccData + 32);
if (spec.factor == 0.0 && spec.offset == 0.0)
{
// 换算表未填写:按恒等处理,避免整列变成 0。
spec.linearConversion = false;
spec.offset = 0.0;
spec.factor = 1.0;
}
}
else if (ccType == static_cast<std::uint8_t>(ConversionType::VALUE_TO_TEXT) && 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();
}
}
}
// 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。
spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64;
return true;
}
/** @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 = master.linearConversion ? (master.offset + master.factor * value) : 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 = spec.linearConversion ? (spec.offset + spec.factor * value) : 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)
{
for (std::size_t i = 0; i < channels.size(); ++i)
{
if (channels[i].syncMaster && channels[i].numeric)
{
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);
// 解析通道链。
std::vector<ChannelSpec> channels;
WalkChain(file, file.Link(cg, 1), "##CN", kMaxChannelsPerGroup,
[&](std::uint64_t cn)
{
ChannelSpec spec;
if (ParseChannel(file, cn, spec))
{
channels.push_back(std::move(spec));
}
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;
}
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