#include "Mf4Loader.hpp" #include "Deflate.hpp" #include "Utf8Util.hpp" #include #include #include #include #include #include #include #include #include namespace jdi { namespace { const double kMissingValue = std::numeric_limits::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(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(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(buffer[i]) << (8u * i); } return value; } std::uint32_t U32(std::uint64_t offset) const { return static_cast(Scalar(offset, 4)); } std::uint16_t U16(std::uint64_t offset) const { return static_cast(Scalar(offset, 2)); } std::uint8_t U8(std::uint64_t offset) const { return static_cast(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 buffer(static_cast(usable) + 1u, '\0'); if (!ReadAt(data, buffer.data(), static_cast(usable))) { return text; } std::size_t length = static_cast(usable); while (length > 0 && buffer[length - 1] == '\0') { --length; } return Utf8ToWide(buffer.data(), static_cast(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 buffer(static_cast(2u * declared)); if (!ReadAt(data + 4, buffer.data(), buffer.size())) { return text; } text.reserve(static_cast(declared)); for (std::uint64_t i = 0; i < declared; ++i) { const wchar_t c = static_cast(static_cast(buffer[2 * i]) | (static_cast(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 buffer(static_cast(declared) + 1u, '\0'); if (!ReadAt(data + 4, buffer.data(), static_cast(declared))) { return text; } return Utf8ToWide(buffer.data(), static_cast(declared)); } } // 回退:数据段直接是 NUL 结尾的文本(UTF-16 或 UTF-8)。 std::vector buffer(static_cast(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(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(c); } return Utf8ToWide(raw.data(), static_cast(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 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(value); } double value = 0.0; std::memcpy(&value, buffer, 8); return value; } if (spec.bitCount == 32u) { std::uint32_t bits = (static_cast(buffer[0]) << 24) | (static_cast(buffer[1]) << 16) | (static_cast(buffer[2]) << 8) | static_cast(buffer[3]); float value = 0.0f; std::memcpy(&value, &bits, 4); return static_cast(value); } std::uint64_t bits = 0; for (int i = 0; i < 8; ++i) { bits = (bits << 8) | static_cast(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(buffer[i]); } } else { for (std::size_t i = 0; i < byteCount; ++i) { raw = (raw << 8) | static_cast(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(static_cast(raw)); } return static_cast(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& 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(payload)); if (!file.ReadAt(data, out.data() + base, static_cast(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 compressed(static_cast(usable)); if (!file.ReadAt(data + 28u, compressed.data(), compressed.size())) { return false; } std::vector 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& 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(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(spec.bitCount, static_cast(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); spec.bitCount = bitCount; spec.bitOffset = static_cast(byteOffset) * 8u + bitOffset; spec.allInvalid = (flags & 0x1u) != 0u; spec.hasInvalidBit = (flags & 0x2u) != 0u; spec.invalidBitPos = invalidBit; spec.type = static_cast(type); spec.syncType = static_cast(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(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(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(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 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& 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& segments, std::uint64_t cycles, std::size_t recordBytes, const std::function& visit, const std::function& cancelled = std::function()) { if (recordBytes == 0 || cycles == 0) { return false; } std::vector carry(recordBytes); std::size_t carryBytes = 0; std::uint64_t row = 0; std::vector 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 compressed(static_cast(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 buffer; std::uint64_t consumed = 0; while (consumed < segment.bytes && row < cycles) { const std::size_t want = static_cast( std::min(kReadChunkBytes, segment.bytes - consumed)); buffer.resize(std::max(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 channels; /**< 非时间通道。 */ std::vector time; /**< 时间轴数值。 */ std::vector> values; /**< 每个非时间通道的采样值。 */ }; /** * @brief 把 `##SI` 采样索引转换成「逐记录有效位图」。 * * 只有当索引表**至少两条**且**全部落在记录范围内**时才生效; * 否则(例如某些工具给每个通道挂一个只含 `513` 的 `##SI`)返回空向量表示「全有效」, * 与不支持 `##SI` 时的行为完全一致。 */ std::vector BuildSampleMask(const ChannelSpec& spec, std::uint64_t cycles) { std::vector 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(cycles), 0); for (const std::uint64_t index : spec.sampleIndex) { mask[static_cast(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& segments, std::uint64_t cycles, std::size_t recordBytes, std::size_t masterIndex, ChannelGroupData& group, const std::function& cancelled = std::function()) { if (cycles == 0 || recordBytes == 0 || segments.empty()) { return false; } group.time.assign(static_cast(cycles), kMissingValue); group.values.assign(group.channels.size(), std::vector(static_cast(cycles), kMissingValue)); // `##SI`:每个通道各自的「有效记录」位图(空 = 每个记录都有效)。 std::vector> 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> 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*> 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(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(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(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(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& 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 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(processedGroups) / static_cast(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(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 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 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(cgDataBytes); } else if (!anyCompressed && payload % cycles == 0 && payload / cycles > 0 && payload / cycles <= (1u << 20)) { recordBytes = static_cast(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(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(file.BytesRead(), file.Size()); info.totalBytes = file.Size(); progress(info); } return true; } } // namespace jdi