#include "Mf4Loader.hpp" #include "Deflate.hpp" #include "Utf8Util.hpp" #include #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`)。 * * ⚠️ 数值换算(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(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(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 判断某个偏移是否为一个有效块的开头(且标识匹配)。 * * ⚠️ **必须同时校验链接表落在块内**(`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 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,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 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 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 arrayDims; /** * @brief `##CA` 各维的周期数(`ca_cycle_count`);空 = 文件没写这个字段。 * * 规范里 `ca_cycle_count[i] > 1` 表示该维的元素被**拆到多条记录**里, * 这时单个记录内并不含全部元素 ⇒ 不能按「记录内连续」展开(见 `ExpandArrayChannel`)。 * ⚠️ 实测文件里这个字段**不存在**(`##CA` 块长度恰好到 `dim_size[]` 就结束), * 因此读取前必须先按块长度做边界检查,不能盲读(盲读会拿到下一个块头当作数字)。 */ std::vector 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(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 位整数再整体右移:`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(static_cast(raw)); } return static_cast(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& 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& 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 样本的元素位宽(`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& 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(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(length)); } if (elementBits == 0 || elementBits > 64u) { return kMissingValue; } const std::size_t elementBytes = (elementBits + 7u) / 8u; if (static_cast(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); 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(); } } } } // 换算:数值换算(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 { std::vector 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(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(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(file.U8(caData + 0)) : 0; spec.arrayStorage = (caUsable >= 2) ? static_cast(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(axis) + 1u) > caUsable) { dimsOk = false; // 维度表被截断:不做任何猜测 break; } const std::size_t dim = static_cast(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(rank) + 4u * static_cast(axis); if (cycleOffset + 4u > caUsable) { cyclesPresent = false; break; } spec.arrayCycles.push_back(static_cast(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& 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(cycles)) { expandable = false; } if (expandable && recordBytes > 0) { const std::uint64_t needBits = static_cast(spec.bitOffset) + static_cast(count) * static_cast(spec.bitCount); if (needBits > static_cast(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 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 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 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 = ApplyConversion(master, 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 = ApplyConversion(spec, 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) { // 优先同步主通道,但 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 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); // 解析通道链。 // `##CA` 数组通道就地展开:需要 `cg_data_bytes`(单个记录容量)与 `cycles`(规模护栏)。 std::vector 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(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(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; } // ⚠️ `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(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(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