#include "Mf4Loader.hpp" #include "Utf8Util.hpp" #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`。 */ RATIONAL = 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 读取一段原始字节。 */ 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; } return std::fread(dst, 1, bytes, m_file) == bytes; } /** @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` 块里的文本(MDF 的 TEXT 块)。 */ std::wstring TextBlock(std::uint64_t offset) const { std::wstring text; 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; }; /** @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; /**< 无效位的绝对位偏移。 */ bool syncMaster = false; /**< `cn_sync_type == 1`:时间主通道。 */ bool linearConversion = false; /**< 是否带线性换算。 */ double offset = 0.0; /**< 线性换算常数项。 */ double factor = 1.0; /**< 线性换算系数(无法换算时为 1)。 */ /** 数值有效性:只有整数与浮点通道可以绘制。 */ 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 解析一个 CN 块为读取方案。 */ 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); 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.syncMaster = (sync == 1); // 换算:只支持“无换算”与“线性”。 const std::uint64_t cc = file.Link(cn, 4); 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; } } } // 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。 spec.numeric = IsNumericType(spec.dataType) && spec.bitCount > 0 && spec.bitCount <= 64; (void)type; 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 一个通道组(CG)解析出的时间轴 + 信号列。 */ struct ChannelGroupData { std::wstring timeName; /**< 时间列名(主通道名或 `time`)。 */ std::vector channels; /**< 非时间通道。 */ std::vector time; /**< 时间轴数值。 */ std::vector> values; /**< 每个非时间通道的采样值。 */ }; /** * @brief 读取一个通道组的全部记录(流式,按 4MB 分块)。 * * @param [IN] file 文件访问器。 * @param [IN] dt 数据块偏移(`##DT`),记录从 `DataOffset(dt)` 开始连续存放。 * @param [IN] cycles 记录条数。 * @param [IN] recordBytes 单条记录字节数。 * @param [IN,OUT] group 输出(`timeName`/`channels` 已填好,函数填充 `time`/`values`)。 */ bool ReadChannelGroupRecords(const MdfFile& file, std::uint64_t dt, std::uint64_t cycles, std::size_t recordBytes, std::size_t masterIndex, ChannelGroupData& group) { if (cycles == 0 || recordBytes == 0) { return false; } const std::uint64_t dataStart = file.DataOffset(dt); const std::uint64_t payload = file.BlockLength(dt) - (dataStart - dt); if (payload < cycles * recordBytes) { return false; // 数据不足(损坏或带压缩) } group.time.assign(static_cast(cycles), kMissingValue); group.values.assign(group.channels.size(), std::vector(static_cast(cycles), kMissingValue)); std::vector buffer(kReadChunkBytes); std::uint64_t done = 0; while (done < cycles) { const std::uint64_t remainingRecords = cycles - done; const std::uint64_t wantBytes = std::min( remainingRecords * recordBytes, static_cast(buffer.size())); const std::uint64_t wantRecords = wantBytes / recordBytes; if (wantRecords == 0) { break; } const std::size_t bytes = static_cast(wantRecords * recordBytes); if (!file.ReadAt(dataStart + done * recordBytes, buffer.data(), bytes)) { return false; } for (std::uint64_t r = 0; r < wantRecords; ++r) { const unsigned char* record = buffer.data() + r * recordBytes; const std::size_t row = static_cast(done + r); // 时间主通道。 { 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[row] = value; } // 各信号通道。 for (std::size_t c = 0; c < group.channels.size(); ++c) { if (c == masterIndex) { continue; } const ChannelSpec& spec = group.channels[c]; double 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][row] = value; } } done += wantRecords; } return true; } /** * @brief 从通道组里选出时间主通道。 * @return 主通道下标;找不到时返回 `channels.size()`(表示没有可用时间轴)。 */ std::size_t SelectMasterChannel(const std::vector& channels) { 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; } std::wstring lower; lower.reserve(channels[i].name.size()); for (const wchar_t c : channels[i].name) { lower.push_back((c >= L'A' && c <= L'Z') ? static_cast(c - L'A' + L'a') : c); } if (lower == L"time" || lower == L"t" || lower == L"timestamp") { 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); std::size_t groupCount = 0; WalkChain(file, firstDg, "##DG", kMaxGroups, [&](std::uint64_t dg) { ++groupCount; const std::uint64_t cg = file.Link(dg, 1); const std::uint64_t dt = file.Link(dg, 2); if (!file.IsBlock(cg, "##CG") || !file.IsBlock(dt, "##DT")) { return true; // 跳过不完整的数据组 } const std::uint64_t cgData = file.DataOffset(cg); const std::uint64_t cycles = file.U64(cgData + 8); if (cycles == 0) { return true; } const std::uint64_t payload = file.BlockLength(dt) - (file.DataOffset(dt) - dt); // 解析通道链。 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()) { return true; } // 记录长度:优先用「数据块大小 / 记录数」反推(最可靠), // 否则按通道覆盖的位宽向上取整到 4 字节。 std::size_t recordBytes = 0; if (payload % cycles == 0 && payload / cycles > 0) { 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) { return true; } const std::size_t masterIndex = SelectMasterChannel(channels); if (masterIndex >= channels.size()) { return true; // 没有可用时间轴通道 } ChannelGroupData group; group.timeName = channels[masterIndex].name; if (group.timeName.empty()) { group.timeName = L"time"; } group.channels = std::move(channels); if (!ReadChannelGroupRecords(file, dt, cycles, recordBytes, masterIndex, group)) { return true; } // 组织成一个时间轴块:时间列 + 其余数值通道。 TableBlock block; block.timeHeader = group.timeName; block.table.headers.push_back(group.timeName); block.table.columns.push_back(std::move(group.time)); 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])); } if (block.table.columns.size() < 2) { return true; // 只有时间列:没有可绘制信号 } block.table.rowCount = static_cast(cycles); source.blocks.push_back(std::move(block)); if (progress) { LoadProgressInfo info; info.ratio = (groupCount > 0) ? std::min(1.0, static_cast(groupCount) / 64.0) : 0.0; info.hasBytes = false; if (!progress(info)) { return false; } } return true; }); if (source.blocks.empty()) { error = L"未在文件中找到可绘制的数值通道。"; return false; } if (progress) { LoadProgressInfo info; info.ratio = 1.0; progress(info); } return true; } } // namespace jdi